#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) 脱出ゲームの実行: 脱出ゲームの画像(例)
(2) SOKOBANタイプのゲーム実行: SOKOBANタイプのゲーム画像(例)
(3) 床が滑るタイプのゲーム実行: 床が滑るタイプのゲーム画像(例)
#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階のみならず多層階部分のブロック等も設置することです。
// 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を示します。 グラウンド内や壁による移動制限等を規定しています。
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階分のみの上昇としています。
リフトのテクスチャー画像
// 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部分は以下の通りです。
// 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軸となります)
// 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:ボックスのリスト・アップを表示
ボックスもリフト・アップ可能ですので、その処理部分を以下に示します。
// 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:ボックスのドロップ・ダウンを表示
プレーヤーがボックスを下階に落とす際の処理を次に示します。 ここでもボックスを複数階分落とすことが出来ます。
// 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フォーマットで保存しました。
// 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;
}
// 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:脱出パズルの完成形を表示
最後に敵(ゴースト)を表示させ、ランダムに動かしました。 敵の処理を以下に示します。 これで基本的な脱出パズルは完成です。
// 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処理を以下に示します。
// 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をボックスの設置場所に変更し複数にした事とゲーム・クリアーの判定処理の変更のみです。
GOALをボックスの設置場所に変更した箇所は次の通りです。
// 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
ゲームクリアの判定変更箇所は、次の通りです。
// 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」で床が滑る場合と滑らない場合(リフト上)を設定しました。 以下にその処理の一部を示します。
// 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
- Babylon.js: Home page of Babylon.js
- かんたんBlender講座: 高知工科大学のかんたんBlender講座
- PROJECT-6B: デフォルメ人物キャラクター(女の子)のダウンロード・サイト
- Let's Play with Free Sound Effects !
- あみたろの声素材工房
- Cocos2d.JSでゲームを作ってみた
- Phaser3.JSでゲームを作ってみた
- Phina.JSでゲームを作ってみた
- Babylon.jsで3Dアニメーションを含むグラフィックを描画してみる
- Babylon.jsで3Dゲームを作成してみた(その1:迷路編)
- Babylon.jsで空中散歩はいかが?
以上