Phaser3+typescriptでSRPGを開発してみる #010 各メソッドのコールと状態遷移
今回は、ちょいとブレイクタイムして、今まで調査してきたものを踏まえて、Phaser3のScene
クラスの振る舞いと状態遷移を整理したいと思います。(ネタの調査・試行が追いつかなくなってきたのが、バレますね。。)
あまりにデバッグがやりづらかったので、デバッグモードも作ってみましたので、ご紹介させていただきます。
各メソッドのコールと状態遷移
各メソッドのコール
改めてですが、Phaser3は下記のような構造を取っています。
Phaser.Game Phaser.Scene init preload create update
Game
クラスが全体を管理するクラスで、Game
内の各画面・場面を定義するのがScene
です。ですので、Game
に対してScene
は1:多です。使い方としては、Scene
クラスを継承した子クラスを定義して使うのが、一番良さそうです。
そのScene
クラスでは予め、init
、preload
、create
、update
の各メソッドが定義されています。それぞれの役割は下表の通りです(#004のときの使い回しですみません。。)
関数名 | 概要 |
---|---|
init | preloadに先駆けて呼ばれるメソッド。どんな場合に使うのかはもう少し確認が必要 |
preload | アセット(画像・音楽・データ等)を呼び出すときに使用。メモリ上にデータをキャッシュ |
create | シーンスタート時にゲームオブジェクトを作るときに使用 |
update | シーンに対して更新を掛けるときに使用。FPSの指定タイミング(描画間隔)で呼び出される |
図に書くとこのような呼ばれ方になります。
updateの実装パターン
ですので、preload
やcreate
は普通にコーディングすれば良いのですが、update
は、1秒間に何度も呼ばれるメソッド、実質、無限ループみたいに捉える必要があります。
そのため、update
の処理の中で、FPSよりも長く掛かる非同期メソッドがあると、次のupdate
が呼ばれるまでに終わり切らず、CPUがすごい頑張ってしまい熱暴走しますし、UI側もスローダウンして、入力への反応ができなくなります。結果、Webサーバを強制終了する羽目になります。
そのためupdate
の典型的な実装パターンは下記のようになります。
- 何か処理中(特に非同期処理の場合)は、以降のロジックを飛ばして、不要な計算をなるべく排除する
- そのため処理中は、クラスのメンバー変数を定義し、フラグをもたせることが必要になります
update()
メソッドで実行する処理は、その瞬間の状態を書けば良いです。1/n秒単位に呼ばれて、繋げると動いてるように見えます。例えば、等速運動させたいときには、位置を前回対比+○○にする、といった書き方で良いです。 (「動画は静止画の集まり」の原理と一緒ですね)
export class GameScene extends Phaser.Scene { // メンバー変数を用意し、どこからでもアクセスできるようにする private _flag1: boolean = false; private _flag2: GameUnit = null; update () { // フラグがオンの時は、以降の処理を実行しない if (this._flag1) return; if (this._flag2) { // 処理を実行 this.moveUnit (this_.flag2); return; }; // キャラを動かすとき、フラグをオンにする if(readyMove) { this._flag2 = this.getSelectedUnit(); } } // 実際の処理 moveUnit(unit: GameUnit) { // ユニットを右に10pxずつ移動 unit.sprite.x += 10; // 処理が終わったらフラグをオフに戻す if(unit.sprite.x === 300) { this._flag2 = null; } } }
実コードではなくイメージが伝わるように書いてみましたが、状態を管理するメンバー変数を用意して、update()
の最初の段階で判定してあげて、以降の処理を実行しないみたいなのが必要かなと思います。
【応用】デバッグモード
この原理を利用して、デバッグモードを作ってみました。単に、その瞬間の状態を表示するものです。
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 }; /** デバッグ表示領域 */ private __debugDisplayArea?: Phaser.GameObjects.Container; /** デバッグ表示内容 */ private __debugText: Phaser.GameObjects.Text[] = Array<Phaser.GameObjects.Text>(10); /** デバッグ内の文字スタイル */ private __debugStyle: object = { fontSize: '14px', color: 'white', wordWrap: { width: 720, useAdvancedWrap: true } } /** デバッグ表示インターバル */ private __debugInterval: number = 100; create() { // デバッグモードの表示 this.startDebugMode(); // カーソル処理取得 this.initializeCursor(); } update() { // デバッグモード実行 this.debugModeOn(); // カーソル処理 if (this._cursor.isMoving) { this.updateCursorPosition(); return; } this.setCursorMoving(); } /** * カーソルを設定 */ private initializeCursor(): void { this._cursor = new Cursor(); this._cursor.setScope(640, 480); 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()内での移動 */ 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; } /** * デバッグモードを開始(create()から呼び出し) */ private startDebugMode(): void { this.__debugDisplayArea = this.add.container(550, 350); for (var i = 0; i < 10; i++) { this.__debugText[i] = this.add.text(0, -300 + 20 * i, "", this.__debugStyle); } this.__debugDisplayArea.add([...this.__debugText]); this.__debugDisplayArea.setVisible(true); } /** * デバッグモード(update()から呼び出し) */ private debugModeOn(): void { if (this.__debugInterval === 0) { this.__debugText[0].setText("cursor.position = (" + this._cursor.currentX + "," + this._cursor.currentY + ")"); this.__debugDisplayArea.setVisible(true); this.__debugInterval = 50; } else { this.__debugInterval--; } }
今度は実アプリからそのまま引用してきました。Cursor
クラスは#009のものを使ってみてください。
このようにすると、デバッグ画面を表示できます。
カーソルを動かすと、たまに数字が更新されます。このたまに、というのも重要で、毎回メソッドを呼ばれると負荷が重いので、このサンプルでは__debugInterval
メンバ変数で管理して、update
メソッドが100回呼び出される都度としています。
まとめ
今回は、Phaser.Scene
クラスで定義されている各メソッド、とりわけupdate()
について詳しく考察しました。
次回は、ユニットを動かす、にチャレンジしますが、まずは、Phaser.GameObject.Tween
クラスを試します。
過去の日記
タイトル | 記載内容 | |
---|---|---|
#001 | プロローグ | Phaser3とは |
#002 | 環境設定 | インストール・ビルド環境設定 |
#003 | はじめてのPhaser3 | 初回稼働・キャンバス表示 |
#004 | マップ作成 | タイルセットの読込、画面手動作成、Sceneクラスのライフサイクル |
#005 | Tiledの利用 | Tiledによる画面マップJSON作成、マルチレイヤーの背景画面 |
#006 | JSONファイルの読込 | データのpreload |
#007 | ファイルのキャッシュ | ゲームデータの管理、シングルトンパターン |
#008 | データモデル(ユニット編) | SRPGに必要なデータ・管理方法 |
#009 | カーソルの移動 | キー入力受け付け、Spriteの活用 |