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

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

山田

むむ……手強てごわいべ

りこ

せーんせ、スマホでなにやってるの?

山田

うむ……わたすもむかしドラクエはやったべけど、アルくんを見ていたら、久しぶりにもう一度やりたくなってしまったべ。そこでまだプレイしていなかったのをやってみてるんだべけど、これがはまってしまって……

しかし、いまはスマホでもドラクエができてしまうんだべな……

りこ

スマホ便利べんりだよね。そういえば、くろねこラビリンスもスマホでできるようにするんでしょ?

アル

はぁ、はぁ……ごめーん。国語こくご教科書きょうかしょ、まちがえてとなりの席の子の持ってきちゃってて、返してきたんだ

りこ

ふふっ。アルったら、ドジね

アル

あ、それ。ドラクエだ!

山田

うむ。久しぶりにわたすもやってみてるべ

アル

おうじゃのけんは手に入れた? たしかオリハルコンを……

山田

あ、あ、あ、言っちゃだめだべー

目次
  1. スプライトを回転できるようにしてみよう!
  2. viewportを設定してみよう!
  3. スマホでタッチされた位置を取得できるようにしてみよう!
  4. タッチされたときにシーンが切り替わるようにしてみよう!
  5. スマホでキャラクターを操作できるようにしてみよう!

スプライトを回転できるようにしてみよう!

つづいて、スプライトを回転かいてんさせることができる機能きのうを作っていきましょう!

山田

つぎはスプライトを回転させる機能を作っていくべよ!

アル

へぇ、回転かぁ。いろんなところで使えそうだね

山田

うむ。ゲームをスマホ操作そうさするD-Padの機能でも使うべから、しっかり作っておくべよ

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;
		//スプライトの角度
		this.rotate = 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' );
		//スプライトを回転させるときの中心位置を変更するための、canvasの原点の移動量
		const _translateX = this.x + this.width/2 + this.shiftX;
		const _translateY = this.y + this.height/2 + this.shiftY;
		//描画状態を保存する
		_ctx.save();
		//canvasの原点の移動
		_ctx.translate( _translateX, _translateY );
		//canvasを回転
		_ctx.rotate( this.rotate * Math.PI / 180 );
		//移動したcanvasの原点を戻す
		_ctx.translate( -1*_translateX, -1*_translateY );
		//画家さんに、絵を描いてとお願いする
		_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
		);
		//保存しておいた描画状態に戻す
		_ctx.restore();
	} //render() 終了

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

}
山田

まず32行目が、スプライトをどのぐらい回転させるかのプロパティだべ

りこ

うんうん……あれ、67〜77行目、すごくむずかしそう……
あと91行目もよく分かんない……

山田

うむ……これがちょっとむずかしいんだべ
まず、スプライトを回転させるには、そのスプライトの中心位置ちゅうしんいち計算けいさんする必要ひつようがあるべ

りこ

そっか。中心がずれてたら、きれいに回転しないもんね

山田

そうなんだべ。そしてその中心位置を出しているのが、68〜69行目だべ

アル

あ、スプライトの位置に、半分はんぶんの大きさと、ずらした分を足しているんだね
これでスプライトの中心が分かるんだ!

山田

んだべ
そして71行目は、今の状態じょうたい保存ほぞんしているんだべ

アル

今の状態?

山田

色とか、どれだけ回転してるかとか、そういったいろいろな状態だべ
保存しておいて、あとからこの状態を復元するんだべ

アル

なんとなく分かったような分からないような……

りこ

むずかしい……

山田

なんとなくでも、こういうものなんだべな、と思うだけでいいと思うべ
そして73行目で、さきほど計算したスプライトの中心位置に、キャンバスの原点げんてん移動いどうしているべ

りこ

この位置で回転させるのね

山田

んだんだ、だべさ
そして75行目で実際じっさいに回転させてるべ

りこ

Math.PIってなぁに?

山田

円周率えんしゅうりつだべ
3.14159……ってやつだべ

りこ

ああ、算数さんすうならったよ!

山田

この円周率が、JavaScriptでは180度をあらわすんだべ

りこ

ええっ!

アル

意味いみが分からない!

山田

これには、ふつうの猫には分からない、とてもむずかしい理由があるんだべ
ラジアンというものなんだべが……プログラミングにはあまり関係かんけいない話になってしまうべから、ここでは省略しょうりゃくするべ

とにかく、円周率は180度とおぼえてほしいんだべ

アル

そうなんだ

山田

つまり、円周率÷180は、普段ふだん使う角度かくどの1度になるべ

りこ

あ、そっか。this.rotateに普段使う角度のあたいが入ってても、そこに円周率÷180の値をかければ、円周率が180度っていうあわらしかたの数値すうち変換へんかんできる!

山田

そうだべ!
この方法は、普段使っている角度で、かんたんに回転できるようになるんだべよ!

そして77行目でキャンバスの原点を元に戻して、91行目でさきほど保存した状態に戻しているんだべ

アル

スプライトを回転させるのって、大変なんだね……

山田

だべな……
そしてもうひとつ、charactertile.jsの方も、同じように変更しておくべ

js/engine/charactertile.js

'use strict'

/**
 * キャラクタータイルに関するクラス
 */
class CharacterTile extends Tile {

	/**
	 * 引数
	 * img : 画像ファイルまでのパス
	 * size : タイルの一辺の長さ
	 *
	 * ※注意
	 * directionやanimationを指定すると自動的にスプライト画像も変更されるが、画像自体を対応したものにする必要がある
	 * CharacterTileクラスで、frameを使うことはできない
	 */
	constructor( img, size ) {
		//親クラスのコンストラクタを呼び出す
		super( img, size );
		//キャラクターの向き(0:正面 1:左 2:右 3:後ろ)
		this.direction = 0;
		//スプライトのアニメーション。1が通常。0~2を切り替えることで、歩いているアニメーションを作ることができる
		this.animation = 1;
	} //constructor() 終了

	/**
	 * 画像などを画面に表示するためのメソッド
	 *
	 * 引数
	 * canvas : 紙(キャンバス)
	 */
	render( canvas ) {
		//画面の外にスプライトがあるとき、表示しないようにする
		if ( this.x + this.shiftX < -1 * this.size || this.x + this.shiftX > canvas.width ) return;
		if ( this.y + this.shiftY < -1 * this.size || this.y + this.shiftY > canvas.height ) return;
		//画家さん(コンテキスト)を呼ぶ
		const _ctx = canvas.getContext( '2d' );
		//スプライトを回転させるときの中心位置を変更するための、canvasの原点の移動量
		const _translateX = this.x + this.width/2 + this.shiftX;
		const _translateY = this.y + this.height/2 + this.shiftY;
		//描画状態を保存する
		_ctx.save();
		//canvasの原点の移動
		_ctx.translate( _translateX, _translateY );
		//canvasを回転
		_ctx.rotate( this.rotate * Math.PI / 180 );
		//移動したcanvasの原点を戻す
		_ctx.translate( -1*_translateX, -1*_translateY );
		//画家さんに、絵を描いてとお願いする
		_ctx.drawImage(
			this.img,
			this.size * this.animation,
			this.size * this.direction,
			this.size,
			this.size,
			this.x + this.shiftX,
			this.y + this.shiftY,
			this.size,
			this.size
		);
		//保存しておいた描画状態に戻す
		_ctx.restore();
	} //render() 終了

}
山田

ためしに、わたすのキャラクタータイルを、60度回転させてみたべ
もし同じようにためしたら、あとで消しておくべよ

yamada.rotate = 60;
山田先生が60度回転している!
りこ

ふふふっ、先生がかたむいてる

viewportを設定してみよう!

つづいて、viewportを設定せっていしていきましょう!

山田

さて、こんどはスマホ対応たいおうへとうつっていくべ

アル

スマホでもできるようになるんだね!

りこ

きゃー、すごい!

山田

ではまず、index.htmlをこのようにしてみてしいんだべ

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/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>
山田

追加ついかしたのは5行目だべ
これはスマホなどの端末たんまつで、ホームページがちゃんと表示ひょうじされるようにするためのものだべ

アル

えっ、これがないと、うまく表示されないの?

山田

そうなんだべ
たとえば、ゲームがスマホはしに小さく表示されてたら、プレイしづらいべな

アル

たしかに……

山田

そこで今回は、viewportの横幅よこはば画面がめんのサイズに、倍率ばいりつを1.0に、そしてユーザーが拡大かくだいできないようにしているんだべ

りこ

viewport?

山田

表示する領域りょういきのことだべ

アル

ううん……よく分からない

山田

つまり、この指定していをしておくと、スマホでも見やすくなり、さらに2本指で広げても拡大できなくなるんだべ

Chromeの検証けんしょうから、iPhoneの画面をエミュレートしてみたべ

スマホでも見やすい大きさになる!
アル

ホントだ! こう変わってくるのかぁ

りこ

拡大もできなくなってる!

スマホでタッチされた位置を取得できるようにしてみよう!

つづいて、スマホでタッチされた位置いち取得しゅとくできるようにしてみましょう!

山田

つぎに、スマホでタッチされたゆびの位置を取得するプログラムを作っていくべ!

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];

		//メインループを呼び出す
		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() 終了

}
山田

今回取得しているのは、タッチされたとき、動かされたとき、そしてはなされたときの、指の位置だべ

アル

へぇ、それぞれ取得するんだ

山田

そうなんだべよ
104〜109行目で、それぞれのタイプのタッチがあったとき、_touchEventを呼び出しているべ

りこ

_touchEventは、86〜102行目の部分ぶぶんね!

山田

90、92行目で、タッチされた場所ばしょを知るための情報じょうほうを取得しているべ
なんだか、ちょっと変わった取得の方法ほうほうだべけど、こういうものなんだべ

アル

引数ひきすうのeには、いろんな情報が入ってるんだね
そして、e.changedTouches[0]は、指の位置とか、そういう情報が入ってるんだ!

山田

だべだべ
そして、e.changedTouches[0]には、タッチしたターゲットの情報も入ってるべよ

りこ

それがcanvasなのね!

山田

そうだべ!
92行目では、そのcanvasにたいして、getBoundingClientRectメソッドでまたあらたな情報を取得して、変数へんすう_rectに代入だいにゅうしているべ!
これには、canvasのサイズや、canvasがブラウザのどの位置にあるのかといった情報が入っているんだべよ

アル

引数のeには、こんなにたくさんの情報が入ってたのかぁ!

山田

んだんだ
そして94〜97行目では、取得した情報から、タッチされた場所を計算けいさんして、_fingerPositionのプロパティ、xとyに入れているんだべ

りこ

こうやって計算すればいいんだ

山田

99行目はイベントタイプを取得しているべ

りこ

えっと、イベントタイプってなんだっけ?

山田

今回ならば、touchstart、touchmove、touchendのどれかだべ

りこ

ああ、そういうことか!

山田

さらに101行目では、現在げんざいのシーンのassignToucheventメソッドを呼び出しているべ

アル

assignToucheventメソッドって?

山田

ふっふっふ。
assignToucheventメソッドは、タッチイベントを、シーンやオブジェクトにてるためのもので、今から作っていくべよ!

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() {}

}
山田

まずはSceneクラスにassignToucheventメソッドを作ってみたべ!
42〜64行目を見て欲しいべ!

りこ

44〜57行目で、イベントタイプによって呼び出すメソッドを変えているのね!

山田

その呼び出すメソッドが、74、79、84行目だべ

アル

あ、なにも書かれてないメソッドだ

りこ

オーバーライドするためのメソッドね

山田

うむ。だんだん分かってきたようだべな
さらに、60〜63行目で、こんどはシーンに追加ついかされたオブジェクトのassignToucheventメソッドを呼び出しているべ

アル

なるほど。ってことはSpriteクラスとかにもassignToucheventメソッドを作るってこと?

山田

そうだべさ
では、さっそく作っていくべ!

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;
		//スプライトの角度
		this.rotate = 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' );
		//スプライトを回転させるときの中心位置を変更するための、canvasの原点の移動量
		const _translateX = this.x + this.width/2 + this.shiftX;
		const _translateY = this.y + this.height/2 + this.shiftY;
		//描画状態を保存する
		_ctx.save();
		//canvasの原点の移動
		_ctx.translate( _translateX, _translateY );
		//canvasを回転
		_ctx.rotate( this.rotate * Math.PI / 180 );
		//移動したcanvasの原点を戻す
		_ctx.translate( -1*_translateX, -1*_translateY );
		//画家さんに、絵を描いてとお願いする
		_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
		);
		//保存しておいた描画状態に戻す
		_ctx.restore();
	} //render() 終了

	/**
	 * タッチした指の、相対的な位置(タッチしたオブジェクトの左上からの位置)を取得できるメソッド
	 *
	 * 引数
	 * fingerPosition : 指の位置の座標
	 */
	getRelactiveFingerPosition( fingerPosition ) {
		//タッチしたものの、左上部分からの座標
		const _relactiveFingerPosition = {
			x: fingerPosition.x - this.x - this.shiftX,
			y: fingerPosition.y - this.y - this.shiftY
		};

		//数値が範囲内にあるかどうかを取得できる関数
		const inRange = ( num, min, max ) => {
			//数値が範囲内にあるかどうか
			const _inRange = ( min <= num && num <= max );
			//結果を返す
			return _inRange;
		}

		//タッチした位置がオブジェクトの上の場合、相対的な位置を返す
		if ( inRange( _relactiveFingerPosition.x, 0, this.width ) && inRange( _relactiveFingerPosition.y, 0, this.height ) ) return _relactiveFingerPosition;
		//オブジェクトから外れていれば、falseを返す
		return false;
	} //getRelactiveFingerPosition() 終了

	/**
	 * タッチイベントを割り当てるためのメソッド
	 *
	 * 引数
	 * eventType : イベントのタイプ
	 * fingerPosition : 指の位置
	 */
	assignTouchevent( eventType, fingerPosition ) {
		//相対的な座標(タッチしたオブジェクトの、左上からの座標)を取得
		const _relactiveFingerPosition = this.getRelactiveFingerPosition( fingerPosition );

		//イベントのタイプによって呼び出すメソッドを変える
		switch ( eventType ) {
			case 'touchstart' :
				//指の場所がスプライトの上にあるとき、ontouchstartメソッドを呼び出す
				if ( _relactiveFingerPosition ) this.ontouchstart( _relactiveFingerPosition.x, _relactiveFingerPosition.y );
				break;
			case 'touchmove' :
				//指の場所がスプライトの上にあるとき、ontouchmoveメソッドを呼び出す
				if ( _relactiveFingerPosition ) this.ontouchmove( _relactiveFingerPosition.x, _relactiveFingerPosition.y );
				break;
			case 'touchend' :
				//ontouchendメソッドを呼び出す
				this.ontouchend( _relactiveFingerPosition.x, _relactiveFingerPosition.y );
				break;
		}
	} //assignTouchevent() 終了

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

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

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

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

}
山田

まず100〜119行目は、相対的そうたいてきな指の位置を取得するためのメソッドだべ

りこ

相対的?

山田

うむ。キャンバスの左上からの座標ざひょう絶対的ぜったいてきな位置として、それに対してスプライトの左上からの位置が相対的な位置だべ

りこ

スプライトがどこにあっても、その左上の部分が、xが0、yが0の座標になるってこと?

山田

うむ、そうだべ!

りこ

なるほど!

山田

そして102〜105行目で、絶対的な指の位置から、相対的な指の位置を計算しているべ

アル

そっか。絶対的な指の位置から、スプライトの位置とずらした分を引けば、相対的な指の位置が出せるんだね!

山田

そういうことだべ
そして108〜113行目は、数値が範囲内はんいないにあるかどうかを、調べてくれる関数かんすうだべ
これを使えば、タッチされた場所がスプライトの上かどうかが、かんたんに分かるべよ

りこ

ほんとだ
116行目で使われてる!

山田

うむ。115〜118行目は、指の位置がスプライトの上ならば相対的な指の位置を、スプライトの上でなければfalseを返しているんだべ

そしてこの、getRelactiveFingerPositionメソッドは、130行目で使われているべ

りこ

あっ、assignToucheventメソッドの中で呼び出してるんだ

山田

ふっふっふ
128〜147行目が、タッチイベントを割り当てるメソッド、assignToucheventだべ

りこ

これでスプライトにもタッチイベントを割り当てるのね!

山田

そうだべよ!
そして133〜146行目で、それぞれのタッチ操作があったときのメソッドを呼び出しているんだべ

アル

そうか、これでスプライトがタッチされたときのイベントも、かんたんに作れるようになるのか

山田

オーバーライドして使うためのメソッドも忘れないようにするべよ

アル

うん。157、162、167行目だね!

山田

さて、同じように、こんどはTextクラスの方もなおしていくべ!

js/engine/text.js

'use strict'

/**
 * テキストに関してのクラス
 */
class Text {

	/**
	 * 引数
	 * text : 表示する文字列
	 */
	constructor( text ) {
		//this.textに表示する文字列を代入
		this.text = text;
		//デフォルトのフォントを指定
		this.font = "游ゴシック体, 'Yu Gothic', YuGothic, sans-serif";
		//テキストを表示する位置
		this.x = this.y = 0;
		//数値によってテキストを移動させることができる(移動速度)
		this.vx = this.vy = 0;
		//テキストのベースラインの位置
		this.baseline = 'top';
		//テキストのサイズ
		this.size = 20;
		//テキストの色
		this.color = '#ffffff';
		//テキストの太さ
		this.weight = 'normal';
		//テキストの幅
		this._width = 0;
		//テキストの高さ
		this._height = 0;
		//テキストを左右中央の位置にするかどうか
		this._isCenter = false;
		//テキストを上下中央の位置にするかどうか
		this._isMiddle = false;
	} //constructor() 終了

	/**
	 * 呼び出すと、テキストを左右中央の位置に配置できるメソッド
	 */
	center() {
		//テキストを左右中央の位置に配置するかどうかのプロパティにtrueを代入
		this._isCenter = true;
		//thisを返すことで、メソッドをチェーンにすることができる
		return this;
	} //center() 終了

	/**
	 * 呼び出すと、テキストを上下中央の位置に配置できるメソッド
	 */
	middle() {
		//テキストのベースラインの位置を変更
		this.baseline = 'middle'
		//テキストを上下中央の位置に配置するかどうかのプロパティにtrueを代入
		this._isMiddle = true;
		//thisを返すことで、メソッドをチェーンにすることができる
		return this;
	} //middle() 終了

	/**Gameクラスのメインループからずっと呼び出され続ける
	 *
	 * 引数
	 * canvas : 紙(キャンバス)
	 */
	update( canvas ) {
		//画家さん(コンテキスト)を呼ぶ
		const _ctx = canvas.getContext( '2d' );

		//テキストの太さ、サイズ、フォントを設定
		_ctx.font = `${this.weight} ${this.size}px ${this.font}`;
		//テキストの色を設定
		_ctx.fillStyle = this.color;
		//テキストのベースラインの位置を設定
		_ctx.textBaseline = this.baseline;

		//テキストの幅を計算
		this._width = _ctx.measureText( this.text ).width;
		//テキストの高さを計算
		this._height = Math.abs( _ctx.measureText( this.text ).actualBoundingBoxAscent ) + Math.abs( _ctx.measureText( this.text ).actualBoundingBoxDescent );

		//テキストを左右中央に配置したいときの、X座標の計算
		if ( this._isCenter ) this.x = ( canvas.width - this._width ) / 2;
		//テキストを上下中央に配置したいときの、Y座標の計算
		if ( this._isMiddle ) this.y = canvas.height / 2;

		//画像などを画面に表示するためのメソッドを呼び出す
		this.render( canvas, _ctx );
		//テキストを動かしたりするために使うメソッドを呼び出す
		this.onenterframe();

		//テキストを移動する
		this.x += this.vx;
		this.y += this.vy;
	} //update() 終了

	/**
	 * テキストを画面に表示するためのメソッド
	 *
	 * 引数
	 * canvas : 紙(キャンバス)
	 */
	render( canvas, ctx ) {
		//画面の外にテキストがあるとき、表示しないようにする
		if ( this.x < -1 * this._width || this.x > canvas.width ) return;
		if ( this.y < -1 * this._height || this.y > canvas.height + this._height ) return;
		//テキストを表示
		ctx.fillText( this.text, this.x, this.y );
	} //render() 終了

	/**
	 * タッチした指の、相対的な位置(タッチしたオブジェクトの左上からの位置)を取得できるメソッド
	 *
	 * 引数
	 * fingerPositionX : 指の位置の座標
	 */
	getRelactiveFingerPosition( fingerPosition ) {
		//タッチしたテキストの、左上部分からの座標。テキストのbaselineによって位置を調節する
		let _relactiveFingerPosition = {
			x: fingerPosition.x - this.x,
			y: fingerPosition.y - this.y + this._height
		};
		if ( this.baseline === 'top' || this.baseline === 'hanging' ) {
			_relactiveFingerPosition = {
				x: fingerPosition.x - this.x,
				y: fingerPosition.y - this.y
			};
		}
		if ( this.baseline === 'middle' ) {
			_relactiveFingerPosition = {
				x: fingerPosition.x - this.x,
				y: fingerPosition.y - this.y + this._height/2
			};
		}

		//数値が範囲内にあるかどうかを取得できる関数
		const inRange = ( num, min, max ) => {
			//数値が範囲内にあるかどうか
			const _inRange = ( min <= num && num <= max );
			//結果を返す
			return _inRange;
		}

		//タッチした位置がオブジェクトの上の場合、相対的な位置を返す
		if ( inRange( _relactiveFingerPosition.x, 0, this._width ) && inRange( _relactiveFingerPosition.y, 0, this._height ) ) return _relactiveFingerPosition;
		//オブジェクトから外れていれば、falseを返す
		return false;
	} //getRelactiveFingerPosition() 終了

	/**
	 * タッチイベントを割り当てるためのメソッド
	 *
	 * 引数
	 * eventType : イベントのタイプ
	 * fingerPosition : 指の位置
	 */
	assignTouchevent( eventType, fingerPosition ) {
		//相対的な座標(タッチしたオブジェクトの、左上からの座標)を取得
		const _relactiveFingerPosition = this.getRelactiveFingerPosition( fingerPosition );

		//イベントのタイプによって呼び出すメソッドを変える
		switch ( eventType ) {
			case 'touchstart' :
				//指の場所がテキストの上にあるとき、ontouchstartメソッドを呼び出す
				if ( _relactiveFingerPosition ) this.ontouchstart( _relactiveFingerPosition.x, _relactiveFingerPosition.y );
				break;
			case 'touchmove' :
				//指の場所がテキストの上にあるとき、ontouchmoveメソッドを呼び出す
				if ( _relactiveFingerPosition ) this.ontouchmove( _relactiveFingerPosition.x, _relactiveFingerPosition.y );
				break;
			case 'touchend' :
				//ontouchendメソッドを呼び出す
				this.ontouchend( _relactiveFingerPosition.x, _relactiveFingerPosition.y );
				break;
		}
	} //assignTouchevent() 終了

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

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

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

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

}
山田

だいたいはSpriteクラスと同じだべけど、微妙に違うところもあるべ。たとえば、指の相対的な位置を出すところで、テキストのベースラインの位置によって、調節しているべ
119〜134行目を見て欲しいんだべ

アル

なるほど。それぞれのbaselineで、左上が、xが0、yが0になるようにしてるんだね!

山田

そういうことだべ
そしてここでも、オーバーライドする用のメソッドを忘れてはいけないべよ

りこ

186、191、196行目ね! ちゃんと書いたよ!

山田

では、Tilemapクラスにも、同じように作っていくべよ!

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 = [];
		//壁や天井など、移動できないタイルを指定できる
		this.obstacles = [0];
	} //constructor() 終了

	/**
	 * タイルマップの上にタイルを重ねるように追加できるメソッド
	 *
	 * 引数
	 * tile : 追加したいタイル
	 */
	add( tile ) {
		//引数がTileのとき
		if ( tile instanceof Tile ) {
			//タイルのマップ座標を計算
			tile.mapX = tile.x / this.size;
			tile.mapY = tile.y / this.size;
			//もし、タイルがタイルマップと同期していないときは、マップ座標を計算しなおす
			if ( !tile.isSynchronize ) {
				tile.mapX = ( tile.x - this.x ) / this.size;
				tile.mapY = ( tile.y - this.y ) / this.size;
			}
			//this.tilesの末尾にtileを追加
			this.tiles.push( tile );
		}
		//引数がTileでなければ、コンソールにエラーを表示
		else console.error( 'Tilemapに追加できるのはTileだけだよ!' );
	} //add() 終了

	/**
	 * 指定された場所のタイルが、移動できないかどうかを取得できるメソッド
	 *
	 * 引数
	 * mapX : タイルマップ上のX座標
	 * mapY : タイルマップ上のY座標
	 */
	hasObstacle( mapX, mapY ) {
		//指定された場所のタイルが、壁や天井など、移動できないかどうか
		const _isObstacleTile = this.obstacles.some( obstacle => obstacle === this.data[mapY][mapX] );
		//移動できないかどうかを返す
		return _isObstacleTile;
	} //hasObstacle() 終了

	/**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 );

			//タイルのマップ座標を計算
			this.tiles[i].mapX = this.tiles[i].x / this.size;
			this.tiles[i].mapY = this.tiles[i].y / this.size;
			//もし、タイルがタイルマップと同期していないときは、マップ座標を計算しなおす
			if ( !this.tiles[i].isSynchronize ) {
				this.tiles[i].mapX = ( this.tiles[i].x - this.x ) / this.size;
				this.tiles[i].mapY = ( this.tiles[i].y - this.y ) / this.size;
			}
		}
	} //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() 終了

	/**
	 * タッチした指の、相対的な位置(タッチしたオブジェクトの左上からの位置)を取得できるメソッド
	 *
	 * 引数
	 * fingerPositionX : 指の位置の座標
	 */
	getRelactiveFingerPosition( fingerPosition ) {
		//タッチしたものの、左上部分からの座標
		const _relactiveFingerPosition = {
			x: fingerPosition.x - this.x,
			y: fingerPosition.y - this.y
		};

		//数値が範囲内にあるかどうかを取得できる関数
		const inRange = ( num, min, max ) => {
			//数値が範囲内にあるかどうか
			const _inRange = ( min <= num && num <= max );
			//結果を返す
			return _inRange;
		}

		//タッチした位置がオブジェクトの上の場合、相対的な位置を返す
		if ( inRange( _relactiveFingerPosition.x, 0, this.size*this.data[0].length ) && inRange( _relactiveFingerPosition.y, 0, this.size*this.data.length ) ) return _relactiveFingerPosition;
		//オブジェクトから外れていれば、falseを返す
		return false;
	} //getRelactiveFingerPosition() 終了

	/**
	 * タッチイベントを割り当てるためのメソッド
	 *
	 * 引数
	 * eventType : イベントのタイプ
	 * fingerPosition : 指の位置
	 */
	assignTouchevent( eventType, fingerPosition ) {
		//相対的な座標(タッチしたオブジェクトの、左上からの座標)を取得
		const _relactiveFingerPosition = this.getRelactiveFingerPosition( fingerPosition );

		//目的のオブジェクト以外の場所がタッチされた場合は、この下をスキップして、次から繰り返し
		if ( !_relactiveFingerPosition ) return;

		//イベントのタイプによって呼び出すメソッドを変える
		switch ( eventType ) {
			case 'touchstart' :
				//現在のシーンのオブジェクトの、touchstartメソッドを呼び出す
				this.ontouchstart( _relactiveFingerPosition.x, _relactiveFingerPosition.y );
				break;
			case 'touchmove' :
				//現在のシーンのオブジェクトの、touchmoveメソッドを呼び出す
				this.ontouchmove( _relactiveFingerPosition.x, _relactiveFingerPosition.y );
				break;
			case 'touchend' :
				//現在のシーンのオブジェクトの、touchendメソッドを呼び出す
				this.ontouchend( _relactiveFingerPosition.x, _relactiveFingerPosition.y );
				break;
		}

		//タイルマップの上の、タイルの数だけ繰り返す
		for ( let i=0; i<this.tiles.length; i++ ) {
			//タイルマップの上のタイルの、タッチイベントを割り当てるためのメソッドを呼び出す
			this.tiles[i].assignTouchevent( eventType, fingerPosition );
		}
	} //assignTouchevent() 終了

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

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

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

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

}
山田

これで、シーンやスプライトの、ontouchstart、ontouchmove、ontouchendやメソッドをオーバーライドすれば、指の位置を取得できるんだべ!

かんたんに使い方を紹介しょうかいするべ!
例えば、シーンをタッチしたときの座標をコンソールに表示したいときは、このようにするべ!

scene.ontouchstart = ( x, y ) => {
	console.log( `X=${x} Y=${y}` );
}
りこ

引数で座標が取得できるのね!

山田

ただ、ゲームにはまだ使わないから、もし同じように試したのならば、戻しておいて欲しいんだべ

ちなみにブラウザで確認すると、このようになるべよ!

タッチされたところの座標が表示される
山田

ちなみに、タップ操作そうさスマホのみでしか対応たいおうしていないべから、Chromeの検証けんしょうからスマホ画面をエミュレートして、ためして欲しいべ

タッチされたときにシーンが切り替わるようにしてみよう!

つづいて、タッチされたときでも、シーンがわるように作っていきましょう!

山田

さきほど、タッチ機能きのうをつけるための仕組しくみを作ったべ
この機能をつかって、画面がタッチされたときに、シーンが切り替わるようにしてみるべ!

りこ

さっそく使ってみるのね!

山田

うむ。では、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 );

		//キャラクターのアニメーションのための変数
		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;
				//山田先生の画像を切り替える
				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 ) {
					//方向キーが押されているときは、タイルマップの移動速度と、山田先生の向きに、それぞれの値を代入する
					if ( game.input.left ) {
						tilemap.vx = WALKING_SPEED;
						yamada.direction = 1;
					}
					else if ( game.input.right ) {
						tilemap.vx = -1 * WALKING_SPEED;
						yamada.direction = 2;
					}
					else if ( game.input.up ) {
						tilemap.vy = WALKING_SPEED;
						yamada.direction = 3;
					}
					else if ( game.input.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;
					//もし移動後のマップ座標に障害物があるならば、移動量に0を代入する
					if ( tilemap.hasObstacle( yamadaCoordinateAfterMoveX, yamadaCoordinateAfterMoveY ) ) tilemap.vx = tilemap.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();

} );
山田

追加ついかしたのは24〜28行目だべ
さきほど作ったontouchstartメソッドをオーバーライドして、現在げんざいのシーンをメインシーンに切り替えているんだべ

タップでシーンを切り替えられる
アル

ホントだ、切り替わった!

りこ

タップ、タップ

山田

ふたたびのおねがいだべけど、対応たいおうしているのはタップのみで、クリックには対応していないべから、ためすときは、Chromeの検証けんしょうから、スマホをエミュレートして、やってみて欲しいべ

スマホでキャラクターを操作できるようにしてみよう!

つづいて、スマホでキャラクターを操作そうさできるようにしていきましょう!

アル

うーん。タップでシーンを切り替えられるようにはなったけど、キャラクターをスマホで操作できなくちゃ意味がないよね

りこ

そうね……シーンだけ切り替えられてもね……

山田

ふっふっふ。ではこれから、スマホでもキャラクターを操作できるようにしていくべよ!

アル

ホントに! やったぁ

りこ

やったぁ!

山田

では、画面がめんにコントローラを表示して、それを使ってキャラクターを操作できるようにするべ!
まず、engineフォルダに、dpad.jsというファイルを作るべ!

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

そしてindex.htmlからdpad.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/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>
山田

dpad.jsは、このようにして欲しいんだべ

js/engine/dpad.js

'use strict'

/**
 * D-Padに関してのクラス
 */
class DPad extends Sprite {

	/**
	 * 引数
	 * img : 画像ファイルまでのパス
	 * size : D-Padの大きさ
	 */
	constructor( img, size ) {
		//親クラスのコンストラクタを呼び出す
		super( img, size, size );
		//this.sizeに、指定されたサイズを代入
		this.size = size;
		//方向ボタンが押されているかどうか
		this.arrow = {
			up: false,
			down: false,
			left: false,
			right: false
		};
	} //constructor() 終了

	/**
	 * 画面がタッチされたときに呼ばれるメソッド
	 *
	 * 引数
	 * fingerPositionX: タッチされたX座標
	 * fingerPositionY: タッチされたY座標
	 */
	ontouchstart( fingerPositionX, fingerPositionY ) {
		//_applyToDPadメソッドを呼び出す
		this._applyToDPad( fingerPositionX, fingerPositionY );
	} //ontouchstart() 終了

	/**
	 * タッチされた指が動かされたときに呼ばれるメソッド
	 *
	 * 引数
	 * fingerPositionX: 指が触れている部分のX座標
	 * fingerPositionY: 指が触れている部分のY座標
	 */
	ontouchmove( fingerPositionX, fingerPositionY ) {
		//_applyToDPadメソッドを呼び出す
		this._applyToDPad( fingerPositionX, fingerPositionY );
	} //ontouchmove() 終了

	/**
	 * 画面から指がはなされたときに呼ばれるメソッド
	 *
	 * 引数
	 * fingerPositionX: 指がはなされた部分のX座標
	 * fingerPositionY: 指がはなされた部分のY座標
	 */
	ontouchend( fingerPositionX, fingerPositionY ) {
		//画像を切り替える
		this.frame = 0;
		//ボタンを初期化
		this.arrow = {
			up: false,
			down: false,
			left: false,
			right: false
		};
	} //ontouchend() 終了

	/**
	* D-Padに反映させる
	*
	* 引数
	* fingerPositionX: 指が触れている部分のX座標
	* fingerPositionY: 指が触れている部分のY座標
	*/
	_applyToDPad( fingerPositionX, fingerPositionY ) {
		//画像を切り替える
		this.frame = 1;
		//ボタンを初期化
		this.arrow = {
			up: false,
			down: false,
			left: false,
			right: false
		};
		//上ボタンが押されたとき、arrow.upをtrueにし、D-Padの角度を0度にする
		if ( fingerPositionX > fingerPositionY && fingerPositionX < this.size - fingerPositionY ) {
			this.arrow.up = true;
			this.rotate = 0;
		}
		//下ボタンが押されたとき、arrow.downをtrueにし、D-Padの角度を180度にする
		else if ( fingerPositionX > this.size - fingerPositionY && fingerPositionX < fingerPositionY ) {
			this.arrow.down = true;
			this.rotate = 180;
		}
		//左ボタンが押されたとき、arrow.leftをtrueにし、D-Padの角度を270度にする
		else if ( fingerPositionY > fingerPositionX && fingerPositionY < this.size - fingerPositionX ) {
			this.arrow.left = true;
			this.rotate = 270;
		}
		//右ボタンが押されたとき、arrow.rightをtrueにし、D-Padの角度を90度にする
		else if ( fingerPositionY > this.size - fingerPositionX && fingerPositionY < fingerPositionX ) {
			this.arrow.right = true;
			this.rotate = 90;
		}
	} //_applyToDPad() 終了
}
りこ

そっか。画面に表示するコントローラも、スプライトだから、Spriteクラスを継承けいしょうするのね!

山田

そうなんだべよ。D-Padは方向ボタンのことで、Dpadクラスは方向ボタンをボタンとして機能きのうするスプライトとして表示するためのクラスだべ

そして19〜24行目では、方向ボタンが押されているかどうかのオブジェクトを初期化しょきかしているんだべ

アル

なるほど。方向ボタンの、それぞれのボタンを、falseにしておくんだね!

山田

うむ
そして、58〜68行目を見て欲しいべ

アル

これは、指が画面からはなされたときに呼ばれるメソッドだね
あ、ここでもそれぞれのボタンをfalseにしてる!

山田

そうなんだべ。60行目でD-Padの画像をもとに戻して、62〜67行目ですべてのプロパティにfalseを代入してるんだべ
これで、指がはなされたときは、ボタンが押されていないことになるんだべよ

アル

なるほどー

りこ

34〜37行目、46〜49行目では、おなじメソッドを呼び出してるの?

山田

うむ。タッチされたときと、指が動かされたときは、おなじことをすればいいべ
こんかいは_applyToDpadメソッドを呼び出しているべよ

りこ

_applyToDpadメソッドは……あ、77〜107行目だ

アル

なにこれ。なにしてるのかさっぱりだよ……

山田

ひとつずつ見ていくべよ
まず79行目では、D-Padの画像をボタンが押されたものに切り替えているべ

アル

うん、これはむずかしくないね

山田

81〜86行目は、それぞれボタンを初期化しているべ

りこ

これも分かるね

山田

そして87〜106行目は、指の位置から、どのボタンが押されたのかを計算しているべ!

りこ

どういうこと?

アル

ここがさっぱりなんだよな……

山田

ではちょっと考えてみて欲しいべ
まず、これがD-Padの拡大画像だべ

山田

この画像の、たとえば上ボタンの部分の範囲はんいを出すにはどうすればいいと思うべ?

アル

ええと、境目さかいめが斜めになってるから、X座標ざひょうやY座標で指定していしようとしてもうまくいかないな……

りこ

分かんないよ

山田

じゃあ、ヒントだべ
この画像は正方形で、境目はかくのちょうど半分の角度になっているべ

アル

直角が90度だから、境目の角度は45度ってこと?

りこ

あ、45度ってことは、境目の位置のX座標とY座標は、どの部分でも同じになる!

アル

そうか。右上と左下との境目は、X座標 = Y座標ってことか。ってことは、指の位置のX座標が、Y座標よりも大きければ、上ボタンと右ボタンの範囲になるんだ!

X座標が、Y座標よりも大きいときの範囲
りこ

おなじように考えれば、上ボタンだけの範囲も出せそうね!

アル

そうだよね。えっと、上ボタンと右ボタンの境目は……あれ、X座標が大きくなるほど、Y座標が小さくなる
あー、あたまがこんがらがる!

りこ

Y座標が小さくなるってことは、境目のX座標は、画像の高さから境目のY座標を引いたものと同じってことよ!

アル

そうか! じゃあ上ボタンの範囲は、境目のX座標が、画像の高さから境目のY座標を引いたものよりも、小さいときだ!

X座標が、Y座標よりも大きく、画像の高さからY座標を引いたものよりも、小さいときの範囲
山田

うむ。ふたりともよく自分たちの力でこの難題なんだいいたべ!
そして、この範囲の条件じょうけんをifを使って書くならば、このようになるべ!

if ( fingerPositionX > fingerPositionY && fingerPositionX < this.size - fingerPositionY )
山田

同じように考えれば、そのほかのボタンの範囲も出せるんだべよ!

では、おまちかね、実際じっさいにゲームにD-Padを表示させてみるべ!

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 );

		//変数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;
				//山田先生の画像を切り替える
				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が押されているときは、タイルマップの移動速度と、山田先生の向きに、それぞれの値を代入する
					if ( game.input.left || dpad.arrow.left ) {
						tilemap.vx = WALKING_SPEED;
						yamada.direction = 1;
					}
					else if ( game.input.right || dpad.arrow.right ) {
						tilemap.vx = -1 * WALKING_SPEED;
						yamada.direction = 2;
					}
					else if ( game.input.up || dpad.arrow.up ) {
						tilemap.vy = WALKING_SPEED;
						yamada.direction = 3;
					}
					else if ( game.input.down || dpad.arrow.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;
					//もし移動後のマップ座標に障害物があるならば、移動量に0を代入する
					if ( tilemap.hasObstacle( yamadaCoordinateAfterMoveX, yamadaCoordinateAfterMoveY ) ) tilemap.vx = tilemap.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();

} );
山田

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

タッチすると山田先生が移動する
りこ

わー、ちゃんとスマホでも動かせるんだ!

アル

わーい

山田

ためすときは、Chromeの検証けんしょうから、スマホをエミュレートすることを忘れてはいけないべよ

つぎのページでは、ゲーム画面をブラウザのサイズに拡大かくだいしたり、りこちゃんとアルくんが山田先生の後ろをついてくるようにしたりしてみます。

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

  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で、画像つきのおみくじゲームを作ろう!

54件のコメント 「JavaScriptでRPGを作ろう!スマホにも対応したゲームの作り方」

  1. お世話になっております。
    青鬼のようにアイテムを入手したり、セーブをできるようにするにはどこをどのように変更すればよいか教えていただけませんでしょうか?ご返信お待ちしております。

    1. >mitoさん
      こんにちは。コメントありがとうございます。

      アイテムの管理やセーブ機能を作ろうと思うと、スクリプトの変更だけではむずかしいです。
      その前に、アイテムの管理画面やセーブロード画面を呼び出すための、メニュー画面から作る必要がありますし、そのメニュー画面上で方向キーを押したときに、キャラクターが動かないようにする必要もあります。

      メニュー画面を作るだけでも、それなりに大変な作業が必要になります。
      さらにそこから、アイテムの管理とセーブロード機能を作るわけですから、申し訳ないのですが、さくっとお答えできるものではないです。

      また、プログラミングとはあまり関係なくなってしまうのですが、青鬼はRPGツクールで制作されたものですので、もしそのようなゲームが作りたいならば、RPGツクールはおすすめです。

      もし、青鬼のようなゲームをプログラミングで作りたいとなると、今回のように0から作り始めるのは、かなりの時間と忍耐力が必要です。なにかライブラリを使った方がいいかもしれません。

      お役に立てず心苦しいですが、また遊びに来てください。
      よろしくお願いします。

  2. あれ、ひょっとして昨日の文章はちゃんと送信できてませんでしたかね?
    もしそうだとすると「 自己解決したので一応報告しておきます」って、何のこっちゃ状態ですが、すいません。無視してください笑

    1. >名無しの権兵衛さん
      こんにちは。コメントありがとうございます。
      昨日のメッセージは届いていないようですが、大丈夫です。
      大丈夫ってなんのこっちゃですが、大丈夫です。

      私の方でも、もういちど記事のコードをテストしてみたのですが、止まることなく動いているようです。
      原因はちょっと分からないのですが、ご報告と解決方法のご提供ありがとうございます。
      もし今後、プリロードがうまくいかないときに試してみたいと思います。

      ひとまず、ぶじ問題が解決できたようでよかったです。
      またよろしくお願いします。

  3. こんにちは。
    自己解決したので一応報告しておきます。
    プリロードの中で、いつまでたっても成功も失敗もしない状態にいるプロミスがときどき発生して困っていました。
    mainから先に進まないのでそこで止まってしまいます。

    どのファイルで停止したのか見ようと思い、末尾にsetTimeoutでrejectを加えたのですが、、、
    結果、この追加をしただけで、今のところ停止することがなくなってしまいました。
    停止する問題が解消されたのは嬉しいのですが、原因が明確でないのでモヤッとしてます。。。

    preload() {
    const _assets = arguments;

    for (let i = 0; i {
    if (_assets[i].match(/\.(jpg|jpeg|png|gif)$/i)) {
    //
    } else if (_assets[i].match(/\.(wav|wave|mp3|ogg)$/i)) {
    //
    } else {
    reject(`「${_assets[i]}」の形式は、プリロードに対応していないよ!`);
    }
    setTimeout(
    () => reject(`「${_assets[i]}」の読み込みがタイムアウトしました!`),
    1000
    );
    });
    }
    ~~~
    }

  4. いつも楽しく学ばせていただいております。
    ありがとうございます。

    js/main.jsの「75〜77行目……elseってのが入ってる」という部分ですが、else を入れると、ななめ移動をできなくすることができるのか、ご教示いただけませんでしょうか?

    そのまま(単にif文を記載している)でも、同じのような気がするので、補足いただけますと幸いです。

    よろしくお願いします。

    対象URL:https://original-game.com/make-an-rpg-with-javascript/6/

    1. >とおりすがりさん
      こんにちは。コメントありがとうございます。

      まず、ひとつ目の質問の、「なぜupdate()とrender()を別にするのか」です。
      これは、このさきでクラスの継承というものが出てきますが、ここでメソッドの上書き(オーバーライド)という仕組みを使います。
      今回はupdate()とrender()に分け、render()を上書きして機能を少しだけ変更する、といったことを行なっています。

      つづいて、「なぜstart()と_mainLoop()が別なのか」です。
      これは、ゲームをはじめるとき、ループの前にゲームの設定などを行なう必要があり、いきなり_mainLoop()を呼び出してしまうと、そういったことができなくなってしまいます。
      そこで、start()を呼び出し、そこから_mainLoop()を呼び出すことで、ループに入る前にさまざまな設定を行えるようになります。

      「呼び出されているupdate()は、Spriteのupdate()という理解で合っていますでしょうか?」というご質問については、まさにその通りです。素晴らしいです。

      最後の、「なぜ、elseを入れると、ななめ移動をできなくすることができるのか」です。
      ご存知の通り、elseは「それ以外のとき」という意味です。
      elseを使わずに、そのままif文を並べた場合、たとえば「もし上キーが押されたときは上に移動」と「もし右キーが押されたときは右に移動」というのが、同時に存在することになります。
      つまり「上キーと右キーを同時に押したら、右上に移動する」ということになります。
      対して、elseを入れることで、「上キーが押されたときは上に移動」、それ以外のとき「右キーが押されたときは右に移動」となり、ななめに移動することはなくなります。

      参考になれば幸いです。

      1. とても参考になりました。
        ご回答いただき、ありがとうございました。

        1. >とおりすがりさん
          こちらこそ、私の記事をご覧いただき、ありがとうございます。
          またよろしくお願いします。

  5. いつも楽しく学ばせていただいております。
    ありがとうございます。

    以下の部分ですが、
    呼び出されているupdate()は、Spriteのupdate()という理解で合っていますでしょうか?

    理由 → TileはSpriteを継承しているため。


    (js/engine/tilemap.js)

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

    対象URL:https://original-game.com/make-an-rpg-with-javascript/4/

  6. いつも楽しく学ばせていただいております。ありがとうございます。
    以下、ご質問させてください。

    なぜ、update()とrender()は別々にするのですか?
    同様に、game.jsで、start()と_mainLoop()が別な理由も教えていただきたいです。
    よろしくお願いします。

    ◆対処のページ:https://original-game.com/make-an-rpg-with-javascript/2/

  7. 初めまして。プログラミング初心者で気になったので勉強させてもらいました。
    移動するだけだと少し物足りないので戦闘シーンのようなものを作りたいのですがどう作ればいいかわからず困ってしまいました。わかりやすく作り方を教えていただけませんか。

    1. >yuukiさん
      コメントありがとうございます。

      戦闘シーンを作るには、ゲームエンジンをパワーアップしていく必要があると思います。
      ただ、その方法を紹介しようと思うと、膨大な内容になりますし、プログラムもかなり複雑になると思います。

      コメント欄では解説しきれませんし、また、解説記事の作成も時間がとれそうにありません。
      お力になれず、申し訳ございません。

  8. 初めまして、お世話になります。
    非常に勉強になりました。

    自分の勉強のために、こちらのプロジェクトをTypeScriptで書き直してみたものをGitHubにpushいたしました。
    そのなかで、このプロジェクトの画像ファイル、音声ファイルを再使用しております。
    事後報告で申し訳ありませんが、よろしくお願いいたします。

    よいお年をお過ごしください。m(_ _)m

    1. >とれいすさん
      はじめまして。あけましておめでとうございます。

      ご連絡いただき、ありがとうございます。
      GitHubへのpushと、素材の利用の件、承知いたしました。

      今年も良い年でありますように。

  9. はじめまして。Javascriptを学んでいる者です。RPGの記事、大変参考になりました。有難うございます!
    折角なので戦闘シーンも追加してみようとトライしているのですが、上手くいかず行き詰まっております…。

    以下2つの方法でいずれも壁に当たっております。ご助言頂けますと幸いです。

    ①戦闘シーンをメインシーンとは別で作成し、エンカウントした際にシーンを切り替える(戦闘終了したらメインシーンに戻す)
     →戦闘シーンへの切り替えは問題ないのですが、メインシーンに戻した時、キャラクターがゲーム開始時の初期位置に戻ってしまいます。エンカウントした時の位置を記憶する方法が思いつかず、行き詰まってしまいました…。
    ②上記の問題を避けるために、メインシーンの中で変数phaseを用意し、phase = 0ならフィールド、phase =1になったら戦闘のウインドウを出す、という形にする
     →変数mainSceneの中でphaseを宣言した上で、タイルの描画や移動の箇所をif文で括ったのですが、全く動かなくなってしまいました…。

    質問自体が分かりにくいかも知れず大変恐縮ですが、どうぞよろしくお願い申し上げます。

    1. >YTさん
      はじめまして。コメントありがとうございます。

      この記事では、ゲームエンジンを自作してしますが、やはりプログラミングの学習を目的としているため、ゲームエンジンとしてはまだまだ機能が少ないと言わざるを得ません。
      そのため、戦闘シーンを作ろうと思うと、ゲームエンジンの方をもっとパワーアップしていく必要があると思います。

      まず、YTさんがお試しになった②の方法ですが、メインシーンの中のscene.onenterframeの部分以外、最初の一度しか呼び出されないので、if文で括って戦闘シーンへ分岐するのはできないと思います。

      ①の方法を使いたいのですが、これもすこし問題点があって、今回のゲームエンジンでは、シーンの切り替えは可能ですが、シーンの上にシーンを重ねることができません。
      そして、シーンに対しては画像などのオブジェクトを直接addする形になっていますので、同じシーン上に戦闘の画面を作るのはむずかしいです。

      そこで、例えばゲームエンジンにレイヤー(層)の機能を作って、シーンにはレイヤーオブジェクトのみを追加できるようにし、そのレイヤーオブジェクトに対して、画像やテキストなどのオブジェクトを追加できるようにしてみるのはいかがでしょうか。

      あとは、メインシーンのレイヤーと戦闘シーンのレイヤーを作って重ねる、というイメージです。

      本当は私も実際に動かしてみるべきだとは思うのですが、なかなかその時間が取れないので、もしかしたらうまく動かない部分があるかもしれません。

      アイデアのひとつとして、参考にしていただけると幸いです。

      1. お忙しい中お返事いただき有難うございます!
        なるほど、シーン自体を重ねられるようにするにはエンジン自体の結構大掛かりな修正が必要になるのに対し、シーンの中でもう一つ層を増やして(レイヤー)それをいじるのであれば今の設計を活かしてできる(であろう)ということですね。承知いたしました。
        記事の中でシーンを追加していたページ(3ページ目)を参考にレイヤーの追加をトライしてみます。
        改めて、年末のお忙しい折、ご対応誠に有難うございます。

        1. >YTさん
          いろいろと試して、ゲームエンジンをどんどんパワーアップしていくと、ゲーム開発の夢が広がりますよね。
          うまくいくことを願っています。

          良いお年をお迎えください。

  10. 初めまして!
    Sceneを3つ作りたいと思い
    const titleScene = () => {


    scene.onenterframe = () => {
    if ( game.input.space )game.currentScene = leScene();
    }

    return scene;
    }
    const leScene = () => {


    scene.onenterframe = () => {
    if ( game.input.space )game.currentScene = mainScene();
    }

    return scene;
    }
    const mainScene{



    game.add( titleScene() );
    game.add( leScene() );
    game.add( mainScene() );
    としたのですがleSceneが反映されません。
    色々調べたのですがどこを改善すればよいのかが分かりません。
    よろしくお願いします。

    1. >はとさん
      はじめまして。
      コメントありがとうございます。

      はとさんのソースコードの、

      const mainScene{
      ⋮
      ⋮
      }
      

      のところを、

      const mainScene = () => {
      ⋮
      ⋮
      }
      

      に直しても、反映されませんか?

      お試しください。

      1. お返事ありがとうございます。
        その個所を修正してもtitleSceneとmainSceneの2つが読み込まれ、スペースキーを押すとtitleSceneからmainSceneに遷移しますが、leSceneが読み込まれず遷移することが出来ません。titleScene→leScene→mainSceneの順番に読み込みたいと考えています。

        1. >はとさん
          もしかすると、titleSceneでスペースキーを押したときに、leSceneのなかのif ( game.input.space )game.currentScene = mainScene();の方もほぼ同時に動作してしまって、leSceneが一瞬しか表示されていないのかもしれません。

          以下のようにしてみてはいかがでしょうか。
          ソースコードのなかのコメント部分が重要なところです。ご自身のプログラムに当てはめてみてください。

          'use strict'
          
          addEventListener( 'load', () => {
          
              let isSpaceKeyPushed = false;  // スペースキーが押されているかどうか
          
              const game = new Game();
              game.keybind( 'space', ' ' );
          
              const titleScene = () => {
                  const scene = new Scene();
                  const text = new Text( 'titleScene' );
                  scene.add(text)
                  scene.onenterframe = () => {
                      if ( game.input.space ) {
                          game.currentScene = leScene();
                          isSpaceKeyPushed = true;  // スペースキーが押されている
                      }
                  }
                  return scene;
              }
          
              const leScene = () => {
                  const scene = new Scene();
                  const text = new Text( 'leScene' );
                  scene.add(text)
                  scene.onenterframe = () => {
                      // スペースキーが離されたあと、スペースキーが押されたとき
                      if ( game.input.space && !isSpaceKeyPushed){
                          game.currentScene = mainScene();
                      }
                      // スペースキーが離されたとき、isSpaceKeyPushedをfalseにする
                      if ( !game.input.space ) {
                          isSpaceKeyPushed = false;
                      }
                  }
                  return scene;
              }
          
              const mainScene = () => {
                  const scene = new Scene();
                  const text = new Text( 'mainScene' );
                  scene.add(text)
                  return scene;
              }
          
              game.add( titleScene() );
              game.add( leScene() );
              game.add( mainScene() );
          
              game.start();
          } );
          

          参考になれば幸いです。

          1. 上手く実行することが出来ました!ありがとうございます!

          2. >はとさん
            うまくいったとのこと、私もうれしいです。
            また、よろしくお願いします。

  11. はじめまして! 
    ゴールした時に表示される文字にURLを埋め込んでその文字をクリックすると別のページに遷移させる機能を追加させることは可能でしょうか?
    また、最初のタイトルシーンからメインシーンの切り替えの時にメインシーンではなく他のWEBページなどの別のURLを取得しているページに遷移させることは可能でしょうか?
    色々と調べても解決が出来なかったのでこちらで質問をさせていただきました。
    よろしくお願いします。

    1. >よちゃさん
      はじめまして。
      コメントありがとうございます。

      まず、文字にリンクを埋め込む機能を作るのは、なかなかむずかしいと思います。
      もし作るのであれば、リンクにしたい「文字の範囲」がクリックされたときに別のページに移動、というふうになると思います。

      そのためには、まず、リンクにしたい文字がどれなのか、というのを判断する機能が必要です。
      HTMLでいうaタグのようなもので、挟んだところがリンクになる、というふうに作るといいかな、と思います。
      もちろん、そのaタグのようなものは、画面上に表示されないように削除する必要があります。

      さらに、リンクにしたい文字の座標を取得する機能も作らなくてはなりません。
      これは、全体のテキストの位置、文字のサイズ、文字間のサイズなどから割り出せると思います。

      しかし、上記に関しては検証する時間がとれず、実際に試すことができていないので、うまくいかないところがあるかもしれません。

      もうひとつの質問の、シーン切り替え時に別サイトへ進むプログラムですが、シーン切り替えのところに次のように入力すれば、「サイトURL」のところに入力したURLに進むことができます。

      
      open('サイトURL', '_blank');
      
      

      参考になれば幸いです。

      1. ご返信ありがとうございます!とても役に立ちました!シーン切り替え時のものは動作が上手くできたので、自分の思い描いているものが作れそうです!

        1. >よちゃさん
          お役に立てたようで、私も嬉しいです。
          ぜひ、ご自身の納得できるゲームを完成させてください。

          またよろしくお願いします。

  12. お世話になっております。いつもかなり勉強させて頂いております。プログラムの内容に関して質問させて頂きたく思います。party.jsの内容に関しまして、ソース文を写させていただき実行してみましたら、リコちゃんとアルくんが左右移動の時は行ったり来たりして、上下移動の際は斜めに荒ぶった動きをしてしまいました。ご経験お有りでしょうか?写し間違いがないか何度も確認してみたのですが無さそうで…もしアドバイスをいただければ幸いです!

    1. >Mura muraさん
      こんにちわ。コメントありがとうございます。

      その現象についてですが、私も作る途中で何度か経験しました。
      いろんな原因があると思うので確実な答えは出せないのですが、いまのところ思い浮かぶ原因として、それぞれのキャラクターが32で割り切れない座標にいるのかもしれません。
      タイルひとつが32pxなので、ずれてしまうと、うまくマップ上の位置(上から、または左から、何番目のタイルにいるのか)を出せなくなってしまいます。

      以下を、main.jsの、scene.onenterframe = () => { }のなかの、if ( ( tilemap.x - TILE_SIZE/2 ) % TILE_SIZE === 0 && ( tilemap.y - TILE_SIZE/2 ) % TILE_SIZE === 0 ) { }のなかにコピペして、それぞれのキャラクターが、その数のタイルの位置にいるのかを確かめてみてください。(タイルの枚数は、0から数える必要があります)

      console.log( `山田(X=${yamada.mapX}, Y=${yamada.mapY}) りこ(X=${rico.mapX}, Y=${rico.mapY}) アル(X=${aru.mapX}, Y=${aru.mapY})` );

      この値が小数になっていたりした場合は、それぞれのキャラクターの初期位置が32で割り切れる場所にいない、または移動したときに32pxずつ動いていない、という原因が考えられます。
      また、ここからほかの原因を探ることができるかもしれません。

      一度お試しください。

      1. 早速のご返信ありがとうございます。ご連絡頂きました内容を実行しましたところ、下方向に2歩移動しました結果は①(スタート地点)山田(X=1,Y=2)りこ(X=3,Y=2)アル(X=3,Y=3)②山田(X=1,Y=3)りこ(X=1,Y=1)アル(X=3,Y=0)③山田(X=1,Y=4)りこ(X=1,Y=4)アル(X=1,Y=1)  となりまして小数点にはなっておりませんでした。他に怪しい箇所が無いか再度確認してみたいと思います。他にアドバイスいただけましたら幸いです!

        1. >Mura muraさん
          こんにちわ。
          こちらでいろいろと試してみたのですが、全員、または、後ろをついてくるキャラクターの移動速度(後ろに.vx, .vyとついているもの)を2倍してみたところ、Mura muraさんの作ったものと同じ結果が得られました。

          party.jsには、後ろをついてくるキャラクターの移動速度を設定するメソッド(setMemberVelocity)がありますので、その辺りでミスをしてしまっている可能性が高いかな、と思います。
          party.jsのどこかで、this.member[i].vxやthis.member[i].vyを2倍してしまっているようなところはありませんか?

          もしかすると、原因は違うところにあるのかもしれませんが、私の方ではこのぐらいしか分かりませんでした。

          またなにかありましたら、お気軽にお訊ねください。

          1. 現象の再現までご確認いただき誠に有難うございます!party.jsのソース文を再度確認してみたのですが2倍をしてそうな箇所が見つからず色々悩んだ後にvx,vy
            の数式箇所全てを÷2しましたら正常に動作しました。ありがとうございます!(やはり他の箇所で2倍してしまっている箇所があるということですよね…探してみたいと思います!)今後は色々ページにて紹介して頂いております内容を応用させて頂いてRPGっぽく発展させていきたいと思います!

          2. >Mura muraさん
            まずは、ぶじ正常に動いたようで、よかったです。

            プログラミングは、エラーが出ていないのにうまく動かないときが、一番大変ですよね。
            移動速度2倍のことはいったん忘れて、ぜひゲームの発展を楽しんでください。
            そしてその過程で、ふと原因が分かるかもしれません。

            私も、よりよい記事を書けるように精進していきます。お互いに頑張りましょう。

            また遊びにきてください。

  13. はじめまして。
    質問です
    マップなどを使わさせていただいて、RPGを作っているのですが、家の中と外を移動するために
    マップを2つ作って、この2つのマップを移動するためにはどうしたらいいですか?
    よろしくを願いします

    1. >sgruさん
      コメントありがとうございます。

      2つのマップを移動する方法ですが、最もかんたんなのは、tilemap.dataの値を書き換えてしまう方法だと思います。
      記事の一番最後のページにある、ソースのまとめをご覧ください。

      「js/map.js」で、マップを作成していますが、ここに新たな変数を作って、家の中のマップを作成します。ここではmyHouseMapという変数にマップを作ったとします。

      「js/main.js」の67行目で、代入する変数を、さきほど作成した家の中のマップの変数(myHouseMap)に変更してみて、マップが変わるかどうかテストしてみてください。

      また、同じく「js/main.js」の164〜188行目では、キャラクターがゴールしたときのイベントを作成しています。
      このゴールのように、特定の場所に来たときになにかを起こす、といったふうに、出入り口に来たときにマップを切り替えるプログラムを作成します。つまりここでは、このゴールのタイルが出入り口の扉、ということにします。
      といっても、ゴールのタイルに重なったときにマップを切り替えるだけなので、以下のようになります。

      //山田先生のタイルがゴールのタイルと重なっているとき、イベントを発生させる
      if ( yamada.isOverlapped( goal ) ) {
      tilemap.data = myHouseMap;
      }

      これで、マップを切り替えることができます。
      あとは、双方から移動可能にしたり、エフェクトを入れてみたりなど、お好みにアレンジしてください。

      参考になれば幸いです。

      1. すみませんが、もう一つお願いします。
        マップを移動したときにX座標とY座標がそのままになってしまうので、
        X座標とY座標を変えてキャラクターを移動させる方法も教えて下さい。
        お願いします。

        1. キャラクターの移動はできました。
          すみませんが、また問題が起きてしまったのでお願いします。
          下のようにマップを変数で変更するときにはどうしたら良いでしょうか?
          var map = [
          [ 1, 1, 1, 1, m1 , 1],
          [ 1, 1, 1, 1, m1 , 1],
          [ 1, 1, 1, 1, m1 , 1],
          [ 1, 1, 1, 1, m1 , 1],
          [ 1, 1, 1, 1, m1 , 1]
          ];
          var m1 = 1;
          m1 = 3;
          マップの中に「m1」という変数を入れて初めは「1」だが、「3」にするということをしたいです。
          しかし、普通にやってしまうと、マップがそのままで、変更後が読み込めません。
          よろしくおねがいします。

          1. >sgruさん
            返信が遅くなってしまい、申し訳ありません。
            キャラクターの移動がうまくいったようで、私も嬉しいです。

            今回のマップの一部分だけを変更したい場合ですが、いちばん手軽に試せるのは、変更前と変更後のマップふたつ作りそれを切り替える、という方法だと思います。

            const map = [
            	[ 1, 1, 1, 1, 1, 1],
            	[ 1, 1, 1, 1, 1, 1],
            	[ 1, 1, 1, 1, 1, 1],
            	[ 1, 1, 1, 1, 1, 1],
            	[ 1, 1, 1, 1, 1, 1]
            ];
            
            const map2 = [
            	[ 1, 1, 1, 1, 3, 1],
            	[ 1, 1, 1, 1, 3, 1],
            	[ 1, 1, 1, 1, 3, 1],
            	[ 1, 1, 1, 1, 3, 1],
            	[ 1, 1, 1, 1, 3, 1]
            ];
            

            また、マップを生成する関数をつくる方法もあります。

            //マップを生成する関数
            const createMap = ( tile = 1 ) => {
            	//マップの作成
            	const _map = [
            		[ 1, 1, 1, 1, tile, 1],
            		[ 1, 1, 1, 1, tile, 1],
            		[ 1, 1, 1, 1, tile, 1],
            		[ 1, 1, 1, 1, tile, 1],
            		[ 1, 1, 1, 1, tile, 1]
            	];
            	
            	return _map;
            }
            
            //マップの作成
            tilemap.data = createMap();
            
            //マップを切り替えたいとき
            if ( 条件 ) {
            	tilemap.data = createMap( 3 );
            }
            

            上の関数は、サンプルとして作ったかんたんなものですので、実際にゲームに組み込むならば、さまざまなマップに対応できるように工夫する必要があります。
            お好みの方法をお試しください。

            参考になれば幸いです。

          2. 何度も教えてくださりありがとうございました。
            とっても役に立ちました。

  14. いつも参考にさせてもらっております。ありがとうございます。
    タイトル画面でスペースキーを押すと、Access to internal resource at ‘略’ from origin ‘null’ has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, chrome-untrusted, https.なるエラーが発生し画面が切り替わりません。対処法を教えていただけると幸いです。

    1. >てかりんさん
      コメントありがとうございます。

      まず、私ももう一度、記事にある最終的なソースをWindowsとMacで試してみたのですが、私の環境ではエラーはなく、正常に動かすことができました。

      今回のエラーでは、CORS policyによってブロックされた、となっていますので、Chromeがセキュリティ対策として機能を制限してしまっているのだと思います。
      正直なところあまり自信はないのですが、ローカルサーバを立てて実行すると、うまくいくかもしれません。

      ただ、私の環境では、ローカルサーバを使わなくても、WindowsとMacで正常に動作しており、どうしてそのようなエラーが表示されてしまっているのか、大変申し訳ないのですが、分からないです。

      原因は、環境の違いか、ソースの違いのどちらかだと思います。

      もし、記事の内容の通りに進めていただけているのであれば、index.htmlファイルを確認してみてください。必要のないタグを間違えて入れてしまっている可能性があります。
      私の環境では、<link rel="manifest" … >といったタグを入れてみたところ、同じエラーが発生しました。

      もし、オンライン上の開発環境を使っているのであれば、一度ローカルでお試しください。
      ローカルで開発しているのであれば、この記事の最後のページに、すべてのソースをまとめてありますので、参考にしてください。このとき、画像や音声、ソースファイルの、ディレクトリにご注意ください。

      お役に立てたか分かりませんが、またよろしくお願いいたします。

      1. htmlファイルのというタグを消したところ想定通り動くようになりました。ありがとうございます。

        1. >てかりんさん
          無事解決したようで、私も嬉しいです。
          またなにかあれば、お気軽にコメントください。

  15. こんばんは。はじめまして。
    参考にさせてもらっているのですが、
    const yamada = new Sprite( ‘img/yamada.png’ );

    を打ち込むと黒かった画面が真っ白になってしまい、画像どころかなにもでません。
    エラーはバリデーターの方でチェックしましたが、エラーなしでした。
    色々考えたり試したりしたのですが解決しませんでしたのでこちらに失礼します。ぜひよろしくお願いします。

    1. >はなさん

      はじめまして。
      私の記事を読んでいただき、ありがとうございます。

      黒かった画面が真っ白になった、とのことですが、これはなにかミスがあったときによく起こります。
      しかしミスがあれば、おそらくエラーが表示されるはずだと思うのです。

      Chromeの画面を右クリックして「検証」で表示された画面から、「Console」を選択したときも、なにも表示されていませんか?

      また、「const yamada = new Sprite( 'img/yamada.png' );」と打ち込むとエラーになるということは、sprite.jsがindex.htmlからうまく読み込めていないか、sprite.jsのどこかに入力ミスがあるかの、どちらかかな、と思います。

      参考になれば幸いです。
      またなにかあれば、お気軽にご連絡ください。

      1. お返事ありがとうございます。
        とても丁寧で初心者の私でも助かります。
        IO Error: HTTP resource not retrievable. The HTTP status from the remote server was: 401.
        とバリデーターで出てしまったのですが、gitpodの環境ではこちらのプログラミングはできないものなのでしょうか?
        よろしくお願いします。

        1. >はなさん

          コメントありがとうございます。
          私もgitpodで試してみました。本当に画像の表示の部分で真っ白になり、エラーも表示されませんね。
          私はgitpodを使ったことがなかったので、その環境でできるのかどうか、方法があるのかは分からないですが、今回のゲーム開発においては、gitpodの環境はちょっと使いにくいかな、と思います。

          私ははなさんがローカルで開発しているものだとばかり思っていたので、さきほどは間違った回答をしてしまいました。すみません。

          JavaScriptは、環境やブラウザによって、うまく動いたり、動かなかったりするんですよ。よくオンラインゲームで推奨ブラウザや推奨OSなどいったことが書いてあるのはそのためです。
          できれば、多くの方が使っている環境やブラウザで作ったほうがいいかな、と思います。

          もしgitpodの環境で、どうしてもうまくいかないようでしたら、一度ローカルで試してみるのがいいと思います。
          開発用にフォルダを作って、index.htmlやjsファイルを作成し、Chromeで確認する、一番シンプルな方法です。
          今回のような、ライブラリ等を使わないJavaScriptでの開発であれば、特別なものを用意する必要もないですし、もしそれでうまくいけば、環境がエラーの原因だったと分かります。

          あと、その方法でしたら、私もお答えできると思います。

  16. 初めまして!
    いつもとてもわかりやすい記事ありがとうございます。
    JavaScriptの勉強になり、とても助かっています。

    下記をもしよろしければ教えていただくことは可能でしょうか?

    【質問内容】
    こちらのgame.jsの48行目のfor文の箇所(「ゲームに登場する全てのもの(オブジェクト)の数だけ繰り返す」)のところでエラーが出てしまい、山田先生の画像が表示されません。

    【調べた内容】
    this.objs[i].update(this.canvas);のコードでUncaught TypeError: this.objs[i].update is not a functionとコンソールに表示されています。
    調べてみたところ、this.objs[i]が関数として呼び出されていないのではないかと考えました。

    for文の中のthis.objs[i].update(this.canvas);の上でconsole.log(this.objs[i])を試したところ、下記が取得できました。
    Sprite {img: img, y: 0, x: 0}this.objs[i].update(this.canvas);

    試しにこちらのサイトのコードを全てコピーして貼り付けてみましたが、エラーは変わっておりません。
    Gameクラス内のthis.objs = [];の場所なども関係しているのかと思い、場所を変えるなどしましたが、解決できませんでした。

    JavaScriptについてしっかり理解しておらず恐縮ですが、こちらのエラー原因のヒントなどいただけますと幸いです。
    よろしくお願いいたします。

    1. >るるさん

      はじめまして。
      コメントありがとうございます。
      私の記事を参考にしていただけているようで、とても嬉しいです。

      うまくプログラムが動かない、ということで、慌てて私もその部分のソースをコピーして、もう一度試してみました。
      私の環境ではちゃんと動作しているようです。

      まず、「Uncaught TypeError: this.objs[i].update is not a function」というエラーですが、これは「this.objs[i].updateは関数じゃないよ」というものです。
      ですので、「this.objs[i]が関数として呼び出されていない」というよりも、「this.objs[i].updateを関数として呼び出せない」という意味になります。

      this.objs[i].update」というのは、sprite.jsにある「update( canvas ) { 〜省略〜 }」の部分です。
      つまり「this.objs[i].update( this.canvas );」というのは、今の時点ではSpriteクラスのupdateメソッドを呼び出していることになります。これは、this.objsの中にSpriteが入っているからです。
      るるさんが「console.log(this.objs[i]);」を試したときSpriteを取得できた、ということは、ちゃんとthis.objsSpriteが入っているということです。

      さて、そうなると、エラーの原因は「Spriteクラスのupdateを関数として呼び出せないよ」ということになります。
      Spriteクラスはsprite.jsファイルに書かれているので、そのなかのupdateメソッドの辺りでなにかミスをしてしまっているのではないか、と思います。

      お役に立てれば幸いです。

      1. お忙しい中ご返信ありがとうございます!
        エラーの意味や処理の流れまで解説していただいて本当にありがたいです。
        おかげさまで理解が深まりました。

        大変お恥ずかしいのですが、sprite.jsのupdateがudpateになっていました…
        ちゃんとエラーも出ていたのに、完全に確認不足でした。
        お手数をお掛けしてすみません。今後はちゃんと確認するようにします!
        難しくてなかなか理解が進んでいないのですが、少しずつでも完成に近づけるように頑張ります!
        ありがとうございました!

        1. >るるさん

          無事解決できたとのこと、私も嬉しいです。
          プログラミングのエラーを直すのはとても大変ですが、うまく動いたときの喜びはこの上ないですよね。

          私も、もっと良い記事が書けるよう、努力していきたいと思います。
          ぜひ、また遊びにきてください。

コメントを残す(コメントは承認後に反映されます)

メールアドレスが公開されることはありません。 が付いている欄は必須項目です




オリジナルゲーム.com