1. HOME»
  2. プログラミング・Web»
  3. JavaScript»
  4. JavaScriptでRPGを作ろう!スマホにも対応したゲームの作り方

JavaScriptでRPGを作ろう!スマホにも対応したゲームの作り方

山田

ふみ……ふみ……

りこ

あ、山田先生が、ふみふみしてる! かわいい!

山田

ふみ……ふみ……うむ。うどんを作っているんだべ

りこ

えぇ、うどん作ってたんだ!

アル

へぇ、うどんってこうやって作るのかぁ

りこ

うどんはできた?

山田

うむ……あれ? ぜんぜんこねられてないべ……

りこ

ははは、きっとかるすぎたのね

アル

ねぇ、ふくろあなだらけだよ……

山田

し……しまったべ! ツメで穴がいてしまったべ……

目次
  1. ゲーム画面をブラウザのサイズに拡大して表示してみよう!
  2. ほかのキャラクターが後ろをついてくるようにしよう!
  3. パーティメンバー全員を、アニメーションさせてみよう!
  4. シーンが変更されたときに一度だけ呼ばれるメソッドを作ろう!

ゲーム画面をブラウザのサイズに拡大して表示してみよう!

つづいて、ゲームの画面がめんブラウザのサイズに拡大かくだいして表示してみましょう!

山田

次はゲームの画面をブラウザサイズにわせてみるべ!

アル

おぉっ、ゲーム画面が小さいなって、ちょっと思ってたんだ!

りこ

見やすくなるね!

js/engine/game.js

'use strict'

/**
 * ゲームづくりの基本となるクラス
 */
class Game {

	/**
	 * 引数
	 * width : ゲームの横幅
	 * height : ゲームの縦幅
	 */
	constructor( width, height ) {
		//canvas要素を作成
		this.canvas = document.createElement( 'canvas' );
		//作成したcanvas要素をbodyタグに追加
		document.body.appendChild( this.canvas );
		//canvasの横幅(ゲームの横幅)を設定。もし横幅が指定されていなければ320を代入
		this.canvas.width = width || 320;
		//canvasの縦幅(ゲームの縦幅)を設定。もし縦幅が指定されていなければ320を代入
		this.canvas.height = height || 320;

		//シーンを入れておくための配列
		this.scenes = [];
		//現在のシーンをいれておくためのもの
		this.currentScene;

		//ゲームに使用するキーと、そのキーが押されているかどうかを入れるための連想配列
		//例 { up: false, down: false }
		this.input = {};
		//登録されたキーに割り当てられたプロパティ名と、キー名を、関連づけるための連想配列
		//例 { up: "ArrowUp", down: "ArrowDown" }
		this._keys = {};
	} //constructor() 終了

	/**
	 * startメソッドを呼び出すことで、メインループが開始される
	 */
	start() {
		//デフォルトのキーバインドを登録する(使いたいキーを登録する)
		this.keybind( 'up', 'ArrowUp' );
		this.keybind( 'down', 'ArrowDown' );
		this.keybind( 'right', 'ArrowRight' );
		this.keybind( 'left', 'ArrowLeft' );

		//現在のシーン(currentScene)になにも入っていないときは、scenes[0]を代入
		this.currentScene = this.currentScene || this.scenes[0];

		//ゲームがはじまったときと、ブラウザのサイズが変わったときに呼ばれる。縦横の比を変えずに、canvasを拡大縮小できる
		const _resizeEvent = () => {
			//ブラウザとcanvasの比率の、縦と横を計算し、小さいほうを_ratioに代入する
			const _ratio = Math.min( innerWidth / this.canvas.width, innerHeight / this.canvas.height );
			//canvasのサイズを、ブラウザに合わせて変更する
			this.canvas.style.width = this.canvas.width*_ratio + 'px';
			this.canvas.style.height = this.canvas.height*_ratio + 'px';
		} //_resizeEvent() 終了

		//ブラウザのサイズが変更されたとき、_resizeを呼び出す
		addEventListener( 'resize', _resizeEvent, { passive: true } );
		//_resizeを呼び出す
		_resizeEvent();

		//メインループを呼び出す
		this._mainLoop();

		//イベントリスナーをセットする
		this._setEventListener();
	} //start() 終了

	/**
	 * イベントリスナーをセットするためのメソッド
	 */
	_setEventListener() {
		//なにかキーが押されたときと、はなされたときに呼ばれる
		const _keyEvent = e => {
			//デフォルトのイベントを発生させない
			e.preventDefault();
			//_keysに登録された数だけ繰り返す
			for ( let key in this._keys ) {
				//イベントのタイプによって呼び出すメソッドを変える
				switch ( e.type ) {
					case 'keydown' :
						//押されたキーが、登録されたキーの中に存在するとき、inputのそのキーをtrueにする
						if ( e.key === this._keys[key] ) this.input[key] = true;
						break;
					case 'keyup' :
						//押されたキーが、登録されたキーの中に存在するとき、inputのそのキーをfalseにする
						if ( e.key === this._keys[key] ) this.input[key] = false;
						break;
				}
			}
		}
		//なにかキーが押されたとき
		addEventListener( 'keydown', _keyEvent, { passive: false } );
		//キーがはなされたとき
		addEventListener( 'keyup', _keyEvent, { passive: false } );

		//画面がタッチされたり、指が動いたりしたときなどに呼ばれる
		//シーンや、スプライトなどのオブジェクトの左上端から見た、それぞれの指の位置を取得できるようになる
		const _touchEvent = e => {
			//デフォルトのイベントを発生させない
			e.preventDefault();
			//タッチされた場所などの情報を取得
			const _touches = e.changedTouches[0];
			//ターゲット(今回はcanvas)のサイズ、ブラウザで表示されている部分の左上から見てどこにあるか、などの情報を取得
			const _rect = _touches.target.getBoundingClientRect();
			//タッチされた場所を計算
			const _fingerPosition = {
				x: ( _touches.clientX - _rect.left ) / _rect.width * this.canvas.width,
				y: ( _touches.clientY - _rect.top ) / _rect.height * this.canvas.height
			};
			//イベントのタイプを_eventTypeに代入
			const _eventType = e.type;
			//タッチイベントを割り当てるためのメソッドを呼び出す
			this.currentScene.assignTouchevent( _eventType, _fingerPosition );
		} //_touchEvent() 終了

		//タッチされたとき
		this.canvas.addEventListener( 'touchstart', _touchEvent, { passive: false } );
		//指が動かされたとき
		this.canvas.addEventListener( 'touchmove', _touchEvent, { passive: false } );
		//指がはなされたとき
		this.canvas.addEventListener( 'touchend', _touchEvent, { passive: false } );
	} //_setEventListener() 終了

	/**
	 * メインループ
	 */
	_mainLoop() {
		//画家さん(コンテキスト)を呼ぶ
		const ctx = this.canvas.getContext( '2d' );
		//塗りつぶしの色に、黒を指定する
		ctx.fillStyle = '#000000';
		//左上から、画面のサイズまでを、塗りつぶす
		ctx.fillRect( 0, 0, this.canvas.width, this.canvas.height );

		//現在のシーンのupdateメソッドを呼び出す
		this.currentScene.update();

		//現在のシーンの、ゲームに登場する全てのもの(オブジェクト)の数だけ繰り返す
		for ( let i=0; i<this.currentScene.objs.length; i++ ) {
			//現在のシーンの、すべてのオブジェクトのupdateメソッドを呼び出す
			this.currentScene.objs[i].update( this.canvas );
		}

		//自分自身(_mainLoop)を呼び出して、ループさせる
		requestAnimationFrame( this._mainLoop.bind( this ) );
	} //_mainLoop() 終了

	/**
	 * ゲームにシーンに追加できるようになる、addメソッドを作成
	 *
	 * 引数
	 * scene : 追加したいシーン
	 */
	add( scene ) {
		//引数がSceneのとき、this.scenesの末尾にsceneを追加
		if ( scene instanceof Scene ) this.scenes.push( scene );
		//引数がSceneでなければ、コンソールにエラーを表示
		else console.error( 'Gameに追加できるのはSceneだけだよ!' );
	} //add()終了

	/**
	 * 使いたいキーを登録できるようになる、keybindメソッドを作成
	 *
	 * 引数
	 * name : キーにつける名前
	 * key : キーコード
	 */
	keybind( name, key ) {
		//キーの名前と、キーコードを関連づける
		this._keys[name] = key;
		//キーが押されているかどうかを入れておく変数に、まずはfalseを代入しておく
		this.input[name] = false;
	} //keybind() 終了

}
山田

まず50〜56行目で、ゲーム画面のサイズを変更へんこうするための関数かんすうを作っているべ!

りこ

ふむふむ

山田

52行目では、ブラウザ表示領域ひょうじりょういきとキャンバスのサイズの比率ひりつを、縦横たてよこ両方りょうほう計算けいさんして、小さい方を変数へんすう_ratioにおぼえておいてもらっているんだべ

りこ

なるほど、Math.minってのが、小さい方を取得しゅとくできるのね!

アル

へぇ。でもどうして小さい方を覚えておいてもらうの?

山田

ゲームによっては、画面が横長だったり縦長だったりするべな?

アル

うん。パソコン用は横長で、スマホ用は縦長だったりするよね

山田

今回取得した比率は「ブラウザはばはキャンバスの幅の何%か」というものだべ
もちろんこれは、100%以上の数値すうちになることの方が多いべ!

りこ

あ、ゲームのサイズはブラウザのサイズより小さいことの方が多いからだ!

山田

そういうことだべ
そしてこの「何%か」というのが、縦幅と横幅でどちらが小さいかを比べるべ
そして小さいほうの「何%か」を、縦幅と横幅の両方にかけ算するんだべ! これが54〜55行目だべな

りこ

そっか。同じ数を縦幅と横幅にかければ、ゲームの縦横の比率をたもったまま、拡大できるんだ!

アル

なるほど。縦横の比率を保ったまま、ブラウザのサイズに合わせられるんだね!

山田

そして、59行目はブラウザのサイズが変更されたとき、61行目はゲームがはじまったときに、この関数を呼び出しているんだべ

アル

これでつねにブラウザのサイズに合わせられるんだ!

ゲームがブラウザのサイズに合わせて拡大縮小するようになる

ほかのキャラクターが後ろをついてくるようにしよう!

つづいて、他のキャラクターが後ろをついてくるようにしてみましょう!

山田

さて、いまはわたすだけがマップを歩いている状態じょうたいだべ……
なんだかさみしいべな……

りこ

うーん、たしかにちょっとさみしいね

アル

山田先生だけ冒険ぼうけんにでかけるなんてずるい! さみしい!

りこ

そっち!?

山田

いっしょに行きたいべか?

アル

行きたい!

りこ

わたしも!

山田

よし、だべ
では、engineフォルダに、party.jsを作成して欲しいんだべ

rpg/
|-- img (省略)/
|-- index.html
|-- js/
|   |-- engine/
|   |   |-- game.js
|   |   |-- dpad.js
|   |   |-- charactertile.js
|   |   |-- party.js (追加)
|   |   |-- scene.js
|   |   |-- sprite.js
|   |   |-- text.js
|   |   |-- tile.js
|   |   `-- tilemap.js
|   `-- main.js
`-- sound (省略)/
山田

そしてindex.htmlから、party.jsを読み込むべ!

index.html

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
	<title>くろねこラビリンス</title>
	<style>
		* {
			padding: 0;
			margin: 0;
		}
		canvas {
			display: block;
		}
	</style>
</head>
<body>
	<script src="js/engine/scene.js"></script>
	<script src="js/engine/sprite.js"></script>
	<script src="js/engine/dpad.js"></script>
	<script src="js/engine/party.js"></script>
	<script src="js/engine/text.js"></script>
	<script src="js/engine/tile.js"></script>
	<script src="js/engine/charactertile.js"></script>
	<script src="js/engine/tilemap.js"></script>
	<script src="js/engine/game.js"></script>
	<script src="js/main.js"></script>
</body>
</html>
山田

さらにparty.jsは、このようにするべ!

js/engine/party.js

'use strict'

/**
 * パーティに関してのクラス
 */
class Party {

	/**
	 * 引数には、パーティのメンバーを制限なく入れることができる
	 */
	constructor() {
		//引数をthis.memberに追加
		this.member = arguments;
		//パーティの移動速度
		this.speed = 1;
	} //constructor() 終了

	/**
	 * 先頭以外のキャラの、移動速度を設定するためのメソッド
	 *
	 * 引数
	 * direction : 先頭のキャラの移動方向
	 */
	setMemberVelocity( direction ) {
		//先頭以外のキャラの数だけ繰り返す
		for ( let i=1; i<this.member.length; i++ ) {
			//ひとつ前のキャラとX座標が同じときの、計算用変数
			let _samePrevX;
			//ひとつ前のキャラとY座標が同じときの、計算用変数
			let _samePrevY;
			//ひとつ前のキャラよりもX座標が小さいときの、計算用変数
			let _lessThanPrevX;
			//ひとつ前のキャラよりもY座標が小さいときの、計算用変数
			let _lessThanPrevY;
			//ひとつ前のキャラよりもX座標大きいときの、計算用変数
			let _moreThanPrevX;
			//ひとつ前のキャラよりもY座標が大きいときの、計算用変数
			let _moreThanPrevY;
			//ひとつ前のキャラよりも、タイルひとつ分以上離れているときの計算用変数
			let _moreThanOneTile;

			//先頭のキャラの移動方向によって、それぞれの変数に値を代入する
			if ( direction === 'up' ) {
				_samePrevX = 0;
				_samePrevY = 1
				_lessThanPrevX = -1;
				_lessThanPrevY = 0;
				_moreThanPrevX = 1;
				_moreThanPrevY = 2;
				_moreThanOneTile = -1;
			}
			else if ( direction === 'down' ) {
				_samePrevX = 0;
				_samePrevY = -1;
				_lessThanPrevX = -1;
				_lessThanPrevY = -2;
				_moreThanPrevX = 1;
				_moreThanPrevY = 0;
				_moreThanOneTile = 1;
			}
			else if ( direction === 'left' ) {
				_samePrevX = 1;
				_samePrevY = 0;
				_lessThanPrevX = 0;
				_lessThanPrevY = -1;
				_moreThanPrevX = 2;
				_moreThanPrevY = 1;
				_moreThanOneTile = -1;
			}
			else if ( direction === 'right' ) {
				_samePrevX = -1;
				_samePrevY = 0;
				_lessThanPrevX = -2;
				_lessThanPrevY = -1;
				_moreThanPrevX = 0;
				_moreThanPrevY = 1;
				_moreThanOneTile = 1;
			}

			//ひとつ前のキャラとX,Y座標が同じとき
			if ( this.member[i-1].mapX === this.member[i].mapX ) this.member[i].vx = this.speed * _samePrevX;
			if ( this.member[i-1].mapY === this.member[i].mapY ) this.member[i].vy = this.speed * _samePrevY;

			//ひとつ前のキャラのほうが、X座標が小さいとき
			if ( this.member[i-1].mapX < this.member[i].mapX ) {
				this.member[i].vx = this.speed * _lessThanPrevX;
				//先頭のキャラの移動方向が左で、ひとつ前のキャラとのX座標の差が、タイルひとつよりも大きいとき
				if ( direction === 'left' && this.member[i].mapX - this.member[i-1].mapX > 1 ) this.member[i].vx = this.speed * _moreThanOneTile;
			}
			//ひとつ前のキャラのほうが、X座標が大きいとき
			if ( this.member[i-1].mapX > this.member[i].mapX ) {
				this.member[i].vx = this.speed * _moreThanPrevX;
				//先頭のキャラの移動方向が右で、ひとつ前のキャラとのX座標の差が、タイルひとつよりも大きいとき
				if ( direction === 'right' && this.member[i-1].mapX - this.member[i].mapX > 1 ) this.member[i].vx = this.speed * _moreThanOneTile;
			}
			//ひとつ前のキャラのほうが、Y座標が小さいとき
			if ( this.member[i-1].mapY < this.member[i].mapY ) {
				this.member[i].vy = this.speed * _lessThanPrevY;
				//先頭のキャラの移動方向が上で、ひとつ前のキャラとのY座標の差が、タイルひとつよりも大きいとき
				if ( direction === 'up' && this.member[i].mapY - this.member[i-1].mapY > 1 ) this.member[i].vy = this.speed * _moreThanOneTile;
			}
			//ひとつ前のキャラのほうが、Y座標が大きいとき
			if ( this.member[i-1].mapY > this.member[i].mapY ) {
				this.member[i].vy = this.speed * _moreThanPrevY;
				//先頭のキャラの移動方向が下で、ひとつ前のキャラとのY座標の差が、タイルひとつよりも大きいとき
				if ( direction === 'down' && this.member[i-1].mapY - this.member[i].mapY > 1 ) this.member[i].vy = this.speed * _moreThanOneTile;
			}
		}
	} //setMemberVelocity() 終了
}
山田

このPartyクラスは、パーティをつくるための機能きのうをクラスにしたものだべ
今回は、キャラクターをパーティに追加して、仲間なかまが後ろをついてきてくれるようになる機能を作ってみたべよ

アル

なんだかプログラムが複雑ふくざつだよ……

りこ

うん。むずかしい……

山田

そうなんだべよ……こんかいのRPGでは、キャラクターではなくマップが動くべから、マップの移動いどうに合わせて、後ろの仲間を移動させなくてはいけないんだべよ

この移動は、すす方向ほうこうや、ひとつ前のキャラの位置いちによって、変わってくるんだべ

アル

そりゃ複雑になるわけだ……

山田

そこで、それぞれのキャラの移動量いどうりょう計算けいさんする変数を作ってみたべ。それが27〜40行目だべよ

りこ

ひとつ前のキャラとの位置の違いによって、この変数それぞれに数値すうち代入だいにゅうすることになるのね!

山田

うむ。42〜78行目で、先頭せんとうのキャラの移動方向いどうほうこうよって、移動量を計算するためのあたいを代入しているべよ

りこ

すごくむずかしいけど、とにかくこの数値をもとに、移動量が計算できるのよね

山田

そうだべよ。そして実際じっさいに計算しているのが、80〜107行目だべよ

りこ

へぇ、こうやっていろんな場合を考えて、移動量を計算してるのね!

山田

そうなんだべ

アル

ねぇ、はやくためしてみようよ!

山田

では、main.jsをこのようにするべよ!

js/main.js

'use strict'

//ブラウザがページを完全に読みこむまで待つ
addEventListener( 'load', () => {

	//変数gameに、あなたはゲームですよ、と教える
	const game = new Game();
	//使いたいキーとして、スペースキーを登録する
	game.keybind( 'space', ' ' );

	//タイトルシーン
	const titleScene = () => {

		//変数sceneに、あなたはシーンですよ、と教える
		const scene = new Scene();

		//変数titleTextに、あなたは「くろねこラビリンス」というテキストだよ、と教える
		const titleText = new Text( 'くろねこラビリンス' );
		//テキストを上下左右中央の位置にする
		titleText.center().middle();
		//シーンにテキストを追加
		scene.add( titleText );

		//シーンがタッチされたとき
		scene.ontouchstart = () => {
			//メインシーンに切り替える
			game.currentScene = mainScene();
		} //scene.ontouchstart() 終了

		//ループから常に呼び出される
		scene.onenterframe = () => {
			//スペースキーが押されたとき、メインシーンに切り替える
			if ( game.input.space ) game.currentScene = mainScene();
		} //scene.onenterframe() 終了

		//作ったシーンを返す
		return scene;

	} //titleScene() 終了

	//メインシーン
	const mainScene = () => {

		//マップの作成
		const map = [
			[11,11,11,11,11,11,11,11,11,11],
			[11,10,10,10,10,10,10,10,10,11],
			[11, 4, 4, 4, 4, 4, 4, 4, 4,11],
			[11, 4,11, 4, 4,11,11,11, 4,11],
			[11, 4,11,11,11,11,10,10, 4,11],
			[11, 4,11,10,10,11, 4, 4, 4,11],
			[11, 4,11, 4, 4,11,11,11, 4,11],
			[11, 4, 9, 4, 4, 9,10,11, 4,11],
			[11, 4, 4, 4, 4, 4, 4,11, 4,11],
			[11,11,11,11,11,11,11,11,11,11]
		];
		//タイルのサイズ
		const TILE_SIZE = 32;
		//歩く速さ
		const WALKING_SPEED = 4;

		//変数sceneに、あなたはシーンですよ、と教える
		const scene = new Scene();

		//変数tilemapに、あなたはタイルマップですよ、と教える
		const tilemap = new Tilemap( 'img/tile.png' );
		//tilemap.dataに、どんなマップなのか教える
		tilemap.data = map;
		//マップ全体の位置をずらす
		tilemap.x = TILE_SIZE*4 - TILE_SIZE/2;
		tilemap.y = TILE_SIZE*3 - TILE_SIZE/2;
		//移動できないタイルを指定する
		tilemap.obstacles = [0, 3, 6, 7, 8, 9, 10, 11];
		//マップを登録する
		scene.add( tilemap );

		//変数startに、あなたはスタートのタイルですよ、と教える
		const start = new Tile( 'img/start.png' );
		//マップ左上からの座標を指定する
		start.x = TILE_SIZE;
		start.y = TILE_SIZE*2;
		//スタートのタイルを、tilemapに追加して、とお願いする
		tilemap.add( start );

		//変数goalに、あなたはゴールのタイルですよ、と教える
		const goal = new Tile( 'img/goal.png' );
		//マップ左上からの座標を指定する
		goal.x = TILE_SIZE*8;
		goal.y = TILE_SIZE*8;
		//ゴールのタイルを、tilemapに追加して、とお願いする
		tilemap.add( goal );

		//変数yamadaに、あなたは山田先生のキャラクタータイルですよ、と教える
		const yamada = new CharacterTile( 'img/yamada.png' );
		//山田先生を画面の中央に配置
		yamada.x = yamada.y = TILE_SIZE*5 - TILE_SIZE/2;
		//タイルマップの動きと同期させない
		yamada.isSynchronize = false;
		//tilemapに、山田先生のタイルを追加して、とお願いする
		tilemap.add( yamada );

		//変数ricoに、あなたはりこちゃんのキャラクタータイルですよ、と教える
		const rico = new CharacterTile( 'img/rico.png' );
		//りこちゃんの位置を決める
		rico.x = TILE_SIZE*7 - TILE_SIZE/2;
		rico.y = TILE_SIZE*5 - TILE_SIZE/2;
		//タイルマップの動きと同期させない
		rico.isSynchronize = false;
		//tilemapに、りこちゃんのキャラクタータイルを追加して、とお願いする
		tilemap.add( rico );

		//変数aruに、あなたはアルくんのキャラクタータイルですよ、と教える
		const aru = new CharacterTile( 'img/aru.png' );
		//アルくんの位置を決める
		aru.x = TILE_SIZE*7 - TILE_SIZE/2;
		aru.y = TILE_SIZE*6 - TILE_SIZE/2;
		//タイルマップの動きと同期させない
		aru.isSynchronize = false;
		//tilemapに、アルくんのキャラクタータイルを追加して、とお願いする
		tilemap.add( aru );

		//変数partyに、あなたは山田先生とりこちゃんとアルくんのパーティですよ、と教える
		const party = new Party( yamada, rico, aru );
		//パーティの歩く速さに、WALKING_SPEEDの値を代入する
		party.speed = WALKING_SPEED;

		//変数dpadに、あなたはD-Padですよ、と教える
		const dpad = new DPad( 'img/dpad.png', 80 );
		//D-Padの位置を指定する
		dpad.x = 10;
		dpad.y = 230;
		//sceneに、D-Padを追加して、とお願いする
		scene.add( dpad );

		//キャラクターのアニメーションのための変数
		let toggleForAnimation = 0;
		//ゴールのテキストが表示されているかどうかの変数
		let hasDisplayedGoalText = false;
		//移動可能かどうかの変数
		let isMovable = true;

		//ループから常に呼び出される
		scene.onenterframe = () => {
			//タイルマップの位置がタイルのサイズで割り切れるとき
			if ( ( tilemap.x - TILE_SIZE/2 ) % TILE_SIZE === 0 && ( tilemap.y - TILE_SIZE/2 ) % TILE_SIZE === 0 ) {
				//タイルマップの移動速度に0を代入する
				tilemap.vx = tilemap.vy = 0;
				//パーティ全員の移動速度に0を代入する
				for ( let i in party.member ) {
					party.member[i].vx = party.member[i].vy = 0;
				}
				//山田先生の画像を切り替える
				yamada.animation = 1;

				//山田先生のタイルがゴールのタイルと重なっているとき、イベントを発生させる
				if ( yamada.isOverlapped( goal ) ) {
					//ゴールのテキストが表示されていないとき
					if ( !hasDisplayedGoalText ) {
						//変数goalTextに、あなたは「ゴールだべ!」というテキストだよ、と教える
						const goalText = new Text( 'ゴールだべ!' );
						//テキストサイズを変更
						goalText.size = 50;
						//テキストを上下左右中央の位置にする
						goalText.center().middle();
						//シーンにテキストを追加
						scene.add( goalText );
						//ゴールのテキストが表示されているかどうかの変数にtrueを代入
						hasDisplayedGoalText = true;
						//移動ができないようにする
						isMovable = false;
						//6秒たったら、タイトルシーンに切り替える
						setTimeout( () => {
							game.currentScene = titleScene();
						}, 6000 );
					}
				}

				//移動可能なとき
				if ( isMovable ) {
					//方向キー、もしくはD-Padが押されているときは、setMemberVelocityメソッドを呼び出し、タイルマップの移動速度と、山田先生の向きに、それぞれの値を代入する
					if ( game.input.left || dpad.arrow.left ) {
						party.setMemberVelocity( 'left' );
						tilemap.vx = WALKING_SPEED;
						yamada.direction = 1;
					}
					else if ( game.input.right || dpad.arrow.right ) {
						party.setMemberVelocity( 'right' );
						tilemap.vx = -1 * WALKING_SPEED;
						yamada.direction = 2;
					}
					else if ( game.input.up || dpad.arrow.up ) {
						party.setMemberVelocity( 'up' );
						tilemap.vy = WALKING_SPEED;
						yamada.direction = 3;
					}
					else if ( game.input.down || dpad.arrow.down ) {
						party.setMemberVelocity( 'down' );
						tilemap.vy = -1 * WALKING_SPEED;
						yamada.direction = 0;
					}

					//移動後のマップ座標を求める
					const yamadaCoordinateAfterMoveX = yamada.mapX - tilemap.vx/WALKING_SPEED;
					const yamadaCoordinateAfterMoveY = yamada.mapY - tilemap.vy/WALKING_SPEED;
					//もし移動後のマップ座標に障害物があるとき
					if ( tilemap.hasObstacle( yamadaCoordinateAfterMoveX, yamadaCoordinateAfterMoveY ) ) {
						//移動量に0を代入する
						tilemap.vx = tilemap.vy = 0;
						//パーティ全員の移動速度に0を代入する
						for ( let i in party.member ) {
							party.member[i].vx = party.member[i].vy = 0;
						}					
					}
				}
			}
			//タイルマップのXとY座標両方がタイルのサイズで割り切れるとき以外で、タイルの半分のサイズで割り切れるとき
			else if ( ( tilemap.x + TILE_SIZE/2 ) % ( TILE_SIZE/2 ) === 0 && ( tilemap.y + TILE_SIZE/2 ) % ( TILE_SIZE/2 ) === 0 ) {
				//0と1を交互に取得できる
				toggleForAnimation ^= 1;
				//toggleForAnimationの数値によって、山田先生の画像を切り替える
				if ( toggleForAnimation === 0 ) yamada.animation = 2;
				else yamada.animation = 0;
			}
		} //scene.onenterframe 終了

		//作ったシーンを返す
		return scene;

	} //mainScene() 終了

	//gameに、シーンを追加して、とお願いする
	game.add( titleScene() );
	game.add( mainScene() );

	//gameに、ゲームをスタートして、とお願いする
	game.start();

} );
山田

まず102〜110行目、112〜120行目で、りこちゃんとアルくんのキャラクタータイルを、タイルマップに追加しているべ

アル

これでぼくたちが表示されるんだね!

山田

そして123行目で、わたすと、りこちゃん、そしてアルくんの、パーティを作っているんだべよ

りこ

へぇ。これでPartyクラスの機能が使えるのね!

山田

そうだべよ
そして149〜151行目で、パーティ全員の移動速度に0を代入しているべ。この移動速度はマップの移動とは別物べつものだから注意ちゅうい必要ひつようだべよ

りこ

そっか。マップが移動してると、キャラクターが移動しているように見えるけど、キャラクターの位置はまた別だもんね!

山田

りこちゃんやアルくんの移動は、182、187、192、197行目で、Partyクラスのメソッドを呼び出すことで行なっているべ

アル

なるほど、キーやボタンがされるたびに呼び出すんだね!

山田

そしてもちろん、移動したい方向にかべなどの障害物しょうがいぶつがあるときは、移動量に0を代入するべよ
これは210〜212行目だべ

りこ

これでみんな動かなくなるね!

山田

えっ……

アル

ひっ……

りこ

な……なんできゅうにそんな反応はんのうするのよ!

山田

あ、そういうことだべか……うむ。障害物の方向へ移動しようとしても、みんな動かないべ

アル

そういうことかぁ

りこ

どういうことよ!

山田

まぁまぁ。実際じっさいブラウザ確認かくにんするとこうなるべ!

りこちゃんとアルくんが後ろをついてくる
アル

すごい! ぼくたちもついていってる!

りこ

これでみんなさみしくないね!

パーティメンバー全員を、アニメーションさせてみよう!

つづいて、先頭のキャラ以外のパーティメンバーもアニメーションするようにしていきましょう!

山田

さて、さきほどはみんながわたすの後ろをついてくるようになったべけど、まだ歩くアニメーションをしていなんだべよ

アル

うーん。やっぱりアニメーションがないと歩いてるように見えないよね……

山田

そこで、次はパーティのメンバー全員が、アニメーションしながら歩くように作っていくべよ!

js/engine/party.js

'use strict'

/**
 * パーティに関してのクラス
 */
class Party {

	/**
	 * 引数には、パーティのメンバーを制限なく入れることができる
	 */
	constructor() {
		//引数をthis.memberに追加
		this.member = arguments;
		//パーティの移動速度
		this.speed = 1;
	} //constructor() 終了

	/**
	 * 先頭以外のキャラの、向きを設定するためのメソッド
	 */
	setMemberDirection() {
		//先頭のキャラ以外の数だけ繰り返す
		for ( let i=1; i<this.member.length; i++ ) {
			//ひとつ前のキャラよりもX座標が大きいとき、左向きにする
			if ( this.member[i-1].mapX < this.member[i].mapX ) this.member[i].direction = 1;
			//ひとつ前のキャラよりもX座標が小さいとき、右向きにする
			else if ( this.member[i-1].mapX > this.member[i].mapX ) this.member[i].direction = 2;
			//ひとつ前のキャラよりもY座標が大きいとき、後ろ向きにする
			else if ( this.member[i-1].mapY < this.member[i].mapY ) this.member[i].direction = 3;
			//ひとつ前のキャラよりもY座標が小さいとき、前向きにする
			else if ( this.member[i-1].mapY > this.member[i].mapY ) this.member[i].direction = 0;
		}
	} //setMemberDirection() 終了

	/**
	 * 先頭以外のキャラの、移動速度を設定するためのメソッド
	 *
	 * 引数
	 * direction : 先頭のキャラの移動方向
	 */
	setMemberVelocity( direction ) {
		//先頭以外のキャラの数だけ繰り返す
		for ( let i=1; i<this.member.length; i++ ) {
			//ひとつ前のキャラとX座標が同じときの、計算用変数
			let _samePrevX;
			//ひとつ前のキャラとY座標が同じときの、計算用変数
			let _samePrevY;
			//ひとつ前のキャラよりもX座標が小さいときの、計算用変数
			let _lessThanPrevX;
			//ひとつ前のキャラよりもY座標が小さいときの、計算用変数
			let _lessThanPrevY;
			//ひとつ前のキャラよりもX座標大きいときの、計算用変数
			let _moreThanPrevX;
			//ひとつ前のキャラよりもY座標が大きいときの、計算用変数
			let _moreThanPrevY;
			//ひとつ前のキャラよりも、タイルひとつ分以上離れているときの計算用変数
			let _moreThanOneTile;

			//先頭のキャラの移動方向によって、それぞれの変数に値を代入する
			if ( direction === 'up' ) {
				_samePrevX = 0;
				_samePrevY = 1
				_lessThanPrevX = -1;
				_lessThanPrevY = 0;
				_moreThanPrevX = 1;
				_moreThanPrevY = 2;
				_moreThanOneTile = -1;
			}
			else if ( direction === 'down' ) {
				_samePrevX = 0;
				_samePrevY = -1;
				_lessThanPrevX = -1;
				_lessThanPrevY = -2;
				_moreThanPrevX = 1;
				_moreThanPrevY = 0;
				_moreThanOneTile = 1;
			}
			else if ( direction === 'left' ) {
				_samePrevX = 1;
				_samePrevY = 0;
				_lessThanPrevX = 0;
				_lessThanPrevY = -1;
				_moreThanPrevX = 2;
				_moreThanPrevY = 1;
				_moreThanOneTile = -1;
			}
			else if ( direction === 'right' ) {
				_samePrevX = -1;
				_samePrevY = 0;
				_lessThanPrevX = -2;
				_lessThanPrevY = -1;
				_moreThanPrevX = 0;
				_moreThanPrevY = 1;
				_moreThanOneTile = 1;
			}

			//ひとつ前のキャラとX,Y座標が同じとき
			if ( this.member[i-1].mapX === this.member[i].mapX ) this.member[i].vx = this.speed * _samePrevX;
			if ( this.member[i-1].mapY === this.member[i].mapY ) this.member[i].vy = this.speed * _samePrevY;

			//ひとつ前のキャラのほうが、X座標が小さいとき
			if ( this.member[i-1].mapX < this.member[i].mapX ) {
				this.member[i].vx = this.speed * _lessThanPrevX;
				//先頭のキャラの移動方向が左で、ひとつ前のキャラとのX座標の差が、タイルひとつよりも大きいとき
				if ( direction === 'left' && this.member[i].mapX - this.member[i-1].mapX > 1 ) this.member[i].vx = this.speed * _moreThanOneTile;
			}
			//ひとつ前のキャラのほうが、X座標が大きいとき
			if ( this.member[i-1].mapX > this.member[i].mapX ) {
				this.member[i].vx = this.speed * _moreThanPrevX;
				//先頭のキャラの移動方向が右で、ひとつ前のキャラとのX座標の差が、タイルひとつよりも大きいとき
				if ( direction === 'right' && this.member[i-1].mapX - this.member[i].mapX > 1 ) this.member[i].vx = this.speed * _moreThanOneTile;
			}
			//ひとつ前のキャラのほうが、Y座標が小さいとき
			if ( this.member[i-1].mapY < this.member[i].mapY ) {
				this.member[i].vy = this.speed * _lessThanPrevY;
				//先頭のキャラの移動方向が上で、ひとつ前のキャラとのY座標の差が、タイルひとつよりも大きいとき
				if ( direction === 'up' && this.member[i].mapY - this.member[i-1].mapY > 1 ) this.member[i].vy = this.speed * _moreThanOneTile;
			}
			//ひとつ前のキャラのほうが、Y座標が大きいとき
			if ( this.member[i-1].mapY > this.member[i].mapY ) {
				this.member[i].vy = this.speed * _moreThanPrevY;
				//先頭のキャラの移動方向が下で、ひとつ前のキャラとのY座標の差が、タイルひとつよりも大きいとき
				if ( direction === 'down' && this.member[i-1].mapY - this.member[i].mapY > 1 ) this.member[i].vy = this.speed * _moreThanOneTile;
			}
		}
	} //setMemberVelocity() 終了
}
山田

追加したのは18〜33行目だべ!
これは先頭キャラ以外の向きを設定せっていするためのメソッドで、ひとつ前のキャラの方向の画像にえてくれるようにしたんだべ

りこ

えっと、X座標ざひょうやY座標の大きさによって、向きを切り替えているのね!

山田

うむ、だべさ
そしてこのメソッドを使って、キャラクターをアニメーションさせてみるべ!

js/main.js

'use strict'

//ブラウザがページを完全に読みこむまで待つ
addEventListener( 'load', () => {

	//変数gameに、あなたはゲームですよ、と教える
	const game = new Game();
	//使いたいキーとして、スペースキーを登録する
	game.keybind( 'space', ' ' );

	//タイトルシーン
	const titleScene = () => {

		//変数sceneに、あなたはシーンですよ、と教える
		const scene = new Scene();

		//変数titleTextに、あなたは「くろねこラビリンス」というテキストだよ、と教える
		const titleText = new Text( 'くろねこラビリンス' );
		//テキストを上下左右中央の位置にする
		titleText.center().middle();
		//シーンにテキストを追加
		scene.add( titleText );

		//シーンがタッチされたとき
		scene.ontouchstart = () => {
			//メインシーンに切り替える
			game.currentScene = mainScene();
		} //scene.ontouchstart() 終了

		//ループから常に呼び出される
		scene.onenterframe = () => {
			//スペースキーが押されたとき、メインシーンに切り替える
			if ( game.input.space ) game.currentScene = mainScene();
		} //scene.onenterframe() 終了

		//作ったシーンを返す
		return scene;

	} //titleScene() 終了

	//メインシーン
	const mainScene = () => {

		//マップの作成
		const map = [
			[11,11,11,11,11,11,11,11,11,11],
			[11,10,10,10,10,10,10,10,10,11],
			[11, 4, 4, 4, 4, 4, 4, 4, 4,11],
			[11, 4,11, 4, 4,11,11,11, 4,11],
			[11, 4,11,11,11,11,10,10, 4,11],
			[11, 4,11,10,10,11, 4, 4, 4,11],
			[11, 4,11, 4, 4,11,11,11, 4,11],
			[11, 4, 9, 4, 4, 9,10,11, 4,11],
			[11, 4, 4, 4, 4, 4, 4,11, 4,11],
			[11,11,11,11,11,11,11,11,11,11]
		];
		//タイルのサイズ
		const TILE_SIZE = 32;
		//歩く速さ
		const WALKING_SPEED = 4;

		//変数sceneに、あなたはシーンですよ、と教える
		const scene = new Scene();

		//変数tilemapに、あなたはタイルマップですよ、と教える
		const tilemap = new Tilemap( 'img/tile.png' );
		//tilemap.dataに、どんなマップなのか教える
		tilemap.data = map;
		//マップ全体の位置をずらす
		tilemap.x = TILE_SIZE*4 - TILE_SIZE/2;
		tilemap.y = TILE_SIZE*3 - TILE_SIZE/2;
		//移動できないタイルを指定する
		tilemap.obstacles = [0, 3, 6, 7, 8, 9, 10, 11];
		//マップを登録する
		scene.add( tilemap );

		//変数startに、あなたはスタートのタイルですよ、と教える
		const start = new Tile( 'img/start.png' );
		//マップ左上からの座標を指定する
		start.x = TILE_SIZE;
		start.y = TILE_SIZE*2;
		//スタートのタイルを、tilemapに追加して、とお願いする
		tilemap.add( start );

		//変数goalに、あなたはゴールのタイルですよ、と教える
		const goal = new Tile( 'img/goal.png' );
		//マップ左上からの座標を指定する
		goal.x = TILE_SIZE*8;
		goal.y = TILE_SIZE*8;
		//ゴールのタイルを、tilemapに追加して、とお願いする
		tilemap.add( goal );

		//変数yamadaに、あなたは山田先生のキャラクタータイルですよ、と教える
		const yamada = new CharacterTile( 'img/yamada.png' );
		//山田先生を画面の中央に配置
		yamada.x = yamada.y = TILE_SIZE*5 - TILE_SIZE/2;
		//タイルマップの動きと同期させない
		yamada.isSynchronize = false;
		//tilemapに、山田先生のタイルを追加して、とお願いする
		tilemap.add( yamada );

		//変数ricoに、あなたはりこちゃんのキャラクタータイルですよ、と教える
		const rico = new CharacterTile( 'img/rico.png' );
		//りこちゃんの位置を決める
		rico.x = TILE_SIZE*7 - TILE_SIZE/2;
		rico.y = TILE_SIZE*5 - TILE_SIZE/2;
		//タイルマップの動きと同期させない
		rico.isSynchronize = false;
		//tilemapに、りこちゃんのキャラクタータイルを追加して、とお願いする
		tilemap.add( rico );

		//変数aruに、あなたはアルくんのキャラクタータイルですよ、と教える
		const aru = new CharacterTile( 'img/aru.png' );
		//アルくんの位置を決める
		aru.x = TILE_SIZE*7 - TILE_SIZE/2;
		aru.y = TILE_SIZE*6 - TILE_SIZE/2;
		//タイルマップの動きと同期させない
		aru.isSynchronize = false;
		//tilemapに、アルくんのキャラクタータイルを追加して、とお願いする
		tilemap.add( aru );

		//変数partyに、あなたは山田先生とりこちゃんとアルくんのパーティですよ、と教える
		const party = new Party( yamada, rico, aru );
		//パーティの歩く速さに、WALKING_SPEEDの値を代入する
		party.speed = WALKING_SPEED;

		//変数dpadに、あなたはD-Padですよ、と教える
		const dpad = new DPad( 'img/dpad.png', 80 );
		//D-Padの位置を指定する
		dpad.x = 10;
		dpad.y = 230;
		//sceneに、D-Padを追加して、とお願いする
		scene.add( dpad );

		//キャラクターのアニメーションのための変数
		let toggleForAnimation = 0;
		//ゴールのテキストが表示されているかどうかの変数
		let hasDisplayedGoalText = false;
		//移動可能かどうかの変数
		let isMovable = true;

		//ループから常に呼び出される
		scene.onenterframe = () => {
			//タイルマップの位置がタイルのサイズで割り切れるとき
			if ( ( tilemap.x - TILE_SIZE/2 ) % TILE_SIZE === 0 && ( tilemap.y - TILE_SIZE/2 ) % TILE_SIZE === 0 ) {
				//タイルマップの移動速度に0を代入する
				tilemap.vx = tilemap.vy = 0;
				//パーティ全員の数だけ繰り返す
				for ( let i in party.member ) {
					//パーティ全員の移動速度を0にする
					party.member[i].vx = party.member[i].vy = 0;
					//パーティ全員の画像を切り替える
					party.member[i].animation = 1;
				}
				//山田先生の画像を切り替える(削除)
				//yamada.animation = 1;(削除)

				//山田先生のタイルがゴールのタイルと重なっているとき、イベントを発生させる
				if ( yamada.isOverlapped( goal ) ) {
					//ゴールのテキストが表示されていないとき
					if ( !hasDisplayedGoalText ) {
						//変数goalTextに、あなたは「ゴールだべ!」というテキストだよ、と教える
						const goalText = new Text( 'ゴールだべ!' );
						//テキストサイズを変更
						goalText.size = 50;
						//テキストを上下左右中央の位置にする
						goalText.center().middle();
						//シーンにテキストを追加
						scene.add( goalText );
						//ゴールのテキストが表示されているかどうかの変数にtrueを代入
						hasDisplayedGoalText = true;
						//移動ができないようにする
						isMovable = false;
						//6秒たったら、タイトルシーンに切り替える
						setTimeout( () => {
							game.currentScene = titleScene();
						}, 6000 );
					}
				}

				//移動可能なとき
				if ( isMovable ) {
					//方向キー、もしくはD-Padが押されているときは、setMemberVelocityメソッドを呼び出し、タイルマップの移動速度と、山田先生の向きに、それぞれの値を代入する
					if ( game.input.left || dpad.arrow.left ) {
						party.setMemberVelocity( 'left' );
						tilemap.vx = WALKING_SPEED;
						yamada.direction = 1;
					}
					else if ( game.input.right || dpad.arrow.right ) {
						party.setMemberVelocity( 'right' );
						tilemap.vx = -1 * WALKING_SPEED;
						yamada.direction = 2;
					}
					else if ( game.input.up || dpad.arrow.up ) {
						party.setMemberVelocity( 'up' );
						tilemap.vy = WALKING_SPEED;
						yamada.direction = 3;
					}
					else if ( game.input.down || dpad.arrow.down ) {
						party.setMemberVelocity( 'down' );
						tilemap.vy = -1 * WALKING_SPEED;
						yamada.direction = 0;
					}

					//移動後のマップ座標を求める
					const yamadaCoordinateAfterMoveX = yamada.mapX - tilemap.vx/WALKING_SPEED;
					const yamadaCoordinateAfterMoveY = yamada.mapY - tilemap.vy/WALKING_SPEED;
					//もし移動後のマップ座標に障害物があるとき
					if ( tilemap.hasObstacle( yamadaCoordinateAfterMoveX, yamadaCoordinateAfterMoveY ) ) {
						//移動量に0を代入する
						tilemap.vx = tilemap.vy = 0;
						//パーティ全員の移動速度に0を代入する
						for ( let i in party.member ) {
							party.member[i].vx = party.member[i].vy = 0;
						}					
					}

					//タイルマップが動いているとき、パーティメンバーの向きを変える
					if ( tilemap.vx !== 0 || tilemap.vy !== 0 ) party.setMemberDirection();
				}
			}
			//タイルマップのXとY座標両方がタイルのサイズで割り切れるとき以外で、タイルの半分のサイズで割り切れるとき
			else if ( ( tilemap.x + TILE_SIZE/2 ) % ( TILE_SIZE/2 ) === 0 && ( tilemap.y + TILE_SIZE/2 ) % ( TILE_SIZE/2 ) === 0 ) {
				//0と1を交互に取得できる
				toggleForAnimation ^= 1;
				//パーティメンバーの数だけ繰り返す
				for ( let i in party.member ) {
					//toggleForAnimationの数値によって、パーティ全員の画像を切り替える
					if ( toggleForAnimation === 0 ) party.member[i].animation = 2;
					else party.member[i].animation = 0;
				}
			}
		} //scene.onenterframe 終了

		//作ったシーンを返す
		return scene;

	} //mainScene() 終了

	//gameに、シーンを追加して、とお願いする
	game.add( titleScene() );
	game.add( mainScene() );

	//gameに、ゲームをスタートして、とお願いする
	game.start();

} );
山田

まず149〜154行目で、パーティメンバー全員の、移動速度を0にし、画像を両足がそろってるものに切り替えているんだべ

アル

ふむふむ。止まってる状態ってことだね!

山田

うむ。そして219行目で、さきほど作ったメソッド、setMemberDirectionを呼び出して、キャラクター全員の向きを変えているんだべ

りこ

そっか。タイルマップが動いてるってことは、キャラクターが移動してるってことだから、ここで向きを変えるのね!

山田

そういうことだべ
そして227〜231行目で、パーティ全員を、歩くアニメーションさせているんだべ

アル

これでみんな、アニメーションするんだ!

山田

では、ブラウザで確認してみるべ!

パーティ全員がアニメーションしながらついてくるようになる
りこ

きゃー。ちゃんと歩きながらついていってる!

アル

すごいよ! みんなで冒険ぼうけんに行けるんだ。ゆめみたい!

山田

ふっふっふ。やっぱりRPGは夢があっていいべな

……さて、だいぶゲームもかたちになったべ。でももうちょっとだけ付き合って欲しいんだべ

りこ

もちろん!

アル

ぼくも!

シーンが変更されたときに一度だけ呼ばれるメソッドを作ろう!

つづいて、シーンが変更されたときに一度だけ呼び出されるメソッドを作ってみましょう!

山田

つぎは、シーンが変更されたときに、一度だけ呼び出されるメソッドを作るべ!

アル

あれ、いまさらそんなの作って意味いみあるの?

りこ

アルー、ちゃんと先生の話聞かないと!

アル

ごめーん

山田

まぁまぁ。もう少しあとで、ゲームに音楽を入れるんだべけど、シーンの切り替えのときに、同時どうじに音楽も切り替えたいんだべよ

アル

なるほど!

山田

では、こんなふうに作って欲しいんだべ!

js/engine/game.js

'use strict'

/**
 * ゲームづくりの基本となるクラス
 */
class Game {

	/**
	 * 引数
	 * width : ゲームの横幅
	 * height : ゲームの縦幅
	 */
	constructor( width, height ) {
		//canvas要素を作成
		this.canvas = document.createElement( 'canvas' );
		//作成したcanvas要素をbodyタグに追加
		document.body.appendChild( this.canvas );
		//canvasの横幅(ゲームの横幅)を設定。もし横幅が指定されていなければ320を代入
		this.canvas.width = width || 320;
		//canvasの縦幅(ゲームの縦幅)を設定。もし縦幅が指定されていなければ320を代入
		this.canvas.height = height || 320;

		//シーンを入れておくための配列
		this.scenes = [];
		//現在のシーンをいれておくためのもの
		this.currentScene;

		//現在のシーンを一時的に入れておくためのもの。シーンが切り替わったかどうかを判断するのに使う
		this._temporaryCurrentScene;

		//ゲームに使用するキーと、そのキーが押されているかどうかを入れるための連想配列
		//例 { up: false, down: false }
		this.input = {};
		//登録されたキーに割り当てられたプロパティ名と、キー名を、関連づけるための連想配列
		//例 { up: "ArrowUp", down: "ArrowDown" }
		this._keys = {};
	} //constructor() 終了

	/**
	 * startメソッドを呼び出すことで、メインループが開始される
	 */
	start() {
		//デフォルトのキーバインドを登録する(使いたいキーを登録する)
		this.keybind( 'up', 'ArrowUp' );
		this.keybind( 'down', 'ArrowDown' );
		this.keybind( 'right', 'ArrowRight' );
		this.keybind( 'left', 'ArrowLeft' );

		//現在のシーン(currentScene)になにも入っていないときは、scenes[0]を代入
		this.currentScene = this.currentScene || this.scenes[0];

		//ゲームがはじまったときと、ブラウザのサイズが変わったときに呼ばれる。縦横の比を変えずに、canvasを拡大縮小できる
		const _resizeEvent = () => {
			//ブラウザとcanvasの比率の、縦と横を計算し、小さいほうを_ratioに代入する
			const _ratio = Math.min( innerWidth / this.canvas.width, innerHeight / this.canvas.height );
			//canvasのサイズを、ブラウザに合わせて変更する
			this.canvas.style.width = this.canvas.width*_ratio + 'px';
			this.canvas.style.height = this.canvas.height*_ratio + 'px';
		} //_resizeEvent() 終了

		//ブラウザのサイズが変更されたとき、_resizeを呼び出す
		addEventListener( 'resize', _resizeEvent, { passive: true } );
		//_resizeを呼び出す
		_resizeEvent();

		//メインループを呼び出す
		this._mainLoop();

		//イベントリスナーをセットする
		this._setEventListener();
	} //start() 終了

	/**
	 * イベントリスナーをセットするためのメソッド
	 */
	_setEventListener() {
		//なにかキーが押されたときと、はなされたときに呼ばれる
		const _keyEvent = e => {
			//デフォルトのイベントを発生させない
			e.preventDefault();
			//_keysに登録された数だけ繰り返す
			for ( let key in this._keys ) {
				//イベントのタイプによって呼び出すメソッドを変える
				switch ( e.type ) {
					case 'keydown' :
						//押されたキーが、登録されたキーの中に存在するとき、inputのそのキーをtrueにする
						if ( e.key === this._keys[key] ) this.input[key] = true;
						break;
					case 'keyup' :
						//押されたキーが、登録されたキーの中に存在するとき、inputのそのキーをfalseにする
						if ( e.key === this._keys[key] ) this.input[key] = false;
						break;
				}
			}
		}
		//なにかキーが押されたとき
		addEventListener( 'keydown', _keyEvent, { passive: false } );
		//キーがはなされたとき
		addEventListener( 'keyup', _keyEvent, { passive: false } );

		//画面がタッチされたり、指が動いたりしたときなどに呼ばれる
		//シーンや、スプライトなどのオブジェクトの左上端から見た、それぞれの指の位置を取得できるようになる
		const _touchEvent = e => {
			//デフォルトのイベントを発生させない
			e.preventDefault();
			//タッチされた場所などの情報を取得
			const _touches = e.changedTouches[0];
			//ターゲット(今回はcanvas)のサイズ、ブラウザで表示されている部分の左上から見てどこにあるか、などの情報を取得
			const _rect = _touches.target.getBoundingClientRect();
			//タッチされた場所を計算
			const _fingerPosition = {
				x: ( _touches.clientX - _rect.left ) / _rect.width * this.canvas.width,
				y: ( _touches.clientY - _rect.top ) / _rect.height * this.canvas.height
			};
			//イベントのタイプを_eventTypeに代入
			const _eventType = e.type;
			//タッチイベントを割り当てるためのメソッドを呼び出す
			this.currentScene.assignTouchevent( _eventType, _fingerPosition );
		} //_touchEvent() 終了

		//タッチされたとき
		this.canvas.addEventListener( 'touchstart', _touchEvent, { passive: false } );
		//指が動かされたとき
		this.canvas.addEventListener( 'touchmove', _touchEvent, { passive: false } );
		//指がはなされたとき
		this.canvas.addEventListener( 'touchend', _touchEvent, { passive: false } );
	} //_setEventListener() 終了

	/**
	 * メインループ
	 */
	_mainLoop() {
		//画家さん(コンテキスト)を呼ぶ
		const ctx = this.canvas.getContext( '2d' );
		//塗りつぶしの色に、黒を指定する
		ctx.fillStyle = '#000000';
		//左上から、画面のサイズまでを、塗りつぶす
		ctx.fillRect( 0, 0, this.canvas.width, this.canvas.height );

		//現在のシーンのupdateメソッドを呼び出す
		this.currentScene.update();

		//一時的に入れておいたシーンが現在のシーンではないとき(シーンが切り替わったとき)、現在のシーンのonchangesceneメソッドを呼び出す
		if ( this._temporaryCurrentScene !== this.currentScene ) this.currentScene.onchangescene();

		//現在のシーンの、ゲームに登場する全てのもの(オブジェクト)の数だけ繰り返す
		for ( let i=0; i<this.currentScene.objs.length; i++ ) {
			//現在のシーンの、すべてのオブジェクトのupdateメソッドを呼び出す
			this.currentScene.objs[i].update( this.canvas );
		}

		//現在のシーンを覚えておいてもらう
		this._temporaryCurrentScene = this.currentScene;

		//自分自身(_mainLoop)を呼び出して、ループさせる
		requestAnimationFrame( this._mainLoop.bind( this ) );
	} //_mainLoop() 終了

	/**
	 * ゲームにシーンに追加できるようになる、addメソッドを作成
	 *
	 * 引数
	 * scene : 追加したいシーン
	 */
	add( scene ) {
		//引数がSceneのとき、this.scenesの末尾にsceneを追加
		if ( scene instanceof Scene ) this.scenes.push( scene );
		//引数がSceneでなければ、コンソールにエラーを表示
		else console.error( 'Gameに追加できるのはSceneだけだよ!' );
	} //add()終了

	/**
	 * 使いたいキーを登録できるようになる、keybindメソッドを作成
	 *
	 * 引数
	 * name : キーにつける名前
	 * key : キーコード
	 */
	keybind( name, key ) {
		//キーの名前と、キーコードを関連づける
		this._keys[name] = key;
		//キーが押されているかどうかを入れておく変数に、まずはfalseを代入しておく
		this.input[name] = false;
	} //keybind() 終了

}
山田

まず29行目で、現在げんざいのシーンを一時的いちじてきにいれておくためのプロパティを作っているべ

アル

ふむふむ。どうやって使うの?

山田

これに入れておいた現在のシーンと、次のループの現在のシーンをくらべれば、シーンが切り替わったかどうかが分かるべ

りこ

次のループで、現在のシーンが変わってたら、シーンが切り替わったことになるのね!

山田

うむ。それを実際に行なっているのが、144、153行目だべ

りこ

えっと……あ、144行目でonchangesceneメソッドを呼び出してる!

山田

そうだべ! このonchangesceneが、シーンが切り替わったときに、一度だけ呼び出されるメソッドだべ
……でも、オーバーライドする用のメソッドを書き忘れてはいけないべよ

js/engine/scene.js

'use strict'

/**
 * シーンに関してのクラス
 */
class Scene {

	constructor() {
		this.objs = [];
	} //constructor() 終了

	/**
	 * シーンにオブジェクトを追加するときに呼び出されるメソッド
	 *
	 * 引数
	 * obj : スプライトやテキストなど(オブジェクト)
	 */
	add( obj ) {
		//引数がSprite、Text、Tilemapのとき、this.objsの末尾にobjを追加
		if ( obj instanceof Sprite || obj instanceof Text || obj instanceof Tilemap ) this.objs.push( obj );
		//引数がSprite、Text、Tilemapでなければ、コンソールにエラーを表示
		else console.error( 'Sceneに追加できるのはSprite、Text、Tilemapだけだよ!' );
	} //add() 終了

	/**Gameクラスのメインループからずっと呼び出され続ける
	 *
	 * 引数
	 * canvas : 紙(キャンバス)
	 */
	update( canvas ) {
		//スプライトを動かしたり、なにかのきっかけでイベントを発生させたりするために使うメソッドを呼び出す
		this.onenterframe();
	} //update() 終了

	/**
	 * タッチイベントを割り当てるためのメソッド
	 *
	 * 引数
	 * eventType : イベントのタイプ
	 * fingerPosition : 指の位置
	 */
	assignTouchevent( eventType, fingerPosition ) {
		//イベントのタイプによって呼び出すメソッドを変える
		switch ( eventType ) {
			case 'touchstart' :
				//現在のシーンのtouchstartメソッドを呼び出す
				this.ontouchstart( fingerPosition.x, fingerPosition.y );
				break;
			case 'touchmove' :
				//現在のシーンのtouchmoveメソッドを呼び出す
				this.ontouchmove( fingerPosition.x, fingerPosition.y );
				break;
			case 'touchend' :
				//現在のシーンのtouchendメソッドを呼び出す
				this.ontouchend( fingerPosition.x, fingerPosition.y );
				break;
		}

		//シーンにあるオブジェクトの数だけ繰り返す
		for ( let i=0; i<this.objs.length; i++ ) {
			//シーンにあるオブジェクトの、タッチイベントを割り当てるためのメソッドを呼び出す
			this.objs[i].assignTouchevent( eventType, fingerPosition );
		}
	} //assignTouchevent() 終了

	/**
	 * 常に呼び出され、スプライトの移動やイベントの発生などに使うメソッド。空なのはオーバーライド(上書き)して使うため
	 */
	onenterframe() {}

	/**
	 * タッチされたときに呼び出される
	 */
	ontouchstart() {}

	/**
	 * 指が動かされたときに呼び出される
	 */
	ontouchmove() {}

	/**
	 * 指がはなされたときに呼び出される
	 */
	ontouchend() {}

	/**
	 * シーンが切り替わったときに呼び出される
	 */
	onchangescene() {}

}
アル

うーん。ちゃんと動くかな……

山田

やっぱり心配だべな……もしこのメソッドを使うときは、こんなふうにするべ

scene.onchangescene = () => {
	console.log( 'タイトルシーンだよ!' );
}
山田

試しに、タイトルシーンとメインシーンに、上のようなふうに入力してみたべ
もし同じように入力したら、まだ使わないからちゃんと消しておいて欲しいべ!

シーンが切り替わると、一度だけ呼ばれる
アル

うん、表示されてる。安心したよ

次のページでは、素材そざいのプリロード、そして音楽の再生さいせいができるようにしていきます。

このシリーズの一覧はこちら

  1. 小学生からのプログラミング入門。プログラミングってなぁに?
  2. Scratchの使い方と、ゲーム作りの基礎知識を学ぼう! 小学生からのプログラミング入門
  3. Scratchでじゃんけんゲームを作ろう! 小学生からのプログラミング入門
  4. Scratchでシューティングゲームを作ろう! 小学生からのプログラミング入門
  5. Scratchでピアノ鍵盤を作って音を鳴らそう! 小学生からのプログラミング入門
  6. テキストエディタ(Visual Studio Code)をインストールしてみよう! 小学生からのプログラミング入門
  7. Visual Studio Codeを日本語化してみよう! 小学生からのプログラミング入門
  8. JavaScriptでおみくじを作ろう! 小学生からのプログラミング入門
  9. JavaScriptで今月の残り日数を計算してみよう! 小学生からのプログラミング入門
  10. JavaScriptで画像を表示してみよう! 小学生からのプログラミング入門
  11. JavaScriptで画像を移動してみよう! 小学生からのプログラミング入門
  12. 【JavaScript】キー入力でキャラを動かしてみよう! 小学生からのプログラミング入門
  13. 【JavaScript】ファイルを分けて管理してみよう! 小学生からのプログラミング入門
  14. 【JavaScript】オブジェクトを使ってみよう! 小学生からのプログラミング入門
  15. 【JavaScript】ゲームのメインループを作ってみよう! 小学生からのプログラミング入門
  16. 【JavaScript】キャラを決まった間隔ずつ動かす! 小学生からのプログラミング入門
  17. HTML5とCanvasを使ってみよう! 小学生からのプログラミング入門
  18. 【JavaScript】迷路やRPGで使えるマップを作ってみよう! 小学生からのプログラミング入門
  19. 【JavaScript】マップでキャラを動かせるようにしよう! 小学生からのプログラミング入門
  20. 【JavaScript】クラスの概念をしっかりと理解しよう! 小学生からのプログラミング入門
  21. 【JavaScript】プログラム全体をクラスを使って作ってみよう! 小学生からのプログラミング入門
  22. 【JavaScript】文字を表示するクラスを作ってみよう! 小学生からのプログラミング入門
  23. 【JavaScript】改行と一文字ずつ画面に表示する方法! 小学生からのプログラミング入門
  24. 【JavaScript】ノベルゲーム風にキー入力で文字を切り替える方法! 小学生からのプログラミング入門
  25. JavaScriptでRPGを作ろう!スマホにも対応したゲームの作り方
  26. webpackを使ってゲームエンジンを作ろう!(JSライブラリの作り方)
  27. WindowsにPythonをインストールしてみよう!小学生からのPython入門
  28. MacにPythonをインストールしてみよう!小学生からのPython入門
  29. Pythonでじゃんけんゲームを作ってみよう!小学生からのPython入門
  30. Pythonのtkinterを使って、ウィンドウを表示してみよう!
  31. Pythonのtkinterで、画像つきのおみくじゲームを作ろう!
オリジナルゲーム.com