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

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

Phaser3+typescriptでSRPGを開発してみる #007 ファイルの中身のキャッシュ

今日はファイルのキャッシュということで、こちらもPhaser3だと簡単に実現できますが、それを使いやすい形でtypescriptで実現するには、多少工夫が必要になるかと思います。

ファイルの中身のキャッシュ

本日のプログラムソース構成

<root>
 |- index.html
 |- src
 |    |- index.ts
 |    |- model
 |    |      |- CacheManager.ts
 |    |      |- InitialUnit.ts
 |    |      |- InitialUnitManager.ts
 |    |      |- JobClass.ts
 |    |      |- JobClassManager.ts
 |    |      |- Weapon.ts
 |    |      |- WeaponManager.ts
 |    |- view
 |           |- game
 |           |     |- GameDataManager.ts
 |           |- scene
 |                 |- Scenes.ts
 |                 |- GameScene.ts
 |- dist
      |- index.js

キャッシュの必要性

キャッシュは大きく2つの理由で必要だと考えています。

  • ファイルIO、DBアクセスなど、NWやストレージを通ることでデータの取得に時間がかかるのを回避するため
  • 同じデータを複数回、メモリに載せないようにするため

2点目の方は、JavaだとGC(ガレージ・コレクション)が発生することで使っていないオブジェクトをメモリから追い出すことができますが、GCの動き見ると、都度発生して適切にメンテナンスできてないこともありますし、2回目以降のRead時に1点目の問題が発生するため、やはりキャッシュした方が良いと思います。

Phaser3におけるキャッシュ

Phaser3では比較的簡単にキャッシュできます。 preloadメソッドの中でthis.load.****()というメソッドを呼ぶだけです。画像の場合は、this.load.image()ですし、JSONの場合はthis.load.json()です。

Phaser3+typescriptでSRPGを開発してみる #006 JSONファイルの読込 - 旧SEによる日曜プログラミング日記 でも書きましたが、 this.load.****(【管理キー名】,【ファイルパス】)メソッドはpreload()メソッドの中で呼び出す必要があります。

これらを使う場合には、create()メソッドや update()メソッドの中で、this.cache.get.****(【管理キー名】)で呼び出せます。

逆にcreate()の中でloadして、その直後にgetしてもundefinedが出てしまいます。

一般的なJSONの場合

但しこのままでは使いづらいです。JSONをいちいちparseしなきゃいけないとかもあります。 ということで、typescriptのobjectとしてインターフェースを定義した上で型キャストします。このあたりは#006をお読みください。

Singleton(シングルトン)パターン

キャッシュしたオブジェクトを渡すパターンとして有名なデザインパターンが、Singleton(シングルトン)パターンです。GoFGang of Four)のデザインパターンの一つで、Java等のマルチスレッド言語で使われることが多いです。

Javaの書き方は下記の通り。私が初めて出会った2000年代から、最新版はちょっと書き方が変わっています。 (クラス名=Singletonとしていますが、もちろん何でも良いです)

public class Singleton {
    private static Singleton _instance = new Singleton();
    public synchronized static Singleton getInstance() {
        return _instance;
    }
}

よくコンストラクタを宣言して、その中にログ出力しているものを見受けられますが、しっかり初期化が終わってキャッシュされたか?を確認するために出力したくなるものだと思います。

最近では、下記のように内部クラスを作るパターンもよく紹介されてますね。

public class Singleton2 {
    public static Singleton2 getInstance() {
        return SingletonHolder.INSTANCE;
    }
    private static class SingletonHolder {
        private static final Singleton2 INSTANCE = new Singleton2();
    }
}

さて、Typescriptで書くとどうなるか、ですが、下記のように書いています。

export class Singleton {
    private static INSTANCE:Singleton = new Singleton();
    private constructor() {
    public static get instance(): Singleton {
        return Singleton.INSTANCE;
    }
}

SRPGに使うと

さて、開発中のSRPGに適用しようと思い、今回は下記構成にしてみました。

f:id:mtmusic34:20210625094317p:plain
クラス図
CacheManagerにユニット情報やジョブクラス情報、武器情報をキャッシュするManagerを集約し、それぞれのInitialUnitManagerJobClassManagerWeaponManagerでシングルトンを使ってキャッシュします。

多少掻い摘んでプログラムソースを載せますと、下記の通りです。

/view/scene/GameScene.ts

import { CacheManager } from "../../model/CacheManager";
import GameDataManager from "../../game/GameDataManager";
import { JobClass } from "../../model/JobClass";

export default class GameScene extends Phaser.Scene {

    // ゲームデータ管理
    private _gameexec?: GameDataManager;

    // データ格納先
    private static JSON_GAME_DATA_DIRPATH = "/props/game/";
    private static JSON_FILEPATH = {
        UNIT_ALLY_DATA: "/props/data/UnitAlly.json",
        JOBCLASS_DATA: "/props/data/JobClass.json",
        WEAPON_DATA: "/props/data/Weapon.json",
    };

    // データキー
    private static JSON_KEY = {
        GAME_DATA: "game",
        UNIT_ALLY_DATA: "unitAlly",
        JOBCLASS_DATA: "jobClass",
        WEAPON_DATA: "weapon",
    };


    preload() {

        // ファイルの読込
        this.load.json(GameScene.JSON_KEY.UNIT_ALLY_DATA, GameScene.JSON_FILEPATH.UNIT_ALLY_DATA);
        this.load.json(GameScene.JSON_KEY.JOBCLASS_DATA, GameScene.JSON_FILEPATH.JOBCLASS_DATA);
        this.load.json(GameScene.JSON_KEY.WEAPON_DATA, GameScene.JSON_FILEPATH.WEAPON_DATA);
    }

   create() {
       // キャッシュをロード
        console.log(" *** 各種データ load START ***");
        var cacheManager: CacheManager = this.createCacheManager();
        console.log(" *** 各種データ load END ***");

        var jobClass: JobClass = cacheManager.jobClassManager.getJobClassByName("ロードナイト");
        this.add.text(30, 20, "id : " + jobClass.id);
        this.add.text(30, 40, "name : " + jobClass.name)
        this.add.text(30, 60, "lv1 status maxHP:" + jobClass.lv1UnitStatus?.maxHp);
        this.add.text(30, 80, "lv1 status 力  : " + jobClass.lv1UnitStatus?.attack);
    }

    
    // データをロードしCacheManagerにて管理
    private createCacheManager(): CacheManager {
        var unitAllyData = this.cache.json.get(GameScene.JSON_KEY.UNIT_ALLY_DATA);
        var unitJobClassData = this.cache.json.get(GameScene.JSON_KEY.JOBCLASS_DATA);
        var weaponData = this.cache.json.get(GameScene.JSON_KEY.WEAPON_DATA);
        let cacheManager: CacheManager = new CacheManager(
            unitAllyData,
            unitJobClassData,
            weaponData
        );
        return cacheManager;
    }

@src/view/game/GameDataManager.ts

import { CacheManager } from "@src/model/CacheManager";
export default class GameDataManager {

    private _cacheManager: CacheManager;
    constructor(manager: CacheManager) {
        this._cacheManager = manager;
    }
}

/model/CacheManager.ts

import { JobClassManager } from "./JobClassManager";
import { InitialUnitManager } from "./InitialUnitManager";
import { WeaponManager } from "./WeaponManager";


export class CacheManager {

    private _jobClassManager?: JobClassManager;
    private _unitManager?: InitialUnitManager;
    private _weaponManager?: WeaponManager;

    constructor(
        allyUnitClassJson: any[],
        jobClassJson: any[],
        weaponJson: any[]
    ) {
        this.initialize(allyUnitClassJson, jobClassJson, weaponJson);
    }

    private initialize(
        allyUnitClassJson: any[],
        jobClassJson: any[],
        weaponJson: any[]
    ) {
        console.log("***** CacheManager のロード START *****");
        var tempUnitMgr: InitialUnitManager = new InitialUnitManager();
        var tempJobClassMgr: JobClassManager = new JobClassManager();
        // var tempSkillMgr: SkillManager = new SkillManager();
        var tempWeaponMgr: WeaponManager = new WeaponManager();

        // ジョブクラス・スキル・武器データを読込
        tempUnitMgr.loadJsonData(allyUnitClassJson);
        tempJobClassMgr.loadJsonData(jobClassJson);
        tempWeaponMgr.loadJsonData(weaponJson);
        this._unitManager = tempUnitMgr;
        this._jobClassManager = tempJobClassMgr;
        this._weaponManager = tempWeaponMgr;
        console.log("***** CacheManager のロード END *****");
    }

    public get jobClassManager(): JobClassManager {
        if (this._jobClassManager === undefined) {
            throw Error("JobClassManagerが読み込めていません。");
        }
        return this._jobClassManager;
    }

    public get unitManager(): InitialUnitManager {
        if (this._unitManager === undefined) {
            throw Error("UnitManagerが読み込めていません。");
        }
        return this._unitManager;
    }

    public get weaponManager(): WeaponManager {
        if (this._weaponManager === undefined) {
            throw Error("WeaponManagerが読み込めていません。");
        }
        return this._weaponManager;
    }
}

/model/InitialUnitManager.ts (WeaponManagerやJobClassManagerも同様の実装)

import { InitialUnit } from "./InitialUnit";

export class InitialUnitManager {

    private _unitDefaultList: InitialUnit[] = [];

    // JSONデータをJobClassクラスとしてキャッシュ
    public loadJsonData(loadUnitJson: any[]): void {
        console.log("***** UnitManager読込 START *****");
        var returnList: InitialUnit[] = [];
        loadUnitJson.forEach((loadUnit: InitialUnit) => {
            returnList.push(loadUnit);
        });
        this._unitDefaultList = returnList;
        console.log("***** UnitManager読込 FINISH (" + returnList.length + "件) *****");
    }

    // ユニットオブジェクトを取得するメインメソッド
    // UnitManagerクラスで管理済
    public getInitialUnitByName(unitName: string): InitialUnit {
        const findResult: InitialUnit = this._unitDefaultList.find((j) => j.name === unitName);
        if (findResult === undefined) {
            throw Error("ユニットを取得できませんでした。ユニット名=" + unitName);
        }
        return findResult;
    }

    public getInitialUnitById(unitId: string): InitialUnit {
        const findResult: InitialUnit = this._unitDefaultList.find((j) => j.id === unitId);
        if (findResult === undefined) {
            throw Error("ユニットを取得できませんでした。ユニットID=" + unitId);
        }
        return findResult;
    }

/model/InitialUnit.ts

/**
 * ユニットの初期値
 */
export interface InitialUnit {

    id: string;                          // ID
    name: string;                        // 名前
    caption: string;                 // 説明
    jobClassName: string;                // クラス名
    weaponName: string;                  // 武器名
    level: number;                       // レベル

    statusType: StatusType              // ステータス種別(汎用 or 特別)
    initStatus: UnitStatus;       // 初期ステータス
    initWeaponLevelSP: WeaponLevelSP; // 初期武器レベルSP
    growthRate: GrowthRate;           // 各ステータスの成長率

    teamName: string;                    // チーム名
    isLeader: boolean;                   // リーダーフラグ
    initPositionX: number;               // 初期ポジションx座標
    initPositionY: number;               // 初期ポジションy座標

    actStrategyType: ActStrategyType; // 行動アルゴリズム

    mapImageUri: string;             // マップ画像表示時のURI
    faceImageUri: string;                // 顔画像表示のURI

}

今回はあるユニットのLevel1時の最大HPと攻撃力を出してみました。 以下のように表示されます。

f:id:mtmusic34:20210625151840p:plain
表示結果

今回のポイント

  • tempUnitMgr.loadJsonData(allyUnitClassJson);のようにloadするメソッドをコンストラクタの中で呼んでいます。

  • CacheManager.initialize()に中身が時間かかるからと言って時間かかる処理にawait入れたり、asyncメソッドにしたり、JSON読込むところをPromise.all()使ったりする必要は全くありません。

2点目について補足すると、 私も最初そういう作りにしてて、それはそれで、ちゃんと同期をとって動きます。ですが、非同期を確り理解ぜずに雑な作り込みすると少し直しただけで動かなくなります。。

私は相当ハマりました。

undefinedが頻発すると真っ先に同期化しなきゃ、と思うのですが、むしろ逆で、非同期関数でなければ、逐次読込で完了したら次の文に進みます。大抵、●●Syncと付くのが同期、何もつかないのが非同期のようです。CやJavaを経験した人が、ファイル読込やDBアクセスからjavascriptに入ると、疑心暗鬼になると思います(私のことです!)。

ですが、大抵、エラーで変数の値がundefinedになるのは、同期化が原因ではなく、単なるエラーにより処理が正常にされないまま、次の処理に行ってしまったため、ということが多いと思います。ですので、その前のロジックをデバッグすることをお奨めします。

まとめ

今回もコードで一杯になってしまいましたが、ファイルの中身のキャッシュと、その呼び出し方についてご説明しました。

次はユニットのデータモデルを考えます。 mtmusic34.hatenablog.com

過去の日記

タイトル 記載内容
#001 プロローグ Phaser3とは
#002 環境設定 インストール・ビルド環境設定
#003 はじめてのPhaser3 初回稼働・キャンバス表示
#004 マップ作成 タイルセットの読込、画面手動作成、Sceneクラスのライフサイクル
#005 Tiledの利用 Tiledによる画面マップJSON作成、マルチレイヤーの背景画面
#006 JSONファイルの読込 データのpreload