ブロックくずしを作る7「ブロックを配置する」

前回、ブロックの配置仕様や座標計算などを行ったので、それに基づいたコーディングをしていきましょう。

ブロック座標へのアクセス機能を追加する

ブロックくずしをつくる5の最後に、「ブロックの座標をbrick.Rect.Xとbrick.Rect.Yで指定していますが、これはあまり適切なコーディングではありません。」としていたので、まずここを変更します。

export class Brick implements IGameCanvasPart {
    /**
     * ブロック本体の四角
     */
    private _body = new Rectangle(0, 0,
        GameParameter.Brick.Width, GameParameter.Brick.Height);

    /**
     * ブロックの中心のX座標
     */
    public get X() {
        return this._body.X;
    }
    public set X(value) {
        this._body.X = value;
    }

    /**
     * ブロックの中心のYY座標
     */
    public get Y() {
        return this._body.Y;
    }
    public set Y(value) {
        this._body.Y = value;
    }
}

8-26行目のように、XとYへのgetter/setterを追加し、_bodyのX,Yにget/setできるようにしただけです。

ブロックセットを追加する

前回、座標計算するために、「ブロックを並べて配置してできる全体の長方形」をBrickSetとしました。せっかくなのでこれをそのままクラスとします。

BrickSetはBrick全体を管理するクラスであり、Brickを作ったり、配置(座標をセット)したりします。

import { IGameCanvasPart } from './game/GameCanvasPart';
import { Brick } from './Brick';
import { GameMng } from './GameMng';
import { Rectangle } from './shape/Rectangle';
import { GameParameter } from './GameParameter';
import { ISize } from './shape/ISize';
/**
 * ブロックセットクラス
 * 
 * 全てのBrickを管理するクラス
 */
export class BrickSet implements IGameCanvasPart {
    /**
     * ブロック
     */
    private _brickList: Brick[] = [];

    /**
     * コンストラクタ
     */
    constructor(private _game: GameMng) {
    }

    /**
     * ブロックの並びの列(横方向)の数
     */
    private _numberOfColumn = 0;

    /**
     * ブロックの並びの行(縦方向)の数
     */
    private _numberOfRow = 0;

    /**
     * 全ブロックを配置するエリアの長方形
     */
    private _areaToPlaceBricks = new Rectangle(0, 0, 0, 0);

    /**
     * ゲーム開始時の初期化処理
     */
    public Init() {
        let gameArea = this._game.Area;
        if (gameArea === undefined) {
            throw new Error("gameArea が未定義です。");
        }
        let bw = GameParameter.Brick.Width;
        let bh = GameParameter.Brick.Height;
        let bg = GameParameter.Brick.Gap;

        // _ブロックの並びの行列の数を計算する
        this._numberOfColumn = Math.floor((gameArea.Width + bg) / (bw + bg));
        this._numberOfRow = Math.floor(gameArea.Height / (bh + bg) / 3);

        // 全ブロックを並べて配置したときの全体的な四角形の横幅と高さを算出する
        let width = (bw + bg) * this._numberOfColumn - bg;
        let height = (bh + bg) * this._numberOfRow - bg;

        // 全ブロックを配置するエリアを設定する
        this._areaToPlaceBricks.Width = width;
        this._areaToPlaceBricks.Height = height;
        this._areaToPlaceBricks.X = gameArea.X;
        this._areaToPlaceBricks.Y = gameArea.Top + bg + height / 2;

        // _brickListを更新する。
        this._updateBrickList();
    }

    /**
     *  _brickListを更新する。
     * 
     * 一度空にクリアし、新たにブロックを作成して登録する。
     */
    private _updateBrickList() {
        let bw = GameParameter.Brick.Width;
        let bh = GameParameter.Brick.Height;
        let bg = GameParameter.Brick.Gap;

        // brick0(最も左上のブロック)の座標を算出する。
        let brick0 = {
            X: this._areaToPlaceBricks.Left + bw / 2,
            Y: this._areaToPlaceBricks.Top + bh / 2,
        };

        // ブロックを新たに作成する
        this._brickList = [];
        for (let row = 0; row < this._numberOfRow; row++) {
            for (let col = 0; col < this._numberOfColumn; col++) {
                let brick = new Brick(this._game);
                this._brickList.push(brick);
                brick.X = brick0.X + (bw + bg) * col;
                brick.Y = brick0.Y + (bh + bg) * row;
                // ブロックの位置に応じて、色を変える。
                // デバッグを兼ねたお試しコードなので、将来変更する可能性あり
                if (row < this._numberOfRow / 4) {
                    brick.Color = "red";
                }
                else if (row < this._numberOfRow / 2) {
                    brick.Color = "yellow";
                }
            }
        }
    }

    Update(): void {
    }

    DrawCanvas(context: CanvasRenderingContext2D): void {
        this._brickList.forEach(brick => brick.DrawCanvas(context));
    }

    /**
     * ブロックセットの観点から算出する、ゲームエリアの最小サイズ
     */
    public static get MinimumSizeOfGameArea(): ISize {
        let bw = GameParameter.Brick.Width;
        let bh = GameParameter.Brick.Height;
        let bg = GameParameter.Brick.Gap;
        let minColumn = GameParameter.Brick.MinColumn;
        let minRow = GameParameter.Brick.MinRow;
        return {
            Width: minColumn * (bw + bg) - bg,
            Height: 3 * minRow * (bh + bg),
        };
    }
}

BrickSetはIGameCanvasPartを継承していて、ゲームキャンバスの部品にしました。同時に、Brickはゲームキャンバスの直接の部品ではなく、BrickSetを仲介して部品となる形にします。

すべてのBrickをリストで保持するようにします(13-16行目)。そして、DrawCanvas()メソッドでリスト内のすべてのブロックのDrawCanvas()を実行します(108-110行目)。

Update()メソッドも同様にするべきですが、もともとのBrick.Update()が何もしていないので、今回は何もしていません(105-106行目)。

前回の説明でnやmとしていた値は、そのままでは名称として不適切なので、_numberOfColumn, _numberOfRowという名称に変えています(24-32行目)。BrickSetとしていた長方形も、_areaToPlaceBricksという名前にしています(34-37行目)。

これらはゲームエリアが確定していないと算出できないので、Init()メソッドの中で算出したりセットしたりしています(43-63行目)。

ブロックの作成のタイミング

ブロックの作成は_updateBrickList()メソッドの中で行っています。同時に座標を計算し、ブロックを配置する処理も行っています(69-103行目)。_updateBrickList()メソッドは呼び出し元をさかのぼると、GameMng.InitSituation()メソッド(ゲームの状態を初期化)からコールされることになります。

これは、ゲームエリアが決定するまで、ブロックの座標だけでなく、作成するブロックの数が確定できないためです。そのためブロックの作成と配置を同じタイミングで行うようにしています。

_updateBrickList()の処理はInit()の中にそのまま入れることができますが2つの理由からメソッドを分けました。1つ目の理由はコードが長くなったので、まとまった処理をサブメソッドにしてコードを読みやすくするためです。

2つめの理由は、ブロックの作成と配置はいろいろなアレンジをする可能性が高いからです。例えば今回の配置を「1面」とし、別の配置や異なる種類のブロックを配置した「2面」「3面」を作っていくこともあるかも知れません。そのためブロックの作成と配置を別にしておくと、変更しやすくなるでしょう。

MinimumSizeOfGameArea()

MinimumSizeOfGameArea()メソッド(112-125行目)は前回の「ゲームエリアの最小サイズ」で説明したサイズを計算して返します。この値はゲームエリアが設定される前に算出できますし、ゲームのパラメーターとしてあらかじめ計算して、GameParameterで定義することもできます。しかし、「ブロックの配置仕様」に関連して算出される値なのでBrickSetの中で計算することにしました。

GameParameterの追加と変更

ブロックの配置仕様に伴い、GameParameterも変更しました。

/**
 * ゲームパラメーター
 */
export const GameParameter = {
    /**
     * ゲームエリアに関連するパラメーター
     */
    GameArea: {
        /**
         * ゲームエリアの上下左右のmargin値
         */
        Margin:{
            Top: 20,
            Bottom: 40,
            Left: 20,
            Right: 20,
        } as const,
    } as const,

    /**
     * ブロックに関連するパラメーター
     */
    Brick: {
        /**
         * ブロックの横幅
         */
        Width: 28,
        /**
         * ブロックの縦幅
         */
        Height: 13,
        /**
         * ブロックの間隔
         */
        Gap: 2,
        /**
         * ブロックの並びの横の列の最低数
         */
        MinColumn: 5,
        /**
         * ブロックの並びの縦の行の最低数
         */
        MinRow: 3,
    } as const,
} as const;

ブロックに関連するパラメーター

ブロックを並べて配置する時のブロック間の距離(ギャップ)をBrick.Gapとして定義しました(33-35行目)。

また、ブロックを並べる最低の数を、Brick.MinColumn, Brick.MinRowとして定義しました(36-43行目)。

ゲームエリアに関連するパラメーター

新たにゲームエリアに関連するパラメーターを追加しました(5-18行目)。

ブロックくずしを作る3では、ゲームエリアをセットするときに、下のようなコードになっていました。

        let width = this.Canvas.Width;
        let height = this.Canvas.Height;
        this.Area = new Rectangle(20, 20, width - 20, height - 40);

ここで20や40と言った数値は、キャンバスの各辺とゲームエリアの各辺の間隔を表す数値です。こういう値はC言語などでは意味の分かりやすいdefineマクロ変数などになっているはずです。これを、キャンバスに対する「ゲームエリアのマージン」と考えて、GameArea.Marginを追加しました。

ブロックセットをGameMngに追加する

ブロックのクラスができましたので、GameMngのプロパティに追加し、ゲームエリアに表示してみましょう。

export class GameMng extends GameLoop {
    /**
     * ブロック
     */
    public BrickSet = new BrickSet(this);

    /**
     * コンストラクタ
     */
    constructor() {
        super();
        // 部品を登録
        this.AddPart(this.Canvas);

        // CANVAS部品を登録
        // ゲームエリア(を示す枠)
        let tmp = new GameCanvasPart();
        tmp.onDrawCanvas = context => {
            if (this.Area) {
                context.strokeStyle = "white";
                context.strokeRect(this.Area.Left, this.Area.Top, this.Area.Width, this.Area.Height);
            }
            else {
                let text = `ゲームエリアを設定できません。`;
                context.strokeStyle = "red";
                context.strokeText(text, 0, 12);
            }
        }
        this.Canvas.AddPart(tmp);
        // 球
        this.Canvas.AddPart(this.Ball);
        // ラケット
        this.Canvas.AddPart(this.Racket);
        // ブロック
        this.Canvas.AddPart(this.BrickSet);

        this._AfterReadyCanvas();
    }

    /**
     * ゲームの状態を初期化する
     */
    public InitSituation() {
        // 球を初期化する
        this.Ball.Init();

        // ラケットを初期化する。
        this.Racket.Init();

        // ブロックを初期化する。
        this.BrickSet.Init();
    }

    /**
     * ゲームキャンバスの準備ができてから行なう処理
     */
    private async _AfterReadyCanvas() {
        await this.Canvas.WaitUntil_GetHtmlElement();
        let width = this.Canvas.Width;
        let height = this.Canvas.Height;

        // キャンバスの最小サイズを計算する
        let minSize = this._GetMinimumSizeOfCanvas();

        // キャンバスサイズが最小サイズ以上なら、ゲームエリアを作成する
        if (width >= minSize.Width && height >= minSize.Height) {
            let margin = GameParameter.GameArea.Margin;
            this.Area = new Rectangle(
                margin.Left, margin.Top,
                width - margin.Right, height - margin.Bottom);

            // ゲーム状態の初期化
            this.InitSituation();
        }
    }

    /**
     * キャンバスの最小サイズを計算する。
     * @returns キャンパスの最小サイズ
     */
    private _GetMinimumSizeOfCanvas(): ISize {
        let margin = GameParameter.GameArea.Margin;
        let minGameArea = BrickSet.MinimumSizeOfGameArea;
        return {
            Width: margin.Left + margin.Right + minGameArea.Width,
            Height: margin.Top + margin.Bottom + minGameArea.Width,
        };
    }
}

ブロックリストからブロックセットに変更する

ブロックくずしをつくる5ではブロック保持するリストを追加しましたが、これをブロックセットに変更しました(2-5行目)。ブロックセット自体はキャンバスの情報が無くても構築できるので、プロパティの宣言と同時にnewしています。

そして、ブロックをnewしてブロックリストに追加したり、ゲームキャンバスに登録したりする処理を削除し、ブロックセットをゲームキャンバスに登録するようにしました(34-35行目)。

InitSituation()では、ブロックの配置を行っていましたが、それ等の処理はBrickSet.Init()で行うので、それをコールするように変更しました(50-51行目)。

キャンバスのサイズチェック処理を変更する

キャンバスの最小サイズに関する仕様も決まったので、サイズチェック部分を更新しました(62-70行目と77-88行目)。

また、Areaを作成するときに、以前は20,40などの数値を直接書いていましたが、GameParameter.GameAreaを参照するように変更しました。

_AfterReadyCanvas()のコールタイミングの見直し

ブロック配置とは関係のない修正になるのですが、_AfterReadyCanvas()のコールタイミングをconstructorの最後にしました。(以前はconsructorのsuper()のすぐ後にコールしていました。)

このメソッドはキャンバス情報にアクセスできるのを待ってから行う処理をまとめて管理するためのメソッドです。super()の直後だと、constructorの外の処理が先に行なわれるのか、_AfterReadyCanvas()の中身の処理が先に行なわるのか、あやしくなってしまうので、コールタイミングを変更しました。

ここまでを実行する

ここまでを実際に実行すると下のようになります。
キャンバスサイズで配置されるブロック数が変わるのを見るにはたくさんの例を見せればよいのですが、プログラムの性質上1ページには1つの例しか見せることができません。そこで今回はいつもより大きめの480×640のキャンバスにしてみました。普段のキャンバスサイズだとどうなるのかは、今後の実行例で示すことができるでしょう。

SCORE:10000
メッセージ

コメント