実行時の状態はどこに置くか:ECSのコンポーネントのための4つのルール
設定、エンティティごとの状態、ライブラリが所有する状態、派生データ。Hydroではそれぞれの置き場所が一つに決まっています。一度決めたことで、ある種のバグがまるごとなくなりました。
HydroはEnTTを使っており、コンポーネントは純粋なデータです。デフォルト値付きのpublicなフィールドと小さな純粋関数だけで、仮想関数も更新処理も持ちません。システムは、コンポーネントのビューに対して実行される関数です。
ここまでは簡単です。難しい問いはパーティクルシステムを作るときに出てきました。実行時の状態はどこに置くのか?スプライトアニメーションの現在のフレーム、Box2Dのボディ、GPU上のパーティクルバッファ。置き場所を間違えると、エディタがそれをシーンファイルに保存してしまったり、プレイモードが古い値から始まったり、2か所で食い違ったりします。
そこで一つのルールを書き出し、すべてに適用しました。
ルール
- 保存する設定はコンポーネントに置く。
- 小さな実行時の状態もコンポーネントに置いてよい。ただし固定サイズで、エンティティごとで、コンポーネントの外に何も必要としない場合に限ります。1つの
Stateメンバーにまとめ、保存せず、ゲーム開始時にリセットし、書き込むのはそのシステムだけです。例:スプライトアニメーションの現在のフレームとタイマー。 - それ以外はシーンごとのオブジェクトに置く。可変サイズのデータ、ライブラリやGPUが所有するデータ、エンティティより長く存在する必要があるデータは、シーンが所有する
...Sceneオブジェクトに置きます。Box2DのボディならPhysicsScene、JoltのボディとキャラクターならPhysicsScene3Dです。そしてその場合は、そのコンポーネントの実行時の状態をすべてそこに置き、正しい情報源を一つにします。 - スクリプトとエディタツールはファサードを通す。(
Physics2D、Physics3D、CharacterBody、SpriteAnimator)実行時のフィールドに直接触れることはしません。
派生データは別物
その後、シーンのヒエラルキーと実行時のUIによって5種類目のデータが現れました。エンティティのワールド変換のように、作成されたコンポーネントから計算される値です。これは実行時の状態でも設定でもありません。
派生データには専用のコンポーネントを用意し、EnTTの生成・破棄シグナルで元のコンポーネントと一緒に追加・削除します。TransformComponentに対するWorldTransformComponent、UIRectComponentに対するUIResolvedRectComponentです。計算するパスは値を書き込むだけで、レジストリの構造を変えないので、イテレーションが安全に保たれます。派生コンポーネントは保存されず、プレイ時にコピーされず、インスペクターにも表示されません。
書き出す価値があった理由
- プレイモードがそのまま動く。プレイ用のシーンのコピーはコンポーネントをコピーします。ルール2の状態は開始時にリセットされ、ルール3の状態はシーンオブジェクトが作り直します。あちこちに散らばったリセット処理は不要です。
- シーンファイルがきれいに保たれる。保存されるのは設定だけなので、シーンファイルの差分に意味があります。
- 新しいシステムで迷わない。パーティクル、物理、UIはどれも、自分との設計会議なしにルールに収まりました。
- 検査できる。コードの形がどちらかを示すので、エージェントもプロジェクト検証ツールも設定と状態を区別できます。
Unityの仕事にも持ち帰っている一般的な学び:状態の置き場所は一度だけ決め、分類に名前をつけ、フィールドがどの分類かをコードの形で示すこと。