スポンサーリンク

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

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

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

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

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

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

各回一覧

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

各回の一覧はこちら⇩

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

完成品の確認

この記事では、敵機を画面に表示するまでが目標です。

自機とは別の敵機(緑色)が表示されてゲームっぽくなってきましたね!
早速コードを見ていきましょう!

main.js

// - 変数 - //
var Canvas;
var info;
var ctx;
var Run = true;
var mouse = new Point();
var fire = false;
var Chara_Shot_Count = 10;


var Enemy_Count = 10;
var counter = 0;
var position = new Point();
var i;
var j;


// - クラス - //
// Character
function Chara(){
    this.position = new Point();
    this.size = 0;
}


Chara.prototype = {
  init:function(size){
  //サイズ設定
    this.size = size;
 }
}


// Point
function Point(){
    this.x = 0;
    this.y = 0;
}


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


Chara_Shot.prototype = {
    set:function(position,size,speed){
    //座標
      this.position.x = position.x
      this.position.y = position.y
     //サイズ
      this.size = size;
      //スピード
      this.speed = speed;
          
    // 生存フラグを立てる
    this.alive = true;
    },


   move:function(){
    // 座標を真上にspeed分だけ移動
    this.position.y -= this.speed;


    // 一定以上の座標に到達⇒false
    if(this.position.y < -this.size){
        this.alive = false;
    }
   }
};


// Enemy
function Enemy(){
    this.position = new Point();
    this.size = 0;
    this.type = 0;
    this.alive = false;
}


Enemy.prototype = {
  //setメソッド
  set:function(postion,size,type){
    // 座標
    this.position.x = postion.x;
    this.position.y = postion.y;


    // サイズ
    this.size = size;


    // 生存フラグを立てる
    this.alive = true;


    // タイプ
  this.type = type;
},


  //moveメソッド
  move:function(){
    // タイプに応じて分岐
    switch(this.type){
        case 0:
            // X方向 に進む
            this.position.x += 2;


            // 画面右端~左端に到達⇒生存フラグを降ろす
            if(this.position.x > this.size + Canvas.width){
                this.alive = false;
            }
            break;
        case 1:
            // マイナスX方向 に進む
            this.position.x -= 2;


            // 画面左端~右端に到達⇒生存フラグを降ろす
            if(this.position.x < -this.size){
                this.alive = false;
            }
            break;
    }
 }
};


// - メイン - //
window.onload = function(){


    // スクリーンの初期化
    Canvas = document.getElementById('screen');
    Canvas.width = 256;
    Canvas.height = 256;


    // 2dコンテキスト
    ctx = Canvas.getContext('2d');
    
    // イベントの登録
  Canvas.addEventListener('mousemove', mouseMove);
  window.addEventListener('keydown', keyDown);
  Canvas.addEventListener('mousedown', mouseDown);


    // その他のエレメント関連
    info = document.getElementById('info');


    // 自機 初期化
    var chara = new Chara();
    chara.init(10);


    //自機ショット 初期化
  var charaShot = new Array(Chara_Shot);
 for(i = 0; i < Chara_Shot_Count; i++){
    charaShot[i] = new Chara_Shot();
}
  
  // 敵機 初期化
  var enemy = new Array(Enemy_Count);
for(i = 0; i < Enemy_Count; i++){
    enemy[i] = new Enemy();
}
  
 // 画面を繰り返し呼び出す
    (function(){


      // カウンタをインクリメント
counter++;
      
        // HTMLを更新
        info.innerHTML = mouse.x + ' : ' + mouse.y;


        // screenクリア
        ctx.clearRect(0, 0, Canvas.width, Canvas.height);


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


        // 自機の位置を設定
        chara.position.x = mouse.x;
        chara.position.y = mouse.y;


        // 自機を描くパスを設定
        ctx.rect(chara.position.x, chara.position.y,30,30);


        // 自機の色を設定する
        ctx.fillStyle = "black";


        // 自機を描く
        ctx.fill();      
          
// fireフラグ trueなら処理が進む
if(fire===true){
    // すべての自機ショットを調査する
    for(i = 0; i < Chara_Shot_Count; i++){
        // 自機ショットが既に発射されているかチェック
        if(charaShot[i].alive === false){
            // 自機ショットを新しくセット
            charaShot[i].set(chara.position, 5, 10);


            // ループ抜ける
            break;
        }
    }
    // フラグをfalseにする
    fire = false;
}
         
// パスの設定を開始
ctx.beginPath();


// すべての自機ショット(=10)を調べる
for(i = 0; i < Chara_Shot_Count; i++){
    // 自機ショットが既に発射されているかチェック
    if(charaShot[i].alive===true){
        // 自機ショットを動かす
        charaShot[i].move();


        // 自機ショットを描くパスを設定
        ctx.arc(
            charaShot[i].position.x,
            charaShot[i].position.y,
            charaShot[i].size,
            0, Math.PI * 2, false
        );


        // パスをセーブ
        ctx.closePath();
    }
}


// 自機ショットの色を設定する
ctx.fillStyle = "red";


// 自機ショットを描く
ctx.fill();
      
// 100フレームに1度敵機を出現
if(counter % 100 === 0){
 // すべてのエネミー(=10)を調べる
    for(i = 0; i < Enemy_Count; i++){
     // エネミーが既に10体出現したかチェック
        if(!enemy[i].alive){
      // タイプを決定する
            j = (counter % 200) / 100;


           // タイプから初期位置を決める
            var enemySize = 15;
            position.x = -enemySize + (Canvas.width + enemySize * 2) * j;
            position.y = Canvas.height / 2;


            // エネミーを新規作成
            enemy[i].set(position, enemySize, j);


            // 1体出現したのでループ抜ける
            break;
        }
    }
}
// パスの設定を開始
ctx.beginPath();


 // すべての敵機(=10)を調べる
for(i = 0; i < Enemy_Count; i++){
// 敵機が既に10機出現したかチェック
if(enemy[i].alive){
// 敵機を移動
enemy[i].move();


// 敵機(=○)を描くパスを設定
ctx.arc(
enemy[i].position.x,
enemy[i].position.y,
enemy[i].size,
0, Math.PI * 2, false
);


// パスを閉じる
ctx.closePath();
}
}


// 敵機の色を設定する
ctx.fillStyle = "green";


// 敵機を描く
ctx.fill();
      
        // フラグにより再帰呼び出し
        if(Run){setTimeout(arguments.callee, 1000/30);}
    })();
};


// 関数
function mouseMove(event){
    // マウスカーソル座標の更新
    mouse.x = event.clientX;
    mouse.y = event.clientY;
}


function keyDown(event){
    // キーコード取得
    var ck = event.key;


    // spaceKeyで画面が止まる
    if(ck === " "){Run = false;}
}


function mouseDown(event){
    // フラグを立てる
    fire = true;
}

今回も引き続き、javascriptのコードについて解説していきます。
ポイントは以下5つ

・変数
・クラス
・敵機の初期化
・敵機の出現管理
・敵機の描画

まずは、改めに増えた変数から見ていきましょう。

変数

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

var Enemy_Count = 10;
var counter = 0;
var position = new Point();
var i;
var j;

Enemy_Countは敵機の上限、最大で10機出現するという意味です。

counterは画面管理に使います。
1づつ増やして「10まで増えたら敵機をださないようにする」と条件分岐する為のもの

positionは何度もでてきたPointインスタンス
敵機の座標の管理に使います。

i,jは中身に何もいれずにグローバル関数として宣言だけしておきます。

クラス

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

Enemy コンストラクタ

function Enemy(){
this.position = new Point();
this.size = 0;
this.alive = false;
this.type = 0;
}

position・size・alive については第2回で解説しておりますので、省略いたします。

今回、初登場はtype
敵キャラには種類、個性を持たせたいものですよね。moveメソッドの欄で詳しく説明いたします。⇩

Enemy メソッド

Enemy.prototype = {
//setメソッド
set:function(postion,size,type){
// 座標
this.position.x = postion.x;
this.position.y = postion.y;

// サイズ
this.size = size;

// 生存フラグを立てる
this.alive = true;

// タイプ
  this.type = type;
},

move:function(){
// タイプに応じて分岐
switch(this.type){
case 0:
// X方向 に進む
this.position.x += 2;

// 画面右端~左端に到達⇒生存フラグを降ろす
if(this.position.x > this.size + Canvas.width){
this.alive = false;
}
break;
case 1:
// マイナスX方向 に進む
this.position.x -= 2;

// 画面左端~右端に到達⇒生存フラグを降ろす
if(this.position.x < -this.size){
this.alive = false;
}
break;
}
 }
};

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

typeが増えているだけですので、ここはtypeによって敵機の種類が変化するmoveメソッドを解説します。

enemy moveメソッド

this.positionを使って、「画面の端に到着したら消える(=aliveをfalseにする)」ように設定しています。

if(this.position.x < -this.size)
 {this.alive = false
}

また、if~switchで2パターンの敵機を設定しています。

type:0 ⇒画面右端~左端に移動
type:1 ⇒画面左端~右端に移動

このようにswitch×変数(今回はtype)  の組み合わせで敵機にバリエーションを持たせることができます。

右からでる、左からでる、斜めに動く、他キャラより早い…などを設定する為にこの変数を設定しているわけですね。

敵機が画面に反映するまで

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

・初期化 :敵機を配列で作る
・カウント:counter をインクリメント
・出現管理:どのタイプの敵を出現させるかチェック
・描画  :ctx.beginPath~ctx.fill()

となります。

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

敵機の初期化~カウント

初期化とは「Enemy_Countの数だけ初期化して配列を作ります」という意味
第2回で解説した「Chara_Shot_Count」と全く同じ考え方でOKです。

// 敵機初期化
var enemy = new Array(Enemy_Count);
for(i = 0; i < Enemy_Count; i++){
enemy[i] = new Enemy();
}

//
counter++;

countは、シーンを管理するためのものです。
countが増えない場合「今敵機が○○体でてきたよ」というシーンが保持されず、⇩のように敵機がくっついて一度にまとめて出現してしまいます。

地味ですが、とても大事な考え方であります。

敵機の出現管理

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

// エネミーの出現管理
// 100 フレームに一度出現させる
if(counter % 100 === 0){
// すべてのエネミー(=10)を調べる
for(i = 0; i < ENEMY_MAX_COUNT; i++){
// エネミーが既に10体出現したかチェック
if(!enemy[i].alive){
// タイプを決定する
j = (counter % 200) / 100;

// タイプから初期位置を決める
var enemySize = 15;
position.x = -enemySize + (Canvas.width + enemySize * 2) * j;
position.y = Canvas.height / 2;

// エネミーを新規作成
enemy[i].set(position, enemySize, j);

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

具体的なコードの説明の前に「100フレームに1度って何?」を先に説明しておきます。

フレームって何?

まずはこちらをご覧ください。

// フラグにより再帰呼び出し
if(Run){setTimeout(arguments.callee, 1000/30)

これは第1回の「function(){}で画面を繰り返し呼び出す」で解説したところ
画面を何度も繰り返して表示させる為の処理、再帰的に呼び出して~というやつです。

「1000/30」これがどれくらいの頻度で関数を呼び出すかを指します。
いわゆるフレームレート。割り算すれば理解が簡単です。

「1000/30」=「100/3」

100=1秒ですので『1秒に3回、関数を呼び出す』と理解ができます。

// countが100で割って余りがない 時だけ~
if(counter % 100 === 0){

// タイプを決定する 
j = (counter % 200) / 100;

つまり、↑コードのこの部分は…それぞれ

・100で割って余りがでない=countが100の倍数になる⇒処理が進む
・200の倍数 ○ ⇒j=1
・200の倍数 × ⇒j=0

となります。
ここまでが理解できたなら、次の解説はとても簡単に感じるかと思います。

タイプを決めよう!

変数jには「0」か「1」が入ることでタイプが決まります。

// タイプを決定する
j = (counter % 200) / 100;

// タイプから初期位置を決める
var enemySize = 15;
position.x = -enemySize + (Canvas.width + enemySize * 2) * j;
position.y = Canvas.height / 2;

// エネミーを新規作成
enemy[i].set(position, enemySize, j);

// 1体出現したのでループ抜ける
break;
}

ここは実際に「0」「1」を代入して計算してみましょう。
jが0なら左から、1なら右が初期座標になります。

canvasは画面の左上が(0,0)であることを思い出しながら、positionx,yがどこにあたるかイメージしていきましょう。

最後に(position, enemySize, j)に数値が入りenemyを生成できます。
一例ですが、jが0なら(左の画面から,敵機の大きさ,0)となります。

敵機の描画

ついに最後の敵機の描画!もうちょっとだけ頑張りましょう!

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

 // すべての敵機(=10)を調べる
for(i = 0; i < Enemy_Count; i++){
// 敵機が既に10機出現したかチェック
if(enemy[i].alive){
// 敵機を移動
enemy[i].move();

// 敵機(=○)を描くパスを設定
ctx.arc(
enemy[i].position.x,
enemy[i].position.y,
enemy[i].size,
0, Math.PI * 2, false
);

// パスを閉じる
ctx.closePath();
}
}

// 敵機の色を設定する
ctx.fillStyle = "green";

// 敵機を描く
ctx.fill();

…と言ってもここまでこれた方なら自機・ショットの描画と同じだと分かると思います。

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

    まとめ

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

    ①変数:countが追加
    ②クラス:敵機の座標・大きさ・速さ + typeを設定
    ③敵機の出現管理:countとフレームレートからtype(0,1)を設定

    特に②③は「switchを使う」「フレームレートの数値を割算して条件分岐する」ので少し難しかったかもしれません。しかし、この流れを大体理解できたかと思います。

    ■クラスで設定
    ⇒座標、大きさ、スピード、タイプを設定
    ⇒初期化して配列を作成
    ⇒条件分岐で生成
    ⇒描画

    第4回以降は全てこの応用です。
    当記事がjavascriptでゲームを作ったりする時の理解の助けになれば幸いですので、お付き合いいただければと思います。

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

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