Phaser3+typescriptでSRPGを開発してみる #011 ユニットの移動(1)
今回は、ユニットを動かしてみます。カーソルでユニットを選択して動ける場所を表示し、実際に動かす、といったことと、敵ユニットの場合は勝手に動いてもらう、とやらなければいけないことが山積してます。
ということで何回かに分けて進めたいと思います。
ユニットの移動(1)
#009の反省
さて、まずは前作ったカーソルの反省からです。
カーソルの場合、#009のやり方で十分かなと思っていますが、このやり方だと実は微妙な欠点があります。それは、FPSに依存して動く速さが変わってしまうことです。
FPSのデフォルトは60ですので、1/60秒都度、updateメソッドが呼ばれます。呼ばれる都度、2ピクセルずつ動くとなると、1セルあたり32ピクセルだと、(32÷2)×1/60=16/60秒で移動します。しかしながら処理を軽くするため、仮にFPSを15にすると、4倍時間がかかるため、カーソル一つの移動に1秒強かかってしまいます。
もちろん、そのときは移動幅を4倍にすれば良いのですが、2つのパラメータをコントロールしないといかなくなるので、あまり良いコーティングとは思えません。
Tweenの利用
そこで、毎度同じ時間で移動できるように、#009でも軽く触れたTweenで実装する必要があります。
Tweenはオブジェクトのアニメーションをコントロールできる仕組みです。何秒で動かすとか、どのオブジェクトを同時に動かすか、といった指定ができます。
コーディング例ですが、こんな感じです。moveGridWalk()
メソッドでTweenを使っています。
GameScene.ts
export class GameScene extends Phaser.Scene { /** * スペースキーでターゲットにしているGameUnit。 * nullの場合はターゲット対象なし。 */ private _targetUnit?: GameUnitPhaser = null; /** 移動中のユニット情報 */ private _movingUnit?: MovingUnit = null; (中略) /** * 対象ユニットを移動させる * @param unit 対象となるユニット * @param toX 移動先のx座標 * @param toY 移動先のy座標 */ private moveUnit(movingUnit: MovingUnit): void { let unit: GameUnit = movingUnit.unit; // GridWalkで移動させる let cursorMoveDirectionX: number = movingUnit.targetX - unit.currentX; let cursorMoveDirectionY: number = movingUnit.targetY - unit.currentY; // 変更差分がなければ何もせずに呼び出し元メソッドへ戻る if (cursorMoveDirectionX === 0 && cursorMoveDirectionY === 0) { return; } // 移動する速さを設定 let moveIntervalByStep: number = 1000; switch (movingUnit.unit.jobClass.unitType) { case UnitType.CAVARLY: moveIntervalByStep = 300; break; case UnitType.FOOT: case UnitType.CAVARLY: moveIntervalByStep = 500; break; case UnitType.ARMOR: moveIntervalByStep = 1000; break; default: } // X方向に移動する this.moveGridWalk( unit, cursorMoveDirectionX, 0, moveIntervalByStep * Math.abs(cursorMoveDirectionX), // 移動距離を与えるため、絶対値を指定 () => { // Y方向に移動する this.moveGridWalk( unit, 0, cursorMoveDirectionY, moveIntervalByStep * Math.abs(cursorMoveDirectionY), // 移動距離を与えるため、絶対値を指定 () => { // ターンオフフラグを付与 unit.turnEnd = true; // unitIsMovingをfalseに戻す this._movingUnit = null; this._targetUnit = null; } ); } ); } /** * 1マスごとゆっくり動く。非同期関数 * @param target 対象オブジェクト(カーソル、ゲームユニット等) * @param moveX x方向の差分(座標) * @param moveY y方向の差分(座標) * @param duration 進むスピード。1マス単位ではなく、全体での移動時間(ms)。長い程ゆっくり動く * @param onCompleteCallback 移動完了後に呼び出されるcallback関数 */ private moveGridWalk( target: PhaserObject, moveX: number, moveY: number, duration: number, onCompleteCallback: () => void): void { let tween: Phaser.Tweens.Tween = this.add.tween({ // 対象カーソル名 targets: [target.sprite], // x座標の移動を設定 x: { getStart: () => target.sprite.x, getEnd: () => target.sprite.x + moveX * GameScene.CELL_PX.WIDTH }, // y座標の移動を設定 y: { getStart: () => target.sprite.y, getEnd: () => target.sprite.y + moveY * GameScene.CELL_PX.HEIGHT }, duration: duration, onComplete: () => { // 位置情報(セル)を更新(カーソル・ユニット共に) target.move(moveX, moveY); // 移動中フラグを停止 tween.stop(); onCompleteCallback(); } }); } } /** * 行動中のユニットの情報を管理 */ class MovingUnit { /** 対象ユニット。spriteを保有 */ unit: GameUnit; /** 行き先のx座標 */ targetX: number; /** 行き先のy座標 */ targetY: number; /** 行き先のx座標(pixel)。マップ表示領域へのオフセットも加味 */ targetPixelX: number; /** 行き先のy座標(pixel)。マップ表示領域へのオフセットも加味 */ targetPixelY: number; /** 出発元のx座標(pixel)。マップ表示領域へのオフセットも加味 */ initialPixelX: number; /** 出発元のy座標(pixel)。マップ表示領域へのオフセットも加味 */ initialPixelY: number; /** * コンストラクタ。セル座標(論理値)をセット * @param unit * @param targetX * @param targetY */ constructor(unit: GameUnit, targetX: number, targetY: number) { this.unit = unit; this.targetX = targetX; this.targetY = targetY; this.init(unit, targetX, targetY); } /** * セル座標を元にピクセル座標をセット * @param unit * @param targetX * @param targetY */ private init(unit: GameUnit, targetX: number, targetY: number) { this.initialPixelX = unit.sprite.x; this.initialPixelY = unit.sprite.y; this.targetPixelX = GameScene.calPosition(targetX, GameScene.CELL_PX.WIDTH, GameScene.MAP_POSITION.X); this.targetPixelY = GameScene.calPosition(targetY, GameScene.CELL_PX.HEIGHT, GameScene.MAP_POSITION.Y); } }
いくつかのメソッドは紹介できていませんので、雰囲気で読んでいただけると助かります。 ある程度、完成したらGitHub等で公開したいと思います。
ポイント
動く速度を指定することができるようになりましたが、ポイントがいくつかあります。
_movingUnit
は移動中のユニットを管理するメンバ変数で、_targetUnit
はユーザが選択しているユニットを管理するメンバ変数で、微妙にライフサイクルを変えています。_targetUnit
で選択して移動先を決めた後、_movingUnit
をセットしTweenを使って移動させます。_movingUnit
をセットする理由としては、update()
で、他のユニットを同時に動かせないようにする等、他処理を実行できないように制御するためです。- X方向、Y方向という形で順序づけていますが、
Tweet#constructor()
のオプション変数であるonComplete
関数プロパティを使えるように、外からfunctionを送り込めるように設計することがポイントです。Tweet#constructor()
は実は非同期実行で、Tweet
クラスのコンストラクタを呼び出した後、即座に次の処理が実行されます。そのため、完全に処理が終わってから、(今回ですと、移動が終わってから)、次の移動をさせるためには、onComplete
の中で再帰的に同じメソッドを呼び出すことになります。 Tweet#constructor()
では、移動先の座標をピクセルで指定するのと、それにかかる時間を指定可能です。今回は、ユニットの種類ごと(騎兵、歩行、重装、飛行)に移動にかかる時間を変えています。PhaserObject
というインターフェースを定義していますが、これはPhaserで動かせるものの共通インターフェースで、Cursor
クラスとGameUnit
クラスが実装しています。sprite
を共通して保有しています。
まとめ
他にもポイントがありそうですが、一旦ここまでにさせていただきました。moveUnit(MovingUnit)
メソッドを呼び出すとユニットが移動できるようになりました。
ただ、このメソッドを作っても何も動きません。カーソルでユニットの移動先を指定させないといけません。その他、画面上全領域にいきなり動けるのもSRPGとして成立しないので、移動可能な領域の表示なども必要になります。その辺りは長くなりそうなので(2)以降でご紹介したいと思います。
過去の日記
タイトル | 記載内容 | |
---|---|---|
#001 | プロローグ | Phaser3とは |
#002 | 環境設定 | インストール・ビルド環境設定 |
#003 | はじめてのPhaser3 | 初回稼働・キャンバス表示 |
#004 | マップ作成 | タイルセットの読込、画面手動作成、Sceneクラスのライフサイクル |
#005 | Tiledの利用 | Tiledによる画面マップJSON作成、マルチレイヤーの背景画面 |
#006 | JSONファイルの読込 | データのpreload |
#007 | ファイルのキャッシュ | ゲームデータの管理・シングルトンパターン |
#008 | データモデル(ユニット編) | SRPGに必要なデータ・管理方法 |
#009 | カーソルの移動 | キー受け付け、Spriteの活用 |
#010 | 各メソッドのコールと状態遷移 | Sceneメソッドのライフサイクル・デバッグモードの作り方 |