むむ……手強いべ
せーんせ、スマホでなにやってるの?
うむ……わたすもむかしドラクエはやったべけど、アルくんを見ていたら、久しぶりにもう一度やりたくなってしまったべ。そこでまだプレイしていなかったのをやってみてるんだべけど、これがはまってしまって……
しかし、いまはスマホでもドラクエができてしまうんだべな……
はぁ、はぁ……ごめーん。国語の教科書、まちがえてとなりの席の子の持ってきちゃってて、返してきたんだ
ふふっ。アルったら、ドジね
あ、それ。ドラクエだ!
うむ。久しぶりにわたすもやってみてるべ
おうじゃのけんは手に入れた? たしかオリハルコンを……
あ、あ、あ、言っちゃだめだべー
スプライトを回転できるようにしてみよう!
つづいて、スプライトを回転させることができる機能を作っていきましょう!
つぎはスプライトを回転させる機能を作っていくべよ!
へぇ、回転かぁ。いろんなところで使えそうだね
うむ。ゲームをスマホで操作する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;
ふふふっ、先生がかたむいてる
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>
えっ、これがないと、うまく表示されないの?
そうなんだべ
たとえば、ゲームがスマホの端に小さく表示されてたら、プレイしづらいべな
たしかに……
そこで今回は、viewportの横幅を画面のサイズに、倍率を1.0に、そしてユーザーが拡大できないようにしているんだべ
viewport?
表示する領域のことだべ
ううん……よく分からない
ホントだ! こう変わってくるのかぁ
拡大もできなくなってる!
スマホでタッチされた位置を取得できるようにしてみよう!
つづいて、スマホでタッチされた位置を取得できるようにしてみましょう!
つぎに、スマホでタッチされた指の位置を取得するプログラムを作っていくべ!
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}` );
}
引数で座標が取得できるのね!
ただ、ゲームにはまだ使わないから、もし同じように試したのならば、戻しておいて欲しいんだべ
ちなみにブラウザで確認すると、このようになるべよ!
タッチされたときにシーンが切り替わるようにしてみよう!
つづいて、タッチされたときでも、シーンが切り替わるように作っていきましょう!
さきほど、タッチ機能をつけるための仕組みを作ったべ
この機能をつかって、画面がタッチされたときに、シーンが切り替わるようにしてみるべ!
さっそく使ってみるのね!
うむ。では、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の検証から、スマホをエミュレートして、やってみて欲しいべ
スマホでキャラクターを操作できるようにしてみよう!
つづいて、スマホでキャラクターを操作できるようにしていきましょう!
うーん。タップでシーンを切り替えられるようにはなったけど、キャラクターをスマホで操作できなくちゃ意味がないよね
そうね……シーンだけ切り替えられてもね……
ふっふっふ。ではこれから、スマホでもキャラクターを操作できるようにしていくべよ!
ホントに! やったぁ
やったぁ!
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座標が小さくなる
あー、頭がこんがらがる!
Y座標が小さくなるってことは、境目のX座標は、画像の高さから境目のY座標を引いたものと同じってことよ!
そうか! じゃあ上ボタンの範囲は、境目のX座標が、画像の高さから境目の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の検証から、スマホをエミュレートすることを忘れてはいけないべよ
つぎのページでは、ゲーム画面をブラウザのサイズに拡大したり、りこちゃんとアルくんが山田先生の後ろをついてくるようにしたりしてみます。
このシリーズの一覧はこちら
- 小学生からのプログラミング入門。プログラミングってなぁに?
- 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で、画像つきのおみくじゲームを作ろう!