#Phaser3.JSで簡単なゲームを作ってみました
JavaScriptベースのゲームエンジンであるPhaser3.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 Phaser3.JS_Sample_games」からダウンロード可能です。 また、そのファイル構成は次の通りです。
・ images: 本ゲームで使用するグラフィックスを収容するフォルダーです。
・ js: ゲーム・プログラム本体を収納するフォルダーです。
- maze02.js: Step-7で使用する迷路作成プログラムです。
- phaser.js, phaser.min.js, phaser-debug.min.js, phlog.js: Phaser3.jsゲームエンジン本体と関連ファイルで、Phaser3.jsからダウンロードしたものです。
- sample_phaser_01.js ~ sample_phaser_07_with_sound_effect.js: 各ステップ用のゲーム・プログラムです。
- stages_01.js: Step-5, Step-6で使用するステージ・データです。
・ sound: サウンド・エフェクト用ファイルを収容するフォルダーです。
・ index_Phaser_01.html ~ index_Phaser_07.html: 各ステップでのGame開始用HTMLファイルです。
##ゲーム作成の説明
以下にゲーム作成の手順をステップバイステップで説明します。
###Step-1: 静止画グラフィックスを表示させる
始めにゲーム開始用のHTMLファイルを示します。 読込むJSファイルの設定等を行い、全てのステップでほぼ同一ですが、各ステップ毎にタイトルと「src="sample_phaser_01.js"」の部分を各ステップ番号に変更しています。
Step-1のデモ実行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Phaser Sample Game #01 by T. Fujita on 2019/9/11</title>
<style>
body {
margin: 0;
}
</style>
</head>
<body>
<div id="game"></div>
<script src="./js/phaser.js"></script>
<script src="./js/sample_phaser_01.js"></script>
</body>
</html>
次にメイン・プログラムである「sample_phaser_01.js」を示します。 背景画像とグラフィック(player)データを読み込み・表示します。
// Sample for phaser.js by T. Fujita on 2019/9/11 (Display a static character)
var config = {
type: Phaser.AUTO,
width: 800,
height: 600,
physics: {
default: 'arcade',
arcade: {
debug: false
}
},
scene: {
preload: preload,
create: create,
update: update
}
};
var scale = 1;
var P_size = 48 * scale;
var player;
var cursors;
var game = new Phaser.Game(config);
function preload ()
{
this.load.image('bg', './images/bg.jpg');
this.load.spritesheet('Player', './images/hukei_01.png', { frameWidth: 48, frameHeight: 48 });
}
function create ()
{
this.add.image(800, 600, 'bg');
player = this.physics.add.sprite(400, 300, 'Player');
}
function update ()
{
}
###Step-2: 静止画グラフィックスを移動させてみる
前述の「sample_phaser_01.js」にマウスをクリックした位置あるいはキー入力の検知を追加し、それに合わせてグラフィック(player)を移動させるものが「sample_phaser_02.js」です。 追加・修正個所を以下に示します。
Step-2のデモ実行
function create ()
{
this.add.image(800, 600, 'bg');
player = this.physics.add.sprite(400, 300, 'Player');
cursors = this.input.keyboard.createCursorKeys();
this.input.mouse.capture = true;
}
function update ()
{
if (cursors.left.isDown)
{
player.setVelocityX(-1 * speed);
}
else if (cursors.right.isDown)
{
player.setVelocityX(speed);
}
else if (cursors.up.isDown)
{
player.setVelocityY(-1 * speed);
}
else if (cursors.down.isDown)
{
player.setVelocityY(speed);
}
else if (game.input.mousePointer.isDown)
{
if (Math.floor(player.x / P_size) < Math.floor(game.input.mousePointer.x / P_size))
{
player.setVelocityX(speed);
}
else if (Math.floor(player.x / P_size) > Math.floor(game.input.mousePointer.x / P_size))
{
player.setVelocityX(-1 * speed);
}
if (Math.floor(player.y / P_size) < Math.floor(game.input.mousePointer.y / P_size))
{
player.setVelocityY(speed);
}
else if (Math.floor(player.y / P_size) > Math.floor(game.input.mousePointer.y / P_size))
{
player.setVelocityY(-1 * speed);
}
}
else
{
player.setVelocityX(0);
player.setVelocityY(0);
}
}
###Step-3: アニメーション・グラフィックスを表示させる
前述の「sample_phaser_01.js」を元にplayerをアニメーションさせる「sample_phaser_03.js」を作成します。 追加・変更部分を次に示します。 Phaser3.JSでは、歩行グラフィックス等のアニメーションには横に連続したグラフィクスが使用できます。
Step-3のデモ実行
function create ()
{
this.add.image(800, 600, 'bg');
player = this.physics.add.sprite(400, 300, 'Player');
this.anims.create({
key: 'down',
frames: this.anims.generateFrameNumbers('Player', { start: 0, end: 2 }),
frameRate: f_rate,
repeat: -1
});
}
function update ()
{
player.setVelocityX(0);
player.setVelocityY(0);
player.anims.play('down', true);
}
###Step-4: アニメーション・グラフィックスを移動させてみる
前述の「sample_phaser_02.js」と「sample_phaser_03.js」を組み合わせると共に前後(上下)左右それぞれの歩行グラフィックスを追加したものが「sample_phaser_04.js」です。 追加の歩行分を以下に示します。
Step-4のデモ実行
function create ()
{
this.add.image(800, 600, 'bg');
player = this.physics.add.sprite(400, 300, 'Player');
this.anims.create({
key: 'left',
frames: this.anims.generateFrameNumbers('Player', { start: 3, end: 5 }),
frameRate: f_rate,
repeat: -1
});
this.anims.create({
key: 'right',
frames: this.anims.generateFrameNumbers('Player', { start: 6, end: 8 }),
frameRate: f_rate,
repeat: -1
});
this.anims.create({
key: 'up',
frames: this.anims.generateFrameNumbers('Player', { start: 9, end: 11 }),
frameRate: f_rate,
repeat: -1
});
this.anims.create({
key: 'down',
frames: this.anims.generateFrameNumbers('Player', { start: 0, end: 2 }),
frameRate: f_rate,
repeat: -1
});
cursors = this.input.keyboard.createCursorKeys();
this.physics.add.collider(player);
}
function update ()
{
if (cursors.left.isDown)
{
player.setVelocityX(-1 * speed);
player.anims.play('left', true);
}
else if (cursors.right.isDown)
{
player.setVelocityX(speed);
player.anims.play('right', true);
}
else if (cursors.up.isDown)
{
player.setVelocityY(-1 * speed);
player.anims.play('up', true);
}
else if (cursors.down.isDown)
{
player.setVelocityY(speed);
player.anims.play('down', true);
}
else if (game.input.mousePointer.isDown)
{
if (Math.floor(player.x / P_size) < Math.floor(game.input.mousePointer.x / P_size))
{
player.setVelocityX(speed);
player.anims.play('right', true);
}
else if (Math.floor(player.x / P_size) > Math.floor(game.input.mousePointer.x / P_size))
{
player.setVelocityX(-1 * speed);
player.anims.play('left', true);
}
if (Math.floor(player.y / P_size) < Math.floor(game.input.mousePointer.y / P_size))
{
player.setVelocityY(speed);
player.anims.play('down', true);
}
else if (Math.floor(player.y / P_size) > Math.floor(game.input.mousePointer.y / P_size))
{
player.setVelocityY(-1 * speed);
player.anims.play('up', true);
}
}
else
{
player.setVelocityX(0);
player.setVelocityY(0);
}
}
###Step-5: 壁等を表示させ、移動を制限する
「sample_phaser_04.js」に「stages_01.js」で設定する各ステージを読み込みタイルデータを表示させたものが「sample_phaser_05.js」です。 ここでは、壁を通り抜け出来ない設定とゴール、再度実行(Aタイル)のみ対応するようにしています。
Step-5のデモ実行
for (var i=0; i<ROOM.length; i++) {
temp[i] = [];
for (var j=0; j<ROOM[i].length; j++) {
temp[i][j] = ROOM[i].substr(j,1);
if(ROOM[i].substr(j,1) == "P") {
player = this.physics.add.sprite(j * P_size, i * P_size, 'Player').setOrigin(0, 0).setScale(scale);
}
else if(ROOM[i].substr(j,1) == "A") {
again = this.add.image(j * P_size, i * P_size, 'Again').setOrigin(0, 0).setScale(scale);
}
else if(ROOM[i].substr(j,1) == "G") {
goal = this.physics.add.sprite(j * P_size, i * P_size, 'Goal').setOrigin(0, 0).setScale(scale);
}
else if(ROOM[i].substr(j,1) == "B") {
BLOCK[BL_counter] = this.physics.add.sprite(j * P_size, i * P_size, 'Block').setOrigin(0, 0).setScale(scale);
BL_counter = BL_counter + 1;
}
else if(ROOM[i].substr(j,1) == "w") {
wall_0 = this.physics.add.sprite(j * P_size, i * P_size, 'Wall_0').setOrigin(0, 0).setScale(scale);
}
else if(ROOM[i].substr(j,1) == "W") {
wall_1 = this.physics.add.sprite(j * P_size, i * P_size, 'Wall_1').setOrigin(0, 0).setScale(scale);
}
}
}
// Process for reaching the goal
function Check_Goal() {
if((pos[px][py] == "G") && (Goal_flag == 0)) {
Goal_flag = 1;
End_flag = "Goal !";
counter = counter + 1;
game.scene.start('Next_Game');
}
}
//Scene_NextGame
class Scene_NextGame extends Phaser.Scene {
constructor (){
super({ key: 'Next_Game' });
}
create(){
let sceneName01 = this.add.text(400, 150, End_flag).setFontSize(30).setFontFamily("Arial").setOrigin(0.5).setColor("#FF0000").setInteractive();
let sceneName02 = this.add.text(400, 250, 'ここで使用しているグラフィクスの一部に「どらぴか」様 \n URL: https://dorapika.wixsite.com/pikasgame \n作成の素材を使用しております。').setFontSize(20).setFontFamily("Arial").setOrigin(0.5).setColor("#FF0000").setInteractive();
let change = this.add.text(400, 350, 'Push Here for Continue !').setFontSize(30).setFontFamily("Arial").setColor("#FF0000").setOrigin(0.5).setInteractive();
change.on('pointerdown', function (pointer) {
sceneName01.destroy();
sceneName02.destroy();
change.destroy();
game.scene.start('Game_Start');
}, this);
}
};
各ステージを構成する「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: ボックスを押して移動可能にさせる
「sample_phaser_06.js」でplayerがBoxを押して移動できるようにしました。 これでゲームの基本部分は完成です。
Step-6のデモ実行
if( flag == "left") {
player.anims.play('left', true);
if((pos[px - 1][py] =="w") ||(pos[px - 1][py] =="W")) {
player.x = px * P_size;
player.y = py * P_size;
} else if((pos[px - 1][py] =="B") && (pos[px - 2][py] =="F")) {
pos[px][py] = "F";
pos[px - 1][py] = "P";
pos[px - 2][py] = "B";
for(var i=0; i<BLOCK.length; i++) {
if((Math.floor(BLOCK[i].x / P_size) == (px - 1)) && (Math.floor(BLOCK[i].y / P_size) == py)) {
BL_counter = i;
}
}
nextBX = px * P_size - P_size * 2;
nextBY = py * P_size;
this.tweens.add({
targets: BLOCK[BL_counter],
x: nextBX,
y: nextBY,
duration: speed,
}, this);
nextX = px * P_size - P_size;
nextY = py * P_size;
this.tweens.add({
targets: player,
x: nextX,
y: nextY,
duration: speed,
}, this);
} else if ((pos[px - 1][py] =="B") && (pos[px - 2][py] !="F")) {
player.x = px * P_size;
player.y = py * P_size;
} else {
if(pos[px - 1][py] != "G") {
pos[px][py] = "F";
pos[px - 1][py] = "P";
}
nextX = px * P_size - P_size;
nextY = py * P_size;
this.tweens.add({
targets: player,
x: nextX,
y: nextY,
duration: speed,
}, this);
}
} else if( flag == "right") {
さらにゴール時の効果音を追加した「sample_phaser_06_with_sound_effect.js」を以下に示します。
Step-6効果音付きのデモ実行
preload ()
{
this.load.image('bg', './images/bg.jpg');
this.load.spritesheet('Player', './images/hukei_01.png', { frameWidth: 48, frameHeight: 48 });
this.load.image('Again', './images/A_48.png');
this.load.image('Block', './images/Box_06.png');
this.load.image('Goal', './images/Goal_00.png');
this.load.image('Wall_0', './images/Block_04.png');
this.load.image('Wall_1', './images/Block_05.png');
this.load.audio('GOAL', ['./sound/info-girl1-goal1.mp3']);
}
create ()
{
this.add.image(800, 600, 'bg');
wall_0 = this.physics.add.staticGroup();
wall_1 = this.physics.add.staticGroup();
goal_sound = this.sound.add('GOAL');
End_flag = " ";
BL_flag = 0;
// Process for reaching the goal
function Check_Goal() {
if((pos[px][py] == "G") && (Goal_flag == 0)) {
Goal_flag = 1;
End_flag = "Goal !";
counter = counter + 1;
this.goal_sound.play();
game.scene.start('Next_Game');
}
}
###Step-7: 迷路を作成しアニメーション・グラフィックスを移動させてみる
「sample_phaser_06_with_sound_effect.js」を若干変更し、「maze_02.js」で作成した迷路を表示させたものが「sample_phaser_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;
}
##Reference
- github Phaser3.JS_Sample_games
- Phaser3.JS
- Home Page of PIKA's GAME
- Home Page of Pipoya
- Algoful : Algorithm for making a maze
- Home Page of soundeffect-lab
##おまけ
JavaScriptベースのゲームエンジンは他にも多数ありますが、その内の幾つかで同様のゲームを作成してみました。 ソースファイルを覘くとゲームエンジン別の特徴がうかがえるかと思います。
- Cocos2d.JSでゲームを作ってみた
- Phaser3.JSでゲームを作ってみた(本記事です)
- Phina.JSでゲームを作ってみた
- Babylon.jsで3Dゲームを作成してみた(その1:迷路編)
- Babylon.jsで3Dゲームを作成してみた(その2:脱出パズル編)
以上