Phaser3+typescriptでSRPGを開発してみる #009 カーソルの移動
今日は、SRPGだとお馴染みのカーソルの移動です。RPGでも使えますが、RPGは常に主人公を動かすと思います。SRPGの場合はカーソル動かして、動かしたいユニットを選ぶUIが多いように思います。
カーソルの移動
キーボード入力方法
キーボード入力は
- 上下左右
- それ以外
と2つ用意されています。
(1) 上下左右
いきなりですが、使い方は下記のような感じです。
GameScene.ts
export default class GameScene extends Phaser.Scene { /** 十字キーを管理 */ private _cursorKeys?: Phaser.Types.Input.Keyboard.CursorKeys; create() { // 上下左右キーの入力受付 this._cursorKeys = this.input.keyboard.createCursorKeys(); } update() { if (this._cursorKeys.left.isDown) { // 左キー押下時の処理 } else if (this._cursorKeys.right.isDown) { // 右キー押下時の処理 } else if (this._cursorKeys.up.isDown) { // 下キー押下時の処理 } else if (this._cursorKeys.down.isDown) { // 上キー押下時の処理 } } }
ポイントですが、
- キーの受付を保持する変数はクラスのメンバ変数にします。
create()
、update()
の両メソッドを跨ぐ変数になるためです else if
で繋いでいるため、上下左右のキーのうち、複数が呼ばれても、最初のif文に引っかかった処理しか実行されません。同時押しに対応するには、elseを外してifのみにください- このサンプルだと、
update()
関数が呼ばれる都度、処理されます。言い換えると、fps(Gameオブジェクト初期化時に設定)のターム、例えば1/30sec都度、呼ばれてしまいます。ですので、押下しているときに実行する処理が少しでも重いと、CPUが追いつかなくなります。。
ということで、どうするか?というと、処理している間はフラグを付けて、実行されないようにするテクニックがあります。(追記部分のみ記載します)
GameScene.ts(一部抜粋)
: private _isCursorDoing: boolean = false; : update() { if (this._isCursorDoing) return; if (this._cursorKeys.left.isDown) { this._isCursorDoing = true; // 左キー押下時の処理 this.doleft(); this._isCursorDoing = false; } : }
(2) それ以外
それ以外のキーは都度登録が必要になります。
GameScene.ts(一部抜粋)
: /** 十字キー以外のキーを管理 */ private _keys?: Map<string, Phaser.Input.Keyboard.Key> = new Map<string, Phaser.Input.Keyboard.Key>(); : create() { : this._keys.set("SPACE", this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE)); : } update() { : if (this._keys.get("SPACE").isDown) { // 押下時の処理 } : }
create()
でキー入力をaddKey()
メソッドで登録、そのキー(このサンプルでは"SPACE"
)で呼び出してupdate()
メソッドの中でisDown
プロパティでコントロールする、という使い方です
素直に、それぞれのキーをメンバ変数として定義して、
var keySpace = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);
と一つずつ宣言して使うこともできますが、変数が多くなりすぎるから見づらいかな、、とも思い、魔が挿してMap使ってしまいましたが、どちらでも良いと思います。連想配列使う人もいますかね。
カーソルの宣言
次はカーソルですが、カーソルのクラスを作るやり方が一般的かと思います。
Cursor.ts
/** * カーソルを管理するクラス */ export class Cursor { /** 左上(0,0)からのx座標 */ private _currentX: number = 0; /** 左上(0,0)からのy座標 */ private _currentY: number = 0; /** 画面上のユニット(スプライト)*/ private _sprite: Phaser.GameObjects.Sprite; /** 一番右下のx座標。setScope()でセット */ private _maxX: number = 1; /** 一番右下のy座標。setScope()でセット */ private _maxY: number = 1; /** 一番左上のx座標 */ private _minX: number = 0; /** 一番左上のy座標 */ private _minY: number = 0; /** カーソルが移動中であることを管理するフラグ */ private _isMoving: boolean = false; /** 行き先のx座標 */ private _targetX: number = 0; /** 行き先のy座標 */ private _targetY: number = 0; /** * カーソルが移動中かどうかを確認 * @returns 移動中であればtrue。停止していればfalse */ public get isMoving(): boolean { return this._isMoving; } /** * カーソルが移動中フラグをセット * @param isMoving 移動中であればtrue、停止中であればfalse */ public set isMoving(isMoving: boolean) { this._isMoving = isMoving; } /** * Phaser3のspriteを紐付け * @param 初期化済みのSprite */ public set sprite(sprite: Phaser.GameObjects.Sprite) { this._sprite = sprite; } /** * Phaser3のspriteを取得 * @returns Phaser.GameObjects.Sprite */ public get sprite(): Phaser.GameObjects.Sprite { return this._sprite; } /** * 現在の位置をセット。 * 最初にユニットを登場させるときに使用し、ゲーム上でユニットが移動するときにはmove()メソッドを使用 * @param x 現在地のx座標 * @param y 現在地のy座標 */ public setPosition(x: number, y: number) { if (x < this._maxX && x >= this._minX) { this._currentX = x; } else { this._currentX = this._maxX; } if (y < this._maxY && y >= this._minY) { this._currentY = y; } else { this._currentY = this._maxY; } } /** * Mapの広さを指定 * @param width 幅(x座標)。maxXにセット * @param height 高さ(y座標)。maxYにセット */ public setScope(width: number, height: number) { this._maxX = width; this._maxY = height; } /** * 現在のx座標を取得 * @override * @returns 現在のx座標 */ public get currentX(): number { return this._currentX; } /** * 現在のy座標を取得 * @override * @returns 現在のy座標 */ public get currentY(): number { return this._currentY; } /** * 移動完了と見なし、現在値座標を更新 */ public finishedMoving(): void { this._currentX = this._targetX; this._currentY = this._targetY; this._targetX = null; this._targetY = null; } /** * 指示先に移動できるかを事前に確認する * @param moveX x座標の移動差分(セルで指定)。右に2歩であれば+2 * @param moveY y座標の移動差分(セルで指定)。上に3歩であれば-3 * @returns 移動可能であればtrue。移動可能でなければfalse */ public canMove(moveX: number, moveY: number): boolean { return (this._currentY + moveY) < this._maxY && (this._currentY + moveY) >= this._minY && (this._currentX + moveX) < this._maxX && (this._currentX + moveX) >= this._minX; } /** * ターゲットの座標を設定 * @param targetX * @param targetY */ public setMove(moveX: number, moveY: number): void { this._targetX = this._currentX + moveX; this._targetY = this._currentY + moveY; } /** * ターゲットのx座標を取得 * @returns ターゲットのx座標(セル) */ public get targetX(): number { return this._targetX; } /** * ターゲットのy座標を取得 * @returns ターゲットのy座標(セル) */ public get targetY(): number { return this._targetY; } }
このCursor
クラスですが、サンプルのようにspriteオブジェクトを委譲変数で持つやり方と、Spriteクラスを継承するやり方と2通り考えられそうです。私は継承よりもDI (Dependency Injection)の方が好きなので、このブログでは、委譲変数タイプで実装します。
そしてこのCursor
のオブジェクトを移動させるメソッドを紹介します。メインクラスであるGameScene
の中で実装します。
GameScene.ts
import { Cursor } from "../../view/phaser/Cursor"; export default class GameScene extends Phaser.Scene { /** 十字キーを管理 */ private _cursorKeys?: Phaser.Types.Input.Keyboard.CursorKeys; /** カーソルの画像のキー */ private static CURSOR_IMAGE_KEY: string = "IMAGE_CURSOR"; /** カーソルオブジェクト */ private _cursor?: Cursor; /** カーソル移動幅 */ private static MOVE_TARGET_INTERVAL_STEP_PX = 2; /** 1セルあたりの解像度(px) */ public static CELL_PX = { WIDTH: 32, HEIGHT: 32 }; /** マップの開始位置(Offset) 左上が(0,0) */ private static MAP_POSITION = { X: 20, Y: 40 }; preload() { // カーソルの画像ファイルをロード this.load.image(GameScene.CURSOR_IMAGE_KEY, "/images/system/Cursor.png"); } create() { // カーソル処理取得(上下左右キーで待ち受け) this.initializeCursor(); }; update() { // カーソルが動いている最中は、入力を受け付けず、決められたピクセル分移動する if (this._cursor.isMoving) { this.updateCursorPosition(); return; } // カーソルが動いていない時には、入力を受け付ける this.setCursorMoving(); } /** * カーソルを設定 */ private initializeCursor(): void { this._cursor = new Cursor(); this._cursor.setScope(640, 480); // 横:640px、縦:480px分なら動いて良しとする this._cursor.setPosition(0, 0); // 初期ポジションは左上 this._cursor.sprite = this.add.sprite( GameScene.calPosition(this._cursor.currentX, GameScene.CELL_PX.WIDTH, GameScene.MAP_POSITION.X), GameScene.calPosition(this._cursor.currentY, GameScene.CELL_PX.HEIGHT, GameScene.MAP_POSITION.Y), GameScene.CURSOR_IMAGE_KEY); this._cursor.sprite.setDepth(10); // 上下左右キーのカーソル受付開始 this._cursorKeys = this.input.keyboard.createCursorKeys(); } /** * カーソルの画像(sprite)が動き始めることをセット */ private setCursorMoving(): void { // 移動座標に変換 if (this._cursorKeys.left.isDown) { this._cursor.setMove(-1, 0); } else if (this._cursorKeys.right.isDown) { this._cursor.setMove(1, 0); } else if (this._cursorKeys.up.isDown) { this._cursor.setMove(0, -1); } else if (this._cursorKeys.down.isDown) { this._cursor.setMove(0, 1); } else { return; } // フラグをセット this._cursor.isMoving = true; console.log("CURSOR TARGET = (%d, %d)", this._cursor.targetX, this._cursor.targetY); } /** * update()内での移動。update()が実行される都度、カーソルが移動中であれば実行されるメソッド */ private updateCursorPosition(): void { // ターゲット(ゴール)のx座標/y座標と違う場合、spriteをターゲットの方に近くなるように移動 var targetXPixel = GameScene.calPosition(this._cursor.targetX, GameScene.CELL_PX.WIDTH, GameScene.MAP_POSITION.X); var targetYPixel = GameScene.calPosition(this._cursor.targetY, GameScene.CELL_PX.HEIGHT, GameScene.MAP_POSITION.Y); // 現在値、左側にいる場合は、右にINTERVAL_STEP_PXピクセル分移動 if (targetXPixel > this._cursor.sprite.x) { this._cursor.sprite.x += GameScene.MOVE_TARGET_INTERVAL_STEP_PX; } // 現在値、右側にいる場合は、左にINTERVAL_STEP_PXピクセル分移動 else if (targetXPixel < this._cursor.sprite.x) { this._cursor.sprite.x -= GameScene.MOVE_TARGET_INTERVAL_STEP_PX; } // 現在値、上側にいる場合は、下にINTERVAL_STEP_PXピクセル分移動 else if (targetYPixel > this._cursor.sprite.y) { this._cursor.sprite.y += GameScene.MOVE_TARGET_INTERVAL_STEP_PX; } // 現在値、下側にいる場合は、下にINTERVAL_STEP_PXピクセル分移動 // else ifで繋いでいるので、x方向が終わったら、y方向に進む else if (targetYPixel < this._cursor.sprite.y) { this._cursor.sprite.y -= GameScene.MOVE_TARGET_INTERVAL_STEP_PX; } // 近くなったら到着とみなす処理を実行 // (CELL幅が移動幅(INTERVAL)で割り切れない時に、行ったり来たりしないようにする対応) if ((Math.abs(targetXPixel - this._cursor.sprite.x) < GameScene.MOVE_TARGET_INTERVAL_STEP_PX) && Math.abs(targetYPixel - this._cursor.sprite.y) < GameScene.MOVE_TARGET_INTERVAL_STEP_PX) { this._cursor.sprite.x = targetXPixel; this._cursor.sprite.y = targetYPixel; // movingUnitを初期化 this._cursor.finishedMoving(); this._cursor.isMoving = false; } } /** * オブジェクトの座標を計算(センタリング機能付き) * @param cellPosition セルポジション * @param interval セル間隔 * @param intercept 描画領域のオフセット * @returns px */ public static calPosition(cellPosition: number, interval: number, intercept: number) { return (cellPosition + 0.5) * interval + intercept; } }
仕組みは下記の通りです。
update()
が呼ばれるごとにMOVE_TARGET_INTERVAL_STEP_PX
変数分(このサンプルでは2ピクセル)ずつカーソルを移動する- 上下左右キーを1度受け付けたら、1セル分(32px、
CELL_PX.WIDTH
やCELL_PX.HEIGHT
で定義)動くまでは、スルーします。それをCursor
クラスのメンバ変数isMoving
で制御しています。 calPosition()
メソッドは、セル位置の座標を指定するstatic
なメソッドです。セル幅(interval
)とマップを表示する位置(intercept
)を指定するのですが、画像ファイルのポジションは、中心の座標を指定する必要がありますので、計算式の方で、セル位置+0.5分、ずらして指定しています
Tween
使うやり方もありそうで、そっちの方がスムーズに動きました。そのやり方は#011で試してみたいと思います。
まとめ
本日は、Phaser3でカーソルの移動がようやくできるようになりました、というご報告でした。
update()の動きがわかってくれば、そこまで難しくないのかなと思います。ということで、次回はupdate()メソッドについて、もう少し整理してみます。
過去の日記
タイトル | 記載内容 | |
---|---|---|
#001 | プロローグ | Phaser3とは |
#002 | 環境設定 | インストール・ビルド環境設定 |
#003 | はじめてのPhaser3 | 初回稼働・キャンバス表示 |
#004 | マップ作成 | タイルセットの読込、画面手動作成、Sceneクラスのライフサイクル |
#005 | Tiledの利用 | Tiledによる画面マップJSON作成、マルチレイヤーの背景画面 |
#006 | JSONファイルの読込 | データのpreload |
#007 | ファイルのキャッシュ | ゲームデータの管理、シングルトンパターン |
#008 | データモデル(ユニット編) | SRPGに必要なデータ・管理方法 |