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

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

山田

さて、見た感じはだいぶゲームらしくなってきたけど、まだやることは半分以上あるんだべ

りこ

そうなんだ……ゲーム開発かいはつってたいへんなのね

アル

まだスタートとゴールもないし、かべも通りけられちゃうもんね
はやく完成させたいなぁ

山田

ついてこられるべか?

りこ

もちろん!

アル

ぼくも!

目次
  1. スプライトを移動させる機能を作ろう!
  2. キャラクターが、決まった間隔ずつ移動するようにしてみよう!
  3. スタートとゴールのタイルを、タイルマップに配置してみよう!
  4. タイルマップと一緒に動かない、固定されたタイルをつくろう!

スプライトを移動させる機能を作ろう!

つづいて、スプライトを移動いどうさせる機能きのうを作ってみましょう!

山田

つぎはスプライトを移動するための機能を作っていくべよ!

りこ

移動するための機能?

山田

これまで、スプライトの移動は、X座標ざひょうやY座標に移動したいだけの数値すうちを足す方法だったべ

りこ

そうだね

山田

これを、移動速度いどうそくどのプロパティに数値を代入だいにゅうすれば、自動的じどうてきに移動してくれる、というふうになおしていくべ

js/engine/sprite.js

'use strict'

/**
 * スプライトに関してのクラス
 */
class Sprite {

	/**
	 * 引数
	 * img : 画像ファイルまでのパス
	 * width : 画像の表示する範囲(横幅)
	 * height : 画像の表示する範囲(縦幅)
	 */
	constructor( img, width, height ) {
		//this.imgに、あなたは画像ですよ、と教える
		this.img = new Image();
		//this.img.srcに画像ファイルまでのパスを代入
		this.img.src = img;
		//画像の初期位置
		this.x = this.y = 0;
		//画像を表示する範囲の横幅。引数widthが指定されていない場合、this.widthに32を代入
		this.width = width || 32;
		//画像を表示する範囲の縦幅。引数heightが指定されていない場合、this.heightに32を代入
		this.height = height || 32;
		//何番目の画像を表示するか
		this.frame = 0;
		//数値によってスプライトを移動させることができる(移動速度)
		this.vx = this.vy = 0;
	} //constructor() 終了

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

	/**
	 * 画像などを画面に表示するためのメソッド
	 *
	 * 引数
	 * canvas : 紙(キャンバス)
	 */
	render( canvas ) {
		//キャンバスの外にスプライトがあるとき、ここでこのメソッドを終了する
		if ( this.x < -1 * this.width || this.x > canvas.width ) return;
		if ( this.y < -1 * this.height || this.y > canvas.height ) return;

		//X,Y方向に、何番目の画像か
		const _frameX = this.frame % ( this.img.width / this.width );
		const _frameY = ~~( this.frame / ( this.img.width / this.width ) );

		//画家さん(コンテキスト)を呼ぶ
		const _ctx = canvas.getContext( '2d' );
		//画家さんに、絵を描いてとお願いする
		_ctx.drawImage(
			this.img,
			this.width * _frameX,
			this.height * _frameY,
			this.width,
			this.height,
			this.x,
			this.y,
			this.width,
			this.height
		);
	} //render() 終了

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

}
山田

まず28行目で、移動速度のプロパティを初期化しょきかしているべ
さらに、42〜43行目で、スプライトの座標に、移動速度を足しているんだべよ

りこ

移動速度に入れる数値によって、スプライトが自動的じどうてきに動くのね!

アル

あぁ、なるほど!

山田

そして、タイルマップにも同じ機能をつけるべよ

js/engine/tilemap.js

'use strict'

/**
 * タイルマップに関するクラス
 */
class Tilemap {

	/**
	 * 引数
	 * img : 画像ファイルまでのパス
	 * size : タイルひとつの大きさ(一辺の長さ)
	 *
	 * タイルひとつは正方形にする
	 */
	constructor( img, size ) {
		//Imageのインスタンスを作成
		this.img = new Image();
		//this.img.srcに画像ファイルまでのパスを代入
		this.img.src = img;
		//画像の初期位置
		this.x = this.y = 0;
		//数値によってタイルマップを移動させることができる(移動速度)
		this.vx = this.vy = 0;
		//引数sizeが指定されていない場合、this.sizeに32を代入
		this.size = size || 32;
		//二次元配列で数値を入力すると、マップをつくることができる
		this.data = [];
		//タイルマップに重ねるように置きたいタイルを追加できる
		this.tiles = [];
	} //constructor() 終了

	/**
	 * タイルマップの上にタイルを重ねるように追加できるメソッド
	 *
	 * 引数
	 * tile : 追加したいタイル
	 */
	add( tile ) {
		//引数がTileのとき、this.tilesの末尾にtileを追加
		if ( tile instanceof Tile ) this.tiles.push( tile );
		//引数がTileでなければ、コンソールにエラーを表示
		else console.error( 'Tilemapに追加できるのはTileだけだよ!' );
	} //add() 終了

	/**Gameクラスのメインループからずっと呼び出され続ける
	 *
	 * 引数
	 * canvas : 紙(キャンバス)
	 */
	update( canvas ) {
		//画像などを画面に表示するためのメソッドを呼び出す
		this.render( canvas );
		//常に呼び出される、オーバーライド用のメソッドを呼び出す
		this.onenterframe();
		//タイルマップを移動する
		this.x += this.vx;
		this.y += this.vy;

		//タイルの数だけ繰り返す
		for ( let i=0; i<this.tiles.length; i++ ) {
			//それぞれのタイルのupdateメソッドを呼び出す
			this.tiles[i].update( canvas );
		}
	} //update() 終了

	/**
	 * Gameクラスのメインループからずっと呼び出され続ける。画像を表示したりするためのメソッド
	 *
	 * 引数
	 * canvas : 紙(キャンバス)
	 */
	render( canvas ) {
		//マップの縦方向の数だけ繰り返す
		for (let y=0; y<this.data.length; y++) {
			//タイルの縦の位置
			const _tileY = this.size * y + this.y;
			//タイルが、画面から縦にはみ出しているとき、この下をスキップして、次から繰り返し
			if ( _tileY < -1 * this.size || _tileY > canvas.height ) continue;

			//マップの横方向の数だけ繰り返す
			for (let x=0; x<this.data[y].length; x++) {
				//タイルの横の位置
				const _tileX = this.size * x + this.x
				//タイルが、画面から横にはみ出しているとき、この下をスキップして、次から繰り返し
				if ( _tileX < -1 * this.size || _tileX > canvas.width ) continue;

				//X方向に、何番目の画像か
				const _frameX = this.data[y][x] % ( this.img.width / this.size );
				//Y方向に、何番目の画像か
				const _frameY = ~~( this.data[y][x] / ( this.img.width / this.size ) );

				//画家さん(コンテキスト)を呼ぶ
				const _ctx = canvas.getContext( '2d' );

				//タイルを表示
				_ctx.drawImage(
					this.img,
					this.size * _frameX,
					this.size * _frameY,
					this.size,
					this.size,
					_tileX,
					_tileY,
					this.size,
					this.size
				);
			}
		}
	} //render() 終了

	/**
	 * 常に呼び出されるメソッド。空なのはオーバーライド(上書き)して使うため
	 */
	onenterframe() {}

}
山田

追加ついかしたのは22〜23行目、55〜57行目だべ
やってることは、Spriteクラスと同じだべよ

りこ

さっそく使ってみましょ

js/main.js

'use strict'

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

	//変数gameに、あなたはゲームですよ、と教える
	const game = new Game();

	//マップの作成
	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;
	//マップを登録する
	scene.add( tilemap );

	//変数yamadaに、あなたは山田先生のタイルですよ、と教える
	const yamada = new Tile( 'img/yamada.png' );
	//山田先生を画面の中央に配置
	yamada.x = yamada.y = TILE_SIZE*5 - TILE_SIZE/2;
	//tilemapに、山田先生のタイルを追加して、とお願いする
	tilemap.add( yamada );

	//ループから常に呼び出される
	scene.onenterframe = () => {
		//タイルマップの移動速度に0を代入する
		tilemap.vx = tilemap.vy = 0;
		//キーが押されたとき、山田先生(マップ)が移動する
		if ( game.input.left ) tilemap.vx = WALKING_SPEED;
		if ( game.input.right ) tilemap.vx = -1 * WALKING_SPEED;
		if ( game.input.up ) tilemap.vy = WALKING_SPEED;
		if ( game.input.down ) tilemap.vy = -1 * WALKING_SPEED;
	} //scene.onenterframe 終了

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

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

} );
山田

49〜55行目を変更へんこうしたべよ

りこ

キーがされたときは、タイルマップの移動速度に、歩くスピードを代入して……あ、左や上の場合ばあいはマイナスをかけてるんだ

あれ? 50行目で移動速度に0を代入してるのね……どうして?

山田

こうしなければ、ずっと移動をつづけてしまうべよ

りこ

あ、そっか。移動速度だから、その移動を止めるおねがいが必要ひつようなんだ!

アル

よく分かんないよ……なにもキーがされていないときだけ、0を代入だいにゅうしなくちゃいけないんじゃないの?

山田

ふっふっふ。最初さいしょに0を代入して、キー入力があったときだけ移動速度を代入すれば、キーが押されていないときは0のままだべよ

アル

そっか! なにも押されていないときに0を代入する、なんてお願いを、わざわざ書く必要がないんだ!

山田

こういうふうに、書く順番じゅんばん工夫くふうすることも、プログラミングでは大切なんだべ!

キャラクターが、決まった間隔ずつ移動するようにしてみよう!

つづいて、キャラクターの移動を、決まった間隔かんかくずつになるようにしていきましょう!

山田

いまのところ、わたすのタイル……つまりキャラクターは、タイルマップ上のどの位置いちにでも止まることができるべ
するとここで、じゃあそのキャラクターは、どのタイルの上にいるのか、という話になってしまうべよ

アル

それじゃダメなの?

山田

もちろん、ダメってことはないべ。ドラクエでもタイルとタイルのあいだで止まれたりするべな
でも、体がすこしだけかべにぶつかったときは自動的に位置をなおしてくれたり、れてさえいれば宝箱たからばこが開けられたり、といった工夫が、ドラクエにはちゃんとされているんだべよ

アル

あっ、たしかに操作そうさしやすいよね!

山田

これが、わずかに壁に触れてるだけで移動できなかったり、ぴったり位置を合わせないと宝箱を開けられなかったりしたら、もうプレイしたくなくなるべ

アル

むむ……そうだよね

りこ

そっかぁ。ぴったりタイルに合わせて止まるようにすれば、そういったことがふせげるんだ

山田

そういうことだべ!
じゃあ、作っていくべよ!

js/main.js

'use strict'

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

	//変数gameに、あなたはゲームですよ、と教える
	const game = new Game();

	//マップの作成
	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;
	//マップを登録する
	scene.add( tilemap );

	//変数yamadaに、あなたは山田先生のタイルですよ、と教える
	const yamada = new Tile( 'img/yamada.png' );
	//山田先生を画面の中央に配置
	yamada.x = yamada.y = TILE_SIZE*5 - TILE_SIZE/2;
	//tilemapに、山田先生のタイルを追加して、とお願いする
	tilemap.add( yamada );

	//ループから常に呼び出される
	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;
			//キーが押されたとき、山田先生(マップ)が移動する
			if ( game.input.left ) tilemap.vx = WALKING_SPEED;
			if ( game.input.right ) tilemap.vx = -1 * WALKING_SPEED;
			if ( game.input.up ) tilemap.vy = WALKING_SPEED;
			if ( game.input.down ) tilemap.vy = -1 * WALKING_SPEED;
		}
	} //scene.onenterframe 終了

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

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

} );
山田

追加したのは49〜50行目と58行目だべ

りこ

えっと……移動の部分ぶぶんのプログラムを、タイルマップの位置がタイルのサイズでれるときだけうごくようにしてるんだね

アル

そうか。タイルのサイズで割り切れるときだけ移動速度のあたいを変えれば、タイルのサイズ分だけスクロールするんだ

山田

そうだべ。ブラウザを読み込みなおすとこうなるべ!

1まいのタイルの上に必ず止まる!
アル

ほんとだ! ちゃんと1まいのタイルの上で止まるよ!

りこ

やったね!

スタートとゴールのタイルを、タイルマップに配置してみよう!

つづいて、スタートとゴールのタイルを、タイルマップに追加ついかして、配置はいちしてみましょう!

山田

では、スタートとゴールのタイルを、マップに配置したいんだべけど……これにはちょっと工夫くふう必要ひつようだべ

アル

えっ、なんで?

山田

タイルマップは、わたすのタイルに合わせて、位置を調節ちょうせつしているべ
たとえば、わたすの初期位置しょきいちの、上から3番目、左から2番目のタイルの位置にスタートのタイルの場所を指定していしても、ずれてしまうべよ

りこ

そっかぁ……ってことは、タイルマップの位置の分、タイルの位置もずらさないといけないよね

山田

そこで、スプライトをずらしてくれる機能きのうを作るべ!

js/engine/sprite.js

'use strict'

/**
 * スプライトに関してのクラス
 */
class Sprite {

	/**
	 * 引数
	 * img : 画像ファイルまでのパス
	 * width : 画像の表示する範囲(横幅)
	 * height : 画像の表示する範囲(縦幅)
	 */
	constructor( img, width, height ) {
		//this.imgに、あなたは画像ですよ、と教える
		this.img = new Image();
		//this.img.srcに画像ファイルまでのパスを代入
		this.img.src = img;
		//画像の初期位置
		this.x = this.y = 0;
		//画像を表示する範囲の横幅。引数widthが指定されていない場合、this.widthに32を代入
		this.width = width || 32;
		//画像を表示する範囲の縦幅。引数heightが指定されていない場合、this.heightに32を代入
		this.height = height || 32;
		//何番目の画像を表示するか
		this.frame = 0;
		//数値によってスプライトを移動させることができる(移動速度)
		this.vx = this.vy = 0;
		//スプライトの位置を、数値の分、ずらすことができる
		this.shiftX = this.shiftY = 0;
	} //constructor() 終了

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

	/**
	 * 画像などを画面に表示するためのメソッド
	 *
	 * 引数
	 * canvas : 紙(キャンバス)
	 */
	render( canvas ) {
		//キャンバスの外にスプライトがあるとき、ここでこのメソッドを終了する
		if ( this.x + this.shiftX < -1 * this.width || this.x + this.shiftX > canvas.width ) return;
		if ( this.y + this.shiftY < -1 * this.height || this.y + this.shiftY > canvas.height ) return;

		//X,Y方向に、何番目の画像か
		const _frameX = this.frame % ( this.img.width / this.width );
		const _frameY = ~~( this.frame / ( this.img.width / this.width ) );

		//画家さん(コンテキスト)を呼ぶ
		const _ctx = canvas.getContext( '2d' );
		//画家さんに、絵を描いてとお願いする
		_ctx.drawImage(
			this.img,
			this.width * _frameX,
			this.height * _frameY,
			this.width,
			this.height,
			this.x + this.shiftX,
			this.y + this.shiftY,
			this.width,
			this.height
		);
	} //render() 終了

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

}
山田

まず、30行目で、スプライトの位置をずらすためのプロパティを初期化しょきかしているんだべ

アル

shiftXやshiftYってのだね

山田

そして56〜57行目は、キャンバスの外にスプライトが出てしまったときの計算を、ずらした分も考えるようになおしたべ

りこ

X座標ざひょうやY座標に、shiftXやshiftYの値を足しているのね

山田

さらに72〜73行目では、shiftXやshiftYの値を、X座標とY座標に足して、位置をずらしているんだべよ

りこ

これでスプライトの基準きじゅんの位置をずらすことができるね

山田

そうなんだべよ。
そしてTileクラスはSpriteクラスを継承けいしょうしているべから、タイルでも同じ機能が使えるようになったべ
こんどは、タイルの基準の位置を、タイルマップの位置にするんだべ

js/engine/tilemap.js

'use strict'

/**
 * タイルマップに関するクラス
 */
class Tilemap {

	/**
	 * 引数
	 * img : 画像ファイルまでのパス
	 * size : タイルひとつの大きさ(一辺の長さ)
	 *
	 * タイルひとつは正方形にする
	 */
	constructor( img, size ) {
		//Imageのインスタンスを作成
		this.img = new Image();
		//this.img.srcに画像ファイルまでのパスを代入
		this.img.src = img;
		//画像の初期位置
		this.x = this.y = 0;
		//数値によってタイルマップを移動させることができる(移動速度)
		this.vx = this.vy = 0;
		//引数sizeが指定されていない場合、this.sizeに32を代入
		this.size = size || 32;
		//二次元配列で数値を入力すると、マップをつくることができる
		this.data = [];
		//タイルマップに重ねるように置きたいタイルを追加できる
		this.tiles = [];
	} //constructor() 終了

	/**
	 * タイルマップの上にタイルを重ねるように追加できるメソッド
	 *
	 * 引数
	 * tile : 追加したいタイル
	 */
	add( tile ) {
		//引数がTileのとき、this.tilesの末尾にtileを追加
		if ( tile instanceof Tile ) this.tiles.push( tile );
		//引数がTileでなければ、コンソールにエラーを表示
		else console.error( 'Tilemapに追加できるのはTileだけだよ!' );
	} //add() 終了

	/**Gameクラスのメインループからずっと呼び出され続ける
	 *
	 * 引数
	 * canvas : 紙(キャンバス)
	 */
	update( canvas ) {
		//画像などを画面に表示するためのメソッドを呼び出す
		this.render( canvas );
		//常に呼び出される、オーバーライド用のメソッドを呼び出す
		this.onenterframe();
		//タイルマップを移動する
		this.x += this.vx;
		this.y += this.vy;

		//タイルの数だけ繰り返す
		for ( let i=0; i<this.tiles.length; i++ ) {
			//タイルマップの位置の分、それぞれのタイルの位置をずらす
			this.tiles[i].shiftX = this.x;
			this.tiles[i].shiftY = this.y;
			//それぞれのタイルのupdateメソッドを呼び出す
			this.tiles[i].update( canvas );
		}
	} //update() 終了

	/**
	 * Gameクラスのメインループからずっと呼び出され続ける。画像を表示したりするためのメソッド
	 *
	 * 引数
	 * canvas : 紙(キャンバス)
	 */
	render( canvas ) {
		//マップの縦方向の数だけ繰り返す
		for (let y=0; y<this.data.length; y++) {
			//タイルの縦の位置
			const _tileY = this.size * y + this.y;
			//タイルが、画面から縦にはみ出しているとき、この下をスキップして、次から繰り返し
			if ( _tileY < -1 * this.size || _tileY > canvas.height ) continue;

			//マップの横方向の数だけ繰り返す
			for (let x=0; x<this.data[y].length; x++) {
				//タイルの横の位置
				const _tileX = this.size * x + this.x
				//タイルが、画面から横にはみ出しているとき、この下をスキップして、次から繰り返し
				if ( _tileX < -1 * this.size || _tileX > canvas.width ) continue;

				//X方向に、何番目の画像か
				const _frameX = this.data[y][x] % ( this.img.width / this.size );
				//Y方向に、何番目の画像か
				const _frameY = ~~( this.data[y][x] / ( this.img.width / this.size ) );

				//画家さん(コンテキスト)を呼ぶ
				const _ctx = canvas.getContext( '2d' );

				//タイルを表示
				_ctx.drawImage(
					this.img,
					this.size * _frameX,
					this.size * _frameY,
					this.size,
					this.size,
					_tileX,
					_tileY,
					this.size,
					this.size
				);
			}
		}
	} //render() 終了

	/**
	 * 常に呼び出されるメソッド。空なのはオーバーライド(上書き)して使うため
	 */
	onenterframe() {}

}
山田

そして、スタートとゴールのタイルを配置するべ!

js/main.js

'use strict'

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

	//変数gameに、あなたはゲームですよ、と教える
	const game = new Game();

	//マップの作成
	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;
	//マップを登録する
	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 Tile( 'img/yamada.png' );
	//山田先生を画面の中央に配置
	yamada.x = yamada.y = TILE_SIZE*5 - TILE_SIZE/2;
	//tilemapに、山田先生のタイルを追加して、とお願いする
	tilemap.add( yamada );

	//ループから常に呼び出される
	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;
			//キーが押されたとき、山田先生(マップ)が移動する
			if ( game.input.left ) tilemap.vx = WALKING_SPEED;
			if ( game.input.right ) tilemap.vx = -1 * WALKING_SPEED;
			if ( game.input.up ) tilemap.vy = WALKING_SPEED;
			if ( game.input.down ) tilemap.vy = -1 * WALKING_SPEED;
		}
	} //scene.onenterframe 終了

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

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

} );
りこ

40〜46行目、48〜54行目で、スタートとゴールのタイルを、タイルマップに追加してるのね。ちゃんとできてるかなぁ……

山田

ブラウザ再読さいよみするべ……けど……

STARTやGOALのタイルは配置されたけど山田先生のタイルがずれてしまった……
アル

あれ、先生のタイル、位置がずれちゃってる……

りこ

ほんとだ……せっかく真ん中に表示されてたのに……

山田

タイルマップとタイルが一緒いっしょに動くようにしたべから、わたすのタイルまでもが、一緒に動いてしまってるんだべ……

りこ

そんなぁ

山田

でも大丈夫だべ
タイルマップと一緒に動かないタイルも作れるようにすればいいんだべ!

タイルマップと一緒に動かない、固定されたタイルをつくろう!

つづいて、タイルマップに追加ついかしても一緒いっしょうごかない、固定こていされたタイルを作れるようにしていきましょう!

山田

さて、さきほどはわたすのタイルが、マップと一緒に動いてしまったべ
つぎはそれをなおしていくべよ!

りこ

やったぁ

山田

ではtile.jsに、このように追加するべ

js/engine/tile.js

'use strict'

/**
 * タイルに関してのクラス
 */
class Tile extends Sprite {

	/**
	 * 引数
	 * img : 画像ファイルまでのパス
	 * size : タイルの大きさ
	 */
	constructor( img, size ) {
		//親クラスのコンストラクタを呼び出す
		super( img, size, size );
		//引数sizeが指定されていない場合、this.sizeに32を代入
		this.size = size || 32;
		//タイルマップと同期して動くかどうか
		this.isSynchronize = true;
	} //constructor() 終了

}
山田

19行目で、タイルマップと一緒に動くかどうかのプロパティを作っているべ
初期値しょきちはtrueだべから、なにも指定していしなければ一緒に動く、というふうになるべ

りこ

ぎゃくに、isSynchronizeにfalseを指定すれば、固定されたタイルになるのね

山田

そういうことだべ
さらにtilemap.jsで、上にかさねるタイルの位置を、isSynchronizeがtrueかfalseかによって切り替えるべ

js/engine/tilemap.js

'use strict'

/**
 * タイルマップに関するクラス
 */
class Tilemap {

	/**
	 * 引数
	 * img : 画像ファイルまでのパス
	 * size : タイルひとつの大きさ(一辺の長さ)
	 *
	 * タイルひとつは正方形にする
	 */
	constructor( img, size ) {
		//Imageのインスタンスを作成
		this.img = new Image();
		//this.img.srcに画像ファイルまでのパスを代入
		this.img.src = img;
		//画像の初期位置
		this.x = this.y = 0;
		//数値によってタイルマップを移動させることができる(移動速度)
		this.vx = this.vy = 0;
		//引数sizeが指定されていない場合、this.sizeに32を代入
		this.size = size || 32;
		//二次元配列で数値を入力すると、マップをつくることができる
		this.data = [];
		//タイルマップに重ねるように置きたいタイルを追加できる
		this.tiles = [];
	} //constructor() 終了

	/**
	 * タイルマップの上にタイルを重ねるように追加できるメソッド
	 *
	 * 引数
	 * tile : 追加したいタイル
	 */
	add( tile ) {
		//引数がTileのとき、this.tilesの末尾にtileを追加
		if ( tile instanceof Tile ) this.tiles.push( tile );
		//引数がTileでなければ、コンソールにエラーを表示
		else console.error( 'Tilemapに追加できるのはTileだけだよ!' );
	} //add() 終了

	/**Gameクラスのメインループからずっと呼び出され続ける
	 *
	 * 引数
	 * canvas : 紙(キャンバス)
	 */
	update( canvas ) {
		//画像などを画面に表示するためのメソッドを呼び出す
		this.render( canvas );
		//常に呼び出される、オーバーライド用のメソッドを呼び出す
		this.onenterframe();
		//タイルマップを移動する
		this.x += this.vx;
		this.y += this.vy;

		//タイルの数だけ繰り返す
		for ( let i=0; i<this.tiles.length; i++ ) {
			//タイルとタイルマップの位置を同期させるとき
			if ( this.tiles[i].isSynchronize ) {
				//タイルマップの位置の分、それぞれのタイルの位置をずらす
				this.tiles[i].shiftX = this.x;
				this.tiles[i].shiftY = this.y;
			}
			//それぞれのタイルのupdateメソッドを呼び出す
			this.tiles[i].update( canvas );
		}
	} //update() 終了

	/**
	 * Gameクラスのメインループからずっと呼び出され続ける。画像を表示したりするためのメソッド
	 *
	 * 引数
	 * canvas : 紙(キャンバス)
	 */
	render( canvas ) {
		//マップの縦方向の数だけ繰り返す
		for (let y=0; y<this.data.length; y++) {
			//タイルの縦の位置
			const _tileY = this.size * y + this.y;
			//タイルが、画面から縦にはみ出しているとき、この下をスキップして、次から繰り返し
			if ( _tileY < -1 * this.size || _tileY > canvas.height ) continue;

			//マップの横方向の数だけ繰り返す
			for (let x=0; x<this.data[y].length; x++) {
				//タイルの横の位置
				const _tileX = this.size * x + this.x
				//タイルが、画面から横にはみ出しているとき、この下をスキップして、次から繰り返し
				if ( _tileX < -1 * this.size || _tileX > canvas.width ) continue;

				//X方向に、何番目の画像か
				const _frameX = this.data[y][x] % ( this.img.width / this.size );
				//Y方向に、何番目の画像か
				const _frameY = ~~( this.data[y][x] / ( this.img.width / this.size ) );

				//画家さん(コンテキスト)を呼ぶ
				const _ctx = canvas.getContext( '2d' );

				//タイルを表示
				_ctx.drawImage(
					this.img,
					this.size * _frameX,
					this.size * _frameY,
					this.size,
					this.size,
					_tileX,
					_tileY,
					this.size,
					this.size
				);
			}
		}
	} //render() 終了

	/**
	 * 常に呼び出されるメソッド。空なのはオーバーライド(上書き)して使うため
	 */
	onenterframe() {}

}
山田

追加したのは、61〜62行目と66行目だべ

アル

あ、そっか。isSynchronizeがtrueのときだけ、タイルの位置をずらしてるんだね

りこ

じゃあ山田先生のタイルのisSynchronizeに、falseを入れてあげればいいのね!

js/main.js

'use strict'

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

	//変数gameに、あなたはゲームですよ、と教える
	const game = new Game();

	//マップの作成
	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;
	//マップを登録する
	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 Tile( 'img/yamada.png' );
	//山田先生を画面の中央に配置
	yamada.x = yamada.y = TILE_SIZE*5 - TILE_SIZE/2;
	//タイルマップの動きと同期させない
	yamada.isSynchronize = false;
	//tilemapに、山田先生のタイルを追加して、とお願いする
	tilemap.add( yamada );

	//ループから常に呼び出される
	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;
			//キーが押されたとき、山田先生(マップ)が移動する
			if ( game.input.left ) tilemap.vx = WALKING_SPEED;
			if ( game.input.right ) tilemap.vx = -1 * WALKING_SPEED;
			if ( game.input.up ) tilemap.vy = WALKING_SPEED;
			if ( game.input.down ) tilemap.vy = -1 * WALKING_SPEED;
		}
	} //scene.onenterframe 終了

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

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

} );
山田

60〜61行目に追加したべよ
そして、ブラウザを読み込みなおすと、こうなるべ!

山田先生のタイルがマップと一緒に動かなくなる
アル

やった。スタートやゴールのタイルはマップと一緒に動いて、山田先生は止まったままだ!

りこ

やったぁ!

次のページでは、マップ上の座標を取得して、壁を通り抜けられないようにしていきます。

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

  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