【ぷよプロ】ぷよぷよプログラミングを始めよう(開発編④)- 無限ループを実装してゲームが進行できるようにしよう

みなさんこんにちは!

本記事は次の記事の続きです。

【ぷよプロ】ぷよぷよプログラミングを始めよう(開発編③)- initializeを実装して初期表示をできるようにしよう

本日は「ぷよプロ」シリーズの第6回、アプリ開発編④をやっていきましょう!

はじめに

前回はたしか、game.jsinitialize()を実装して初期表示するところまでやったんでしたっけ。

次は何をするんでしょうか??

ともとも
ともとも

そうだね。今回は、初期表示した後の処理を実装していこうと思うよ!

まず、game.jsinitialize()が実行されて初期化されると、次にloop()という処理が実行されるよ。だから今回はこのloop()を見ていこうと思う。

ちなみに今回のポイントになるのは、このloop()無限ループで実行されるというところだよ。

無限ループとはいったい何でしょうか??無限に処理を繰り返すということですか??

どうして、無限に処理を繰り返す必要があるんでしょう。。。?

ともとも
ともとも

おっ、無限ループについてはそのとおりだよ!(さすが!)

んー、無限ループを利用して画面を高速で描画しているんだけど、まず少し説明してからコードの解説に入ろうか

今回はgame.jsloop()について解説していきます。

ゲームにおける”フレーム数”とは

ゲームをしているとよくFPS(frames per second)という単位を耳にすることがあります。

これは、1秒あたりに描画されるフレーム数のことです。と言葉で書くとよくわからないと思うので、次の映像を見てください。

左から順番に15fps、30fps、60fps、120fpsの白い球がバウンドしている動画ですが、一番左の球(15fps)と一番右の球(120fps)をみてみると明らかに”動きのヌルヌル感”が段違いですよね?

そして、少し上の動画を深堀すると次の処理が実行されていることがわかると思います。

  1. 球がバウンドする際の位置情報を算出する(経過時間から算出)
  2. ①の処理を無限に繰り返す
  3. ①で算出した位置に球を画面に描画する

そして、③の描画タイミングが1秒間に何回実施されているのかに応じて、動画のようにカクカクするか、ヌルヌル動くかが変わってくるのです。

では、loop()処理を見ていきましょう。

loopの実行イメージ

まず、game.jsloop()のイメージを掴んでおきましょう。次の画像を見てみてください。

game.jsのloop()イメージ

色々書いてありますが、ざっくりいうとこの処理は「modeに応じて処理を実行して、次のmodeに高速で更新し続ける処理」です。

この「ぷよぷよ」は”mode”の管理が肝になってきます。上に書いてある通りですが、modeによって処理を実行しながらmodeを更新することでゲームが進行していきます。そしてこのmodeを無限ループで高速に更新していくわけです。

ステータスによる状態管理について

ここまで細かく状態を管理しているのはゲームアプリの特徴ですが、どのようなアプリを開発する場合でも状態管理は必須です。

このような状態を表す変数のことをステータスと呼びます。

例えば、コンビニで商品を購入する消費者の状態を考えると次のようなステータスを考えることができます。

・商品を探している

・商品を購入している

・商品を購入した

・コンビニから退店した

game.jsのloop

まずはgame.jsloop()に関連するコードを見ていきましょう。

game.js
function loop() {
    switch (mode) {
        case 'start':
            // 最初は、もしかしたら空中にあるかもしれないぷよを自由落下させるところからスタート
            mode = 'checkFall';
            break;
        case 'checkFall':
            // 落ちるかどうか判定する
            if (Stage.checkFall()) {
                mode = 'fall'
            } else {
                // 落ちないならば、ぷよを消せるかどうか判定する
                mode = 'checkErase';
            }
            break;
        case 'fall':
            if (!Stage.fall()) {
                // すべて落ちきったら、ぷよを消せるかどうか判定する
                mode = 'checkErase';
            }
            break;
        case 'checkErase':
            // 消せるかどうか判定する
            const eraseInfo = Stage.checkErase(frame);
            if (eraseInfo) {
                mode = 'erasing';
                combinationCount++;
                // 得点を計算する
                Score.calculateScore(combinationCount, eraseInfo.piece, eraseInfo.color);
                Stage.hideZenkeshi();
            } else {
                if (Stage.puyoCount === 0 && combinationCount > 0) {
                    // 全消しの処理をする
                    Stage.showZenkeshi();
                    Score.addScore(3600);
                }
                combinationCount = 0;
                // 消せなかったら、新しいぷよを登場させる
                mode = 'newPuyo'
            }
            break;
        case 'erasing':
            if (!Stage.erasing(frame)) {
                // 消し終わったら、再度落ちるかどうか判定する
                mode = 'checkFall';
            }
            break;
        case 'newPuyo':
            if (!Player.createNewPuyo()) {
                // 新しい操作用ぷよを作成出来なかったら、ゲームオーバー
                mode = 'gameOver';
            } else {
                // プレイヤーが操作可能
                mode = 'playing';
            }
            break;
        case 'playing':
            // プレイヤーが操作する
            const action = Player.playing(frame);
            mode = action; // 'playing' 'moving' 'rotating' 'fix' のどれかが帰ってくる
            break;
        case 'moving':
            if (!Player.moving(frame)) {
                // 移動が終わったので操作可能にする
                mode = 'playing';
            }
            break;
        case 'rotating':
            if (!Player.rotating(frame)) {
                // 回転が終わったので操作可能にする
                mode = 'playing';
            }
            break;
        case 'fix':
            // 現在の位置でぷよを固定する
            Player.fix();
            // 固定したら、まず自由落下を確認する
            mode = 'checkFall'
            break;
        case 'gameOver':
            // ばたんきゅーの準備をする
            PuyoImage.prepareBatankyu(frame);
            mode = 'batankyu';
            break;
        case 'batankyu':
            PuyoImage.batankyu(frame);
            Player.batankyu();
            break;
    }
    frame++;
    requestAnimationFrame(loop); // 1/60秒後にもう一度呼び出す
}

modeが’start’の処理

まず、思い出してほしいのがgame.jsinitialize()の次のコードです。

mode = 'start';

なので、最初に実施されるのは次のコードになります。

case 'start':
    // 最初は、もしかしたら空中にあるかもしれないぷよを自由落下させるところからスタート
    mode = 'checkFall';
    break;
~~~~~
frame++;
requestAnimationFrame(loop); // 1/60秒後にもう一度呼び出す

modeを’start’から’checkFall’に変更し、その後、frameをインクリメントした後、requestAnimationFrame(loop)を実行します。

requestAnimationFrameとは簡単に言うと「ブラウザで描画可能なタイミングで引数の処理を再実行する」ものです。loop処理の中で、loop処理を呼んでいるため、結果的にこれでブラウザで最速のタイミングで呼び出す無限ループになっているのです。

参考にrequestAnimationFrameを説明したサイトのリンクを載せておきます。

modeが’checkFall’の処理

modeが’checkFall’の時に呼び出されるコードは次のコードです。(以降、’start’と重複する無限ループの処理は記載を省略します)

case 'checkFall':
    // 落ちるかどうか判定する
    if (Stage.checkFall()) {
        mode = 'fall'
    } else {
        // 落ちないならば、ぷよを消せるかどうか判定する
        mode = 'checkErase';
    }
    break;

ここでは、「ぷよが落下できるか?」に応じてmodeを切り替えています。

そしてぷよの落下可否をStage.checkFall()でチェックしています。ここの処理は後で追いますが、一番最初は基本的に落下できるはずです。なので、modeは’fall’に切り替わります。

modeが’fall’の処理

modeが’fall’の時に呼び出されるコードは次のコードです。

case 'fall':
    if (!Stage.fall()) {
        // すべて落ちきったら、ぷよを消せるかどうか判定する
        mode = 'checkErase';
    }
    break;

ここでは、Stage.fall()という処理を実施し、その返り値がfalseの場合は、modeを’checkErase’にしています。

そして、コメントから読み取るにこのStage.fall()という処理は次を実施するものであることが想像できます。

  • 現在操作中のぷよを少し落下させる
  • ぷよがまだ落下できるならtrue、もう落下できないならfalseを返す

なので、落下が完了するまでこのStage.fall()を繰り返します。

落下が完了するとmodeが’checkErase’に切り替わります。

modeが’checkErase’の処理

modeが’checkErase’の時に呼び出されるコードは次のコードです。

case 'checkErase':
    // 消せるかどうか判定する
    const eraseInfo = Stage.checkErase(frame);
    if (eraseInfo) {
        mode = 'erasing';
        combinationCount++;
        // 得点を計算する
        Score.calculateScore(combinationCount, eraseInfo.piece, eraseInfo.color);
        Stage.hideZenkeshi();
    } else {
        if (Stage.puyoCount === 0 && combinationCount > 0) {
        // 全消しの処理をする
        Stage.showZenkeshi();
        Score.addScore(3600);
        }
        combinationCount = 0;
        // 消せなかったら、新しいぷよを登場させる
        mode = 'newPuyo'
    }
    break;

コメントから読み取るに、まずStage.checkErase(frame)でぷよが消せるかどうかを判断しています。おそらく消すことができるぷよがあると何か情報を返しeraseInfoに入れます。

そして、最初にぷよが落下したタイミングでは消すことのできるぷよがないため、eraseInfoは値が入っていないはずなので、else文の処理に流れます。

また、ステージ上にはぷよが2つあるはずなので、Stage.puyoCount > 0のはずです。そのため、最初に実行されるのはcombinationCount = 0mode = 'newPuyo'で、modeが’newPuyo’に切り替わります。

modeが’newPuyo’の処理

modeが’newPuyo’の時に呼び出されるコードは次のコードです。

case 'newPuyo':
    if (!Player.createNewPuyo()) {
        // 新しい操作用ぷよを作成出来なかったら、ゲームオーバー
        mode = 'gameOver';
    } else {
        // プレイヤーが操作可能
        mode = 'playing';
    }
    break;

ここでは、Player.createNewPuyo()でゲームが持続できるか判断しています。

はじめは持続できるはずなので、mode='playing'に切り替わるはずです。

modeが’playing’の処理

modeが’playing’の時に呼び出されるコードは次のコードです。

 case 'playing':
    // プレイヤーが操作する
    const action = Player.playing(frame);
    mode = action; // 'playing' 'moving' 'rotating' 'fix' のどれかが帰ってくる
    break;

コメントから読み取るに、プレイヤー操作により、actionに’playing’・’moving’・’rotating’・’fix’の4つのうちいずれかの値が返ってきます。

例えば、ここは’moving’が返ってきたとしましょう。

modeが’moving’の処理

modeが’moving’の時に呼び出されるコードは次のコードです。

case 'moving':
    if (!Player.moving(frame)) {
        // 移動が終わったので操作可能にする
        mode = 'playing';
    }
    break;

「ぷよ」を動かし、移動が完了するとmode = 'playing'に切り替えます。

そして、再度ユーザ操作を受け付けます。次は’playing’から’fix’が返ってきたとしましょう。

modeが’fix’の処理

modeが’fix’の時に呼び出されるコードは次のコードです。

case 'fix':
    // 現在の位置でぷよを固定する
    Player.fix();
    // 固定したら、まず自由落下を確認する
    mode = 'checkFall'
    break;

まずおそらくユーザが落下する操作を実行するとmodeが’fix’になると思われます。

そしてPlayer.fix()を実行し、modeを’checkFall’に切り替えます。

ここでは、’checkFall’でぷよがそろわない場合、これまでと同じ流れで処理が進み、ゲームが進行していくことがイメージつくと思います。

そして「ぷよ」が積み上がりゲームオーバーとなる瞬間を見ていきましょう。

この場合は、modeが’newPuyo’から’gameOver’に切り替えます。

modeが’gameOver’の処理

modeが’gameOver’の時に呼び出されるコードは次のコードです。

case 'gameOver':
    // ばたんきゅーの準備をする
    PuyoImage.prepareBatankyu(frame);
    mode = 'batankyu';
    break;

コメントから何を実施するかよくわかりませんが、mode = 'batankyu'に切り替わります。

modeが’batankyu’の処理

modeが’batankyu’の時に呼び出されるコードは次のコードです。

case 'batankyu':
    PuyoImage.batankyu(frame);
    Player.batankyu();
    break;

「ばたんきゅー」の処理を実施し、ゲームを終了していることを読み取ることができます。

さいごに

いかがでしたでしょうか?

とにかく、modeを高速でガシャガシャ切り替えてゲームを進行していることがわかると思います。

なので、あとは各modeで呼び出している処理を見ていけばこのゲームで必要な処理がすべてわかるようになるというわけです。今回は、いったんこのゲームの基盤となる処理を見ていきましたが、次回は(一応)最終回としてそうした処理を見ていきたいと思います!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です