TypeScriptでゲームプログラム11「キャンバス関連処理もクラス化する」

キャンバス管理クラスの追加

UpdateGameSItuation()とDrawGameCanvas()をGameLoop内で実行させるためにとりあえず無名クラスを使いましたが、ここを正式なクラス化していきましょう。

UpdateGameSItuation()では球とラケットの座標や速度を変更し、DrawGameCanvas()ではキャンバスに描画しています。すなわち、キャンバスに描画するオブジェクトの状態更新と表示をしていることになります。

そこで、MessageMngクラスで<div id="message">を管理したように、<canvas id="game">を管理するGameCanvasMngクラスを作るのが適切であるように思えます。

その前に共通点を整理する

その前に、MessagaeMng、Button1Mng、そしてこれから作るGameCanvasMngはすべてid付きHTML要素を管理しています。そしてMessagaeMngとButton1Mngを見比べると共通点があります。

その共通点を1つのElementMngクラスとし、MessagaeMngクラスはその継承クラスにしたい。

ElementMngクラスのコードを以下に示します。

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

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

    public abstract Update(): void;
    public abstract Show(): void;
}

共通点は、document.getElementById()で要素を取得し、それをthis._elementにセットする処理と、取得できなかったときのエラー処理です。

idは要素ごとに変わるので、ElementMngではconstructorで指定します。

また、_elementの型も変わるので、ジェネリックにしています。_elementは必ずHTMLElementの派生クラスになるので、T extends HTMLElementとしました。

_getTypeName()は、メソッドのコメントに書いてある通りの動作をしています。エラーメッセージにこだわりが無ければ、なくても良いでしょう。

abstract

ElementMngは必ず継承して使うべきでです。HTML要素ごとに更新処理Update()や表示処理Show()はまったく異なる動作をするはずなのだから…。

そのため、Update()とShow()もabstract指定し、継承クラスで実装することを強制しています。

typecheck

constructorの第2引数のtypecheckは、33行目のinstanceofのために必要な引き数です。

ジェネリックを使う時、以下のようなコードはエラーになります。

if (element instanceof T) {

これに対処するためにtypecheckを使います。

この辺りは私もよくわかっていない(説明できない)所で、"typescript generics instanceof"でネットを検索して見つかった回避策を使っています。次のページが参考になりました。

TypeScriptのジェネリクスでinstanceof Tしたい
How to use instanceof with generics in TypeScript

MessageMngとButton1Mngの修正

それぞれ、ElementMngの派生クラスとなるように修正します。

/**
 * メッセージボックス(\<div id="message">)を管理するクラス
 */
class MessageMng extends ElementMng<HTMLDivElement> {
    /**
     * コンストラクタ
     */
    constructor() {
        super("message", HTMLDivElement);
    }
    // その他は変更なし
}

/**
 * ボタン1(\<button id="button1">)を管理するクラス
 */
class Button1Mng extends ElementMng<HTMLButtonElement> {
    /**
     * コンストラクタ
     */
    constructor() {
        super("button1", HTMLButtonElement);
    }
    // その他は変更なし
}

キャンバス管理クラスの追加(本番)

同様に、ElementMngを継承したGameCanvasMngを作ります。コードを以下に示します。

/**
 * ゲームCANVAS(\<canvas id="game">)を管理するクラス
 */
class GameCanvasMng extends ElementMng<HTMLCanvasElement> {
    /**
     * コンストラクタ
     */
    constructor() {
        super("game", HTMLCanvasElement);
    }

    /**
     * CANVASの部品を更新する。
     */
    public Update() {
        Racket1.Update();
        Ball1.Update();
    }

    /**
     * CANVASの部品を描画する。
     */
    public Show() {
        if (this._element) {
            let width = this._element.width;
            let height = this._element.height;
            let context = this._element.getContext("2d");
            if (context && width > 0 && height > 0) {
                // 実際の描画処理
                // 背景を黒に
                context.fillStyle = "black";
                context.fillRect(0, 0, width, height);

                // Areaの範囲を白で描画
                context.strokeStyle = "white";
                context.strokeRect(Area.Left, Area.Top, Area.Width, Area.Height);

                // ラケットを描画
                Racket1.DrawCanvas(context);

                // 球を黄色で描画
                Ball1.DrawCanvas(context);
            }
        }
    }
}

let GameCanvas = new GameCanvasMng();

Update()の中身はUpdateGameSItuation()と完全に同じです。

Show()もDrawGameCanvas()とほぼ同じです。

そして前回無名クラスにしていたものをGameCanvasに変えて、GameLoopに登録します。

球やラケットを追い出す

上のコードを見ていると、Ball1やRacket1がUpdate()やShow()に含まれています。GameLoopクラスからMessageMngクラスを追い出したように、GameCanvasMngからBal1を追い出したくなることでしょう。

そこで、球やラケットを「CANVAS部品」と見なし、IGameCanvasPartを作ることにします。

/**
 * キャンバスに描画されるゲームの部品を表すインターフェイス
 */
 export interface IGameCanvasPart {
    /**
     * ゲームフレームが更新されたときに、部品の状態を更新するメソッド
     */
    Update(): void;

    /**
     * Updateの後で、部品をCANVASに描画するメソッド
     * @param context 描画するCANVASのCanvasRenderingContext2D
     */
    DrawCanvas(context: CanvasRenderingContext2D): void;
}

これをBallやRacketがimplementsするように変更します。(修正は簡単なので、実際のコードは省略)

そして、GameLoopがIGamePartを取り扱ったのと同じように、GameCanvasMngがIGameCanvasPartを取り扱うようにします。以下にコードを示します。

class GameCanvasMng extends ElementMng<HTMLCanvasElement> {
    /**
     * CANVASの部品を更新する。
     */
    public Update() {
        // ゲーム部品を更新する。
        this._partlist.forEach(part => part.Update());
    }

    /**
     * CANVASの部品を描画する。
     */
    public Show() {
        if (this._element) {
            let width = this._element.width;
            let height = this._element.height;
            let context = this._element.getContext("2d");
            if (context && width > 0 && height > 0) {
                let _context = context;
                // ゲーム部品を表示する。
                this._partlist.forEach(part => part.DrawCanvas(_context));
            }
        }
    }

    /**
     * CANVAS部品のリスト
     */
    protected _partlist: IGameCanvasPart[] = [];

    /**
     * CANVAS部品を登録する
     * @param part CANVAS部品
     */
    public AddPart(part: IGameCanvasPart) {
        this._partlist.push(part);
    }
}

let GameCanvas = new GameCanvasMng();
let background = new class implements IGameCanvasPart {
    Update(): void { }
    DrawCanvas(context: CanvasRenderingContext2D): void {
        // 実際の描画処理
        // 背景を黒に
        context.fillStyle = "black";
        context.fillRect(0, 0, context.canvas.width, context.canvas.height);

        // Areaの範囲を白で描画
        context.strokeStyle = "white";
        context.strokeRect(Area.Left, Area.Top, Area.Width, Area.Height);

    }
};
GameCanvas.AddPart(background)
GameCanvas.AddPart(Racket1);
GameCanvas.AddPart(Ball1);

コメント