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

More than 1 year has 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_008.jpg

前回は衝突判定について解説しました。
敵キャラクターが自機のショットに触れると消滅するようになり、だいぶシューティングゲームのような風体になってきました。

今回は、自機キャラクターにも敵側のショットが当たるようにします。
また、これに伴って、シーンの管理やスコアについても考えます。

シーン管理

ゲームには、通常オープニング画面や、ステージの遷移、メニュー画面の表示など様々なシーンがあります。
本連載では非常に小規模なゲームなのでわざわざオープニング画面などを設けたりはしませんが、最低限、ゲームの開始と終了についてはプレイヤーに伝わるようにしておきます。

まず、ゲームの開始から少しの間は、いきなり敵が登場したりするのではなく[ READY ]などの文字をページ上に表示して、プレイヤーに心理的に準備する時間を設けます。

また、ゲームの開始と同様に、ゲームオーバーになってしまった場合には[ GAME OVER ]の文字を表示し、処理が止まるようにしてみます。

この仕組みを実現するために、敵キャラクターの処理を行う部分に次のような処理を追加します。
無名関数によるループ処理のなかですね。

main.jsのループ処理内に追加するコード
// カウンターの値によってシーン分岐
switch(true){
    // カウンターが70より小さい
    case counter < 70:
        message = 'READY...';
        break;

    // カウンターが100より小さい
    case counter < 100:
        message = 'GO!!';
        break;

    // カウンターが100以上
    default:
        message = '';

        // 以下処理が続く

抜粋しているので少々わかりにくいかもしれませんが、追加する場所は敵キャラクターの処理を行う場所の手前です。

やっていることは単純で、無名関数のなかで毎フレームごとにインクリメントしているカウンターの値を見ながら、処理を行うかどうかを判断しているだけですね。

switch 文の条件にtrueを指定することで、case 節に続く部分に柔軟に処理を書くことができるというのがポイントです。
ちなみにここで出てきたmessageという変数は、グローバル変数を宣言している部分にあらかじめ追加しておいたものを使っています。厳密には、スコープ的な意味ではグローバルである必要はないですが、わかりやすいかなと思ったのでグローバル変数として宣言しました。

また、今回のサンプルではスコアの概念も導入します。
そのために、グローバル変数にはスコア管理用にscoreという変数も併せて宣言してあります。

そのscoreは、自機ショットが敵キャラクターに衝突したときに加算するように処理を追加すればいいですね。

衝突判定の箇所にスコアの処理を追加
// 衝突判定 ---------------------------------------------------
// すべての自機ショットを調査する
for(i = 0; i < CHARA_SHOT_MAX_COUNT; i++){
    // 自機ショットの生存フラグをチェック
    if(charaShot[i].alive){
        // 自機ショットとエネミーとの衝突判定
        for(j = 0; j < ENEMY_MAX_COUNT; j++){
            // エネミーの生存フラグをチェック
            if(enemy[j].alive){
                // エネミーと自機ショットとの距離を計測
                p = enemy[j].position.distance(charaShot[i].position);
                if(p.length() < enemy[j].size){
                    // 衝突していたら生存フラグを降ろす
                    enemy[j].alive = false;
                    charaShot[i].alive = false;

                    // スコアを更新するためにインクリメント
                    score++;

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

前回記述した衝突判定の処理に、スコアを更新するための処理を追加した形です。

最終的にスコアを表示するときは、今回のサンプルでは以下のような感じにしました。

スコアの表示処理
// HTMLを更新
info.innerHTML = 'SCORE: ' + (score * 100) + ' ' + message;

表示する際にscoreの中身を百倍して表示しているだけですね。

シーン管理の補足

シーンをカウンターの値によって管理する今回の方法では、先ほども書いたように 敵キャラクター管理処理の手前 でシーンが分岐するようにしました。

ソースコードの全体の流れを見ればわかりますが、無名関数の中身で行われる処理の順番としては、敵キャラクターの処理の前に自機に関する処理がまず出てきます。なぜ、自機の処理が終わった後、シーン管理の分岐を設けるようにしたのか理由が想像できるでしょうか。

もう一度、先ほどのシーン管理の分岐処理のコードを見てみます。

main.jsのループ処理内に追加した部分
// カウンターの値によってシーン分岐
switch(true){
    // カウンターが70より小さい
    case counter < 70:
        message = 'READY...';
        break;

    // カウンターが100より小さい
    case counter < 100:
        message = 'GO!!';
        break;

    // カウンターが100以上
    default:
        message = '';

        // 以下処理が続く

これを見ると、カウンターの値が一定の値(100以上)になるまでは、メッセージ用の変数に文字列を代入しているだけでbreakしているので、実際にはメッセージだけ更新して他は何もしていません。

もし仮に、自機の処理をこの分岐のなかに含めている場合には、メッセージとして READY が表示されている間は自機の操作もできない状態になります。当然、ショットも打つことができません。

ゲームの実装次第で柔軟に判断して変更すればいいことですが、今回のゲームの場合には、プレイヤーが「あれ、もしかしてフリーズしてる?」とならないようにするため、ゲーム開始直後から自機だけは操作できるようにしておきたいと考えました。ですからあえて、自機キャラクターの処理が終わったその後、敵キャラクターの処理に移る前の段階でシーンを管理する処理を記述したというわけです。

敵ショットと自機との衝突判定

自機と敵のショットとの衝突判定は、前回解説したのと同じようにPointクラスのメソッドをうまく利用して距離を測り判定します。

自機と敵ショットとの衝突判定
// 自機とエネミーショットとの衝突判定
for(i = 0; i < ENEMY_SHOT_MAX_COUNT; i++){
    // エネミーショットの生存フラグをチェック
    if(enemyShot[i].alive){
        // 自機とエネミーショットとの距離を計測
        p = chara.position.distance(enemyShot[i].position);
        if(p.length() < chara.size){
            // 衝突していたら生存フラグを降ろす
            chara.alive = false;

            // 衝突があったのでパラメータを変更してループを抜ける
            run = false;
            message = 'GAME OVER !!';
            break;
        }
    }
}

自機がショットに当たってしまった場合には、無名関数を繰り返し呼び出すかどうかのフラグである変数runfalseを設定してしまいます。
こうすることで、次の無名関数の呼び出しがストップしますので、結果的にゲームの進行は止まります。
messageにはゲームオーバーになってしまった旨のメッセージを入れておきます。

今回は自機にはライフや耐久値といった概念がないので一発ショットが当たっただけでゲームオーバーとなります。

まとめ

カウンターによるシーン分岐、そして敵ショットと自機との衝突判定、さらにスコアという概念についても実装が完了しました。
これで、基本的なシューティングゲームとしての実装はほぼ出揃っている形になりました。

次回は、ボスキャラクターを追加し、ゲームクリアという概念を導入してみましょう。
いよいよ、本連載も大詰めに近づいてきましたね。

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

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.