初心者でも2時間くらいでシューティングゲームを作ろう!

ブラウザゲーム、作りたいかあ!

前回の記事

初心者でも2時間でブラウザゲームを作ってWebにアップしよう!

前回、2時間でブラウザゲームを制作する方法を執筆しました。

みなさん、あれからゲーム制作を続けていますでしょうか。

え?前回教わった内容だとロクなゲームを制作できないって?

確かに前回の記事を体得したところで、画像をクリックするだけのゲームしか作れませんでした。

どうせならもっとゲームっぽいものを作りたい!と思った人も多いと思います。

今回はゲームの王道シューティングゲームを作れるようになりましょう!

今回は、ゲーム制作のアルゴリズムに注力してますので、前回よりは幾分難しくなっております。今回も前回同様サンプルコードをいじって覚えていくスタイルで行きますので、みなさんぜひコードをいじりながらプログラミングを学んでもらえればと思います。

サンプルコードをvisual studio code(vscode)で開く

今回いじるサンプルコードはこちらです。

https://github.com/hothukurou/enchant_js_shooting

githubのページから以下の部分をクリックしてサンプルプロジェクトをダウンロードしてください。

(画像はenchant.js Material からお借りしました。もう閉鎖されてページは存在しないのですが・・・。)

ダウンロードして、zipを解凍しましょう。

解凍したら、Shootingフォルダ内で右クリックをして、下の赤枠をクリックしましょう。

こんなカッコいい画面が出てくると思います。

もうこれで気分はウキウキのプログラマですね。中学生だったらこれだけで友達に自慢したくなるような画面です。

このvscodeですが、細かい使い方については「vscode 初心者」でテキトーに検索すれば見つかると思います。今回は、最小限の使い方だけ説明します。

左の項目が「フォルダ内のファイル一覧」となっています。

前回の講座で説明した通り、基本的にscript.jsだけいじればよいので、script.jsをクリックしましょう。

いよいよプログラム解説へ

いよいよプログラムの解説に入ります。

とりあえずゲームを起動させましょう。下画面のようにフォルダのindex.htmlをクリックしてください。

こんな画面が出てくると思います。

画面をクリックすると、画面下の剣士が爆弾を飛ばします。

シューティングですね!

演習! しばらく遊んでみよう!

さて、次からプログラムについて説明に入るのですが、

今回は「球の発射処理」や「当たり判定」というロジックが入ってくるため、前回よりも難易度が跳ね上がります。

本プログラムはenchant.jsというゲームライブラリを使用しているのですが、enchant.jsがサポートしてくれる機能は「クリック等の入力」「画面表示に関わる出力」だけです。

ゲームの中心となるロジックは自分で考え方(アルゴリズム)を学んで制作する必要があります。

ゲームプログラムは「クリック・タッチ等のイベント入力」によって処理が開始し、ロジックにより処理が行われ、そのロジックを元に「画面に適切な画像や文字が指定した座標に表示する出力」を行うといった流れで処理が行われます。

入力・ロジック・出力という一連の流れを意識すると、ゲームプログラムが非常に書きやすくなることでしょう。

このような種類のプログラミングをイベント駆動型プログラミングと呼びます。

では、これから今回のシューティングゲームで使用する「球の発射」と「当たり判定」についてざっくりアルゴリズムを話していきます。

シューティングゲームのアルゴリズム

球の発射

シューティングゲームといえば球発射ですが、どうやってこれを実現しているのでしょうか。実は球発射は非常に簡単に実現できます。

プログラムの説明は実際においておいて、ここではどういう考え方で球発射を実現しているのかを説明します。

今回用意した球は以下のような仕様です。

この3つの仕様は以下のようなロジックで実現可能です。

・爆弾は毎フレームごとにY座標に-5する (5という数値が爆弾の速度になる)

・爆弾のY座標が-16pxだと画面外なので消滅する

・爆弾は毎フレームごとに「表示している敵全て」と当たり判定チェックする

 

結構簡単に作れそうに見えてきませんか?

こんな感じでみてみると、球の仕様をプログラムに落としやすくなったのではないかと思います。

 

また、この爆弾は画面クリックによりプレイヤーから発射されます。

これが球の発射処理になるのですが、これは以下ロジックで実現します。

・画面をクリックすると、プレイヤーの座標に爆弾を生成する

これも非常にシンプルに実装が可能です。

この後実装するプログラムでも、この処理をそのまま実装しています。

さて、球発射のロジック説明が終わったところで、いよいよ面倒な当たり判定の話に進みましょう。

敵と爆弾の当たり判定

当たり判定は、たいていのゲームで使用する「オブジェクト同士の衝突を判定する処理」のことです。

今回の当たり判定の実装は「判定対象である爆弾座標」に対して「敵全ての座標」を比較することで判定します。

爆弾が2個あった時も、もちろん各々の爆弾に対して全てのモンスターと衝突を判定します。

当たり判定は見てわかる通り、毎フレームに結構な計算が必要になる処理になります。実は高速化するためのアルゴリズムがあるのですが、今回では触れません。実際に処理落ちなどの問題が起きた時に調べてみましょう。

さて、実装方法ですが、各爆弾のonenterframe関数で、表示モンスター全てと距離を測って一定以下なら衝突というロジックで実装できます。

onenterframe関数は毎フレーム関数を実行するイベントハンドラなのですが、これを画面に表示している各爆弾ごとに設定してあげることで、「各々の爆弾全てがonenterframe関数内でモンスター全てと衝突判定」を行ってくれる訳です。

ちなみにモンスターは一定間隔で画面上から出現するのですが、このモンスターは画面に出現するたびにmonsters配列に格納し、また消滅するとmonsters配列から消去します。

monsters配列には画面に表示している全てのモンスターを格納している状態にします。そうすると、爆弾が衝突判定を行う対象はmonsters配列内のモンスターを確認すればよいことになります。

次にこの「モンスターを全て配列で管理する」方法を考えましょう。

生成したモンスター配列に追加することは簡単に実装できます。問題は「モンスターを配列から消去する時」です。

上の例だと青スライムが衝突したので、monsters配列から青スライムだけ削除して新しい配列をmonsters配列としたいのです。

衝突したmonster配列内のmonsterが一致していることを確認するためにはどうしたらよいでしょうか。

今回の実装では全てのモンスターにユニークなidを設定することで、衝突したmonsterと同じidの配列要素だけを削除するようにしています。

ユニークなIDは「時刻と乱数」を組み合わせて生成して、モンスター生成時に付与しています。

とにかく他のモンスターと被らなければよいので、idは生成ごとに1を足していくロジックでも同様に実現可能ですね。そのあたりはお好みでどうぞ。

さて、これでmonsters配列は常に「画面に存在するモンスター全てを管理する配列」として機能するため、爆弾の衝突判定はうまくいきそうです。

では、実際の実装をプログラムを通して確認してみましょう!

プログラムを実際にみてみよう

今回、色々と小手先の技術をいれているので「このプログラムの構文みたことないな」と思われる方もいるかと思います。一旦はコメントアウトに書かれた内容を元に動きを確認して、必要ならばその構文を調べて理解を深めるとよいと思います。

画像のプリロード

9~15行目にて、今回使用する画像をプリロードしています。

前回とは打って変わって、for文を使って連番で画像を読み込んでいます。

今回使用する画像はimageフォルダ内にあります。image1.png~image6.pngまで連番で設定しているので、これを一気に取得するコードを書いています。

setMainSceneとsetEndScene

32~135行目までsetMainScene関数が定義されています。

このsetMainSceneを呼び出すと、「画面にmainSceneガセットされる」ようになります。setEndSceneも同様です。

前回制作したクリックゲームと異なり、今回は「sceneをセットする関数でひとまとめにする」ようにしています。

こうすると、mainSceneに関わるSprite,Label等の表示要素を関数内にひとまとめにしてローカルスコープ内に閉じ込めることで、他シーンと変数名が重複しても別変数として読み込むことができます。

ローカルスコープとは「 { } で区切られた範囲で変数を宣言すると、その範囲だけ定義されて、外部では使用不可能にする」というプログラムの機能です。

具体例を書きます。以下のコードはエラーになります。

{
const test="test";
const test ="test2"; // 重複error!
}

しかし、以下のように書けばエラーになりません。変数はローカルスコープ内でしか生存しないからです。

{
const test="test";
}
{
const test="test2"; //上のtest変数とは異なる変数として扱われ、問題なく動作する
}

mainSceneとendSceneは独立しているため、例えば両方のシーンでconst label=new Label(“テスト”);と書いても、重複エラーが起きないようにすることができます。

基本的に各シーンで使用するオブジェクトは干渉しないはずなので、関数のローカルスコープでまとめた方が、「他シーンの変数名と重複しないように考慮する必要がない」点がメリットになります。

・・・ついてこれてますかね。

戦士の定義(124~135行目)

onenterframeで示した関数内は毎フレーム呼び出されます。

今回は、この中でtime変数を1ずつ増やし、そのtimeを角度として移動する、中心180px、振幅180pxのsin波で移動する自機を作りました。

sinとか三角波ってどう使えばよいかわからん!という方のために、以下の演習を用意しました。

ぜひ試してみてください。

演習! 以下のコードを書き換えて、どうなるか見てみよう!

(1)this.time/10⇒this.time/30に変更

(2)this.x = Math.sin(this.time / 10) * 180 + 180;

⇒this.x = Math.sin(this.time / 10) * 180 + 10;に変更

(3)this.x = Math.sin(this.time / 10) * 180 + 180;

⇒this.x = Math.sin(this.time / 10) * 10 + 180;に変更

 

演習を実際にやると、どのパラメータが動作に影響するかわかると思います。

sin波はゲーム制作において強力な武器になるので、ぜひ使いこなしてください。

ちなみに、今回time変数はheroのプロパティとして持たせているので、

heroonenterframe内で使用するときはthis.timeで取得します。

敵の出現(53~81行目)

敵の出現処理です。

mainScene.time変数を1ずつ増やして、一定以上だと敵モンスターを画面上に配置するようにしています。

if (this.y >= 500) と書かれている通り、もし画面下(this.y>500)に行ってしまったら、

setEndScene関数を呼び出して、リザルト画面を表示するようにしています。

this.frameは現在表示している画像です。実はimage5.pngは以下の3枚画像を合わせた画像(16px*48px)を(16px*16px)のスプライトに代入しています。

このとき、スライムの画像は16px*16pxが3つ分に分割され、this.frame=0の時、左を表示、this,frame=1の時真ん中を表示・・・

とthis.frameで表示画像を変更することができるのです。

今回はthis.frameを0~2で毎フレームごとに繰り返す処理を書いています。

シューティングなので、当たり判定の話をします。

今回、monsterを新たに作る度にmonstersという配列にmonsterオブジェクトを追加しています。

monstersという配列は「球と敵との接触処理」で使用するためのmonster管理配列です。

monster生成時、ユニークな文字列をmonster.idというプロパティに代入しています。これをもとに爆弾と衝突時に配列から衝突したモンスターだけ削除する処理を行います。

演習! 敵出現プログラムをいじろう!

(1)if (this.time >= 60 – Score)

⇒if (this.time >= 1 – Score) にしてみる。

(2) monster.x = Math.random() * 360;

⇒ monster.x = 100; にしてみる。

クリックで球出す処理、当たり判定(85~122行目)

ontouchendはクリック時に行われるイベントリスナーです。

bombオブジェクトを自機の座標に生成しています。

bombはonenterframeで毎フレームごとに上へ移動していきます。

当たり判定は「ある爆弾とmonsters配列に格納されている全てのmonsterを比較する」ことで実現します。

今回衝突判定は、円の衝突判定を使っています。コードを見るとわかりやすいかと思いますが、円の衝突判定はよく使う処理なので丸暗記してください。

衝突後は、monsterとbombは画面から消えます。(114,115行目)

また、monsters配列から、消えたmonsterを削除します。

今回は配列の機能である「filter()メソッド」を使用しました。この

この時、配列のfilter()メソッドを用いて「削除対象のidを持つmonsterだけ削除した配列を新たにmonsters配列とする」という処理を加えています。

filter()メソッドは条件を満たす要素だけの配列を生成します。

今回は衝突したid以外のmonsterのみの配列を生成するために使っています。非常に視認性高く実装ができる点がfilter()メソッドのメリットです。

 

また、敵に爆弾がぶつかると爆発エフェクトが発生します。

103行目のeffectオブジェクトが爆発エフェクトです。

effectが持つonenterframe内で、this.frameを0から毎フレームごとに増やしています。

そして、this.frame=5になったら画面から爆発エフェクトが消えるようになっています。

このコードも演出でよく使う処理です。

setEndScene関数

この部分は前回のプログラムを流用してまとめたものです。

ローカルスコープで囲っているため、外部の変数名と競合することがないようにしています。

プログラムをきれいに書くコツは「ローカルスコープを用いて、変数の適用範囲を小さくする」ことです。

200行上の変数名とバッティングしてしまったがために動作しなくなった!なんてことがあるとデバックにどうしても時間がかかってしまうからです。

さいごに (10分)

これですべての動作アルゴリズムの説明が終わりました。

このプログラムを自分の自由にいじってみて、ぜひ自分だけのシューティングプログラムを完成させてください。

まだわからないことが多すぎるよ!という人のために、以下演習問題を置いておくので、ぜひ解きながらプログラムを学んでいただけますと幸いです。

演習! 

(1)image3.pngを開いてください。実は自機もframeを変えると動くことがわかります。

131行目のonenterframe内にコードを追加して、自機の騎士をフレーム毎に動くようにプログラムを書き換えましょう。

(2)敵の移動を前に進むだけでなく、自機に向かって進むように書き換えてみましょう。

敵の座標と自分の座標がわかれば、移動方向は求められます。(ベクトルの概念を使ってみよう。)

(3)自機の球を3Wayに変更してみましょう。

球生成を3つにして、移動方向を変えてみると3Wayになります。

ブラウザゲーム制作の個人指導承ります

もしわからないことがありましたら、ZOOMにて個人レッスンを承ります。

有償にはなりますが、実際に指導する方が理解度も上がるかと思いますので、お気軽にご相談ください。

mail:tukuchauojisan@gmail.com