Phaser3+typescriptでSRPGを開発してみる #008 データモデル(ユニット編)
今日はユニットのデータモデル設計を進めようかと思います。
ユニットのデータモデル設計
SRPGで必要な要素
早速ですが、シミュレーションRPGに必要な要素の洗い出しです。ER図というかクラス図書いてみようと思いますが、まずはエンティティの洗い出しです。
エンティティ名 | interface名 | 項目例 |
---|---|---|
初期ステータス | InitialUnit | 名前、初期レベル、ジョブクラス名、説明、最大HP |
ゲーム中のステータス | GameUnit | チーム名、レベル、経験値、HP、補正された攻撃力、スキル発動カウント、機動力、装備武器、習得済みスキル、ポジション(x,y座標) |
ジョブクラス | JobClass | ジョブクラス名、標準ステータス、武器種別、移動種別(騎馬、飛行など) |
武器 | Weapon | 武器名、武器種別、攻撃力、特殊効果 |
スキル | Skill | 特殊効果(ターン開始時、戦闘開始時、戦闘終了後)、発動カウント |
そしてこれをER図っぽく表現してみます。
- 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をキーとして、作ってみました。
ということで、今回は下記の通り、実装してみました。 これらを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
インタフェースのように、親インターフェースのメンバ変数としてnumber
やstring
というprimitiveな型ではなく、クラスを型として指定することができます。この場合でも、JSONファイルの読み込みは可能です。- 定数クラス(
enum
)ですが、こちらもA = "A"
というような形でキーと値(文字列)を指定しておくと、JSONの中にstring型で記載されていても、自動的にenum型のメンバ変数に置き換える形で、インターフェースの中で取り扱いが可能です。通常のenum型の形(keyだけにする)とトランスパイルされたとき数字(0, 1, …)に置き換えられてしまうので、JSON上でも数字で定義しなければいけなくなります。A = "A"
の形だとトランスパイル時に文字列に置き換えられます。
まとめ
ということで、今回はデータモデルについてご紹介しました。全クラス・インタフェースについては、ある程度出来上がったら、どこかに公開できると良いなと思っています。
次は、カーソルを動かしてみます。
※本記事は、2021/6/28時点の実装をもとに書いてる記事であり、現状の実装と異なる場合があることをご了承願ます。
過去の日記
タイトル | 記載内容 | |
---|---|---|
#001 | プロローグ | Phaser3とは |
#002 | 環境設定 | インストール・ビルド環境設定 |
#003 | はじめてのPhaser3 | 初回稼働・キャンバス表示 |
#004 | マップ作成 | タイルセットの読込、画面手動作成、Sceneクラスのライフサイクル |
#005 | Tiledの利用 | Tiledによる画面マップJSON作成、マルチレイヤーの背景画面 |
#006 | JSONファイルの読込 | データのpreload |
#007 | ファイルのキャッシュ | ゲームデータの管理 |