ブロックくずしを作る3「ゲームエリア」

ゲームエリア

キャンバス管理クラスができたので、まずはプレイヤーキャラであるラケットを追加…したいところなのですが、その前にラケットや球が移動可能な範囲(ゲームエリア)を設定しましょう。

TypeScriptでゲームプログラム」では、ゲームエリアはGameParameter.Areaで宣言され、点(10,10)と点(310,310)を対角線とする四角形にしていました。

今回のブロックくずしでは、このように固定された四角形ではなく、ゲームエリアをキャンバスサイズに応じて変化する四角形にしてみましょう。<canvas id="gameCanvas">で指定する高さと幅で変化させるのです。

ゲームエリアのプログラム仕様

それではキャンバスサイズとゲームエリアの関係は下図の通りです。

image/svg+xml a a a b ゲームエリア キャンバス

灰色の四角がキャンバス、赤い四角がゲームエリアです。

aの幅は20、bの幅は40とします。

bの幅をaより20も増やしているのは、操作性の問題に対処するためです。(前回のゲームをデバッグしていたとき、ラケットの下側がある程度あいていないと、マウスでラケットを操作することが難しかったのです。)

このようにエリアを定めることで、例えば、キャンバスサイズが400x300ならゲームエリアは(20,20)-(380,260)となります。

また、キャンバスのサイズが40x60より大きくなければゲームエリアが存在しなくなります。キャンバスサイズの最低値は、他の要因(例えば、ブロックの大きさ、最低でも横にいくつ並べるのか…など)によっても変わってくるので、もう少し後で決めることにしましょう。

ゲームエリアを設定する(失敗)

まずはゲームエリアとなる四角形を保持する変数が必要です。とりあえず、GameMngクラスのプロパティとします。

export class GameMng extends GameLoop {
    /**
     * ゲームエリア
     */
    public Area: Rectangle | undefined;
}

Rectangleは四角形を表すクラスです。クラス定義の詳細は以下を見てください。

GameMng.Areaはキャンバスのサイズで決定します。そこで、

export class GameMng extends GameLoop {
    /**
     * id="gameCanvas"のCANVAS
     */
    public Canvas = new GameCanvas("gameCanvas");

    /**
     * ゲームエリア
     */
    public Area: Rectangle | undefined;

    /**
     * コンストラクタ
     */
    constructor() {
        super();
        this.AddPart(this.Canvas);

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

…のようなコードにしたいのですが、これはうまく動作しません。キャンバスの情報は非同期に遅れてセットされるからです。上のコードだと、タイミング的にCanvasのWidthやHeightは0なので、Areaには意図しない値がセットされます。

キャンバス情報がセットされるまで待つ仕組みを作る

ElementConrolを更新して、キャンバス(というよりも、HTML要素)がセットされるまで待つ仕組みを作ります。

/**
 * \<div id="xxxx">や\<button id="yyyy">のような、
 * id付のHTML要素を管理するためのクラスのテンプレート
 */
export abstract class ElementControl<T extends HTMLElement> {
    /**
     * HTML要素のID名
     */
    protected _id;
    /**
     * _idに対するHTML要素
     */
    protected _element: T | undefined;

    /**
     * _elementを使った処理を必須のときに、
     * _elementがセットされるまで待つためのPromise。
     * 次のように使う
     * 
     * this._AsyncElement.then(element => { elementを使った処理})
     */
    protected _AsyncElement: Promise<T>;

    /**
     * コンストラクタ
     * 
     * 本クラスは継承して使うことを前提としているので、継承時の注意
     * 
     * 継承で、\<div id="SCORE">に対する管理クラスを作る時には、
     * そのコンストラクタ内で
     * 
     * super("SCORE",HTMLDivElement);
     * 
     * と記載すること。
     * @param id ID名
     * @param typecheck 型チェックするためにHTMLElementの派生クラスを指定する
     */
    constructor(id: string, typecheck: new (...args: any) => T) {
        this._id = id;
        this._AsyncElement = new Promise<T>((resolve, reject) => {
            document.addEventListener("DOMContentLoaded", () => {
                let element = document.getElementById(id);
                if (element instanceof typecheck) {
                    this._element = element;
                    resolve(element);
                }
                else {
                    let typeName = this._getTypeName(typecheck);
                    reject(new ReferenceError(`id="${id}"の${typeName}要素がありません。`));
                }
            });
        });
    }

    /**
     * typecheckに対応するクラス名が HTML*Element に一致するときに、*の部分の文字列を返す。
     * @param typecheck HTMLElementの派生クラスのコンストラクタ
     * @returns 文字列
     */
    private _getTypeName(typecheck: new (...args: any) => T) {
        let match = typecheck.toString().match(/HTML(.*)Element/);
        if (match) {
            return match[1];
        }
        else {
            return "";
        }
    }

    /**
     * コンストラクタで指定したid名に対応するHTML要素を取得するまで待つ。
     * @returns 取得したHTML要素のPromise
     */
    public async WaitUntil_GetHtmlElement(){
        return this._AsyncElement;
    }
}

70-76行目が追加したコードです。WaitUntil_GetHtmlElement()を使ってキャンバスが設定されるまで待つことができます。publicメソッドなので、ElementControlを継承したクラス(もちろんGameCanvas)もこれを継承しています。

ゲームエリアを設定する

GameMngのコードを下のようにします。

import { GameLoop } from './game/GameLoop';
import { GameCanvas } from './game/GameCanvas';
import { GameCanvasPart } from './game/GameCanvasPart';
import { Rectangle } from './shape/Rectangle';
import { GameParameter } from './GameParameter';

export class GameMng extends GameLoop {
    /**
     * id="gameCanvas"のCANVAS
     */
    public Canvas = new GameCanvas("gameCanvas");

    /**
     * ゲームエリア
     */
    public Area: Rectangle | undefined;

    /**
     * コンストラクタ
     */
    constructor() {
        super();
        this.AddPart(this.Canvas);
        this._AfterReadyCanvas();

        let tmp = new GameCanvasPart();
        tmp.onDrawCanvas = context => {
            if (this.Area) {
                let TL = `${this.Area.Left},${this.Area.Top}`;
                let BR = `${this.Area.Right},${this.Area.Bottom}`
                let text = `ゲームエリアは、(${TL})-(${BR})`;
                context.strokeStyle = "yellow";
                context.strokeText(text, 10, 10);
                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);
    }

    /**
     * ゲームキャンバスの準備ができてから行なう処理
     */
    private async _AfterReadyCanvas() {
        await this.Canvas.WaitUntil_GetHtmlElement();
        let width = this.Canvas.Width;
        let height = this.Canvas.Height;
        if (width > GameParameter.CanvasMinWidth && height > GameParameter.CanvasMinHeight) {
            this.Area = new Rectangle(20, 20, width - 20, height - 40);
        }
    }
}

26-43行目までは、ゲームエリアが見えるようにするための一時的なコードです。

49-55行目までが、ゲームエリアを設定する処理です。

await this.Canvas.WaitUntil_GetHtmlElement(); でキャンバス情報がセットされるまで待った後、キャンバスのWidthとHeightを使ってAreaをセットしています。しかし、WidthとHeightが最小値より大きくない場合はAreaはセットしません。

GameParameter.CanvasMinWidth と GameParameter.CanvasMinHeight は下のようになっています。

/**
 * ゲームパラメーター
 */
export const GameParameter = {
    /**
     * ゲームキャンバスの幅の最小値
     */
    CanvasMinWidth: 40,
    /**
     * ゲームキャンバスの高さの最小値
     */
    CanvasMinHeight: 60,
} as const;

「ゲームエリアのプログラム仕様」でも書きましたが、このままでは球やラケットブロックなどが全く配置できません。キャンバスエリアの最小値を決めた後、40と60は更新する予定です。

ここまでを実行する

ここまでを実際に実行すると下のようになります。

SCORE:10000
メッセージ

コメント