テトリスをJavascriptで作る 実装編(3)

実装編 その3です。
ステージやラインについて解説します。
その1
その2
ソース:tetris.js
ライブラリー:dq-rtg.js,dq-cancas.js(実際に動かすにはライブラリのコアコンポーネントとjQueryも必要です)。
できあがり

TETRIS.Stageクラス

ステージは22本(*1)のラインがあり、ラインは10個の粒(ブロック)で構成されています。
簡単に言えば縦22x横10個の粒で構成されていることになります。
*1 ゲームとして有効なのは20ラインですが、テトリミノが落下する位置として2ライン分追加しています。

ステージの役割は粒の管理で、ラインの削除判定や削除中のラインの状態を管理します。

初期化

ラインを作成します。

DQ.TETRIS.Stage = function (engine) {
    this._engine = engine;
    this.lines = [];
    for (var i = 0; i < 22; i++) {
        this.lines.push(new DQ.TETRIS.Line(i));
    }
}

ゲーム開始時にライン上のチップを削除することで初期化します。

DQ.TETRIS.Stage.prototype = {
    left: DQ.TETRIS.STAGE_LEFT,
    top: -14,
    lines: [],
    clear: function () {
        for (var ln = 0; ln < this.lines.length; ln++) {
            for (var i = 0; i < 10; i++) {
                if (this.lines[ln].chips[i]) {
                    this.lines[ln].chips[i].remove();
                    this.lines[ln].chips[i] = null;
                }
            }
        }
    }
}

当たり判定

Stageクラスが落下中のテトリミノがステージの壁やステージ上の粒にぶつかるかどうかの当たり判定を担当します。

    hitTest: function (mino, dx, dy) {
        ///<summary>
        ///指定のテトリミノの当たり判定をします。
        ///</summary>
        ///<param name="mino" type="Tetrimino">
        ///対象となるテトリミノを指定
        ///</param>
        ///<param name="dx", type="number">
        ///x方向の移動予定幅を指定
        ///</param>
        ///<param name="dy", type="number">
        ///y方向の移動予定幅を指定
        ///</param>

        for (var i = 0; i < 4; i++) {
            var chip = mino.chip(i);
            var cx = Math.floor((chip.x + dx - this.left) / DQ.TETRIS.CHIP_SIZE);
            var cy = Math.floor((chip.y + dy - this.top) / DQ.TETRIS.CHIP_SIZE);
            //wall
            if (cx < 0
                || 10 <= cx
                || 22 <= cy) {
                return true;
            }
            //chip
            if (this.lines[cy].chips[cx]) {
                return true;
            }
        }

        return false;
    }
}

落下中のテトリミノを構成する4つの粒に対して壁とステージ上の粒との当たり判定を実施します。
ステージ上の粒の有無は、対象となる座標に粒のインスタンスがあるかどうかで判定しています。
横移動の場合、hitTest()falseが返ると移動できません。
落下処理の場合は、「遊び」状態へ遷移します。「遊び」状態が放置されると落下中のテトリミノはただの粒へと変化します。

粒への移行

落下が完了したテトリミノをラインへ粒として登録します。

{
    transChip: function (mino) {
        ///<summary>
        ///落下中だったテトリミノをチップへ変換します。
        ///</summary>
        ///<param name="mino" type="Tetrimino">
        ///変換するテトリミノを指定
        ///</param>

        for (var i = 0; i < 4; i++) {
            var chip = mino.chip(i);
            var cx = Math.floor((chip.x - this.left) / DQ.TETRIS.CHIP_SIZE);
            var ln = Math.floor((chip.y - this.top) / DQ.TETRIS.CHIP_SIZE);
            this.lines[ln].chips[cx] = new DQ.Screen.Canvas.Sprite({
                image: chipImg.client[0],
                x: cx * DQ.TETRIS.CHIP_SIZE + this.left,
                y: ln * DQ.TETRIS.CHIP_SIZE + this.top,
                width: DQ.TETRIS.CHIP_SIZE,
                height: DQ.TETRIS.CHIP_SIZE,
                dir: mino.type,
                animation: false,
                numberOfPause: 0
            });
            this._engine._canvas.push(this.lines[ln].chips[cx]);
        }
    }
}

テトリミノを構成する4つの粒を該当する座標の粒として登録します。
粒のインスタンスは描画用のSpriteです。スプライトを作成する際には、座標系を粒から画面のpixel単位へ変換しています。
作成したSpriteは、キャンバスへ登録します(これによって、粒の描画はキャンバス(*2)が適宜実行してくれます)。
*2 念のためですが、このキャンバスは拙作のライブラリのキャンバスクラスの事です。

削除判定

ライン上に新しい粒が登録された後は、ラインの削除判定を実施します。

{
    eraseLine: function () {
        this._lineFlash = [];
        for (var i = 21; 2 <= i; i--) {
            if (this.lines[i].canErase()) {
                //削除可能なラインがあったのでフラッシュ状態へ移行
                this.mode = DQ.TETRIS.StageMode.Flash;
                this._elapse = 0;
                var line = new DQ.Screen.Canvas.Sprite({
                    y: i * DQ.TETRIS.CHIP_SIZE + this.top,
                    x: this.left,
                    width: DQ.TETRIS.CHIP_SIZE * 10,
                    height: DQ.TETRIS.CHIP_SIZE,
                    animation: false,
                    numberOfPause: 0,
                    image: flashImg.client[0]
                });
                this._lineFlash.push(line);
                engine._canvas.push(line);
            }
        }
        return this._lineFlash.length;
    },

全てのラインについて削除判定を行います。判定自体はラインクラスに任せていますが、ようはライン内の全ての領域に粒があれば削除可能です。
削除可能なら画面効果用のスプライトを登録しています。
この時点で、ゲーム全体の状態はゲーム中(Play)状態から削除中(Erase)状態へ遷移します。
また、画面効果として削除可能なラインがフラッシュして、しばらくしてからラインが消え、またしばらくしてから全体が落下するようになっています。
ちなみに、メソッドの名前が微妙だったりしますが、そこは目をつぶります。

状態管理

Stageクラスではラインが削除中の場合だけ状態を管理します。状態はUpdate()の呼び出しによる一定の時間経過後に遷移します。

{
    update: function () {
        if (this.mode == DQ.TETRIS.StageMode.None) {
            return;
        }
        this._elapse += 60 / TETRIS.fps;
        if (this.wait <= this._elapse) {
            //時間経過を超えた
            switch (this.mode) {
                case DQ.TETRIS.StageMode.Flash:
                    //フラッシュ中から削除中へ移行
                    this.mode = DQ.TETRIS.StageMode.Erase;
                    this._elapse = 0;
                    for (var i = 21; 2 <= i; i--) {
                        if (this.lines[i].canErase()) {
                            this.lines[i].clear();
                        }
                    }
                    for (var i = 0; i < this._lineFlash.length; i++) {
                        this._lineFlash[i].remove();
                    }
                    this._lineFlash = [];
                    break;
                case DQ.TETRIS.StageMode.Erase:
                    //削除中から通常へ移行
                    this.mode = DQ.TETRIS.StageMode.None;
                    this._elapse = 0;
                    var count = 0;
                    for (var i = 21; 2 <= i; i--) {
                        if (this.lines[i].canDown()) {
                            this.lines.splice(i, 1);
                            count++;
                        }
                    }
                    //削除した分だけラインを追加
                    for (i = 0; i < count; i++) {
                        this.lines.unshift(new DQ.TETRIS.Line(0));
                    }
                    //ライン番号を振り直す
                    for (i = 0; i < 22; i++) {
                        this.lines[i].line(i);
                    }
                    break;
            }
        }
    }
}

ステージの状態はNoneFlashEraseNoneへと遷移します。
状態遷移後に経過時間を示す_elapseがクリアされ、一定時間経過後にイベント処理が実施されます。
まず、Flash状態にある場合は削除可能なラインをクリアします。それからフラッシュ効果用のスプライトを削除(remove)します。
(スプライトをremove()するとキャンバスクラスからも削除され、描画の対象から外れます。)
次に削除状態にある場合は、ラインを落下させます。
落下処理については、削除可能なラインを配列から削除して、削除した分のラインを配列の先頭に追加することで落下したように見せています。
余談ですが、ぷよぷよなど粒自体が落ちていく場合にはこの手法は使えないですね。

全体の状態遷移

その2では、割愛していた全体の状態遷移の管理です。
updateメソッド中で、状態毎のチェックを実施しています。

    update: function () {
        var mode = DQ.TETRIS.Mode;
        switch (this.game) {
            case mode.Start:
            case mode.GameOver:
                return;
            case mode.Erase:
                //ラインの削除演出中
                this.stage.update();
                if (this.stage.mode == DQ.TETRIS.StageMode.None) {
                    //演出完了
                    this.game = DQ.TETRIS.Mode.Play;
                    //落下したラインに合わせてゴーストを移動
                    this.moveGhost();
                }
                break;
            case mode.Delay:
                //「遊び」中
                this._elapse += 60 / this.fps;
                if (this.wait <= this._elapse) {
                    this.game = DQ.TETRIS.Mode.Play;
                    this.stage.transChip(this.current);
                    this.current.remove();
                    this.shiftCurrent();
                    soundBox.play(1);

                    var n_line = this.stage.eraseLine();
                    if (n_line > 0) {
                        this.score.score += DQ.TETRIS.Game.ScoreTable[n_line];
                        this.score.line += n_line;
                        var pre = this.score.level;
                        this.score.level = Math.floor(this.score.line / 10) + 1;
                        if (pre != this.score.level) {
                            this.levelUp();
                        }
                        this.updateScore();

                        this.game = DQ.TETRIS.Mode.Erase;
                        return;
                    }
                }
                break;
            case mode.Play:
                //「落下中」
                if (this.isAccelerate()) {
                    if (this.current.y - this._startPos > DQ.TETRIS.CHIP_SIZE) {
                        this.score.score++;
                        this.updateScore();
                        this._startPos += DQ.TETRIS.CHIP_SIZE;
                    }
                }
                var dy = this.gdy;
                if (this.hitTest(0, dy + DQ.TETRIS.CHIP_SIZE)) {
                    //「遊び」に移行
                    this.game = DQ.TETRIS.Mode.Delay;
                    this._elapse = 0;
                    this.stopAccelerate();
                    this.current.move(0, dy);
                    this.current.fit();
                } else {
                    this.current.move(0, dy);

                }
                break;
        }

    }
}

ゲーム全体の状態はゲーム開始直後(StartGame)、ゲームオーバー(GameOver)、ゲーム中(Play)、遊び中(Delay)および削除中(Erase)です。
ゲーム開始直後およびゲームオーバー状態はキー操作があるまでやることも状態遷移もありません。
次に基本となるゲーム状態ですが、ここについては前回説明しているので割愛します。
遊び状態は一定時間が経過するとステージへ定着(transChip)します。それからラインの削除判定を実施し、削除可能なラインがなければゲーム中状態へ遷移し、削除可能なラインがあれば削除中へ遷移します。
また、削除可能なラインがあった場合はスコアーの計算やレベルアップの判定も実施していますが、説明は割愛します。
削除中状態ではステージに状態管理を委譲しています。これは、削除中の演出の管理をStageクラスへ局所化する意図があります。

次回は、ゴーストやハードドロップについてです。

M. K. の紹介

IT屋さんです。プログラミングが大好きで今はJavascriptがお気に入りです。
カテゴリー: JavaScript, ゲーム作成, プログラミング   タグ: ,   この投稿のパーマリンク