
次はゴールしたときに、クリアできるようにする?

うーむ。まずはキャラクターが歩くときのアニメーションを作って、それからにしようと思うべ

わぁ、きっと歩いてるみたいに見えるようになるんだ

そっかぁ。楽しみだなぁ……
キャラクターをアニメーションさせてみよう!
つづいて、キャラクターが移動するとき、アニメーションするようにしていきましょう!
rpg/
|-- img (省略)/
|-- index.html
|-- js/
| |-- engine/
| | |-- game.js
| | |-- charactertile.js (追加)
| | |-- scene.js
| | |-- sprite.js
| | |-- tile.js
| | `-- tilemap.js
| `-- main.js
`-- sound (省略)/
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<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/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>

そして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' );
//画家さんに、絵を描いてとお願いする
_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
);
} //render() 終了
}

Tileクラスを継承して、キャラクター専用のタイルの機能を作っているのね

そうだべ
このCharacterTileクラスは、タイルのアニメーションがとてもやりやすくなるように作ったんだべ

へぇ、どういう機能があるの?

キャラクターの向きごとの画像と、移動のアニメーションのそれぞれの画像を、分けて指定することができるんだべ
まぁ、まずは使ってみるべよ
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 CharacterTile( 'img/yamada.png' );
//山田先生を画面の中央に配置
yamada.x = yamada.y = TILE_SIZE*5 - TILE_SIZE/2;
//タイルマップの動きと同期させない
yamada.isSynchronize = false;
//tilemapに、山田先生のタイルを追加して、とお願いする
tilemap.add( yamada );
//キャラクターのアニメーションのための変数
let toggleForAnimation = 0;
//ループから常に呼び出される
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 ( 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 終了
//gameに、シーンを追加して、とお願いする
game.add( scene );
//gameに、ゲームをスタートして、とお願いする
game.start();
} );

キャラクターが歩くとき、右足を出したり、左足を出したりを繰り返すべ
つまりそのアニメーションは、交互に違う画像を表示する必要があるべ

右足を出す、足をそろえる、左足を出す、足をそろえる、ってのを繰り返すってことよね

わかった。その数値によって、左右を切り替えるんだ!

うむ。そして77行目で、yamada.animationに1を代入しているべ
この1というのが、足をそろえている状態だべ

なるほどー

そして、実際に左右を切り替えているのが、102〜109行目だべ
105行目のように、toggleForAnimation ^= 1とすれば、0と1を交互に代入することができるんだべ

へぇ、そうなんだ

また79〜94行目は、方向キーが押されたときにキャラクターの向きも変わるようになおしたべ
yamada.directionがキャラクターの向きを変更するものだべ

この数値だけで向きが変わるんだ! CharacterTileクラス、すごく便利になってる!


ふふっ、歩いてるように見えるね!
イベントを作ってみよう!
つづいて、イベントを作っていきましょう!

さて、ここからはおまちかね、イベントを作っていくだべよ!
ゴールまでたどりついたときに、イベントを発生させるようにしてみるべ!

わーい! やっと作るんだ!

やったぁ!

ではまず、Tileクラスに、タイル同士が重なっているかどうか判定するメソッドを作るべ!
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() 終了
/**
* タイル同士が重なっているかどうかを取得できるメソッド
*
* 引数
* tile : 重なっているかを判定したいタイル
*/
isOverlapped( tile ) {
//引数がTileのとき
if ( tile instanceof Tile ) {
//タイル同士が重なっているかどうか
const _isOverlapped = ( this.mapX === tile.mapX && this.mapY === tile.mapY );
//タイル同士が重なっているかどうかの結果を返す
return _isOverlapped;
}
//引数がTileでなければ、コンソールにエラーを表示
else console.error( 'Tilemapに追加できるのはTileだけだよ!' );
} //isOverlapped() 終了
}

追加したのは24〜40行目だべ
このメソッドは、2つタイルの、mapXとmapYが同じならtrueを、違うならfalseを返すものだべ

じゃあ、このメソッドを使って、trueのときにイベントをおこすようにすればいいんだ!

そういうことだべ
では、わたすのタイルと、ゴールのタイルが同じ場所になったら、コンソールに「ゴールだべ!」と表示するようにしてみるべ
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 CharacterTile( 'img/yamada.png' );
//山田先生を画面の中央に配置
yamada.x = yamada.y = TILE_SIZE*5 - TILE_SIZE/2;
//タイルマップの動きと同期させない
yamada.isSynchronize = false;
//tilemapに、山田先生のタイルを追加して、とお願いする
tilemap.add( yamada );
//キャラクターのアニメーションのための変数
let toggleForAnimation = 0;
//ループから常に呼び出される
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 ) ) console.log( 'ゴールだべ!' );
//方向キーが押されているときは、タイルマップの移動速度と、山田先生の向きに、それぞれの値を代入する
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 終了
//gameに、シーンを追加して、とお願いする
game.add( scene );
//gameに、ゲームをスタートして、とお願いする
game.start();
} );

80行目で、ゴールとわたすのタイルが重なっているとき、コンソールに「ゴールだべ!」と表示するようにしているべ


やったぁ、表示されたよ!

でも、やっぱりちゃんと画面に表示したいよね……

そうだべな。じゃあ次からは画面にテキストを表示する機能を作っていくべ
テキストを表示させてみよう!
つづいて、画面にテキストを表示させる機能を作っていきましょう!
rpg/
|-- img (省略)/
|-- index.html
|-- js/
| |-- engine/
| | |-- game.js
| | |-- charactertile.js
| | |-- scene.js
| | |-- sprite.js
| | |-- text.js (追加)
| | |-- tile.js
| | `-- tilemap.js
| `-- main.js
`-- sound (省略)/
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<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>

そして、text.jsの中身は、このようにしてほしいんだべ
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;
} //constructor() 終了
/**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 );
//画像などを画面に表示するためのメソッドを呼び出す
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() 終了
/**
* 常に呼び出されるメソッド。空なのはオーバーライド(上書き)して使うため
*/
onenterframe() {}
}

これまでの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;
//移動できないタイルを指定する
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;
//ループから常に呼び出される
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.x = 15;
goalText.y = 135;
//シーンにテキストを追加
scene.add( goalText );
//ゴールのテキストが表示されているかどうかの変数にtrueを代入
hasDisplayedGoalText = true;
}
}
//方向キーが押されているときは、タイルマップの移動速度と、山田先生の向きに、それぞれの値を代入する
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 終了
//gameに、シーンを追加して、とお願いする
game.add( scene );
//gameに、ゲームをスタートして、とお願いする
game.start();
} );

これで画面にテキストが表示されるの?


おおぉ! やっぱりクリアしたときにテキストが表示されるといい感じだね!

そうだべな
それともうひとつ、これでシーンに追加できるものをすべて作ったべから、それ以外のものをシーンに追加できないようになおしておくべよ
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() 終了
/**
* 常に呼び出され、スプライトの移動やイベントの発生などに使うメソッド。空なのはオーバーライド(上書き)して使うため
*/
onenterframe() {}
}

これでシーンに入れるものを間違えても、エラーが出るから安心ね!
テキストを中央に表示する機能を作ろう!
つづいて、テキストを中央に表示する機能を作ってみましょう!

さきほど、テキストを表示した位置は、キャンバスの中心にくるように、だいたいの数値を入力したものだべ
そこで、もっとかんたんに中央表示ができる機能を作っていくべよ
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() 終了
/**
* 常に呼び出されるメソッド。空なのはオーバーライド(上書き)して使うため
*/
onenterframe() {}
}

追加したのは33〜36行目、39〜47行目、49〜59行目、そして82〜85行目だべ

えっと、あれ? 46行目と58行目、returnでthisを返してる!
これってどういうこと?

ふっふっふ。returnでthisを返すことで、メソッドをチェーンにできるんだべ
ちょっとやってみたかったべよ

チェーン?

こんなふうに使うことができるようになるんだべ!
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 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;
//ループから常に呼び出される
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.x = 15;(削除)
//goalText.y = 135;(削除)
//テキストを上下左右中央の位置にする
goalText.center().middle();
//シーンにテキストを追加
scene.add( goalText );
//ゴールのテキストが表示されているかどうかの変数にtrueを代入
hasDisplayedGoalText = true;
}
}
//方向キーが押されているときは、タイルマップの移動速度と、山田先生の向きに、それぞれの値を代入する
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 終了
//gameに、シーンを追加して、とお願いする
game.add( scene );
//gameに、ゲームをスタートして、とお願いする
game.start();
} );

93行目を見て欲しいべ

メソッドが2つ繋がってる!

もしかして、2つ繋げることで、左右も、上下も、中央に表示されるってこと?


メソッドを繋げられるなんて、おもしろいね!
ゴールしたあと、キャラクターが移動できないようにしよう!
つづいて、ゴールしたあとにキャラクターが動けないようにしてみましょう!

さて、これでゴールしたときのテキストが表示されたわけだべけど、今のままではゴールしたあとも移動できてしまうべよ

ほんとだ……なんか気になるよね

そこで、ゴールしたあとはキャラクターが移動できないようになおしていくべ
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 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;
}
}
//移動可能なとき
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 終了
//gameに、シーンを追加して、とお願いする
game.add( scene );
//gameに、ゲームをスタートして、とお願いする
game.start();
} );

移動できるところからはじまるから、初期値がtrueなんだね!

移動できなくするんだね!

これでゴールしたあとは動けなくなるんだ


やったぁ。これで本当にゲームらしくなってきたぞ!
次のページでは、シーンを関数にまとめて、さらにタイトルシーンを作っていきます。
このシリーズの一覧はこちら
- 小学生からのプログラミング入門。プログラミングってなぁに?
- 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で、画像つきのおみくじゲームを作ろう!