LoginSignup
3
0

More than 3 years have passed since last update.

Babylon.jsで3Dゲームを作成してみた(その2:脱出パズル編)

Last updated at Posted at 2020-01-04

1.概要

3Dグラフィック・エンジンの一つであるBabylon.jsを使用して、いくつかのパズル・ゲームを作成してみましたので以下に説明します。 なお、Windows10の環境において Microsoft Edge (Ver. 11.0.17763.379)、 Firefox (Ver. 65.0.2/64 bit) 及び Google Chrome (Ver. 73.0.3683.86/64 bit) で動作を確認しました。 また、Androidの一部での動作も確認しました。(iOSでは、一部のみ動作します。) ここで使用している3Dキャラクターは「
デフォルメ人物キャラクター(女の子)は PROJECT-6B
」様から、音声データは、「Let's Play with Free Sound Effects !」及び「あみたろの声素材工房」からダウンロードさせて戴きました。 無料グラフィックを提供されている各位に感謝いたします。
まだまだバグが残っているでしょうが、それなりに遊べると思います。 今回作成したパズルゲームの画像は次の通りです。

(1) 脱出ゲームの実行: 脱出ゲームの画像(例)
Pazzle_20.png
(2) SOKOBANタイプのゲーム実行: SOKOBANタイプのゲーム画像(例)
Pazzle_21.png
(3) 床が滑るタイプのゲーム実行: 床が滑るタイプのゲーム画像(例)
Pazzle_22.png

2.ファイル構成

ここで説明する内容に関するファイルは全て「GitHub Babylon.js_3D_Graphics」からダウンロード可能です。 また、そのファイル構成は次の通りです。
- css: メニュー表示用スタイルシートを保存したフォルダーです。
- scenes: キャラクター(プレイヤー)等の3Dグラフィック・データを保存したフォルダーです。
- texture: 「Babylon.js on GITHUB」からダウンロードしたテクスチュア・データならびに2Dグラフィック・データ等を保存したフォルダーです。
- index_Pazzle.html: 今回作成したパズル・ゲーム用HTMLファイルの選択・実行用メニューです。
- BabylonJS_pazzle_01.html - BabylonJS_pazzle_22.html: 今回作成したパズル・ゲーム用JavaScriptを含むHTMLファイル本体です。
- stage_101.js - stage_301.js: 各パズル・ゲーム用ステージ・データです。(テキスト・エディター等でステージの修正・追加が出来ます)

3.3D脱出パズルの作成

周囲の環境とグラウンドの表示等の基本部分は、「Babylon.jsで3Dゲームを作成してみた(その1:迷路編)」と同一ですので割愛します。 ここでは各ステージの表示、ステージ内を歩行するキャラクターの表示とボックスの移動やその制御等をステップバイステップで例を示します。 なお、Babylon.jsの使い方については、「Babylon.jsで3Dアニメーションを含むグラフィックを描画してみる」を参照ください。
index_Pazzle.html」: ここで説明するプログラム全てをメニューから選択して表示できます。 但し、このメニューはiOSでは動作しませんので悪しからず。

Step-1: パズル用ステージ等の表示

BabylonJS_pazzle_01.html:パズル用ステージを表示
初めにパズル用ステージを作成するためにブロック等を設置する部分のJavaScriptを示します。 迷路編と異なるのは1階のみならず多層階部分のブロック等も設置することです。
Pazzle_01.png

BabylonJS_pazzle_01.htmlの一部
// Create a Stage
    for (var vrt = 0; vrt < Maze_size_Y + 4; vrt++) {
        for (var row = 0; row < Maze_size_X + 4; row++) {
            for (var col = 0; col < Maze_size_Z + 4; col++) {
            if(Temp_Room[vrt][row][col] == "W") {
            var cube = BABYLON.MeshBuilder.CreateBox('Cube', options, scene);
                cube.material = cubeMaterial;
                cube.position = new BABYLON.Vector3(BLOCK_SIZE / 2 + (row - (Maze_size_X + 4) / 2) * BLOCK_SIZE, BLOCK_SIZE * (vrt + 1 / 2), BLOCK_SIZE / 2 + (col - (Maze_size_Z + 4) / 2) * BLOCK_SIZE);
            light1.excludedMeshes.push(cube);
            shadowGenerator.addShadowCaster(cube);
            shadowGenerator.getShadowMap().renderList.push(cube);
                cube.receiveShadows = true;
            }
            if(Temp_Room[vrt][row][col] == "L") {
            LIFTcube[LIFT_count] = BABYLON.MeshBuilder.CreateBox('LIFTCube', options_L, scene);
            LIFTcube[LIFT_count].material = LIFTcubeMaterial;
            LIFTcube[LIFT_count].position = new BABYLON.Vector3(BLOCK_SIZE / 2 + (row - (Maze_size_X + 4) / 2) * BLOCK_SIZE, (BLOCK_SIZE * (vrt - 1 / 2) + 0.1), BLOCK_SIZE / 2 + (col - (Maze_size_Z + 4) / 2) * BLOCK_SIZE);
            LIFT_X[LIFT_count] = row;
            LIFT_Y[LIFT_count] = vrt;
            LIFT_Z[LIFT_count] = col;
            LIFT_YY[LIFT_count] = BLOCK_SIZE * (vrt - 1 / 2);
            Offset_LIFTY[LIFT_count] = 0;
            light1.excludedMeshes.push(LIFTcube[LIFT_count]);
                LIFTcube[LIFT_count].receiveShadows = true;
            LIFT_count = LIFT_count + 1;
            }
            if(Temp_Room[vrt][row][col] == "G") {
            Goal_x = BLOCK_SIZE / 2 + (row - (Maze_size_X + 4) / 2) * BLOCK_SIZE;
            Goal_y = BLOCK_SIZE * vrt;
            Goal_z = BLOCK_SIZE / 2 + (col - (Maze_size_Z + 4) / 2) * BLOCK_SIZE;
            var goalCube = BABYLON.MeshBuilder.CreateBox('goalCube', options_G, scene);
                goalCube.material = goalMaterial;
                goalCube.position = new BABYLON.Vector3(Goal_x, Goal_y + 0.1, Goal_z);
            light1.excludedMeshes.push(goalCube);
                goalCube.receiveShadows = true;
            BABYLON.SceneLoader.ImportMesh("", gltf_dir, gltf_data_03, scene, function (newMeshes2) {
                var lamp = newMeshes2[0];
                lamp.position = new BABYLON.Vector3(Goal_x + 3, Goal_y, Goal_z + 3);
                lamp.scaling = new BABYLON.Vector3(1, 1, 1);
                var materialSphere = new BABYLON.StandardMaterial("sphere0", scene);
                materialSphere.emissiveColor = new BABYLON.Color3(1.0, 0.84, 0.0);
                bulb.position = new BABYLON.Vector3(Goal_x + 3, Goal_y + 17.5, Goal_z + 3);
                bulb.material = materialSphere;
                shadowGenerator.addShadowCaster(lamp);
                shadowGenerator.getShadowMap().renderList.push(lamp);
            });
            }
            if(Temp_Room[vrt][row][col] == "B") {
            var BOX_mat = new BABYLON.StandardMaterial("box_mat", scene);
            var BOX_texture = new BABYLON.Texture("./textures/Box_07.png", scene);
                BOX_mat.diffuseTexture = BOX_texture;
            BOX_X[BOX_count] = row;
            BOX_Y[BOX_count] = vrt;
            BOX_Z[BOX_count] = col;
            BOX[BOX_count] = BABYLON.Mesh.CreateBox("box", BLOCK_SIZE, scene);
            BOX[BOX_count].material = BOX_mat;
                    BOX[BOX_count].position = new BABYLON.Vector3(BLOCK_SIZE / 2 + (row - (Maze_size_X + 4) / 2) * BLOCK_SIZE, BLOCK_SIZE * (vrt + 1 / 2), BLOCK_SIZE / 2 + (col - (Maze_size_Z + 4) / 2) * BLOCK_SIZE);
            BOX[BOX_count].receiveShadows = true;
            Offset_BoxY[BOX_count] = 0;
            BOX_flag[BOX_count] = 0;
            light1.excludedMeshes.push(BOX[BOX_count]);
            shadowGenerator.addShadowCaster(BOX[BOX_count]);
            shadowGenerator.getShadowMap().renderList.push(BOX[BOX_count]);
                BOX[BOX_count].receiveShadows = true;
            BOX_count = BOX_count + 1;
            }
            if(Temp_Room[vrt][row][col] == "E") {
            Enemy_X[ ENEMY_count ] = BLOCK_SIZE / 2 + (row - (Maze_size_X + 4) / 2) * BLOCK_SIZE;
            Enemy_Y[ ENEMY_count ] = BLOCK_SIZE * vrt;
            Enemy_Z[ ENEMY_count ] = BLOCK_SIZE / 2 + (col - (Maze_size_Z + 4) / 2) * BLOCK_SIZE;
            enemyX[ ENEMY_count ] = 0;
            enemyY[ ENEMY_count ] = 0;
            enemyZ[ ENEMY_count ] = 0;
            Temp_Room[vrt][row][col] = "F";
            ENEMY_count = ENEMY_count + 1;
            }
            if(Temp_Room[vrt][row][col] == "P") {
            x = BLOCK_SIZE / 2 + (row - (Maze_size_X + 4) / 2) * BLOCK_SIZE;
            y = BLOCK_SIZE * vrt;
            z = BLOCK_SIZE / 2 + (col - (Maze_size_Z + 4) / 2) * BLOCK_SIZE;
            Temp_Room[vrt][row][col] = "F";
            BABYLON.SceneLoader.ImportMesh("", gltf_dir, gltf_data_01, scene, function (newMeshes1, particleSystems, skeletons) {
                var obj =  newMeshes1[0];
                obj.rotationQuaternion = undefined;
                obj.scaling = new BABYLON.Vector3(0.08, 0.08, 0.08);
                obj.position = new BABYLON.Vector3(x, y, z);
                obj.rotation.y = -90/180 * Math.PI + walk_dir;
                shadowGenerator.addShadowCaster(obj);
                shadowGenerator.getShadowMap().renderList.push(obj);
                camera.target = obj;
            });
            }
            }
        }
    }

Step-2: キャラクターの移動制御

BabylonJS_pazzle_02.html:キャラクター(プレーヤー)の移動を制御
次にキャラクター(プレーヤー)の移動を制御する部分のJavaScriptを示します。 グラウンド内や壁による移動制限等を規定しています。
Pazzle_02.png

BabylonJS_pazzle_02.htmlの一部
          scene.registerBeforeRender(function() {
// Walking inside the ground
                if((moveX == -1) && (obj.position.x <= (BLOCK_SIZE * (Maze_size_X + 2) / -2) + 2)) {
                    moveX = 0;
                }
                if((moveX == 1) && (obj.position.x >= (BLOCK_SIZE * (Maze_size_X + 2) / 2) - 2)) {
                   moveX = 0;
                }
                if((moveZ == -1) && (obj.position.z <= (BLOCK_SIZE * (Maze_size_Z + 2) / -2) + 2)) {
                    moveZ = 0;
                }
                if((moveZ == 1) && (obj.position.z >= (BLOCK_SIZE * (Maze_size_Z + 2) / 2) - 2)) {
                    moveZ = 0;
                }
                if(obj.position.x <= (Maze_size_X / 2 + 0.5) * BLOCK_SIZE * -1) {
                    obj.position.x = (Maze_size_X / 2 + 0.5) * BLOCK_SIZE * -1;
                }
                if(obj.position.x >= (Maze_size_X / 2 + 0.5) * BLOCK_SIZE) {
                    obj.position.x = (Maze_size_X / 2 + 0.5) * BLOCK_SIZE;
                }
                if(obj.position.z <= (Maze_size_Z / 2 + 0.5) * BLOCK_SIZE * -1) {
                    obj.position.z = (Maze_size_Z / 2 + 0.5) * BLOCK_SIZE * -1;
                }
                if(obj.position.z >= (Maze_size_Z / 2 + 0.5) * BLOCK_SIZE) {
                    obj.position.z = (Maze_size_Z / 2 + 0.5) * BLOCK_SIZE;
                }
                x = obj.position.x;
                y = obj.position.y;
                z = obj.position.z;
                pos_row_00 = Math.round(((x - BLOCK_SIZE / 2) / BLOCK_SIZE) + (Maze_size_X + 4) / 2);
                pos_row_01 = Math.round((((x + limit) - BLOCK_SIZE / 2) / BLOCK_SIZE) + (Maze_size_X + 4) / 2);
                pos_row_02 = Math.round((((x - limit) - BLOCK_SIZE / 2) / BLOCK_SIZE) + (Maze_size_X + 4) / 2);
                pos_col_00 = Math.round(((z - BLOCK_SIZE / 2) / BLOCK_SIZE) + (Maze_size_Z + 4) / 2);
                pos_col_01 = Math.round((((z + limit) - BLOCK_SIZE / 2) / BLOCK_SIZE) + (Maze_size_Z + 4) / 2);
                pos_col_02 = Math.round((((z - limit) - BLOCK_SIZE / 2) / BLOCK_SIZE) + (Maze_size_Z + 4) / 2);
                pos_vrt_00 = Math.round((y - BLOCK_SIZE / 2) / BLOCK_SIZE);
                pos_vrt_01 = Math.round((y - BLOCK_SIZE / 2) / BLOCK_SIZE - 1);
                pos_vrt_02 = Math.round((y - BLOCK_SIZE / 2) / BLOCK_SIZE + 1);
// Stop at the wall
                if((moveX == 1)  && (Temp_Room[pos_vrt_00][pos_row_01][pos_col_00] == "W")) {
                    moveX = 0;
                }
                if((moveX == -1) && (Temp_Room[pos_vrt_00][pos_row_02][pos_col_00] == "W")) {
                    moveX = 0;
                }
                if((moveZ == 1)  && (Temp_Room[pos_vrt_00][pos_row_00][pos_col_01] == "W")) {
                    moveZ = 0;
                }
                if((moveZ == -1) && (Temp_Room[pos_vrt_00][pos_row_00][pos_col_02] == "W")) {
                    moveZ = 0;
                }
                if((moveX == 1) && (Temp_Room[pos_vrt_00][pos_row_01][pos_col_00] == "L") && (Temp_Room[pos_vrt_00 + 1][pos_row_01][pos_col_00] != "F")) {
                    moveX = 0;
                }
                if((moveX == -1) && (Temp_Room[pos_vrt_00][pos_row_02][pos_col_00] == "L") && (Temp_Room[pos_vrt_00 + 1][pos_row_02][pos_col_00] != "F")) {
                    moveX = 0;
                }
                if((moveZ == 1) && (Temp_Room[pos_vrt_00][pos_row_00][pos_col_01] == "L") && (Temp_Room[pos_vrt_00 + 1][pos_row_00][pos_col_01] != "F")) {
                    moveZ = 0;
                }
                if((moveZ == -1) && (Temp_Room[pos_vrt_00][pos_row_00][pos_col_02] == "L") && (Temp_Room[pos_vrt_00 + 1][pos_row_00][pos_col_02] != "F")) {
                    moveZ = 0;
                }
// Player's Walk
                obj.position.x = x + walk_step * moveX;
                obj.position.z = z + walk_step * moveZ;
                obj.rotation.y = walk_dir;
                });

Step-3: プレーヤーのリフト・アップ

BabylonJS_pazzle_03.html:プレーヤーのリフト・アップ表示
プレーヤーの上昇手段であるリフトの動作を設定する部分のJavaScriptを示します。 なお、リフトのテクスチャー画像は128×128ドットの正方形画像を横に6個連ねたものを使用しています。(以下の画像を参照下さい) また、リフトは1階分のみの上昇としています。
Pazzle_03.png
リフトのテクスチャー画像
lift_texture_01.png

BabylonJS_pazzle_03.htmlの一部
// Player LIFT up
                if((Temp_Room[pos_vrt_00][pos_row_00][pos_col_00] == "L") || (Temp_Room[pos_vrt_01][pos_row_00][pos_col_00] == "L")) {
                    for (var LIFT_i = 0; LIFT_i < LIFT_count; LIFT_i ++) {
                    var LIFT_x = Math.round(((LIFTcube[LIFT_i].position.x - BLOCK_SIZE / 2) / BLOCK_SIZE) + (Maze_size_X + 4) / 2);
                    var LIFT_z = Math.round(((LIFTcube[LIFT_i].position.z - BLOCK_SIZE / 2) / BLOCK_SIZE) + (Maze_size_Z + 4) / 2);
                    if((pos_row_00 == LIFT_x) && (pos_col_00 == LIFT_z)) {
                        if(LIFTcube[LIFT_i].position.y < LIFT_YY[LIFT_i] + BLOCK_SIZE) {
                        LIFTcube[LIFT_i].position.y = LIFT_YY[LIFT_i] + Offset_LIFTY[LIFT_i];
                        obj.position.y = LIFTcube[LIFT_i].position.y + BLOCK_SIZE / 2;
                        y = obj.position.y;
                        Offset_LIFTY[LIFT_i] = Offset_LIFTY[LIFT_i] + up_step;
                        }
                    }
                    }
                }
                else {
                    for (var LIFT_i = 0; LIFT_i < LIFT_count; LIFT_i ++) {
                    Temp_LIFT_x = Math.round(((LIFTcube[LIFT_i].position.x - BLOCK_SIZE / 2) / BLOCK_SIZE) + (Maze_size_X + 4) / 2);
                    Temp_LIFT_y = Math.round((LIFTcube[LIFT_i].position.y) / BLOCK_SIZE);
                    Temp_LIFT_z = Math.round(((LIFTcube[LIFT_i].position.z - BLOCK_SIZE / 2) / BLOCK_SIZE) + (Maze_size_Z + 4) / 2);
                    if(LIFTcube[LIFT_i].position.y > (LIFT_YY[LIFT_i] + down_step) && Offset_LIFTY[LIFT_i] > down_step && Temp_Room[Temp_LIFT_y + 1][Temp_LIFT_x][Temp_LIFT_z] == "F") {
                        LIFTcube[LIFT_i].position.y = LIFT_YY[LIFT_i] + Offset_LIFTY[LIFT_i];
                        Offset_LIFTY[LIFT_i] = Offset_LIFTY[LIFT_i] - down_step;
                    }
                    }
                }

Step-4: プレーヤーのドロップ・ダウン

BabylonJS_pazzle_04.html:プレーヤーのドロップ・ダウン表示
プレーヤーの降下は単純なものですが、何階分でも降下可能です。 そのJavaScript部分は以下の通りです。
Pazzle_04.png

BabylonJS_pazzle_04.htmlの一部
// Player Drop Down
                if(pos_vrt_01 > 1 && Temp_Room[pos_vrt_01][pos_row_00][pos_col_00] == "F") {
                     DOWN_flag = 1;
                     Offset_ObY = 0;
                }
                if(DOWN_flag == 1 && Offset_ObY < BLOCK_SIZE) {
                    Offset_ObY = Offset_ObY + down_step;
                    obj.position.y = obj.position.y - down_step;
                    y = obj.position.y;
                    if(Offset_ObY >= BLOCK_SIZE) {
                    DOWN_flag = 0;
                    obj.position.y = Math.round(obj.position.y / BLOCK_SIZE) * BLOCK_SIZE + 0.1;
                    y = obj.position.y;
                    }
                    if(obj.position.y < BLOCK_SIZE * 2) {
                    obj.position.y = BLOCK_SIZE * 2 + 0.1;
                    y = obj.position.y;
                    }
                }

Step-5: ボックスの移動

BabylonJS_pazzle_05.html:プレーヤーがボックスを移動させる処理
プレーヤーは、ボックスを押して移動させることが可能ですが、ここでのボックスは1ブロックずつ移動します。 その中でⅩ軸の+方向への移動処理のJavaScript部分を以下に示します。 なお、Ⅹ軸の-方向やZ方向での移動も同様です。(上下方向がY軸となります)
Pazzle_05.png

BabylonJS_pazzle_05.htmlの一部
// Move the Box
                if((moveX == 1) && (Temp_Room[pos_vrt_00][pos_row_01][pos_col_00] == "B")) {
                    if(pos_row_01 >= Maze_size_X + 2) {
                    moveX = 0;
                    }
                    else if((Temp_Room[pos_vrt_00][pos_row_01 + 1][pos_col_00] == "F") || (Temp_Room[pos_vrt_00][pos_row_01 + 1][pos_col_00] == "L")) {
                    var Temp_count = 0;
                    var Temp_count_02 = -1;
                    for(var i = 0; i < BOX.length; i++) {
                        BOX_flag[ i ] = 0;
                        Offset_BoxY[ i ] = 0;
                        if((Math.round(((BOX[ i ].position.x - BLOCK_SIZE / 2) / BLOCK_SIZE) + (Maze_size_X + 4) / 2) == pos_row_01) && (Math.round(((BOX[ i ].position.z - BLOCK_SIZE / 2) / BLOCK_SIZE) + (Maze_size_Z + 4) / 2) == pos_col_00)) {
                        if((Math.round((BOX[ i ].position.y - BLOCK_SIZE / 2) / BLOCK_SIZE)) == pos_vrt_00) {
                            Temp_count = i;
                        }
                        if((Temp_Room[pos_vrt_02][pos_row_01][pos_col_00] == "B") && (Math.round(BOX[ i ].position.y - BLOCK_SIZE / 2) / BLOCK_SIZE) == pos_vrt_02) {
                            Temp_count_02 = i;
                        }
                        if(Temp_Room[pos_vrt_00 - 1][pos_row_01 + 1][pos_col_00] == "L") {
                            Temp_BOX_n = i;
                            Temp_LIFT_n = -1;
                            for(var LIFT_i = 0; LIFT_i < LIFT_count; LIFT_i ++) {
                            if((LIFT_Y[LIFT_i] == pos_vrt_00 - 1) && (LIFT_X[LIFT_i] == pos_row_01 + 1) && (LIFT_Z[LIFT_i] == pos_col_00)) {
                                Temp_LIFT_n = LIFT_i;
                            }
                            }
                        }
                        }
                    }
                    BOX[ Temp_count ].position.x = BOX[ Temp_count ].position.x + BLOCK_SIZE;
                    BOX_X[ Temp_count ] = BOX_X[ Temp_count ] + 1;
                    if(Temp_Room[pos_vrt_00][pos_row_01][pos_col_00] == "B") {
                        Temp_Room[pos_vrt_00][pos_row_01][pos_col_00] = "F";
                    }
                    if(Temp_Room[pos_vrt_00][pos_row_01 + 1][pos_col_00] == "F") {
                        Temp_Room[pos_vrt_00][pos_row_01 + 1][pos_col_00] = "B";
                        if((pos_vrt_00 > 2) && (Temp_Room[pos_vrt_00 - 1][pos_row_01 + 1][pos_col_00] == "F")) {
                        BOX_flag[ Temp_count ] = 1;
                        Temp_DOWN = Temp_count;
                        }
                    }
                    if(Temp_Room[pos_vrt_00][pos_row_01 + 1][pos_col_00] == "L") {
                        if(Temp_Room[pos_vrt_00][pos_row_01][pos_col_00] == "B") {
                        Temp_Room[pos_vrt_00][pos_row_01][pos_col_00] = "F";
                        }
                        Temp_BOX_n = Temp_count;
                        Temp_BOX_x = pos_row_01 + 1;
                        Temp_BOX_y = pos_vrt_00;
                        Temp_BOX_z = pos_col_00;
                    }
                    if(Temp_count_02 >= 0) {
                        BOX[ Temp_count_02 ].position.x = BOX[ Temp_count_02 ].position.x + BLOCK_SIZE;
                        Temp_Room[pos_vrt_02][pos_row_01][pos_col_00] = "F";
                        Temp_Room[pos_vrt_02][pos_row_01 + 1][pos_col_00] = "B";
                        BOX_X[ Temp_count_02 ] = BOX_X[ Temp_count ];
                        BOX_Y[ Temp_count_02 ] = BOX_Y[ Temp_count ] + 1;
                    }
                    } else {
                    moveX = 0;
                    }
                }

Step-6: ボックスのリフト・アップ

BabylonJS_pazzle_06.html:ボックスのリスト・アップを表示
ボックスもリフト・アップ可能ですので、その処理部分を以下に示します。
Pazzle_06.png

BabylonJS_pazzle_06.htmlの一部
// BOX Lift Up
                if(Temp_BOX_n >= 0) {
                    if(Temp_BOX_m < 0) {
                    for(var BOX_i = 0; BOX_i < BOX_count; BOX_i ++) {
                        if((BOX_X[BOX_i] == BOX_X[Temp_BOX_n]) && (BOX_Y[BOX_i] == BOX_Y[Temp_BOX_n] + 1) && (BOX_Z[BOX_i] == BOX_Z[Temp_BOX_n])) {
                        Temp_BOX_m = BOX_i;
                        }
                    }
                    }
                    if(Temp_LIFT_n < 0) {
                    if((Temp_Room[Temp_BOX_y][Temp_BOX_x][Temp_BOX_z] == "L") || (Temp_Room[Temp_BOX_y - 1][Temp_BOX_x][Temp_BOX_z] == "L")) {
                        for (var LIFT_i = 0; LIFT_i < LIFT_count; LIFT_i ++) {
                        var Temp_LIFT_x = Math.round(((LIFTcube[LIFT_i].position.x - BLOCK_SIZE / 2) / BLOCK_SIZE) + (Maze_size_X + 4) / 2);
                        var Temp_LIFT_y = Math.round((LIFTcube[LIFT_i].position.y) / BLOCK_SIZE);
                        var Temp_LIFT_z = Math.round(((LIFTcube[LIFT_i].position.z - BLOCK_SIZE / 2) / BLOCK_SIZE) + (Maze_size_Z + 4) / 2);
                        if((Temp_BOX_x == Temp_LIFT_x) && (Temp_BOX_z == Temp_LIFT_z)) {
                            if(LIFTcube[LIFT_i].position.y < LIFT_YY[LIFT_i] + BLOCK_SIZE + 0.1) {
                            LIFTcube[LIFT_i].position.y = LIFT_YY[LIFT_i] + Offset_LIFTY[LIFT_i];
                            BOX[Temp_BOX_n].position.y = LIFTcube[LIFT_i].position.y + BLOCK_SIZE;
                            if(Temp_BOX_m >= 0) {
                                BOX[Temp_BOX_m].position.x = BOX[Temp_BOX_n].position.x;
                                BOX[Temp_BOX_m].position.y = BOX[Temp_BOX_n].position.y + BLOCK_SIZE;
                                BOX[Temp_BOX_m].position.z = BOX[Temp_BOX_n].position.z;
                            }
                            Offset_LIFTY[LIFT_i] = Offset_LIFTY[LIFT_i] + up_step;
                            Temp_Room[Temp_LIFT_y + 1][Temp_LIFT_x][Temp_LIFT_z] = "B";
                            BOX_X[ Temp_BOX_n ] = Temp_LIFT_x;
                            BOX_Z[ Temp_BOX_n ] = Temp_LIFT_z;
                            if(Offset_LIFTY[LIFT_i] >= BLOCK_SIZE) {
                                BOX_Y[ Temp_BOX_n ] = Temp_LIFT_y + 1;
                                if(Temp_BOX_m >= 0) {
                                BOX[Temp_BOX_m].position.x = BOX[Temp_BOX_n].position.x;
                                BOX[Temp_BOX_m].position.y = BOX[Temp_BOX_n].position.y + BLOCK_SIZE;
                                BOX[Temp_BOX_m].position.z = BOX[Temp_BOX_n].position.z;
                            Temp_Room[Temp_LIFT_y + 2][Temp_LIFT_x][Temp_LIFT_z] = "B";
                                BOX_X[ Temp_BOX_m ] = BOX_X[ Temp_BOX_n ];
                                BOX_Y[ Temp_BOX_m ] = BOX_Y[ Temp_BOX_n ] + 1;
                                BOX_Z[ Temp_BOX_m ] = BOX_Z[ Temp_BOX_n ];
                                }
                                Temp_BOX_n = -1;
                                Temp_BOX_m = -1;
                            }
                            }
                        }
                        }
                    }
                    }
                    else {
                    if(LIFTcube[Temp_LIFT_n].position.y < LIFT_YY[Temp_LIFT_n] + BLOCK_SIZE + 0.1) {
                        LIFTcube[Temp_LIFT_n].position.y = LIFT_YY[Temp_LIFT_n] + Offset_LIFTY[Temp_LIFT_n];
                        BOX[Temp_BOX_n].position.y = LIFTcube[Temp_LIFT_n].position.y + BLOCK_SIZE;
                        Offset_LIFTY[Temp_LIFT_n] = Offset_LIFTY[Temp_LIFT_n] + up_step;
                        Temp_Room[Temp_LIFT_y + 1][Temp_LIFT_x][Temp_LIFT_z] = "B";
                        BOX_X[ Temp_BOX_n ] = Temp_LIFT_x;
                        BOX_Z[ Temp_BOX_n ] = Temp_LIFT_z;
                        if(Offset_LIFTY[Temp_LIFT_n] >= BLOCK_SIZE) {
                        BOX_Y[ Temp_BOX_n ] = Temp_LIFT_y + 1;
                        if(Temp_BOX_m >= 0) {
                            BOX[Temp_BOX_m].position.x = BOX[Temp_BOX_n].position.x;
                            BOX[Temp_BOX_m].position.y = BOX[Temp_BOX_n].position.y + BLOCK_SIZE;
                            BOX[Temp_BOX_m].position.z = BOX[Temp_BOX_n].position.z;
                            BOX_X[ Temp_BOX_m ] = BOX_X[ Temp_BOX_n ];
                            BOX_Y[ Temp_BOX_m ] = BOX_Y[ Temp_BOX_n ] + 1;
                            BOX_Z[ Temp_BOX_m ] = BOX_Z[ Temp_BOX_n ];
                            Temp_BOX_m = -1;
                        }
                        Temp_BOX_n = -1;
                        Temp_LIFT_n = -1;
                        }
                    }
                    }
                }

Step-7: ボックスのドロップ・ダウン

BabylonJS_pazzle_07.html:ボックスのドロップ・ダウンを表示
プレーヤーがボックスを下階に落とす際の処理を次に示します。 ここでもボックスを複数階分落とすことが出来ます。
Pazzle_07.png

BabylonJS_pazzle_07.htmlの一部
// BOX Drop Down
                if(Temp_DOWN >= 0) {
                    if(Temp_BOX_m < 0) {
                    for(var BOX_i = 0; BOX_i < BOX_count; BOX_i ++) {
                        if((BOX_X[BOX_i] == BOX_X[Temp_DOWN]) && (BOX_Y[BOX_i] == BOX_Y[Temp_DOWN] + 1) && (BOX_Z[BOX_i] == BOX_Z[Temp_DOWN])) {
                        Temp_BOX_m = BOX_i;
                        }
                    }
                    }
                    if(BOX_flag[Temp_DOWN] == 0 && (BOX_Y[Temp_DOWN] > 2) && Temp_Room[BOX_Y[Temp_DOWN] - 1][BOX_X[Temp_DOWN]][BOX_Z[Temp_DOWN]] == "F") {
                    BOX_flag[Temp_DOWN] = 1;
                    Offset_BoxY[Temp_DOWN] = 0;
                    }
                    if(BOX_flag[Temp_DOWN] == 1 && Offset_BoxY[Temp_DOWN] < BLOCK_SIZE) {
                    Offset_BoxY[Temp_DOWN] = Offset_BoxY[Temp_DOWN] + down_step;
                    BOX[Temp_DOWN].position.y = BOX[Temp_DOWN].position.y - down_step;
                    if(Temp_BOX_m >= 0) {
                        BOX[Temp_BOX_m].position.y = BOX[Temp_DOWN].position.y + BLOCK_SIZE;
                    }
                    if(Offset_BoxY[Temp_DOWN] >= BLOCK_SIZE) {
                        Temp_Room[BOX_Y[Temp_DOWN]][BOX_X[Temp_DOWN]][BOX_Z[Temp_DOWN]] = "F";
                        Temp_Room[BOX_Y[Temp_DOWN] - 1][BOX_X[Temp_DOWN]][BOX_Z[Temp_DOWN]] = "B";
                        BOX_flag[Temp_DOWN] = 0;
                        BOX[Temp_DOWN].position.y = BLOCK_SIZE * BOX_Y[Temp_DOWN] - BLOCK_SIZE / 2;
                        BOX_Y[Temp_DOWN] = BOX_Y[Temp_DOWN] - 1;
                        if(BOX[Temp_DOWN].position.y < BLOCK_SIZE * 5/2) {
                        BOX[Temp_DOWN].position.y = BLOCK_SIZE * 5/2;
                        BOX_Y[Temp_DOWN] = 2;
                        Temp_Room[BOX_Y[Temp_DOWN] + 1][BOX_X[Temp_DOWN]][BOX_Z[Temp_DOWN]] = "F";
                        Temp_Room[BOX_Y[Temp_DOWN]][BOX_X[Temp_DOWN]][BOX_Z[Temp_DOWN]] = "B";
                        Offset_BoxY[Temp_DOWN] = 0;
                        }
                        if(Temp_BOX_m >= 0) {
                        Temp_Room[BOX_Y[Temp_DOWN] + 2][BOX_X[Temp_DOWN]][BOX_Z[Temp_DOWN]] = "F";
                        Temp_Room[BOX_Y[Temp_DOWN] + 1][BOX_X[Temp_DOWN]][BOX_Z[Temp_DOWN]] = "B";
                        BOX[Temp_BOX_m].position.x = BOX[Temp_DOWN].position.x;
                        BOX[Temp_BOX_m].position.y = BOX[Temp_DOWN].position.y + BLOCK_SIZE;
                        BOX[Temp_BOX_m].position.z = BOX[Temp_DOWN].position.z;
                        BOX_X[ Temp_BOX_m ] = BOX_X[ Temp_DOWN ];
                        BOX_Y[ Temp_BOX_m ] = BOX_Y[ Temp_DOWN ] + 1;
                        BOX_Z[ Temp_BOX_m ] = BOX_Z[ Temp_DOWN ];
                        }
                        if((BOX_Y[ Temp_DOWN ] <= 2) || ((BOX_Y[ Temp_DOWN ] > 2) && (Temp_Room[BOX_Y[Temp_DOWN] - 1][BOX_X[Temp_DOWN]][BOX_Z[Temp_DOWN]] != "F"))) {
                        Temp_DOWN = -1;
                        }
                        Temp_BOX_m = -1;
                    }
                    }
                }

Step-8: ボックス移動時のプレーヤー動作変更

BabylonJS_pazzle_08.html:ボックス移動時のプレーヤーのアニメーションを変更して表示
これまでは、プレーヤーの動作は全て同一でしたが、ボックスを移動させる場合のプレーヤーのアニメーションを変更してみます。 プレイヤーのアニメーションは、予め2種類準備しそれぞれ同一の動作を設定しています。 1種類のプレイヤーは、Y軸上で表示されない位置に置き「Change_flag」の値でY軸の位置を切り替えており、その処理のJavaScript部分を以下に示します。 なお、キャラクターのアニメーションはBlenderで作成しGLTFフォーマットで保存しました。
Pazzle_08.png

BabylonJS_pazzle_08.htmlの一部
// Set Player's Change Flag
                if((moveX == 1)  && ((Temp_Room[pos_vrt_00][pos_row_01][pos_col_00] == "B") || (Temp_Room[pos_vrt_00][pos_row_01 + 1][pos_col_00] == "B"))) {
                    Change_flag = 1;
                }
                else if((moveX == -1) && ((Temp_Room[pos_vrt_00][pos_row_02][pos_col_00] == "B") || (Temp_Room[pos_vrt_00][pos_row_02 - 1][pos_col_00] == "B"))) {
                    Change_flag = 1;
                }
                else if((moveZ == 1)  && ((Temp_Room[pos_vrt_00][pos_row_00][pos_col_01] == "B") || (Temp_Room[pos_vrt_00][pos_row_00][pos_col_01 + 1] == "B"))) {
                    Change_flag = 1;
                }
                else if((moveZ == -1) && ((Temp_Room[pos_vrt_00][pos_row_00][pos_col_02] == "B") || (Temp_Room[pos_vrt_00][pos_row_00][pos_col_02 - 1] == "B"))) {
                    Change_flag = 1;
                }
                else {
                    Change_flag = 0;
                }
BabylonJS_pazzle_08.htmlの一部
// Normal Player
            BABYLON.SceneLoader.ImportMesh("", gltf_dir, gltf_data_01, scene, function (newMeshes1, particleSystems, skeletons) {
                var obj_N =  newMeshes1[0];
                obj_N.rotationQuaternion = undefined;
                obj_N.scaling = new BABYLON.Vector3(0.08, 0.08, 0.08);
                obj_N.position = new BABYLON.Vector3(x, y, z);
                obj_N.rotation.y = -90/180 * Math.PI + walk_dir;
                obj_N.position.y = -1 * Offset_Bobj;
                shadowGenerator.addShadowCaster(obj_N);
                shadowGenerator.getShadowMap().renderList.push(obj_N);

                scene.registerBeforeRender(function() {
                if(Change_flag == 0) {
                    obj_N.position.x = x;
                    obj_N.position.y = y;
                    obj_N.position.z = z;
                    obj_N.rotation.y = walk_dir;
                } else {
                    obj_N.position.x = x;
                    obj_N.position.y = y - Offset_Bobj;
                    obj_N.position.z = z;
                    obj_N.rotation.y = walk_dir;
                }
                });
            });

// Player at Pushing the Box
            BABYLON.SceneLoader.ImportMesh("", gltf_dir, gltf_data_02, scene, function (newMeshes2, particleSystems, skeletons) {
                var obj_P =  newMeshes2[0];
                obj_P.rotationQuaternion = undefined;
                obj_P.scaling = new BABYLON.Vector3(0.08, 0.08, 0.08);
                obj_P.position = new BABYLON.Vector3(x, y, z);
                obj_P.rotation.y = -90/180 * Math.PI + walk_dir;
                obj_P.position.y = -1 * Offset_Bobj;
                shadowGenerator.addShadowCaster(obj_P);
                shadowGenerator.getShadowMap().renderList.push(obj_P);

                scene.registerBeforeRender(function() {
                if(Change_flag == 0) {
                    obj_P.position.x = x;
                    obj_P.position.y = y - Offset_Bobj;
                    obj_P.position.z = z;
                    obj_P.rotation.y = walk_dir;
                } else {
                    obj_P.position.x = x;
                    obj_P.position.y = y;
                    obj_P.position.z = z;
                    obj_P.rotation.y = walk_dir;
                }
                });
            });
            }
            }
        }
    }

Step-9: 敵(ゴースト)を追加しゲームを完成させる

BabylonJS_pazzle_09.html:脱出パズルの完成形を表示
最後に敵(ゴースト)を表示させ、ランダムに動かしました。 敵の処理を以下に示します。 これで基本的な脱出パズルは完成です。
Pazzle_09.png

BabylonJS_pazzle_09.htmlの一部
// Enemy
    if(ENEMY_count > 0) {
        BABYLON.SceneLoader.ImportMesh("", gltf_dir, enemy_data, scene, function (newMeshes3) {
        ENEMY[ 0 ] = newMeshes3[ 0 ];
        ENEMY[ 0 ].rotationQuaternion = undefined;
        ENEMY[ 0 ].position = new BABYLON.Vector3(Enemy_X[ 0 ], Enemy_Y[ 0 ], Enemy_Z[ 0 ]);
        ENEMY[ 0 ].scaling = new BABYLON.Vector3(0.1, 0.15, 0.1);
        ENEMY[ 0 ].alpha = 0.5;     // Not Working
//          shadowGenerator.addShadowCaster(ENEMY[ 0 ]);
//          shadowGenerator.getShadowMap().renderList.push(ENEMY[ 0 ]);
        if(ENEMY_count > 1) {
            for(var i = 1; i < ENEMY_count; i++) {
                    for (var index = 1; index < newMeshes3.length; index++) {
                        ENEMY[ i ] = newMeshes3[index].createInstance("Enemy" + index);
                ENEMY[ i ].rotationQuaternion = undefined;
                ENEMY[ i ].rotation.y = 90/180 * Math.PI * index;
                ENEMY[ i ].position = new BABYLON.Vector3(Enemy_X[ i ], Enemy_Y[ i ], Enemy_Z[ i ]);
                ENEMY[ i ].scaling = new BABYLON.Vector3(0.1, 0.1 + Math.random() * 0.1, 0.1);
                ENEMY[ i ].alpha = 0.5;     // Not Working
//              shadowGenerator.addShadowCaster(ENEMY[ i ]);
//              shadowGenerator.getShadowMap().renderList.push(ENEMY[ i ]);
                    }
            }
        }

Step-10: ボックスの移動方法を変更

BabylonJS_pazzle_20.html:ボックスの移動をプレイヤーに追随するように変更
上記 Step-9 までボックスの移動が1ブロックずつでしたが、プレイヤーの動きに追随するように変更しました。 色々追加処理が必要でしたが、その一部として変更後のⅩ軸+方向のJavaScript処理を以下に示します。
Pazzle_20.png

BabylonJS_pazzle_20.htmlの一部
// Move the Box
                if((Move_flag == 0) && (moveX == 1) && (Temp_Room[pos_vrt_00][pos_row_01][pos_col_00] == "B")) {
                    if((Temp_Room[pos_vrt_00][pos_row_01 + 1][pos_col_00] == "F") || (Temp_Room[pos_vrt_00][pos_row_01 + 1][pos_col_00] == "L")) {
                    Move_flag = 1;
                    for(var i = 0; i < BOX.length; i++) {
                        BOX_flag[ i ] = 0;
                        Offset_BoxY[ i ] = 0;
                        if((Math.round(((BOX[ i ].position.x - BLOCK_SIZE / 2) / BLOCK_SIZE) + (Maze_size_X + 4) / 2) == pos_row_01) && (Math.round(((BOX[ i ].position.z - BLOCK_SIZE / 2) / BLOCK_SIZE) + (Maze_size_Z + 4) / 2) == pos_col_00)) {
                        if((Temp_count < 0) && (Math.round((BOX[ i ].position.y - BLOCK_SIZE / 2) / BLOCK_SIZE)) == pos_vrt_00) {
                            Temp_count = i;
                        }
                        if((Temp_Room[pos_vrt_02][pos_row_01][pos_col_00] == "B") && (Math.round(BOX[ i ].position.y - BLOCK_SIZE / 2) / BLOCK_SIZE) == pos_vrt_02) {
                            Temp_count_02 = i;
                        }
                        }
                    }
                    } else {
                    moveX = 0;
                    Move_flag = 0;
                    }
                }
                if(Move_flag == 1 && Temp_count >= 0) {
                    BOX[ Temp_count ].position.x = BOX[ Temp_count ].position.x + walk_step;
                    if(Temp_count_02 >= 0) {
                    BOX[ Temp_count_02 ].position.x = BOX[ Temp_count ].position.x;
                    }
                    BOX_move_flag = BOX_move_flag + walk_step;
                    if(BOX_move_flag >= BLOCK_SIZE) {
                    BOX_X[ Temp_count ] = BOX_X[ Temp_count ] + 1;
                    BOX[ Temp_count ].position.x = BLOCK_SIZE / 2 + (BOX_X[ Temp_count ] - (Maze_size_X + 4) / 2) * BLOCK_SIZE;
                    if(Temp_Room[BOX_Y[ Temp_count ]][BOX_X[ Temp_count ] - 1][BOX_Z[ Temp_count ]] == "B") {
                        Temp_Room[BOX_Y[ Temp_count ]][BOX_X[ Temp_count ] - 1][BOX_Z[ Temp_count ]] = "F";
                    }
                    if(Temp_Room[BOX_Y[ Temp_count ]][BOX_X[ Temp_count ]][BOX_Z[ Temp_count ]] == "F") {
                        Temp_Room[BOX_Y[ Temp_count ]][BOX_X[ Temp_count ]][BOX_Z[ Temp_count ]] = "B";
                        if((BOX_Y[ Temp_count ] > 2) && (Temp_Room[BOX_Y[ Temp_count ] - 1][BOX_X[ Temp_count ]][BOX_Z[ Temp_count ]] == "F")) {
                        BOX_flag[ Temp_count ] = 1;
                        Temp_DOWN = Temp_count;
                        moveX = 0;
                        }
                    }
                    if(Temp_Room[pos_vrt_00][pos_row_01 + 1][pos_col_00] == "L" || Temp_Room[pos_vrt_01][pos_row_01 + 1][pos_col_00] == "L") {
                        if(Temp_Room[pos_vrt_00][pos_row_01][pos_col_00] == "B") {
                        Temp_Room[pos_vrt_00][pos_row_01][pos_col_00] = "F";
                        }
                        Temp_BOX_n = Temp_count;
                        Temp_BOX_x = pos_row_01 + 1;
                        Temp_BOX_y = pos_vrt_00;
                        Temp_BOX_z = pos_col_00;
                    }
                    if(Temp_count_02 >= 0) {
                        BOX[ Temp_count_02 ].position.x = BOX[ Temp_count ].position.x;
                        Temp_Room[BOX_Y[ Temp_count_02 ]][BOX_X[ Temp_count ] - 1][BOX_Z[ Temp_count ]] = "F";
                        Temp_Room[BOX_Y[ Temp_count_02 ]][BOX_X[ Temp_count ]][BOX_Z[ Temp_count ]] = "B";
                        BOX_X[ Temp_count_02 ] = BOX_X[ Temp_count ];
                        BOX_Y[ Temp_count_02 ] = BOX_Y[ Temp_count ] + 1;
                    }
                    BOX_move_flag = 0;
                    Temp_count = -1;
                    Temp_count_02 = -1;
                    Move_flag = 0;
                    }
                }

4.他の3Dパズルの作成

4-1.「SOKOBAN」タイプのゲームに変更

BabylonJS_pazzle_21.html:「SOKOBAN」タイプのゲームを実行
上記 Step-10 の脱出ゲームを元に「SOKOBAN」タイプのゲームに変更してみました。 変更箇所は、GOALをボックスの設置場所に変更し複数にした事とゲーム・クリアーの判定処理の変更のみです。
Pazzle_21.png
GOALをボックスの設置場所に変更した箇所は次の通りです。

BabylonJS_pazzle_21.htmlの一部
// Start of Change Point
            if(Temp_Room[vrt][row][col] == "G") {
            GOAL_X[GOAL_count] = row;
            GOAL_Y[GOAL_count] = vrt;
            GOAL_Z[GOAL_count] = col;
            GOAL_SET[GOAL_count] = 0;
            var Temp_X = BLOCK_SIZE / 2 + (row - (Maze_size_X + 4) / 2) * BLOCK_SIZE;
            var Temp_Y = BLOCK_SIZE * vrt;
            var Temp_Z = BLOCK_SIZE / 2 + (col - (Maze_size_Z + 4) / 2) * BLOCK_SIZE;
            var goalCube = BABYLON.MeshBuilder.CreateBox('goalCube', options_G, scene);
                goalCube.material = goalMaterial;
                goalCube.position = new BABYLON.Vector3(Temp_X, Temp_Y + 0.1, Temp_Z);
            light1.excludedMeshes.push(goalCube);
                goalCube.receiveShadows = true;
            Temp_Room[vrt][row][col] = "F";
            GOAL_count = GOAL_count + 1;
            }
// End of Change Point

ゲームクリアの判定変更箇所は、次の通りです。

BabylonJS_pazzle_21.htmlの一部
// Confirm the condition for the Box on the correct position
    for(var Goal_i = 0; Goal_i < GOAL_count; Goal_i ++) {
        if(Temp_Room[GOAL_Y[Goal_i]][GOAL_X[Goal_i]][GOAL_Z[Goal_i]] == "B") {
        GOAL_SET[Goal_i] = 1;
        }
        else {
        GOAL_SET[Goal_i] = 0;
        }
    }
-------------------------------------

// Start of Change Point
        var temp_goal = 1;
        for(var Goal_i = 0; Goal_i < GOAL_count; Goal_i ++) {
            temp_goal = temp_goal * GOAL_SET[Goal_i];
        }
        if(temp_goal == 1) {
// End of Change

4-2.「Slip Floor」タイプのゲームに変更

BabylonJS_pazzle_22.html:「Slip Floor」タイプのゲーム実行
次に床が滑るタイプのゲームに変更してみます。 ここでは床や壁用ブロック、プレイヤーのアニメーションを変更しています。 また、「SLIP_flag」で床が滑る場合と滑らない場合(リフト上)を設定しました。 以下にその処理の一部を示します。
Pazzle_22.png

BabylonJS_pazzle_22.htmlの一部
// Render loop for VirtualJoystick
    scene.onBeforeRenderObservable.add(()=>{
    if(SLIP_flag == 1) {
        moveX=0;
        moveZ=0;
    }
                if(leftJoystick.pressed && SLIP_flag >= 0){
            if(leftJoystick.deltaPosition.x <= -0.5) {
                walk_dir = 0 / 180 * Math.PI;
                moveX = 0;
                moveZ = 1;
                SLIP_flag = -1;
            } else if(leftJoystick.deltaPosition.x >= 0.5) {
                walk_dir = 180 / 180 * Math.PI;
                moveX = 0;
                moveZ = -1;
                SLIP_flag = -1;
            } else {
//              moveZ = 0;
            }
            if(leftJoystick.deltaPosition.y <= -0.5) {
                walk_dir = -90 / 180 * Math.PI;
                moveX = -1;
                moveZ = 0;
                SLIP_flag = -1;
            } else if(leftJoystick.deltaPosition.y >= 0.5) {
                walk_dir = 90 / 180 * Math.PI;
                moveX = 1;
                moveZ = 0;
                SLIP_flag = -1;
            } else {
//              moveX = 0;
            }

5. Reference

以上

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0