旧SEによる日曜プログラミング日記

趣味は作曲、自分で一から作りたがりのユーザIT子会社の旧SE(かつてはプログラマー)によるリハビリのための日曜プログラミング日記です。

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)以降でご紹介したいと思います。

mtmusic34.hatenablog.com

過去の日記

タイトル 記載内容
#001 プロローグ Phaser3とは
#002 環境設定 インストール・ビルド環境設定
#003 はじめてのPhaser3 初回稼働・キャンバス表示
#004 マップ作成 タイルセットの読込、画面手動作成、Sceneクラスのライフサイクル
#005 Tiledの利用 Tiledによる画面マップJSON作成、マルチレイヤーの背景画面
#006 JSONファイルの読込 データのpreload
#007 ファイルのキャッシュ ゲームデータの管理・シングルトンパターン
#008 データモデル(ユニット編) SRPGに必要なデータ・管理方法
#009 カーソルの移動 キー受け付け、Spriteの活用
#010 各メソッドのコールと状態遷移 Sceneメソッドのライフサイクル・デバッグモードの作り方