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

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

Phaser3+typescriptでSRPGを開発してみる #008 データモデル(ユニット編)

今日はユニットのデータモデル設計を進めようかと思います。

ユニットのデータモデル設計

SRPGで必要な要素

早速ですが、シミュレーションRPGに必要な要素の洗い出しです。ER図というかクラス図書いてみようと思いますが、まずはエンティティの洗い出しです。

エンティティ名 interface名 項目例
初期ステータス InitialUnit 名前、初期レベル、ジョブクラス名、説明、最大HP
ゲーム中のステータス GameUnit チーム名、レベル、経験値、HP、補正された攻撃力、スキル発動カウント、機動力、装備武器、習得済みスキル、ポジション(x,y座標)
ジョブクラス JobClass ジョブクラス名、標準ステータス、武器種別、移動種別(騎馬、飛行など)
武器 Weapon 武器名、武器種別、攻撃力、特殊効果
スキル Skill 特殊効果(ターン開始時、戦闘開始時、戦闘終了後)、発動カウント

そしてこれをER図っぽく表現してみます。

f:id:mtmusic34:20210703154123p:plain
クラス名(エンティティ関連)

  • GameDataが、そのゲームに関する初期情報を格納しているJSONファイルを読み込むためのクラスで、敵ユニットの情報(初期ステータス(InitialUnit)、ジョブクラス名、武器名等)や味方ユニットの初期メンバー・初期位置等を格納しています。
  • ユニットのそれぞれの詳細情報は、別のJSONファイル(AllyUnits.json)に記載しており、そのデータ構造はInitialUnitと同じです。GameDataのJSONの中には最低限の情報しか記載されていません(設定の集約化)。SRPGのストーリーが先に進むにつれて、当然、成長(レベルアップ)していくため、のステータス情報は成長したデータを別ファイルで管理(Save)することになります。
  • JSON Fileを読み込むManagerクラスと読み込んだデータ格納先インターフェースを、各々用意しています。

typescriptでの実装

では、JSONで実装してきますが、ここでのポイントは、

  • RDBMSの考え方ではなく、結果整合性で捉えた方が、頭を使わなくて済む

です。

RDBMSのように無意味なIDを発行してキー項目に使う場合、下記のようなメリットがあると思います。

(1) 名前が重複する恐れがある

例えば、日本国民台帳を作る、となると、名前が重複する恐れがあるので、マイナンバーという一意にレコードが決まる項目をキーにすると思います*1。ですが、名前が同じものが出てこないで、名前で一意(ユニーク)になるのであれば、意味を持たない識別子、IDとかは不要かなと思います。

(2) 性能上げるためには主キーや外部キーを設定してディスクI/Oの観点でより効率良くデータを取りたい

超大量データがあって、APサーバのメモリ上に乗らない場合、DBサーバでデータを作り込んで(テーブルをJOINして)から、ロードしたいと思うかと思います。

ただ、データ量が多くなければ、全部APサーバでインメモリで取り扱って方が楽です。その場合、正規化することはあまり重要ではなく、ミスが少なく直感的に作りやすいデータの方が良いかなと感じます。

とはいいながらも、これらをサービスとして個別に管理し、RESTful APIでアクセスして取ってくる、となると、もしかしたら2byte文字だと扱いづらく、IDをASCII文字で定義した方が使い勝手が良いかもしれません。。と思いますが、まずはnameをキーとして、作ってみました。

ということで、今回は下記の通り、実装してみました。

f:id:mtmusic34:20210703130459p:plain
クラス図(項目)
これらをinterfaceで定義し、とりあえずはJSONで作成したいと思います。いくつか抜粋してソースコードを示します。

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

    id: string;                          // ID(自動採番とかではなく、味方・的で一意に。デバッグ目的でのみ使用)
    name: string;                        // 名前(実質的なキー項目)
    caption: string;                 // 説明
    jobClassName: string;                // クラス名
    level: number;                       // レベル

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

    teamName: string;                    // チーム名
    initPositionX: number;               // 初期ポジションx座標
    initPositionY: number;               // 初期ポジションy座標

    activityType: ActivityType;           // 行動アルゴリズム

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

}

/**
 * 初期値ステータス
 */
export interface UnitStatus {
    level: number;           // レベル
    hp: number;              // HP
    attack: number;          // 力
    magic: number;           // 魔力
    speed: number;           // 速さ
    defence: number;     // 物理守備力
    resist: number;          // 魔法防御力
    skill: number;           // 技
    luck: number;            // 運
}

/**
 * ユニット種別
 */
export const enum StatusType {
    GENERAL = "GENERAL", // 汎用
    SPECIAL = "SPECIAL"        // 特別
}

今回のポイントです。

  • UnitStatusインタフェースのように、親インターフェースのメンバ変数としてnumberstringというprimitiveな型ではなく、クラスを型として指定することができます。この場合でも、JSONファイルの読み込みは可能です。
  • 定数クラス(enum)ですが、こちらもA = "A"というような形でキーと値(文字列)を指定しておくと、JSONの中にstring型で記載されていても、自動的にenum型のメンバ変数に置き換える形で、インターフェースの中で取り扱いが可能です。通常のenum型の形(keyだけにする)とトランスパイルされたとき数字(0, 1, …)に置き換えられてしまうので、JSON上でも数字で定義しなければいけなくなります。A = "A"の形だとトランスパイル時に文字列に置き換えられます。

まとめ

ということで、今回はデータモデルについてご紹介しました。全クラス・インタフェースについては、ある程度出来上がったら、どこかに公開できると良いなと思っています。

次は、カーソルを動かしてみます。

mtmusic34.hatenablog.com

※本記事は、2021/6/28時点の実装をもとに書いてる記事であり、現状の実装と異なる場合があることをご了承願ます。

過去の日記

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

*1:とはいえ、マイナンバーは個人を特定できるセンシティブなデータなので、実際、マイナンバーをキーにすることはないと思います