TypeScriptでゲームプログラム12「イベントハンドラをクラスに移動する」

ボタン1のクリック処理

ボタン1を押すことでゲームを開始したり、中断したりしている部分のコードは、これまでのコード整理の対象にはなっていません。初期のコードのまま下のようになっています。

document.addEventListener("DOMContentLoaded", () => {
    let button1 = document.getElementById("button1");
    if (button1) {
        button1.addEventListener("click", () => {
            switch (GameStatus) {
                case "game":
                    GameStatus = "stop";
                    break;
                case "stop":
                    GameStatus = "game";
                    break;
                case "start":
                case "gameover":
                    GameStatus = "game";
                    InitGameSituation();
                    break;
            }
        })
    }
});

これまでのコード整理でボタン1はButton1Mngで管理するようにコードを整理してきました。上のコードはボタン1をクリックしたときのイベントハンドラなのだから、Button1Mngの内部に置きたい!

クリックイベントを追加する(失敗例)

Button1Mngに_ClickHandlerを追加し、そこにクリック時の動作を置きました。以下にコードを示します。

class Button1Mng extends ElementMng<HTMLButtonElement> {
    /**
     * コンストラクタ
     */
    constructor() {
        super("button1", HTMLButtonElement);
        this._element.addEventListener("click", (e) => {
            this._ClickHandler(e);
        })
    }

    /**
     * ボタンクリックイベントハンドラ
     */
    protected _ClickHandler(e: MouseEvent){
        switch (GameStatus) {
            case "game":
                GameStatus = "stop";
                break;
            case "stop":
                GameStatus = "game";
                break;
            case "start":
            case "gameover":
                GameStatus = "game";
                InitGameSituation();
                break;
        }
    }
}

しかし、このコードは正しく動作しません。

コンストラクタのsuper()を実行した直後(コードの7-9行目)は_elementが存在していないからでっす。

_elementのセットは基底クラスElementMngのコンストラクタで行ってい低下のようなコードになっています。このとき、_elementは非同期にセットされます。

        document.addEventListener("DOMContentLoaded", () => {
            let element = document.getElementById(id);
            // 以降の処理は省略
     }

そのため最初に示したコードでは、this._element.addEventListener()の時点で_elementがundefinedになっている可能性があります。意図したように動作しないばかりか、そもそもトランスパイルできません。

Show()では_elementをチェックし、definedのときはなにもしていません。しかし、同じように「addEventListener()をしない」というコードにはできません。どこかで必ずaddEventListener()しなければ、ボタン1のクリックイベントをプログラムが認識でなくなります。

_elementが確定するまで待つ

これに対応するには、Promiseを使います。_elementがセットされるのを待って、addEventListener()します。ElementMngのconstructorを次のように修正しました。

abstract class ElementMng<T extends HTMLElement> implements IGamePart {
    /**
     * _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 => {
            document.addEventListener("DOMContentLoaded", () => {
                let element = document.getElementById(id);
                if (element instanceof typecheck) {
                    this._element = element;
                    resolve(element);
                }
                else {
                    let typeName = this._getTypeName(typecheck);
                    console.log(`id="${id}"の${typeName}要素がありません。`)
                }
            });
        });
    }
}

Promiseを使うことで_elementが確定するまで待つことができるようになります。

実際のコード例は後述します。

クリックイベントを追加する

そしてButton1Mngのコンストラクタを以下のように修正します。

class Button1Mng extends ElementMng<HTMLButtonElement> {
    /**
     * コンストラクタ
     */
    constructor() {
        super("button1", HTMLButtonElement);
        this._AsyncElement.then(element => {
            element.addEventListener("click", e => { this._ClickHandler(e) });
        });
    }

これで_elementと_ClickHandler()が結び付けられ、意図した通りに動作します。

ゲームキャンバスのmousemove処理

ラケットがゲームキャンバス上のマウスポインタ追随して動くようにするためにゲームキャンバスに対するmousemoveイベントハンドラがあります。これはキャンバスのイベント処理なのですから、GameCanvasMngに移動します。

class GameCanvasMng extends ElementMng<HTMLCanvasElement> {
    /**
     * コンストラクタ
     */
    constructor() {
        super("game", HTMLCanvasElement);
        this._AsyncElement.then(element => {
            element.addEventListener("mousemove", (ev) => {
                let p = this._MouseToCanvas(ev);
                RacketDestinationX = p.X;
            });
        });
    }

    /**
     * マウス座標をevから取得し、CANVAS座標に変換する
     * @param ev 
     * @returns CANVAS座標
     */
    private _MouseToCanvas(ev: MouseEvent) {
        // このメソッドは_elementに対するマウスイベント発生時にコールされるので、
        // _elementは必ず存在する。
        let canvas = this._element!;
        let style = window.getComputedStyle(canvas);
        let paddingLeft = Number(style.paddingLeft.replace("px", ""));
        let paddingTop = Number(style.paddingTop.replace("px", ""));
        let paddingRight = Number(style.paddingRight.replace("px", ""));
        let paddingBottom = Number(style.paddingBottom.replace("px", ""));
        let styleWidth = canvas.clientWidth - paddingLeft - paddingRight;
        let styleHeight = canvas.clientHeight - paddingTop - paddingBottom;
        let x = (ev.offsetX - paddingLeft) * canvas.width / styleWidth;
        let y = (ev.offsetY - paddingTop) * canvas.height / styleHeight;
        return { X: x, Y: y }
    }
}

これもボタン1のクリックに対する修正と同じようにすればよいです。

_MouseToCanvas()はマウスイベントの座標からCANVASの座標に変換しています。詳細は、以下を参照してください。

マウス座標を保持する

ところで、上のコードではmousemoveイベントに連動してRacketDestinationXが更新されます。言い換えれば、マウスが移動するたびにRacketDestinationXが更新されますが、これは必要なことでしょうか?

RacketDestinationXはラケットを操作するための変数であり、ラケットの座標を更新する処理はRacket.Update()の中で行っています。つまり、そのタイミングでのマウス座標がわかればよいのであって、マウスが移動するたびにRacketDestinationXという名前の変数で保持する必要はありません。

マウス座標はもっと適切な名前の変数(あるいはプロパティ)で保持しておき、Racket.Update()のときにそれを参照できれば十分です。そう考えると、GameCanvasMngが、CANVAS座標に変換されたマウス座標を保持したほうが良いはずです。

class GameCanvasMng extends ElementMng<HTMLCanvasElement> {
    /**
     * マウスの情報
     */
    public get Mouse() {
        return {
            /**
             * CANVAS上のX座標
             */
            X: this._mouseCanvasX,
            /**
             * CANVAS上のY座標
             */
            Y: this._mouseCanvasY,
        }
    }
    /**
     * マウスのCANVAS上のX座標
     */
    private _mouseCanvasX = 0;
    /**
     * マウスのCANVAS上のY座標
     */
    private _mouseCanvasY = 0;

    /**
     * コンストラクタ
     */
    constructor() {
        super("game", HTMLCanvasElement);
        this._AsyncElement.then(element => {
            element.addEventListener("mousemove", (ev) => {
                let p = this._MouseToCanvas(ev);
                this._mouseCanvasX = p.X;
                this._mouseCanvasY = p.Y;
            });
        });
    }

Racket.Update()で、RacketDestinationXを参照していたところをGameCanvasMng.Mouse.Xを参照するように変更すれば、元のコードと同じように動作します。

また、GameCanvasMngの内部から、意味的にCANVASには関係のない変数RacketDestinationXを追い出し、コードも整理できました。

コメント