
ねぇ、スタートとゴールができたんだから、ゴールしたら、クリアできるようにしようよ!
あと、壁も通り抜けられないようにしないと!

まぁまぁ、慌てなさんなだべ
それをするには、まず、マップ上の座標を取得できるようにする必要があるべ

あれ? 座標はもう分かってるよね……マップ上の座標ってどういうことだろう……
マップ上の座標を取得できるようにしてみよう!
つづいて、マップ上の座標を取得できるようにしてみましょう!

ねぇ、マップ上の座標ってなぁに?

たとえば、わたすのタイルが、上から何番目で、左から何番目なのか、という座標だべ

なるほど! タイルの数であらわす座標かぁ

これまで使ってきた座標よりも、分かりやすくなりそうだね!

ここではこの座標を、これまでの座標と区別するために、「マップ座標」と呼ぶことにするべよ

うん! 分かった

マップ座標が取得できれば、壁との当たり判定や、わたすがゴールしたときはゲームクリアにする、といったことができるようになるべよ

なるほど! じゃあまずはマップ座標を取得するプログラムを作るんだね!
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;
//マップ座標に0を代入。(マップ座標は、タイルマップの左上から何番目のタイルの位置にあるのか、という意味でここでは使っています)
this.mapX = this.mapY = 0;
//タイルマップと同期して動くかどうか
this.isSynchronize = true;
} //constructor() 終了
}

19行目のmapX、mapYっていうのが、マップ座標かな

そうだべよ。ここでマップ座標を初期化しているんだべ

両方に0を代入してるんだね
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のとき
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() 終了
/**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() 終了
/**
* 常に呼び出されるメソッド。空なのはオーバーライド(上書き)して使うため
*/
onenterframe() {}
}

39〜51行目、81〜88行目が、変更したところだべ

えっと……あ、どっちもマップ座標を計算してるんだ

そうだべよ。タイルを追加したとき、そして追加されたあとは常に、マップ座標を計算してるんだべ
計算は、タイルの位置をタイルサイズで割って、さらにマップと一緒に動かないタイルだった場合は、マップの位置を含めてもう一度計算しなおしているべ

どちらのタイルでも、正しいマップ座標が取得できるようになるのね

では、ちゃんとマップ座標が取得できているか、console.log()を使って実験してみるべ
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;
//コンソールにマップ座標を表示
console.log( `${yamada.mapX} ${yamada.mapY}` );
}
} //scene.onenterframe 終了
//gameに、シーンを追加して、とお願いする
game.add( scene );
//gameに、ゲームをスタートして、とお願いする
game.start();
} );


0から数えて、何番目のタイルに山田先生がいるかが分かるんだね!

これがゴールのマップ座標と同じになったときに、ゴールのイベントをおこせばいいんだね!

でも、壁のタイルの上に乗っちゃってるね……

これじゃあゴールまですぐに行けちゃうね……

ふっふっふ。では次は、壁を通ることができないようにしていくべ!
壁との当たり判定をつくってみよう!
つづいて、壁との当たり判定を作り、山田先生が壁の上にいけないようにしてみましょう。

これまでのプログラムでは、わたすのタイルが、壁の上を進むことができてしまっていたべ
そこで、タイルが壁かどうかを判断するプログラムを作って、わたすのタイルが壁の上を進めないようにするべ!
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() 終了
/**
* 常に呼び出されるメソッド。空なのはオーバーライド(上書き)して使うため
*/
onenterframe() {}
}

まず30〜31行目で、移動できないタイルを入れておくプロパティを初期化してるんだべ

タイルなのに0を入れてるの?

入れるのはタイルを表す数字だべよ


あ、この中から、移動できないタイルの数字をすべて入れておくのね!

さらに58〜70行目で、指定されたマップ座標のタイルが、移動できるかどうかを取得する機能を作っているべ

その機能をはやく使ってみようよ!
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;
//移動できないタイルを指定する
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 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;
else if ( game.input.right ) tilemap.vx = -1 * WALKING_SPEED;
else if ( game.input.up ) tilemap.vy = WALKING_SPEED;
else if ( game.input.down ) tilemap.vy = -1 * WALKING_SPEED;
//移動後のマップ座標を求める
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;
//コンソールにマップ座標を表示(削除)
//console.log( `${yamada.mapX} ${yamada.mapY}` );(削除)
}
} //scene.onenterframe 終了
//gameに、シーンを追加して、とお願いする
game.add( scene );
//gameに、ゲームをスタートして、とお願いする
game.start();
} );

まず38行目で、移動できないタイルをすべて指定しているべ

壁や水面のタイルだね!

あれ、75〜77行目……elseってのが入ってる

それは、キャラクターがななめに移動できなくするためのものだべよ
ななめに移動できてしまうと、マップの作り方によっては、ふつうは行けなさそうなところも、ななめ移動を使って行けてしまったりするんだべ
あえてそれを可能にすることも考えられるべけど、今回はなしの方向で進めていこうと思うべよ

なるほど!

そして、79〜83行目では、移動後のマップ座標に移動できないタイルがあるかどうかで、移動を制限しているんだべ

そっか。移動後のマップ座標で考えなくちゃいけないんだ

移動後のマップ座標は、いまのマップ座標から、タイルの移動速度を歩く速さで割ったものを引けば、出すことができるべ
なぜ引くのかというと、タイルの移動はプレイヤーの移動とは反対に進むからだべよ
……移動速度と歩く速さって言葉が曖昧だべけど、移動速度には向きも含まれてて、値をマイナスにすれば反対方向にも動くべ
たいして歩く速さは、どの方向に進んでも値はプラスだべ

うーん、むずかしいな……でもなんとなく分かったかな……

つぎのページでは、キャラクターのアニメーション、そしてゴールしたときのイベントを作っていきたいと思います。
このシリーズの一覧はこちら
- 小学生からのプログラミング入門。プログラミングってなぁに?
- Scratchの使い方と、ゲーム作りの基礎知識を学ぼう! 小学生からのプログラミング入門
- Scratchでじゃんけんゲームを作ろう! 小学生からのプログラミング入門
- Scratchでシューティングゲームを作ろう! 小学生からのプログラミング入門
- Scratchでピアノ鍵盤を作って音を鳴らそう! 小学生からのプログラミング入門
- テキストエディタ(Visual Studio Code)をインストールしてみよう! 小学生からのプログラミング入門
- Visual Studio Codeを日本語化してみよう! 小学生からのプログラミング入門
- JavaScriptでおみくじを作ろう! 小学生からのプログラミング入門
- JavaScriptで今月の残り日数を計算してみよう! 小学生からのプログラミング入門
- JavaScriptで画像を表示してみよう! 小学生からのプログラミング入門
- JavaScriptで画像を移動してみよう! 小学生からのプログラミング入門
- 【JavaScript】キー入力でキャラを動かしてみよう! 小学生からのプログラミング入門
- 【JavaScript】ファイルを分けて管理してみよう! 小学生からのプログラミング入門
- 【JavaScript】オブジェクトを使ってみよう! 小学生からのプログラミング入門
- 【JavaScript】ゲームのメインループを作ってみよう! 小学生からのプログラミング入門
- 【JavaScript】キャラを決まった間隔ずつ動かす! 小学生からのプログラミング入門
- HTML5とCanvasを使ってみよう! 小学生からのプログラミング入門
- 【JavaScript】迷路やRPGで使えるマップを作ってみよう! 小学生からのプログラミング入門
- 【JavaScript】マップでキャラを動かせるようにしよう! 小学生からのプログラミング入門
- 【JavaScript】クラスの概念をしっかりと理解しよう! 小学生からのプログラミング入門
- 【JavaScript】プログラム全体をクラスを使って作ってみよう! 小学生からのプログラミング入門
- 【JavaScript】文字を表示するクラスを作ってみよう! 小学生からのプログラミング入門
- 【JavaScript】改行と一文字ずつ画面に表示する方法! 小学生からのプログラミング入門
- 【JavaScript】ノベルゲーム風にキー入力で文字を切り替える方法! 小学生からのプログラミング入門
- JavaScriptでRPGを作ろう!スマホにも対応したゲームの作り方
- webpackを使ってゲームエンジンを作ろう!(JSライブラリの作り方)
- WindowsにPythonをインストールしてみよう!小学生からのPython入門
- MacにPythonをインストールしてみよう!小学生からのPython入門
- Pythonでじゃんけんゲームを作ってみよう!小学生からのPython入門
- Pythonのtkinterを使って、ウィンドウを表示してみよう!
- Pythonのtkinterで、画像つきのおみくじゲームを作ろう!