作って学ぶ！ Babylon.js マルバツゲーム

こんにちは。やまゆです。

この記事は [Babylon.js アドベントカレンダー 2022](https://qiita.com/advent-calendar/2022/babylonjs) 10日目の記事です。

今回は「作って学ぶ！ Babylon.js マルバツゲーム」と題しまして、こないだ制作した簡単なゲームを紹介します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332028/9b544a0c-45a1-21eb-1d58-928edbdf9345.png)

ゲーム自体は単純なただのマルバツゲーム（３目並べ, 英語だと tic-tac-toe）です。これを Babylon.js で作るとどういう感じになるかをソースコードを追いながら紹介します。

```ts:playground.ts
/** ゲームの状態を示します */
enum GameState {
    INIT,
    RUNNING,
    OVER,
}

class Playground {
    public static CreateScene(engine: BABYLON.Engine, canvas: HTMLCanvasElement): BABYLON.Scene {
        // ここがエントリーポイントです
        // 変化する状態をここで宣言
        let gameState = GameState.INIT;
        let turn = 0;
        const data = [
            [ 0, 0, 0 ],
            [ 0, 0, 0 ],
            [ 0, 0, 0 ],
        ];

        // シーンの初期化
        const scene = new BABYLON.Scene(engine);
        scene.clearColor = new BABYLON.Color4(1, 1, 1, 1);
        new BABYLON.ArcRotateCamera('mainCamera', 0, 0, 6, BABYLON.Vector3.Zero(), scene);
        new BABYLON.DirectionalLight('mainLight', new BABYLON.Vector3(0.1, -1, 0.1), scene);

        // 背景の準備
        createTableLine(scene);
        createBackground(scene);

        function is1P() {
            return turn % 2 + 1 === 1;
        }
        /** 操作を実行する（ユーザーが出来るのはこのクリック操作のみ） */
        function onPicked() {
            for (let x = 0; x < 3; x++) {
                for (let y = 0; y < 3; y++) {
                    if (data[x][y] !== 0) {
                        // 既に何か置かれているので無視
                        continue;
                    }
                    const mesh = scene.getMeshByName(`ground${x}${y}`);
                    if (mesh.actionManager) {
                        mesh.actionManager.dispose();
                        mesh.actionManager = null;
                    }
                    if (gameState === GameState.OVER) {
                        continue;
                    }
                    // メッシュを Pick(クリック・タップ)した時に実行するコールバックを登録
                    const actionManager = new BABYLON.ActionManager(scene);
                    actionManager.registerAction(new BABYLON.ExecuteCodeAction(
                        BABYLON.ActionManager.OnPickTrigger,
                        (evt) => {
                            if (data[x][y] !== 0 || gameState === GameState.OVER) {
                                return;
                            }
                            data[x][y] = turn % 2 + 1;
                            if (is1P()) {
                                // 丸を置く
                                createCircle(mesh.position, scene);
                            } else {
                                // バツを置く
                                createCross(mesh.position, scene);
                            }
                            if (wasOver() === -1) {
                                // すべての枠に置かれたらドローとして終了(すぐに表示しようとすると、メッシュが置かれる前に alert が出てしまうので少し待つ
                                setTimeout(() => {
                                    alert('DRAW');
                                    gameState = GameState.OVER;
                                }, 10);
                                return;
                            } else if (wasOver() !== 0) {
                                // どちらかが勝利して終了
                                setTimeout(() => {
                                    alert(`${wasOver()}P WIN!`);
                                    gameState = GameState.OVER;
                                }, 10);
                                return;
                            }
                            turn++;
                        },
                    ));
                    mesh.actionManager = actionManager;
                }
            }
        }
        function wasOver() {
            for (let p = 1; p <= 2; p++) {
                // 泥臭いがビンゴしたかどうか判定
                if (
                    (data[0][0] === p && data[0][1] === p && data[0][2] === p) ||
                    (data[1][0] === p && data[1][1] === p && data[1][2] === p) ||
                    (data[2][0] === p && data[2][1] === p && data[2][2] === p) ||
                    (data[0][0] === p && data[1][0] === p && data[2][0] === p) ||
                    (data[0][1] === p && data[1][1] === p && data[2][1] === p) ||
                    (data[0][2] === p && data[1][2] === p && data[2][2] === p) ||
                    (data[0][0] === p && data[1][1] === p && data[2][2] === p) ||
                    (data[2][0] === p && data[1][1] === p && data[0][2] === p)
                ) {
                    return p;
                }
            }
            let fulfilled = true;
            for (let x = 0; x < 3; x++) {
                for (let y = 0; y < 3; y++) {
                    if (data[x][y] === 0) {
                        fulfilled = false;
                    }
                }
            }
            if (fulfilled) {
                return -1;
            }
            return 0;
        }
        onPicked();
        gameState = GameState.RUNNING;

        return scene;
    }
}

/** クリック出来る地面を生成 */
function createBackground(scene: BABYLON.Scene): void {    
    for (let x = 0; x < 3; x++) {
        for (let y = 0; y < 3; y++) {
            const mesh = BABYLON.CreateGround(`ground${x}${y}`, { width: 2, height: 2 }, scene);
            mesh.position = new BABYLON.Vector3(-2 + 2 * x, 0, -2 + 2 * y);
            const gmat = new BABYLON.StandardMaterial(`groundMat${x}${y}`, scene);
            mesh.material = gmat;
        }
    }
}

/** 表の罫線ボックスを生成 */
function createTableLine(scene: BABYLON.Scene): void {
    const lineMat = new BABYLON.StandardMaterial('mat', scene);
    lineMat.diffuseColor = BABYLON.Color3.Black();
    const line1 = BABYLON.CreateBox('line1', { width: 10, height: 0.5, size: 0.1 }, scene);
    line1.material = lineMat;
    line1.position = new BABYLON.Vector3(1, 0, 1);
    const line2 = line1.clone('line2');
    line2.position = new BABYLON.Vector3(1, 0, -1);
    const line3 = line2.clone('line3');
    line3.rotationQuaternion = BABYLON.Quaternion.RotationYawPitchRoll(Math.PI / 2, 0, 0);
    const line4 = line3.clone('line4');
    line4.position = new BABYLON.Vector3(-1, 0, 1);
}

/** 丸を生成 */
function createCircle(position: BABYLON.Vector3, scene: BABYLON.Scene): void {
    const mat = new BABYLON.StandardMaterial('mat', scene);
    mat.diffuseColor = BABYLON.Color3.Red();
    mat.specularPower = 30.0;
    const circle = BABYLON.CreateTorus('disk1', { thickness: 0.2, tessellation: 32 }, scene);
    circle.material = mat;
    circle.position = new BABYLON.Vector3(position.x, 0.1, position.z);
}

/** バツを生成 */
function createCross(position: BABYLON.Vector3, scene: BABYLON.Scene): void {
    const mat = new BABYLON.StandardMaterial('mat', scene);
    mat.diffuseColor = BABYLON.Color3.Blue();
    mat.specularPower = 30.0;
    const cross = BABYLON.CreateCapsule('cross1', { tessellation: 32 }, scene);
    cross.scaling = new BABYLON.Vector3(0.5, 1, 0.5);
    cross.rotationQuaternion = BABYLON.Quaternion.RotationYawPitchRoll(Math.PI / 4, Math.PI / 2, 0);
    cross.material = mat;
    cross.position = new BABYLON.Vector3(position.x, 0.1, position.z);
    const cross2 = BABYLON.CreateCapsule('cross2', { tessellation: 32 }, scene);
    cross2.scaling = new BABYLON.Vector3(0.5, 1, 0.5);
    cross2.rotationQuaternion = BABYLON.Quaternion.RotationYawPitchRoll(-Math.PI / 4, Math.PI / 2, 0);
    cross2.material = mat;
    cross2.position = new BABYLON.Vector3(position.x, 0.1, position.z);
}
```

シンプルなルールなので、実装も単純にすることが出来ました。
ユーザーが操作出来るものは「特定の場所をクリックする」のみなので、その部分を ActionManager でシンプルに実現できています。

みなさんもまずは簡単なゲームを作ってみて、それで色々なクラスの使い方を覚えていけば良いのではないかと思います！