JavaScript

[連載] javascriptで作るシューティングゲーム的な何か(9)

More than 3 years have passed since last update.

さあ、はじめよう

はじめに

本稿は掲題の通り javascript を用いて[ シューティングゲーム的な何か ]を作ろうという試みについて解説するテキストの第九回です。

想定する読者

  • 割と暇である
  • プログラミングに興味がある
  • ゲーム作りに興味がある
  • javascriptの基本をマスターしたけど特に作るものがない
  • javascriptを使った動きのある処理を実装してみたい
  • canvas でなんか作ってみたい

本連載の狙い

本連載はどちらかというと初心者向けです。
このページに検索からやってきた「ゲーム作りてえええええ」と日に三十回くらい叫んでいる小中学生諸君は、まずjavascriptの基本をお勉強してから本連載を読みましょう。
また、最終的に出来上がる[ シューティングゲーム的な何か ]は、そんなに大層なものではありません。シューティングゲームがどのような感じで作られていくのか、その過程を眺めていろいろ考えていただくキッカケを作ることが本連載の狙いです。

また、本連載では伝わりやすさ優先でテキストを書きます。たとえば javascript には厳密にはクラスはありませんが、連載内でクラスという言葉を使って解説します。このあたりは理解しやすさや、雰囲気を伝えることを優先して書きます。

オンラインサンプル

本連載は全10回を予定しています。
各回にはその時点までの[ シューティングゲーム的な何か ]のサンプルが付属します。
各テキストからリンクを張っておきますので、オンラインで実際に動作確認が行えます。
サンプルプログラム一式については著作権とかライセンスとかそういったものは一切ありません。

ちなみに、最終的に完成する[ シューティングゲーム的な何か ]はこんな感じです。
マウスによる移動、クリックによるショットが可能です。ESC キーでプログラムを停止します。

javascriptで作るシューティングゲーム的な何か(1)
javascriptで作るシューティングゲーム的な何か(2)
javascriptで作るシューティングゲーム的な何か(3)
javascriptで作るシューティングゲーム的な何か(4)
javascriptで作るシューティングゲーム的な何か(5)
javascriptで作るシューティングゲーム的な何か(6)
javascriptで作るシューティングゲーム的な何か(7)
javascriptで作るシューティングゲーム的な何か(8)
javascriptで作るシューティングゲーム的な何か(9)
javascriptで作るシューティングゲーム的な何か(最終回)

書いてる人

書いてる人はdoxasという人です。
こんな企画もやってますので、少しでも javascript でシューティングゲームを作成することに興味がわいたら、ぜひ参加してください。待ってますよ!!

さて、つくろう

サンプルの実行結果
sample_009.jpg

前回は衝突判定やゲーム内の進行(シーン遷移)を管理するための処理を記述しました。
また、併せてスコアという概念も取り入れたので、だいぶゲームらしくなってきたと思います。

今回は、シューティングゲームには大抵登場する、ボスという概念を実装してみましょう。
そして、今まで本連載の冒頭部分でリンクを張ってあった、完成する予定の[ シューティングゲーム的な何か ]というのは今回のサンプルの実装までを行ったものとまったく同じものです。

連載は全10回の予定でしたので、今回の修正によってほぼ当初の予定通りのシューティングゲームが完成することになります。

自機の初期位置

さてボスの実装に取り掛かる前に、まず子細な修正個所について触れておきます。

前回までのサンプルでは、自機キャラクターの初期位置の座標が(0, 0)でした。これにより、ゲーム開始の瞬間には自機キャラクターが必ず左上の隅っこにいるような感じで非常に不自然でした。

今回は初期位置をちゃんと与えてあげるように修正します。

自機キャラクターの初期位置指定
// 自機の初期位置を修正
mouse.x = screenCanvas.width / 2;
mouse.y = screenCanvas.height - 20;

無名関数によるループ処理が始まる前に、このような処理を一度挟んでおくようにすれば、canvas 内にマウスカーソルが入るまでのあいだは、自機キャラクターが canvas のちょうど中心より少し下のあたりに描かれるようになります。

上記のような処理を追加すると、X 座標はちょうどスクリーン(canvas)の半分の位置、つまり中心に来ます。Y 座標はスクリーンの下端から 20 ピクセルほど上にいったところになりますね。

ボスキャラクター用のクラスを用意する

さてボスキャラクターを管理する処理について見ていきます。

今回はボスキャラクターを管理するファイルを、敵キャラクターなどを管理するクラスを記述していた character.js とは別のファイルとして用意する形にしました。

boss.js という新しいファイルを用意して、こちらにボスキャラクター用の処理を書いていきます。
index.html から読み込むファイルが増えることになりますので、こちらは忘れずにあらかじめ追加しておきます。

index.html
<!DOCTYPE html>
<html>
    <head>
        <script src="common.js"></script>
        <script src="character.js"></script>
        <script src="boss.js"></script>
        <script src="main.js"></script>

        <style>canvas {border: 1px solid gray;}</style>
    </head>
    <body>
        <canvas id="screen"></canvas>
        <p id="info"></p>
    </body>
</html>

boss.js では、その名の通りボス専用にクラスを設けて処理を記述していきますが、ボス本体の周囲をぐるぐると周回するビットについても併せて記述します。ボスキャラクター本体は、耐久値という概念を持っていて、ショットが複数回ヒットしないと破壊できないようにしてみます。
ビットはそれ単体がひとつの敵キャラクターで、ボス同様に耐久値を持たせます。要は、ボスを援護するお邪魔キャラクターという位置づけです。

それでは、boss.js の中身を見てみます。

boss.js
// - boss ---------------------------------------------------------------------
function Boss(){
    this.position = new Point();
    this.size = 0;
    this.life = 0;
    this.param = 0;
    this.alive = false;
}

Boss.prototype.set = function(p, size, life){
    // 座標をセット
    this.position.x = p.x;
    this.position.y = p.y;

    // サイズ、耐久値をセット
    this.size = size;
    this.life = life;

    // パラメータをリセット
    this.param = 0;

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

Boss.prototype.move = function(){
    var i, j;
    // パラメータをインクリメント
    this.param++;

    // パラメータに応じて分岐
    switch(true){
        case this.param < 100:
            // 下方向へまっすぐ進む
            this.position.y += 1.5;
            break;
        default:
            // パラメータからラジアンを求める
            i = ((this.param - 100) % 360) * Math.PI / 180;

            // ラジアンから横移動量を算出
            j = screenCanvas.width / 2;
            this.position.x = j + Math.sin(i) * j;
            break;
    }
};


// - boss bit -----------------------------------------------------------------
function Bit(){
    this.position = new Point();
    this.parent = null;
    this.size = 0;
    this.life = 0;
    this.param = 0;
    this.alive = false;
}

Bit.prototype.set = function(parent, size, life, param){
    // 母体となるボスをセット
    this.parent = parent;

    // サイズ、耐久値をセット
    this.size = size;
    this.life = life;

    // パラメータに初期値をセット
    this.param = param;

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

Bit.prototype.move = function(){
    var i, x, y;

    // パラメータをインクリメント
    this.param++;

    // パラメータからラジアンを求める
    i = (this.param % 360) * Math.PI / 180;

    // ラジアンから横移動量を算出
    x = Math.cos(i) * (this.parent.size + this.size);
    y = Math.sin(i) * (this.parent.size + this.size);
    this.position.x = this.parent.position.x + x;
    this.position.y = this.parent.position.y + y;
};

ボス本体は先述の通りライフ(耐久値)という概念を持たせるため、setメソッドの引数として、どれくらいのライフを設定するのかを引数で受け取ります。lifeという名前のプロパティに、引数で指定された値を設定しておき、あとで衝突判定を行う段階になったら、自機のショットが衝突するたびにこのライフを減算していくようにします。

そしてボス本体の動きを管理するmoveメソッドでは、paramプロパティをインクリメントしつつ、その値によって動作を変更するような処理が書かれています。

ボス本体の動きを管理する
Boss.prototype.move = function(){
    var i, j;
    // パラメータをインクリメント
    this.param++;

    // パラメータに応じて分岐
    switch(true){
        case this.param < 100:
            // 下方向へまっすぐ進む
            this.position.y += 1.5;
            break;
        default:
            // パラメータからラジアンを求める
            i = ((this.param - 100) % 360) * Math.PI / 180;

            // ラジアンから横移動量を算出
            j = screenCanvas.width / 2;
            this.position.x = j + Math.sin(i) * j;
            break;
    }
};

ボス本体は、画面外に登場したその瞬間から、100フレームに到達するまでは下方向にまっすぐ進んできます。
そして、100フレーム目からはサイン(sin)の値を利用した動きに変化します。

サインを使ってなにかを計算している部分は、サイン波の要領でボスを左右にゆらゆらと往復するような動きで移動させます。サイン波については詳しく解説しませんが、規則正しく反復するような動きには、よくサインやコサインなどが使われます。

ビットの処理

ビットはボス本体の周囲をぐるぐると周ります。
このような動きを行うためには、ビット自身が、親となる ボス本体の位置 を常に把握できる状態が必要です。

ビットにはparentというプロパティを持たせています。
ビットのsetメソッドを見ると、引数でデータを受け取ってparentに代入しているのがわかりますね。

ビットのsetメソッド
Bit.prototype.set = function(parent, size, life, param){
    // 母体となるボスをセット
    this.parent = parent;

    // サイズ、耐久値をセット
    this.size = size;
    this.life = life;

    // パラメータに初期値をセット
    this.param = param;

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

setメソッドを利用する際には、第一引数にボスキャラクターのインスタンスを渡すようにするのですね。

ビットの動きを管理するmoveメソッドのなかでは、実際にこのparentプロパティで間接的にボス本体のデータを参照しながら処理が行われます。

ビットのmoveメソッド
Bit.prototype.move = function(){
    var i, x, y;

    // パラメータをインクリメント
    this.param++;

    // パラメータからラジアンを求める
    i = (this.param % 360) * Math.PI / 180;

    // ラジアンから横移動量を算出
    x = Math.cos(i) * (this.parent.size + this.size);
    y = Math.sin(i) * (this.parent.size + this.size);
    this.position.x = this.parent.position.x + x;
    this.position.y = this.parent.position.y + y;
};

親となるボス本体の位置、そしてボス本体の大きさ、これらを考慮してサインやコサインでビットの位置を調整します。
ものすごくざっくりとした説明になってしまいますが、何かしらのキャラクターを円形に配置する場合には上記のようにサインとコサインを用いるのが便利です。

どうしてこのような計算でボス本体の周囲を円形に移動するビットが実現できるのか、気になる方は自分で調べてみてください。そんなに難しくはありませんので、すぐに理解できるでしょう。

main.jsへボス関連の処理を追加

main.js では、今までもそうしてきたようにボスやビットを管理する為に、いくつかの定数用の変数を追加しておきます。
また、無名関数の呼び出しが行われる前までに、インスタンスを初期化する処理も行います。

main.js
var BOSS_COLOR = 'rgba(128, 128, 128, 0.75)';
var BOSS_BIT_COLOR = 'rgba(64, 64, 64, 0.75)';
var BOSS_BIT_COUNT = 5;

// 中略

// ボス初期化
var boss = new Boss();

// ボスのビットを初期化
var bit = new Array(BOSS_BIT_COUNT);
for(i = 0; i < BOSS_BIT_COUNT; i++){
    bit[i] = new Bit();
}

これらは簡単ですね。

続いて、敵キャラクターを出現させるための処理を記述していた部分を、今回は大幅に修正します。
というのも、前回まではザコ敵キャラクターが延々と出現してくるだけでしたが、今回はボスが登場するのに伴って、ザコキャラクターは一定のフレーム数分だけループしたら出現しなくなるようにしてやります。

該当する箇所だけを抜粋したコードを見てみます。

敵の出現管理箇所
// エネミーの出現管理 -------------------------------------------------
// 1000 フレーム目までは 100 フレームに一度出現させる
if(counter % 100 === 0 && counter < 1000){
    // すべてのエネミーを調査する
    for(i = 0; i < ENEMY_MAX_COUNT; i++){
        // エネミーの生存フラグをチェック
        if(!enemy[i].alive){
            // タイプを決定するパラメータを算出
            j = (counter % 200) / 100;

            // タイプに応じて初期位置を決める
            var enemySize = 15;
            p.x = -enemySize + (screenCanvas.width + enemySize * 2) * j
            p.y = screenCanvas.height / 2;

            // エネミーを新規にセット
            enemy[i].set(p, enemySize, j);

            // 1体出現させたのでループを抜ける
            break;
        }
    }
}else if(counter === 1000){
    // 1000 フレーム目にボスを出現させる
    p.x = screenCanvas.width / 2;
    p.y = -80;
    boss.set(p, 50, 30);

    // 同時にビットも出現させる
    for(i = 0; i < BOSS_BIT_COUNT; i++){
        j = 360 / BOSS_BIT_COUNT;
        bit[i].set(boss, 15, 5, i * j);
    }
}

ほとんど見たままの意味なので、それほど難しくはないと思います。
まず最初の条件分岐で 1000 フレームに達していない場合で、なおかつ 100 の倍数フレームだった場合、という具合に振り分けています。もし、こちらのブロックに処理が進んだ場合には、ザコキャラクターが出現することになります。

カウンタの値が 1000 ちょうどのときには、ボスを出現させる処理を行います。

今回のボスキャラクターは、本体のサイズが 50 でビットのサイズが 30 なので、画面外の -80 を縦方向の初期位置にしています。また、ビットも同時に出現するようにsetメソッドを利用しているのがわかると思います。

ボスとビットを描く

さて、ここからは抜粋するコードの量は多いですが、やっていることは前回までの応用みたいなものです。ボスと、ビット、両者を canvas 上に描く処理を追加します。

ボスとビットを描く
// ボス -------------------------------------------------------
// パスの設定を開始
ctx.beginPath();

// ボスの出現フラグをチェック
if(boss.alive){
    // ボスを動かす
    boss.move();

    // ボスを描くパスを設定
    ctx.arc(
        boss.position.x,
        boss.position.y,
        boss.size,
        0, Math.PI * 2, false
    );

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

// ボスの色を設定する
ctx.fillStyle = BOSS_COLOR;

// ボスを描く
ctx.fill();

// ビット -------------------------------------------
// パスの設定を開始
ctx.beginPath();

// すべてのビットを調査する
for(i = 0; i < BOSS_BIT_COUNT; i++){
    // ビットの出現フラグをチェック
    if(bit[i].alive){
        // ビットを動かす
        bit[i].move();

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

        // ショットを打つかどうかパラメータの値からチェック
        if(bit[i].param % 25 === 0){
            // エネミーショットを調査する
            for(j = 0; j < ENEMY_SHOT_MAX_COUNT; j++){
                if(!enemyShot[j].alive){
                    // エネミーショットを新規にセットする
                    p = bit[i].position.distance(chara.position);
                    p.normalize();
                    enemyShot[j].set(bit[i].position, p, 4, 1.5);

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

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

// ビットの色を設定する
ctx.fillStyle = BOSS_BIT_COLOR;

// ビットを描く
ctx.fill();

パスを設定して、色を設定して、そして描く。今までと同じです。

今回のゲームでは、ボス本体はショットを撃たないようになっていて、その代わりにビットのほうがショットを発射するように実装されているのが見て取れるでしょう。

そして、前回までは自機が撃ったショットとザコ敵キャラクターの衝突判定だけを行えば十分でしたが、今回からは当然ボスキャラクターやビットとの衝突判定も行う必要があります。

そこで以下のコードを追加して、衝突判定が正しく行われるようにしておきます。

衝突判定の追加
// 自機ショットとボスビットとの衝突判定
for(j = 0; j < BOSS_BIT_COUNT; j++){
    // ビットの生存フラグをチェック
    if(bit[j].alive){
        // ビットと自機ショットとの距離を計測
        p = bit[j].position.distance(charaShot[i].position);
        if(p.length() < bit[j].size){
            // 衝突していたら耐久値をデクリメントする
            bit[j].life--;

            // 自機ショットの生存フラグを降ろす
            charaShot[i].alive = false;

            // 耐久値がマイナスになったら生存フラグを降ろす
            if(bit[j].life < 0){
                bit[j].alive = false;
                score += 3;
            }

            // 衝突があったのでループを抜ける
            break;
        }
    }
}

// ボスの生存フラグをチェック
if(boss.alive){
    // 自機ショットとボスとの衝突判定
    p = boss.position.distance(charaShot[i].position);
    if(p.length() < boss.size){
        // 衝突していたら耐久値をデクリメントする
        boss.life--;

        // 自機ショットの生存フラグを降ろす
        charaShot[i].alive = false;

        // 耐久値がマイナスになったらクリア
        if(boss.life < 0){
            score += 10;
            run = false;
            message = 'CLEAR !!';
        }
    }
}

ビットと、ボス本体は、いずれもライフ(耐久値)という概念を持っていますので、衝突と判定した場合にはlifeプロパティを減算するようにします。

衝突した自機ショットは無条件で消滅しますが、ビットやボスに関してはライフの値に応じてその後の処理が変化するようになっていますね。ボス本体の場合は特に、ライフがなくなったらそこでゲームクリアとなりますので、無名関数を再帰的に呼び出す際に利用しているrunfalseを設定して、ゲームループが止まるようにしておきます。

まとめ

さて、いかがでしたか。
現在まで 9 回にわたって段階的にプログラムを拡張しながら解説してきましたが、当初予定していたシューティングゲームは今回のぶんまでで一応完成になります。

次回はいよいよ最終回。
シューティングゲームを作る上で参考になるような tips を仕上げとして解説する予定です。

オンラインサンプル.09では、今回のサンプルが実際に動作するところを確認できます。