スポンサーリンク

【JavaScript】シューティングゲームの作り方④【初学者向け】

javascript
「javaScriptでシューティングゲームを作りたいけど、説明がどれも難しい…

シューティングゲーム、インベーダー・グラディウス・東方projectの弾幕STGなど様々な種類があり大変楽しいものです。

もちろんjavascriptで再現することができますが、難しい説明になりがちです。
「初学者向け」
×「できるだけ丁寧に解説」が目標の記事となります。

理解の助けになれば幸いでございますので、よろしくお願いいたします。

gddfgf筆者
・独学でゲームを作っている男
・javascriptが大好きな男

各回一覧

パート毎に分けておりますが、どの回も「完成品の確認⇒コードの解説」の流れで進みます。

各回の一覧はこちら⇩

・第1回(マウスで移動できる自機)
・第2回(キーを押したら弾を発射できる)
・第3回(敵機を画面に表示する)
・第4回(今回)
・第5回(弾が衝突したら自機/敵機を消す)

完成品の確認

この記事では、敵機が弾を発射するまでが目標です。

敵機から弾が発射されて、STGっぽくなってきましたね!
早速コードを見ていきましょう!

本編の前に…

さて、今回「ピンポイントで○○の部分に□□を追加する」部分があります。
例:「Pointクラスにlengthメソッドを追加」など

計8か所ですが…『いやどこに何を追加するんだよ!』

となってはストレスになってしまいます。
そこでこちらのページで全文コード + コメントで解説しています。

【JavaScript】シューティングゲームの作り方③⇒④の補足

お手数ですが、第3回⇒第4回の順でこられた方は、まずはこちらでコードの修正後に本記事を読んでいただければと思います。

見る⇒コピペする だけですので5分程度で終わるはずです。
よろしくお願いいたしますm(__)m

main.js

全文コードは↑のURLに載せております。
今回のポイントは以下5つ

・変数
・クラス:Point追加分
・クラス:enemy_shot
・敵機ショットの条件設定
・敵機ショットの初期化~描画

まずは、新しく増えた変数から見ていきましょう。

変数

今回追加された変数は1つです。

var Enemy_Shot_Count = 100

Enemy_Shot_Countは敵機ショットの上限、最大で100発まで出現するという意味
初期化、敵機ショットの描画の際に使用します。

クラス:Point追加分

続いて、Pointクラスに追加した3つのメソッドについてです。
説明の前にこちらをご覧ください。①は自機、②は敵機です。

座標はそれぞれ[chara.position]・[enemy[i].position]で取得できます。
今から作成するショットを飛ばす命令を日本語に直すと…

『自機はこの座標にいる。自分(敵機)・自機の角度を計算して発射!』

これを更に言い換えると⇩になります。

『自機は[chara.position]にいる。[chara.position]・[enemy[i].position]から角度を計算して発射!』

つまり、必要な情報は自機と敵機の座標から計算される距離と角度ということです。
⇩のような画像、学生時代に見てきたと思います。

これをイメージしながらメソッドの解説に進んでいきましょう!

distance メソッド

Point.prototype = {
//distance メソッド
distance:function(position){
var q = new Point();
q.x = position.x - this.x;
q.y = position.y - this.y;
return q;
}

distanceメソッドでは、2つの座標(敵機/自機)の引き算をしています。
引き算の結果と位置関係はこちら

自機は左上、敵機は右の真ん中くらいにいますね。

『あーなんか数学の時間でやった、座標の引き算かー』

くらいの理解で今は大丈夫です。

このx,y座標、returnで戻り値として後で使用します。

length メソッド

length:function(){
return Math.sqrt(this.x * this.x + this.y * this.y);
}

続いてlengthメソッドですが、見覚えのある方も多いのではないでしょうか。

『 [2乗⇒足し算⇒√に変換] でたしか2点間の距離が出るんだよな…』

こちらの画像、↑でも貼りましたが今一度見てみましょう。

Math.sqrt()とは()の中身をルートに変換してねという処理です。
折角なので、先ほどの数値(x = -211、y = -128)で計算してみましょう。

・211*211 + 128 * 128 = 44521 + 16384 = 60905

60905を√にすると約「246」となります。
これも戻り値として使われることになります。

normalize メソッド

normalize:function(){
 var i = this.length()
 if(i > 0){
 var j = 1 / i;
 this.x *= j;
 this.y *= j;
 }
}

normalize:正規化は一見何をしたいか分かりづらいものです。

『これで[敵機~自機]への距離と角度を計算できるの?』

結論だけ言うとそうなのですが、少し現状整理と説明が必要です。

現状整理

今必要な情報としては

■[敵機~自機]への距離と角度

…しかしイメージいただくと分かりますが、毎回角度と傾きを計算するのは大変です。
毎フレーム毎に距離は必ず変わりますし角度もずれるでしょう。

そこで本当に今必要な情報はこちらです

・x/yにどれくらい進めば[敵機~自機]にショットが届くか

画像にすると黄色の矢印になるでしょうか。

正規化はこの黄色の矢印(x,yにどれくらい進めばよいか)を計算してくれるのです。

POINTクラス 数値例

ここまでPointクラスに追加されたメソッドの説明でした。
二点間の距離やら正規化やら感覚掴むのが難しいかもしれませんので、console.log()の数値例を載せておきます。ご参考下さいm(__)m

←自機と敵機の位置関係 /// →自機と敵機の座標を引き算した結果

正規化した後のx,y座標
※正規化:x/yにどれくらい進めば[敵機~自機]にショットが届くか

クラス:Enemy_Shot

続いて、敵機データーのEnemyShotクラスを設定しています。
コンストラクタとメソッドに分けてみてみましょう。

Enemy_Shot コンストラクタ

function Enemy_Shot(){
this.position = new Point();
this.size = 0;
this.speed = 0;
this.alive = false;
this.vector = new Point();
}

自機・自機ショット・敵機のクラスとほとんど同じで追加は1つだけです。

vectorはベクトル
Pointの解説にでてきた正規化した座標を受けとる為のものです。

敵機ショット自身の座標とは別にx,yの数値を保持するということですね。

Enemy_Shot メソッド

2つのメソッド(set/move)を設定しています。
setメソッドについては、Chara_Shotとほぼ同じですがvectorが増えています。

setメソッド

Enemy_Shot.prototype = {
 //setメソッド//
  set:function(p, size, speed, vector){
// 座標
this.position.x = p.x;
this.position.y = p.y;
// サイズ
this.size = size;
// スピード
this.speed = speed;
// 生存フラグを立てる
this.alive = true;
    
 // ベクトル
this.vector.x = vector.x;
this.vector.y = vector.y;
}

this.vector.x/yではx/yにどれくらい進めば[敵機~自機]にショットが届くかを設定します。

このsetメソッドで設定⇒moveメソッドで座標に反映するという流れになります。

moveメソッド

//moveメソッド//
move:function(){
// 座標をベクトルに応じてspeed分だけ移動させる
this.position.x += this.vector.x * this.speed;
this.position.y += this.vector.y * this.speed;

// 上下左右どの方向かの画面端に来たらfalseにする
if(
this.position.x < -this.size ||
this.position.y < -this.size ||
this.position.x > this.size + Canvas.width ||
this.position.y > this.size + Canvas.height
) {
   this.alive = false;
  }
}

まず分かりやすいから⇩からいきましょう。

・if( this.position.x < -this.size ||~

これは、第2回(キーを押したら弾を発射できる)の応用です。

自機のショットの時は弾は画面上にしか飛びませんでしたが、敵機のショットはどの方向にも飛びます。そのため、『上下左右どの方向かの画面端でフラグをfalseにする』としています。

ベクトルを使って計算してみよう!

さて、今回で一番難しいであろうベクトルです。
ここは手を動かして実際の数値に触れてみましょう。材料はこちら

【仮の数値】
this.vector.x = -0.8
this.vector.x = -0.5
this.speed = 5

それぞれ計算してみましょう。

・this.vector.x * this.speed = -0.8 * 5 = 「-4」
・this.vector.y * this.speed = -0.5 * 5 = 「-2.5」

「-4」をthis.position.x/「-2.5」をthis.position.yにそれぞれ足し算して終了です。
これをゲーム画面と一緒にイメージすると…

■毎フレーム二点間の座標を調べる
⇒座標から距離を計算
⇒距離と座標が揃う
x/yにどれくらい進めば[敵機~自機]にショットが届くかを計算
⇒setメソッドで数値を設定
⇒moveメソッドで上記の計算をして、座標に反映させる

この流れで敵機のショットが動いているように見せることができます。

敵機ショットが画面に反映するまで

ここまで変数~クラス~メソッドの説明でした。
後は、敵機ショットを画面に反映させるだけです!流れとしては敵機を反映させた第3回と同じ流れとなります。

・初期化:敵機ショットを配列で作る
・作成 :どの方向にどれくらい進めばよいかsetする
・描画 :ctx.beginPath~ctx.fill()

となります。

コードの短い「敵機の初期化」からみていきましょう。

敵機ショットの初期化

第2.3回で解説した「Chara_Shot」「Enemy」と全く同じ考え方でOKです。

// 敵機ショット初期化
var enemyShot = new Array(Enemy_Shot_Count);
for(i = 0; i < Enemy_Shot_Count; i++){
enemyShot[i] = new EnemyShot();
}

ここまでこれた方なら、問題なく理解できると思いますので
恐縮ですがコードを貼るだけと致します。

敵機ショットの作成

続いて、敵機ショットの出現管理です。
あと少し頑張りましょう!

if(enemy[i].param % 30 === 0){
// エネミーショットを調査する
for(j = 0; j < 100; j++){
if(enemyShot[j].alive === false){
// エネミーショットを新規にセットする
 position = enemy[i].position.distance(chara.position);
 position.normalize();
  enemyShot[j].set(enemy[i].position, position, 5, 5);

 // 1個出現させたのでループを抜ける
 break;
 }
}

paramはフレーム毎に増えていくカウンターのようなものです。
これを使って発射タイミングを判定します。

一番難しいのはここでしょうか。

position = enemy[i].position.distance(chara.position)
position.normalize()

1行目は「distanceメソッド(2点間の座標の引き算をする)」をしています。
2行目は正規化の処理をしています。まとめて日本語にするとこうなります。

『positionに、enemy[i]の位置座標とcharaの位置座標を引き算した結果を入れてね。その後に正規化(normalize)すれば、ベクトルに入れるx,yがでるから』

これで計算したpositionをenemyShot[j]にセットします。

enemyShot[j].set(enemy[i].position, position, 5, 5)

「enemy[i].position」は敵機の座標、「position」は正規化したx/yにどれくらい進めば[敵機~自機]にショットが届くかです。

これで描画の準備が整いました。次がラストです!

敵機ショットの描画

あと一息頑張りましょう!

// パスの設定を開始
ctx.beginPath()

// すべてのエネミーショットを調査する
for(i = 0; i < Enemy_Shot_Count; i++){
// エネミーショットが既に発射されているかチェック
if(enemyShot[i].alive){
// エネミーショットを動かす
enemyShot[i].move()

// エネミーショットを描くパスを設定
ctx.arc(
enemyShot[i].position.x,
enemyShot[i].position.y,
enemyShot[i].size,
0, Math.PI * 2, false
)

// パスをいったん閉じる
ctx.closePath()
}
}

// エネミーショットの色を設定する
ctx.fillStyle = "red";

// エネミーショットを描く
ctx.fill();

…といっても特に説明することはありません。
第2、3回で説明したctx.beginPath~ctx.fill()の流れが分かっていれば問題なく理解できると思います

色はred(赤)にしていますが、お好みで変更してみてください。

    まとめ

    最後にポイントをおさらいしましょう。

    ①クラス:Pointにメソッドが追加
    ②敵機ショット:ベクトルの考え方の追加

    ①②両方に関わる「座標」「正規化」が最も難度の高いところかもしれません。
    画像もたくさん貼りましたが、分かりづらければ申し訳ございませんでしたm(__)m

    当記事がjavascriptでゲームを作ったりする時の理解の助けになれば幸いですので、お付き合いいただければと思います。

    また、初学者向けのゲームの作り方と一行づつのコード解説をしております。
    【JavaScript】 15パズルの作り方を初学者向けに1行づつ解説![前編]【ゲーム】

    ゲームを作るのはとても勉強になりますし、モチベーション維持にもつながります。
    興味があればご活用ください。