Phaser3.JSでゲームを作ってみた

#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では動作しませんので悪しからず。
また、ここで使用しているグラフィックの一部に「[​どらぴか様](https://dorapika.wixsite.com/pikasgame)」ならびに「[ぴぽや様](http://blog.pipoya.net/)」作成の素材をサウンドエフェクトには「[効果音ラボ様](https://soundeffect-lab.info/)」作成の素材を使用させていただきました。　フリー素材を提供して下さった各位に感謝いたします。
##ファイル構成
ここで説明する内容に関するファイルは全て「[GitHub Phaser3.JS_Sample_games](https://github.com/To-Fujita/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のデモ実行](https://to-fujita.github.io/Phaser3.JS_Sample_Games/index_Phaser_01.html)

```html:index_Phaser_01.html
<!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）データを読み込み・表示します。

```javascript:sample_phaser_01.js
// 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のデモ実行](https://to-fujita.github.io/Phaser3.JS_Sample_Games/index_Phaser_02.html)

```javascript:sample_phaser_02.js（追加・修正部分）
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のデモ実行](https://to-fujita.github.io/Phaser3.JS_Sample_Games/index_Phaser_03.html)

```javascript:sample_phaser_03.js（追加・修正部分）
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のデモ実行](https://to-fujita.github.io/Phaser3.JS_Sample_Games/index_Phaser_04.html)

```javascript:sample_phaser_04.js（追加・修正部分）
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のデモ実行](https://to-fujita.github.io/Phaser3.JS_Sample_Games/index_Phaser_05.html)

```javascript:sample_phaser_05.js（タイルデータ表示部分）
	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);
		}
	    }
	}
```

```javascript:sample_phaser_05.js（ゴール対応部分）
// 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」の一部を以下に示します。　メモ帳等のエディターで修正・追加してみてください。

```javascript: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のデモ実行](https://to-fujita.github.io/Phaser3.JS_Sample_Games/index_Phaser_06.html)

```javascript:sample_phaser_06.js（左側の壁対応にBOX移動を追加した部分）
	    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効果音付きのデモ実行](https://to-fujita.github.io/Phaser3.JS_Sample_Games/index_Phaser_06_with_sound_effect.html)

```javascript:sample_phaser_06_with_sound_effect.js（preload部分とcreate部分）
    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;
```


```javascript:sample_phaser_06_with_sound_effect.js（ゴール時の効果音を追加した部分）
// 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](http://algoful.com/Archive/Algorithm/MazeDig)」のアルゴリズムを使用させていただきました。
[Step-7のデモ実行](https://to-fujita.github.io/Phaser3.JS_Sample_Games/index_Phaser_07.html
)

```javascript:maze_02.js
// 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
1. [github Phaser3.JS_Sample_games](https://github.com/To-Fujita/Phaser3.JS_Sample_Games)
2. [Phaser3.JS](https://phaser.io/phaser3)
3. [Home Page of PIKA's GAME](https://dorapika.wixsite.com/pikasgame)
4. [Home Page of Pipoya](http://blog.pipoya.net/)
5. [Algoful : Algorithm for making a maze](http://algoful.com/Archive/Algorithm/MazeDig)
6. [Home Page of soundeffect-lab](https://soundeffect-lab.info/)

##おまけ
JavaScriptベースのゲームエンジンは他にも多数ありますが、その内の幾つかで同様のゲームを作成してみました。　ソースファイルを覘くとゲームエンジン別の特徴がうかがえるかと思います。

- [Cocos2d.JSでゲームを作ってみた](https://qiita.com/halboujp/items/3d99c122bdc471caa5b8)
- Phaser3.JSでゲームを作ってみた（本記事です）
- [Phina.JSでゲームを作ってみた](https://qiita.com/halboujp/items/1cae099645d7a4e1bf2f)
- [Babylon.jsで3Dゲームを作成してみた（その１：迷路編）](https://qiita.com/halboujp/items/8b4b07136420c5defb13)　　
- [Babylon.jsで3Dゲームを作成してみた（その２：脱出パズル編）](https://qiita.com/halboujp/items/751c42f3bda10adb7674)　

以上