とあるゲーム制作メモ
ゲームを作ろうと思った。
始まりはただそれだけ。
って事で、ゲーム作成記、はーじまーるよー!
暇があった、ちょうどJetbrainsの統合開発環境のライセンスを買っていた。
そんなことからゲームを作ろうと思い立った話です。
まずは言語選択から。
モダンな環境はUnityだろう。ただ、ただ……MSが気にくわないと言うだけの理由でC#を使うUnityは真っ先に選択肢から外した。
ライブラリを使用するならCやpythonでもいける、ただCはマルチプラットフォームに向いてないし、Pythonはライブラリがディスコンされやすい。
そんなわけでJavaで作ることに決めた、いやUEとかcocosとかあるやん、と言う意見もごもっともなのだが無視することにする。
さあ始めるぞとIntelliJを起動する。
始まりはなんと言ってもド定番のウィンドウを表示してHelloWorldだ。
JavaにはGUI環境としてAWTとSwingがあるが、今の時代に古いAWTを選択する理由はないのでSwingで作ることにする。
ジャンルはハードコーディングのしやすいシューティング、エンジン制作は途方もない道のりなのでまず簡単なところから始めよう。
ところでこの文章を読んでいる方はご存じかもしれないが、シューティングゲームというのはエンジンを使わない限り画面上の全弾と自機の当たり判定を計算することを毎秒何十あるいは何百回と繰り返す。
実に泥臭い作業である。
いやその当たったら検出されるでしょ? それはライブラリやエンジンが頑張ってるんです、ゲームの本質はこの繰り返しなんです。
まずはJFrameを継承したメインクラスを作成する。
そしてコンストラクタ(クラスが生成されたら自動実行されるメソッド(関数))を書く。
JFrameを継承しているのでまずはsuper();でウインドウを生成する。
二行目は
super.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
ここではウィンドウの閉じるボタンを押したときに終了する設定をしている、エンジン無しではそれすらコンピュータに指示してやらないとならない。
その後すぐJPanelを継承したゲーム描画用のパネルのインスタンスを作る。
インスタンスとは何か? これについては説明が難しいことだ。
Cがわかる人なら構造体のポインタにmallocしていると言えば通じるだろうか?
要はクラスの設計図から物体としてのクラスを生成していると言えばいいのか、この辺の解説は詳しい方に譲ろう。
ではゲーム本体部分のプログラミングを……といきたいところだがここで一つ注意がある。
キー入力を受け取るKeyAdapterを継承したクラスを作っておこう。
JPanelクラスではキー入力を受け取ることができない、筆者はこれでドハマりしたのでここでかいておくべきだった、とはいえ後知恵だ。
さて、そろそろゲームの処理本体に移ろう。
まずは自機の座標を入れるローカル変数を作る。
本来こんなコードを書くべきではないのだが面倒なので以下のように書いた。
int x = 0;
int y = 0;
熟練のプログラマが見たらキレそうなコードであるが「完璧よりまず終わらせろ」の原則に則ってコレでいく。
フラグ類も一行なので貼っておこう。
public boolean isRetry,isUp,isDown,isLeft,isRight,isSpace;
publicなのは面倒だったからだ、どこからでも読み書きできる不要なpublicは書くべきではないのだがぶっちゃけ面倒くさい、どうせ自分しか読まないし……
それぞれコンティニュー用、押されたキーボードのカーソルキー、スペースキーのフラグである。
まずは自機を表示させよう、適当なドット絵かなんならdrawRectで四角形を描いてもいい、其れは本質ではないので後回しにしよう。
ウィンドウに描画するためのpaintメソッドを実装する。
できればここでInsetsを使ってウィンドウの枠の幅を取得してtranslateメソッドで原点を変更しておくのがベターだ。
自機の表示は
g.drawImage(myship,x,y,this);
で可能である。
ここではmyshipはメモリ上に展開した画像イメージである。
それを自機の座標に表示する。
プログラミング経験があれば分かるだろうがxとyは左上の点になる。
xとyを直接あたり判定に利用すると変なところに判定が出るので注意しよう。
さて、ここでゲーム本体部分と言ってもいいTimerTaskを継承したインナークラスを作ろう。
と言ってもやることはprivateで宣言したクラスにextends TimerTaskと付けるだけだ。
このクラス定義を書くと多くの環境では「メソッドが足りないよ!」と警告を出してくれる、テキストエディタでJavaを書いていた十数歳の頃から考えると現代の子供たちは恵まれている。
ここで表示された警告通りにクリックすると多くの環境で
public void run()
メソッドが作られる。
ここであたり判定や移動処理を行おう。
注意点としてはMyTimerTaskにできているのでMyTimerTask mtt ってインスタンスを作ってmtt.run()でいいんでしょ?
実はそれはダメなのです。
run()はまず直接呼ぶことはなくThreadClass t内のrun()ならt.start()で実行される。
ここについては文章のみで説明がしづらいのでソースを載せることを許して欲しい。
Timer t = new Timer();
t.schedule(new MyTimerTask(),10,8);
ここで何をしているかと言えばMyTimerTaskを8ミリ秒ごとに実行しろ、と言う命令である。
別に8ミリ秒である必要はないのだがPCで動かすのを前提にしているので144Hzのこうリフレッシュレートでもコマ落ちしない秒数を指定している。
人間の目なら33ミリ秒くらいまでは問題ないような気もする、ヘビーゲーマーな方にはカクカクに見えてしまうかもしれないが……
では自機の移動をしよう。
run()内に
if(isUp == true){
y--;
}
まずは上方への移動だ、前述のKeyAdapterで上キーが押されたときisUpをtrueにしている。
そのときに自機の座標を1ピクセル上に上げている。
数学の座標系と違ってy方向は大きくなるほど下に行くことに注意しよう。
あとはrun()メソッドの末尾にrepaint();を書いて再描画する。
なんとたったこれだけでカーソルキーを押すと上に動いていく
上下左右も変える値以外同じ方法なので残りは割愛する。
さてこれで自機は動くようになったが、何か物足りない……ショットと敵だ、敵のいないゲームは面白くない。いや、そういうゲームを否定はしないがやはり敵は欲しい。
と言うわけでEnemyクラスを作る、自機と違って敵は複数出るかもしれないしクラス化するべきだろう
とはいえ難しくはない。
public class Enemy{
int ex,ey;
boolean isAlive;
}
基本はコレだisAliveについて補足すると敵の生存フラグだ、これがfalseになっていれば撃破済みの敵とする。
C系に慣れていると「メモリの解放どうすんの!?」と思われそうだがJavaなのでガベージコレクションが自動で解放するためそこについてはさほど考慮しない。
どうしても意図的に解放するならインスタンスにnullを入れておけばたいていの場合解放される、コンピュータが判断するので断定はできないことではある。
これではインスタンスができるたびに敵の座標を指定しなければならないのでコンストラクタを作っておこう。
public Enemy(int x,int y){
ex = x;
ey = y;
isAlive = true;
}
newで生成したときにx,y座標を指定して生存フラグをtrueにしておく。
自機の弾もほぼ同じクラスでいい、使い回せるかもしれないがこればかりのクラスをいちいち継承したりも面倒なので別で作る。
敵を複数出して攻撃で減らしたり新しく出てきたりするときには注意が必要で、synchronized節で弾や敵の配列をロックしてから操作しよう。
ロックしないとイテレーション中に数が増えたり減ったりしたときに例外が投げられるので注意しよう。
例外とは平たく言えばエラーだ。実のところJavaでは例外を無視できるcatch(Exception e){}と書くだけであら不思議! 何事もなかったかのようにプログラムが走り続ける(止まることもある)
しかしこれはやってはいけない「例外を握りつぶす」と呼ばれる行為だ。
別にちょっと遊ぶゲームくらいでは問題ないこともあるがコードの寿命は思っていたよりも長いことがある。未来の自分のためにもよい書き方をしよう。
余談ではあるが某銀行では基幹システムにJavaを使っているらしい。さすがにそういった現場では行われていないと信じたい。
それではまずは敵を生成しよう、敵は複数いるので ArrayList<Enemy> enemy; で作る。
ここで一つ注意!
この段階ではEnemyクラスの実体は無い、ここでenemy.add()などを呼ぶとエラーが出る、ちゃんと enemy = new ArrayList<Enemy>();
これでちゃんと配列の実体を作っておこう。作者はここで数十分悩んだ。
<Enemy>←これ何?
これはジェネリクスというもので、この配列にはEnemyが入りますよという宣言だ。
昔は型変換を使って代入のたびに変換していたがあまりに面倒なのでこうなったらしい、バグの元でもあるしいいことだろう……後方互換性にさえ目を瞑れば……
さあ、ここからはあと一歩だ。
多くのシューティングゲームでは敵は複数体出るのだが、ここでは基本と言うことで一体だけ出す。
方法は簡単、enemy.add(x,y);と書くだけだ、xとyには敵を表示させたい座標を指定しよう。
ここではJpanelを継承したクラスのコンストラクタの末尾に書いておく。
「一体なら」ほぼ必要ないがsynchronized(enemy)で配列をロックしておくのが「行儀のいい」書き方だ。
自機の弾も敵とほぼ同じような書き方でかまわない、本来であればちゃんと発射間隔を設定しておかないと、やれば分かるがつながったビームみたいな弾が出るので実装するときは発射間隔を設定しよう。
筆者は拡張for文であたり判定を処理したがもっといい方法があるかもしれない、そのあたりは自分で調査して欲しい。
少々長くなるがあたり判定のコードになる。
if (e.ex > bullet.sx && e.ex < bullet.sx + 16 && e.ey > bullet.sy && e.ey < bullet.sy + 16 && e.isAlive == true)
if文の中が凄く長いので尻込みしそうだが結局のところは自機と敵弾の座標がかぶっているかを判定しているだけだ。
弾幕シューティングを作るのであれば自機も敵弾も点にすると言うこともできるその場合以下のようにとてもシンプルだ。
if(e.ex == bullet.sx && e.ey == bullet.sy)
何故こんなに簡単になるのにしないのかと言えば「弾幕は作るのが大変」だからだ。
弾幕のアルゴリズムを勉強していてそこそこ処理能力のいい環境で動かすならガンガン弾を生成して自機も敵弾も点にすることができる、その辺は好みだ。
ここでは自機と敵弾では無く自弾と敵のあたり判定であるが自機と敵弾の場合も同じだ。
ただ敵と自弾の判定より自機と敵弾の場合は「緩め」に設定しておいた方がよい。かすったらミスになるゲームはストレスが溜まりやすい。
後は実行するだけなのであるが、ここではコードを載せることはできない。
githubに上げるという方法もあるのだが、作者が外部リンクを貼ってなろう運営に怒られた経験済みなので読者のみんなでそれぞれ実装してみて欲しい。
以上、簡単ではあるがゲーム制作記でした。