#Phina.JSで簡単なゲームを作ってみました
JavaScriptベースのゲームエンジンであるPhina.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でもバージョンによっては動作可能です。
また、ここで使用しているグラフィックの一部に「どらぴか様」ならびに「ぴぽや様」作成の素材をサウンドエフェクトには「小森平様」作成の素材を使用させていただきました。 フリー素材を提供して下さった各位に感謝いたします。
##ファイル構成
ここで説明する内容に関するファイルは全て「GitHub PhinaJS_Sample_games」からダウンロード可能です。 また、そのファイル構成は次の通りです。
・ images: ゲームで使用するグラフィックを収納するフォルダーです。
・ js: 各ステップ毎のメイン・プログラムであるJavaScriptを収納するフォルダーです。
- maze_02.js: Step-7で使用する迷路作成プログラムです。
- phina.js, phina.min.js: Phina.JSゲームエンジン本体です。(Phina.JSからダウンロードしたもの)
- phina_sample_01.js ~ phina_sample_09_with_sound_effect.js: 各ステップ毎のゲーム・プログラムです。
- stages_01.js, stages_02.js: Step-5以降で使用するステージ用データで、stage_01.jsは敵がいないバージョン、stage_02.jsは敵がいるバージョンです。
・ sound: ゲームで使用するサウンド・エフェクトを収納するフォルダーです。
・ index_Phina_01.html ~ index_Phina_09_with_sound_effect.html: 各ステップ毎のゲーム開始用HTMLファイルです。
##ゲーム作成の説明
以下にゲーム作成の手順をステップバイステップで説明します。
###Step-1: 静止画グラフィックスを表示させる
始めにHTMLファイルを示します。 Phina.JSゲームエンジンの読込及びゲーム本体のJavaScriptファイルの読込等を行い全てのステップでほぼ同一ですが、各ステップ毎にタイトルと「src='./js/phina_sample_01.js'」の部分を変更しています。
Step-1のデモ実行
<!doctype html>
<html>
<head>
<meta charset='utf-8' />
<meta name="viewport" content="width=device-width, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<title>Sample for phina.js #001 by T. Fujita on 2019/9/12</title>
<!-- phina.js を読み込む -->
<script src='./js/phina.js'></script>
<!-- メイン処理 -->
<script src='./js/phina_sample_01.js'></script>
</head>
<body>
</body>
</html>
次にメインプログラムである「phina_sample_01.js」を示します。 背景画像とグラフックス(player)を読み込み表示します。
/*
* phina.js sample #001 by T. Fujita on 2019/9/12
*/
phina.globalize();
var SCREEN_X = 800;
var SCREEN_Y = 600;
var scale = 1;
var P_size = 48 * scale;
var ASSETS = {
image: {
'Player': './images/ETNR_TOMITA_01.png',
'bg': './images/bg.jpg',
},
};
phina.define('MainScene', {
superClass: 'DisplayScene',
init: function(option) {
this.superInit(option);
this.bg = Sprite("bg").addChildTo(this);
this.bg.origin.set(0, 0);
this.bg.width = SCREEN_X;
this.bg.height = SCREEN_Y;
// this.backgroundColor = '#ffaaaa';
var player = Sprite('Player', 48, 48)
.addChildTo(this)
.setPosition(this.gridX.center(), this.gridY.center())
.setScale(scale);
player.frameIndex = 0;
},
});
phina.main(function() {
var app = GameApp({
startLabel: 'main',
width: SCREEN_X,
height: SCREEN_Y,
assets: ASSETS,
});
app.run();
});
###Step-2: 静止画グラフィックスを移動させてみる
前述の「phina_sample_01.js」にマウスをクリックした位置あるいはキー入力の検知を追加し、それに合わせてplayerを移動させるものが「phina_sample_02.js」です。 追加・修正個所を以下に示します。
Step-2のデモ実行
update: function(app) {
var keyboard = app.keyboard;
var touch_p = app.pointer;
// 左右移動
if (keyboard.getKey('left') || (touch_p.getPointing() && (Math.floor(this.player.x / Step) * Step) > (Math.floor(touch_p.x / Step) * Step))) {
this.player.x -= Step;
if(this.player.x < (P_size / 2)) {
this.player.x = P_size / 2;
}
}
if (keyboard.getKey('right') || (touch_p.getPointing() && (Math.floor(this.player.x / Step) * Step) < (Math.floor(touch_p.x / Step) * Step))) {
this.player.x += Step;
if(this.player.x > (SCREEN_X - P_size / 2)) {
this.player.x = SCREEN_X - P_size / 2;
}
}
// 上下移動
if (keyboard.getKey('up') || (touch_p.getPointing() && (Math.floor(this.player.y / Step) * Step) > (Math.floor(touch_p.y / Step) * Step))) {
this.player.y -= Step;
if(this.player.y < (P_size / 2)) {
this.player.y = P_size / 2;
}
}
if (keyboard.getKey('down') || (touch_p.getPointing() && (Math.floor(this.player.y / Step) * Step) < (Math.floor(touch_p.y / Step) * Step))) {
this.player.y += Step;
if(this.player.y > (SCREEN_Y - P_size / 2)) {
this.player.y = SCREEN_Y - P_size / 2;
}
}
}
###Step-3: アニメーション・グラフィックスを表示させる
「phina_sample_01.js」を元にplayerをアニメーションさせる「phina_sample_03.js」を作成します。 変更部分を次に示します。 Phina.JSでは、RPGツクール等の歩行グラフィックスが使用可能です。
Step-3のデモ実行
var ASSETS = {
image: {
'Player': './images/ETNR_TOMITA_01.png',
'bg': './images/bg.jpg',
},
spritesheet: {
'Player_ss':
{
'frame': {
'width': 48,
'height': 48,
'cols': 12,
'rows': 1,
},
'animations': {
'down': {
'frames': [0, 1, 2],
'next': 'down',
'frequency': F_rate,
},
}
},
}
};
phina.define('MainScene', {
superClass: 'DisplayScene',
init: function(option) {
this.superInit(option);
this.bg = Sprite("bg").addChildTo(this);
this.bg.origin.set(0, 0);
this.bg.width = SCREEN_X;
this.bg.height = SCREEN_Y;
// this.backgroundColor = '#ffaaaa';
var player = Sprite('Player', P_size, P_size).addChildTo(this)
.setPosition(this.gridX.center(), this.gridY.center())
.setScale(scale);
var anim_p = FrameAnimation('Player_ss').attachTo(player);
anim_p.gotoAndPlay('down');
},
});
###Step-4: アニメーション・グラフィックスを移動させてみる
前述の「phina_sample_02.js」と「phina_sample_03.js」を組み合わせて前後(上下)左右の歩行アニメーションを追加したものが「phina_sample_04.js」です。 追加分の一部として左右歩行部分を以下に示します。
Step-4のデモ実行
update: function(app) {
var keyboard = app.keyboard;
var touch_p = app.pointer;
// 左右移動
if (keyboard.getKey('left') || (touch_p.getPointing() && (Math.floor(this.player.x / Step) * Step) > (Math.floor(touch_p.x / Step) * Step))) {
this.player.x -= Step;
if(app.frame % F_rate === 0) {
this.player.frameIndex = (this.player.frameIndex === 3) ? 5:3;
}
}
else if (keyboard.getKey('right') || (touch_p.getPointing() && (Math.floor(this.player.x / Step) * Step) < (Math.floor(touch_p.x / Step) * Step))) {
this.player.x += Step;
if(app.frame % F_rate === 0) {
this.player.frameIndex = (this.player.frameIndex === 6) ? 8:6;
}
}
###Step-5: 壁等を表示させ、移動を制限する
「phina_sample_04.js」に「stages_01.js」で設定する各ステージを読み込みタイルデータを表示させたものが「phina_sample_05.js」です。 ここでは、壁を通り抜け出来ない設定とゴール、再度実行(Aタイル)のみ対応するようにしています。
Step-5のデモ実行
if(counter >= max_rooms) {
counter = 0;
this.exit();
}
ROOM = room[counter];
counter = counter + 1;
this.player = Sprite('Player', 48, 48).addChildTo(this).setScale(scale);
this.player.frameIndex = 0;
this.wall_0Group = DisplayElement().addChildTo(this);
this.wall_1Group = DisplayElement().addChildTo(this);
for (i=0; i<ROOM.length; i++) {
temp[i] = [];
for (j=0; j<ROOM[i].length; j++) {
temp[i][j] = ROOM[i].substr(j,1);
if(ROOM[i].substr(j,1) == "P") {
this.player.setPosition( j * P_size + P_size/2, i * P_size + P_size/2);
}
else if(ROOM[i].substr(j,1) == "A") {
this.again = Again().addChildTo(this).setScale(scale);
this.again.setPosition( j * P_size + P_size/2, i * P_size + P_size/2);
}
else if(ROOM[i].substr(j,1) == "G") {
var goal = Goal().addChildTo(this).setScale(scale);
goal.setPosition( j * P_size + P_size/2, i * P_size + P_size/2);
}
else if(ROOM[i].substr(j,1) == "B") {
BLOCK[BL_counter] = Block().addChildTo(this).setScale(scale);
BLOCK[BL_counter].setPosition( j * P_size + P_size/2, i * P_size + P_size/2);
BL_counter = BL_counter + 1;
}
else if(ROOM[i].substr(j,1) == "w") {
var wall_0 = Wall_0().addChildTo(this.wall_0Group).setScale(scale);
wall_0.setPosition( j * P_size + P_size/2, i * P_size + P_size/2);
}
else if(ROOM[i].substr(j,1) == "W") {
var wall_1 = Wall_1().addChildTo(this.wall_1Group).setScale(scale);
wall_1.setPosition( j * P_size + P_size/2, i * P_size + P_size/2);
}
}
}
if (flag == "left") {
player.x -= Step;
if(app.frame % F_rate === 0) {
player.frameIndex = (player.frameIndex === 3) ? 5:3;
}
if((pos[p_x - 1][p_y] == "W") || pos[p_x - 1][p_y] == "w") {
if(player.x < (p_x) * P_size + P_size/2 + Step) {
player.x = (p_x) * P_size + P_size/2;
}
} else if(pos[p_x - 1][p_y] != "G") {
pos[p_x][p_y] = "F";
pos[p_x - 1][p_y] = "P";
}
} else if (flag == "right") {
if(pos[p_x][p_y] == "G") {
END_falg = "Goal !";
this.nextLabel ="title";
this.exit();
}
again.setInteractive(true);
again.onpointstart = function () {
END_flag = "Again";
}
if(END_flag == "Again") {
this.nextLabel ="title";
this.exit();
}
各ステージを構成する「stage_01.js」の一部を以下に示します。 メモ帳等のエディターで修正・追加してみてください。
// All the rooms are here.
// by T. Fujita
//
// A: Again
// B: Block(Movable)
// F: Floor
// G: Goal
// P: Player
// W: Wall
var room = [];
room[0] = [ "WwwwwGwW",
"WFFFFFFW",
"WFFFFFFW",
"WFPFFFFW",
"WFFFFFFW",
"WwwwwwwA"]
room[1] = [ "WwwwwGwW",
"WFFFFBFW",
"WFFFFFFW",
"WFPFFFFW",
"WFFFFFFW",
"WwwwwwwA"]
room[2] = [ "WwwwwwwwwwwwwGwW",
"WFFFFFFFFFFFFFFW",
"WBBBBBBBBBBBBBBW",
"WFFFFFFFFFFFFFFW",
"WFFFFFFFFFFFFFFW",
"WFFFFFFFFFFFFFFW",
"WFFFFFFFFFFFFFFW",
"WFFFFFFFFFFFFFFW",
"WFFPFFFFFFFFFFFW",
"WFFFFFFFFFFFFFFW",
"WFFFFFFFFFFFFFFW",
"WwwwwwwwwwwwwwwA"]
###Step-6: ボックスを押して移動可能にさせる
「phina_sample_06.js」でplayerがBoxを押して移動できるようにしました。 これでゲームの基本部分は完成です。
Step-6のデモ実行
if (flag == "left") {
player.x -= Step;
if(app.frame % F_rate === 0) {
player.frameIndex = (player.frameIndex === 3) ? 5:3;
}
if((pos[px - 1][py] == "W") || pos[px - 1][py] == "w") {
if(player.x < px * P_size + Step) {
player.x = px * P_size;
}
} else if(pos[px - 1][py] == "B") {
for(i=0; i<BL_counter; i++) {
if((Math.round(BLOCK[i].x / P_size) == (px - 1)) && (Math.round(BLOCK[i].y / P_size) == py)) {
BL_temp = i;
BL_x = BLOCK[i].x;
BL_y = BLOCK[i].y;
}
}
if(pos[px - 2][py] == "F") {
if(player.x < BL_x + P_size + Step) {
BLOCK[BL_temp].x = (px - 2) * P_size;
if(pos[px][py] == "P") {pos[px][py] = "F";}
if(pos[px - 1][py] == "B") {pos[px - 1][py] = "P";}
if(pos[px - 2][py] == "F") {pos[px - 2][py] = "B";}
}
} else {
if(player.x <= BL_x + P_size - Step) {
player.x = px * P_size;
if(pos[px][py] == "F") {pos[px][py] = "P";}
}
}
} else if(pos[px - 1][py] != "G") {
if(pos[px][py] == "F") {pos[px][py] = "P";}
}
if(pos[px + 1][py - 1] == "P") {pos[px + 1][py - 1] = "F";}
if(pos[px + 1][py] == "P") {pos[px + 1][py] ="F";}
if(pos[px + 1][py + 1] == "P") {pos[px + 1][py + 1] = "F";}
} else if (flag == "right") {
さらにゴール時の効果音を追加しました。 「phina_sample_06_with_sound_effect.js」でゴール時に効果音を作動させます。
Step-6効果音付きのデモ実行
var ASSETS = {
image: {
'bg': './images/bg.jpg',
'Player': './images/ETNR_TOMITA_01.png',
'again': './images/A_48.png',
'block': './images/Box_06.png',
'goal': './images/Goal_00.png',
'wall_0': './images/Block_05.png',
'wall_1': './images/Block_04.png',
},
sound: {
'GOAL': './sound/crrect_answer3.mp3',
},
};
if(pos[p_x][p_y] == "G") {
END_falg = "Goal !";
SoundManager.play('GOAL');
this.nextLabel ="title";
this.exit();
}
###Step-7: 迷路を作成しアニメーション・グラフィックスを移動させてみる
「phina_sample_06_with_sound_effect.js」を若干変更し、「maze_02.js」で作成した迷路を表示させたものが「phina_sample_07_with_sound_effect.js」です。 各グラフィックスのサイズは1/2に設定しています。 迷路は、「Algoful : Algorithm for making a maze」のアルゴリズムを使用させていただきました。
Step-7のデモ実行
// This program is based on "http://algoful.com/Archive/Algorithm/MazeDig" by Algoful.
// by T. Fujita
var maze = [];
var startCells = [];
var Maze = function(X, Y)
{
var w = X; // 幅(奇数)
var h = Y; // 高さ(奇数)
if(w < 11) {w = 11;}
if(h < 11) {h = 11;}
if(w % 2 == 0) {w = w - 1;}
if(h % 2 == 0) {h = h - 1;}
var x;
var y;
var results = [];
var currentCells = [];
for (x = 0; x < w; x++) {
maze[x] = new Array();
for (y = 0; y < h; y++) {
if (x == 0 || y == 0 || x == w - 1 || y == h - 1) {
maze[x][y] = "F"; // 外周を通路に設定
} else {
maze[x][y] = "W"; // 残りを壁に設定
}
}
}
startCells[0] = [];
startCells[0][0] = (Math.floor(Math.random() * (w / 2 - 2))) * 2 + 1;
startCells[0][1] = (Math.floor(Math.random() * (h / 2 - 2))) * 2 + 1;
Dig(startCells[0][0], startCells[0][1]);
for (x = 0; x < w; x++)
{
for (y = 0; y < h; y++)
{
if (x == 0 || y == 0 || x == w - 1 || y == h - 1)
{
maze[x][y] = "W"; // 外周を壁に戻す
}
}
}
let i = 0;
while (maze[i][1] == "W") {
if(maze[i + 1][1] == "F") {
maze[i + 1][1] = "P";
break;
}
i = i + 1;
}
i = maze.length - 2;
let j = maze[0].length - 1;
while(maze[i][j] == "W") {
if(maze[i - 1][j - 1] == "F") {
maze[i - 1][j] = "G";
break;
}
i = i - 1;
}
for (y = 0; y < h; y++) {
results[y] = [];
for (x = 0; x < w; x++) {
results[y] = results[y] + maze[x][y];
}
}
// alert(results[0] + "\n" + results[1]);
return results;
}
function Dig(x, y)
{
while (true) {
var directions = [];
if(maze[x][y-1] == "W" && maze[x][y-2] =="W") {directions.push("UP");}
if(maze[x][y+1] == "W" && maze[x][y+2] =="W") {directions.push("DOWN");}
if(maze[x-1][y] == "W" && maze[x-2][y] =="W") {directions.push("LEFT");}
if(maze[x+1][y] == "W" && maze[x+2][y] =="W") {directions.push("RIGHT");}
if(directions.length == 0) {break;}
var rnd = Math.floor(Math.random() * directions.length);
switch (directions[rnd])
{
case 'UP':
SetPath(x, --y);
SetPath(x, --y);
break;
case 'DOWN':
SetPath(x, ++y);
SetPath(x, ++y);
break;
case 'LEFT':
SetPath(--x, y);
SetPath(--x, y);
break;
case 'RIGHT':
SetPath(++x, y);
SetPath(++x, y);
break;
}
// console.log(maze[0] + "\n" + maze[1] + "\n" + maze[2] + "\n" + maze[3] + "\n" + maze[4] + "\n" + maze[5]);
var cell = GetStartCell();
if(cell != null)
{
Dig(cell[0], cell[1]);
}
}
}
function SetPath(x, y)
{
maze[x][y] = "F";
if((x % 2 == 1) && (y % 2 == 1))
{
var Temp = [x, y];
startCells.push(Temp);
}
}
function GetStartCell()
{
if(startCells.length == 0) {return null;}
var rnd = Math.floor(Math.random() * startCells.length);
var cell = startCells[rnd];
startCells.splice(rnd, 1);
return cell;
}
##Step-8: 敵を出現させる
追加として、「phina_sample_06_with_sound_effect.js」に敵(gost)を出現させランダムに移動させてみたものが「phina_sample_08_with_sound_effect.js」です。 もちろんplayerが敵と当たればGame Overとなります。 なお、敵の出現数・位置は「stage_02.js」での"E"文字で設定しており、壁やBOXと同様に表示させています。
Step-8のデモ実行
for(var k = 0; k < EN_counter; k++) {
EN_x[ k ] = Math.floor(ENEMY[ k ].x / P_size);
EN_y[ k ] = Math.floor(ENEMY[ k ].y / P_size);
EC_x[ k ] = Math.ceil(ENEMY[ k ].x / P_size);
EC_y[ k ] = Math.ceil(ENEMY[ k ].y / P_size);
var Temp = Math.random() * 5;
if(Temp < 1 && EN_flag[ k ] == "stay") {
EN_flag[ k ] = "left";
EN_temp[ k ] = 0;
}
else if(Temp < 2 && EN_flag[ k ] == "stay") {
EN_flag[ k ] = "right";
EN_temp[ k ] = 0;
}
else if(Temp < 3 && EN_flag[ k ] == "stay") {
EN_flag[ k ] = "up";
EN_temp[ k ] = 0;
}
else if(Temp < 4 && EN_flag[ k ] == "stay") {
EN_flag[ k ] = "down";
EN_temp[ k ] = 0;
}
else {
}
if(EN_flag[ k ] == "left") {
if(pos[EC_x[ k ] - 1][EN_y[ k ]] == "F") {
EN_temp[ k ] += 1;
ENEMY[ k ].x -=Step;
if(app.frame % F_rate === 0) {
ENEMY[ k ].frameIndex = (ENEMY[ k ].frameIndex === 3) ? 5:3;
}
if(EN_temp[ k ] >= P_size/Step) {
EN_flag[ k ] = "stay";
}
} else {
EN_flag[ k ] = "stay";
}
}
if(EN_flag[ k ] == "right") {
if(pos[EN_x[ k ] + 1][EN_y[ k ]] == "F") {
EN_temp[ k ] += 1;
ENEMY[ k ].x +=Step;
if(app.frame % F_rate === 0) {
ENEMY[ k ].frameIndex = (ENEMY[ k ].frameIndex === 6) ? 8:6;
}
if(EN_temp[ k ] >= P_size/Step) {
EN_flag[ k ] = "stay";
}
} else {
EN_flag[ k ] = "stay";
}
}
if(EN_flag[ k ] == "up") {
if(pos[EN_x[ k ]][EC_y[ k ] - 1] == "F") {
EN_temp[ k ] += 1;
ENEMY[ k ].y -=Step;
if(app.frame % F_rate === 0) {
ENEMY[ k ].frameIndex = (ENEMY[ k ].frameIndex === 9) ? 11:9;
}
if(EN_temp[ k ] >= P_size/Step) {
EN_flag[ k ] = "stay";
}
} else {
EN_flag[ k ] = "stay";
}
}
if(EN_flag[ k ] == "down") {
if(pos[EN_x[ k ]][EN_y[ k ] + 1] == "F") {
EN_temp[ k ] += 1;
ENEMY[ k ].y +=Step;
if(app.frame % F_rate === 0) {
ENEMY[ k ].frameIndex = (ENEMY[ k ].frameIndex === 0) ? 2:0;
}
if(EN_temp[ k ] >= P_size/Step) {
EN_flag[ k ] = "stay";
}
} else {
EN_flag[ k ] = "stay";
}
}
if(EN_flag[ k ] == "stay") {
}
for(var m=0; m<EN_counter; m++) {
if(player.hitTestElement(ENEMY[ m ])) {
END_flag = "Game Over !";
SoundManager.play('GAMEOVER');
this.nextLabel ="title";
this.exit();
}
}
}
##Step-9: 敵をBOXで潰すことができるようにする
さらに敵をBOXで潰すことができるようにしました。 左側での対処プログラムを以下にしまします。
Step-9のデモ実行
if(EN_flag[ k ] == "left") {
ENEMY[ k ].frameIndex = 3;
if(pos[EC_x[ k ] - 1][EN_y[ k ]] == "F") {
EN_temp[ k ] += 1;
ENEMY[ k ].x -=Step;
if(app.frame % F_rate === 0) {
ENEMY[ k ].frameIndex = (ENEMY[ k ].frameIndex === 3) ? 5:3;
}
if(EN_temp[ k ] >= P_size/Step) {
EN_flag[ k ] = "stay";
}
}
else if((pos[EN_x[ k ]][EN_y[ k ]] == "B") && (pos[EN_x[ k ] - 1][EN_y[ k ]] != "F")) {
EN_temp = k;
ENEMY[ EN_temp ].tweener.fadeOut(1000).call(function(){
ENEMY[ EN_temp ].remove();
ENE_flag = 1;
}).play();
} else {
EN_flag[ k ] = "stay";
}
}
else if(EN_flag[ k ] == "right") {
##Reference
- github Cocos2d_Sample_games
- Home Page of Cocos2d-x
- Download page for Cocos2d-JS
- Home Page of PIKA's GAME
- Home Page of Pipoya
- Algoful : Algorithm for making a maze
- 無料効果音で遊ぼう!
##おまけ
JavaScriptベースのゲームエンジンは他にも多数ありますが、その内の幾つかで同様のゲームを作成してみました。 ソースファイルを覘くとゲームエンジン別の特徴がうかがえるかと思います。
- Cocos2d.JSでゲームを作ってみた
- Phaser3.JSでゲームを作ってみた
- Phina.JSでゲームを作ってみた(本記事です)
- Babylon.jsで3Dゲームを作成してみた(その1:迷路編)
- Babylon.jsで3Dゲームを作成してみた(その2:脱出パズル編)
以上