Reactive Entity Sets の統合
目的
このページでは、Tiny History デモが Reactive Entity Sets を Jobs 統合、カスタム GPU レンダリング、履歴スナップショットにどのように使用しているかを説明します。
概要
このサンプルは4つのパターンを示しています。
- Jobs 統合 — 安全な並列処理のためのダブルバッファリング
- カスタム GPU レンダリング — レンダリング用の手動バッファ更新
- 状態クエリ — アロケーションなしの集計とフィルタリング
- 履歴スナップショット — タイムトラベル用のエンティティ状態の保存と復元
使用している Entity Sets
| Entity Set | エンティティ型 | 典型的な数 | 目的 |
|---|---|---|---|
| ArmyStateSet | ArmyState | 最大約20,000 | 位置、目標、強さを持つアクティブな軍隊 |
| ProvinceStateSet | ProvinceData | 約500 | 所有権と地形を持つマップの州 |
| NationStateSet | NationData | 約50 | 首都と生存状態を持つ国家 |
パターン1 — ダブルバッファリングによる Jobs 統合
課題
Unity Jobs は実行中に安定したデータを必要とします。ジョブ実行中にエンティティ状態が変更されると、競合状態やデータ破損が発生する可能性があります。
解決策 — ReactiveEntitySetOrchestrator
このサンプルは ReactiveEntitySetOrchestrator<T> を使用してダブルバッファリングを自動管理します。
graph TB
subgraph "Frame N"
READ_A[Jobs Read<br/>Buffer A]
COMPUTE[Compute Results]
end
subgraph "Between Frames"
APPLY[Apply Results<br/>to Buffer B]
SWAP[Swap Buffers]
end
subgraph "Frame N+1"
READ_B[Jobs Read<br/>Buffer B]
end
READ_A --> COMPUTE --> APPLY --> SWAP --> READ_B
動作の仕組み
- スケジュールフェーズ — Jobs が現在状態の
NativeArrayスナップショットを受け取ります - 実行フェーズ — Jobs がスナップショットから読み取り、別の出力に結果を書き込みます
- 完了フェーズ — 結果が Entity Set に適用されます
- 次フレーム — 更新された状態が次のジョブバッチで利用できます
重要なポイント - Jobs は Entity Set を直接変更しません。スナップショットを使用して作業し、フレーム間でアトミックに適用される結果を生成します。
TinyHistoryは DataOnly 更新を使用します。IDは変化しないため、ID配列のコピーは不要です。
IDやエンティティ数が変わる場合はUpdateMode.DataAndIdsを使い、GetBackBufferIds()に書き込んでください。
Job パイプラインの例
ArmyStateSet (Current State)
│
▼
┌─────────┐
│ Strategy│ → 目標を決定
│ Job │
└────┬────┘
▼
┌─────────┐
│ March │ → 位置を更新
│ Job │
└────┬────┘
▼
┌─────────┐
│ Combat │ → 戦闘を解決
│ Job │
└────┬────┘
▼
ArmyStateSet (Updated State)
パターン2 — カスタム GPU レンダリング
州のレンダリング
州の所有権データは毎フレームの手動更新により Entity Set から GPU バッファへ流れます。
graph LR
subgraph "CPU"
P_SET[(ProvinceStateSet)]
UPDATE[UpdateProvinceBuffer<br/>Per Frame]
end
subgraph "GPU"
P_BUF[GraphicsBuffer<br/>ProvinceState]
SHADER[MapComposition.shader]
end
P_SET --> UPDATE --> P_BUF --> SHADER
GPU へコピーされる内容は以下のとおりです。
- 所有国 ID(州の色を決定します)
- 占領進捗(侵攻オーバーレイを表示します)
- 地形タイプ(見た目に影響します)
注意 - このサンプルは Reactive SO の GPU Sync 機能ではなく、手動の
GraphicsBuffer.SetData()呼び出しを使用しています。このアプローチにより、データを GPU にアップロードするタイミングと方法を完全に制御できます。
軍隊のレンダリング
軍隊の位置は毎フレーム GPU にコピーされ、インスタンスレンダリングに使用されます。
graph LR
subgraph "CPU"
A_SET[(ArmyStateSet)]
CONV[Convert to<br/>ArmyGPU struct]
end
subgraph "GPU"
A_BUF[GraphicsBuffer<br/>ArmyGPU]
INST[DrawMeshInstancedIndirect]
end
A_SET --> CONV --> A_BUF --> INST
ArmyGPU 構造体には以下のデータが含まれます。
- 現在位置
- 目標位置
- 行軍進捗(補間に使います)
- 国家 ID(色ルックアップに使います)
重要なポイント - シェーダーが行軍進捗を使用して軍隊の位置を補間し、軍隊ごとの CPU 更新なしにスムーズな移動を実現します。
パターン3 — 状態クエリ
Entity Sets は集計とフィルタリングのための LINQ スタイルのクエリをサポートしています。
国家滅亡チェック
// この国家が所有する州があるかチェック
bool hasTerritory = provinceStateSet
.Any(p => p.OwnerNationID == nationID);
if (!hasTerritory)
{
// 国家滅亡
nationEliminatedEvent.Raise(currentYear, nationID);
}
国家ごとの軍隊数
// 各国家の軍隊をカウント
foreach (var nation in nationStateSet)
{
int armyCount = armyStateSet
.Count(a => a.NationID == nation.ID);
// 経済計算に使用
}
重要なポイント - クエリは基盤となる
NativeArrayを使用してゼロアロケーション反復を行います。ゲームプレイ中にガベージは生成されません。
パターン4 — 履歴スナップショット
状態のキャプチャ
このサンプルはタイムライン操作のために定期的に Entity Set のスナップショットをキャプチャします。
graph TB
subgraph "Every N Frames"
CAPTURE[HistoryManager.CaptureSnapshot]
end
subgraph "Snapshot Data"
S_A[ArmyState Array]
S_P[ProvinceData Array]
S_N[NationData Array]
end
subgraph "Storage"
BUFFER[Circular Buffer<br/>最大84スナップショット]
end
CAPTURE --> S_A --> BUFFER
CAPTURE --> S_P --> BUFFER
CAPTURE --> S_N --> BUFFER
スナップショットには以下の情報が含まれます。
- すべてのエンティティ配列のコピー
- フレーム番号(年)
- 復元用メタデータ
状態の復元
ユーザーが過去の年にシークすると、Entity Sets がスナップショットから復元されます。
sequenceDiagram
participant User
participant HistoryManager
participant ArmyStateSet
participant ProvinceStateSet
participant NationStateSet
User->>HistoryManager: SeekTo(Year 50)
HistoryManager->>HistoryManager: Year 50 のスナップショットを検索
HistoryManager->>ArmyStateSet: Clear + AddRange(snapshot.armies)
HistoryManager->>ProvinceStateSet: Clear + AddRange(snapshot.provinces)
HistoryManager->>NationStateSet: Clear + AddRange(snapshot.nations)
重要なポイント - コレクションライクな API(
Clear、AddRange)により保存/復元が簡単になります。Entity Sets は標準コレクションのように振る舞いながら、リアクティブな機能を持っています。
タイムライン分岐
過去にシークして再生を再開すると、将来のスナップショットは破棄されます。
Timeline: [Yr10] [Yr20] [Yr30] [Yr40] [Yr50]
▲
ユーザーがここにシーク
再開後:
Timeline: [Yr10] [Yr20] [Yr30] [Yr40] [Yr41] [Yr42] ...
└── 新しい歴史分岐
メモリ効率
このサンプルはメモリアロケーションを最小化するためにいくつかの戦略を使用しています。
| 戦略 | 実装 |
|---|---|
| 事前割り当てバッファ | 初期化時に Entity Sets のサイズを設定 |
| 循環バッファ | 固定スナップショット数、最古を上書き |
| NativeArray ビュー | クエリ用のゼロコピー反復 |
| 構造体エンティティ | 値型によりヒープアロケーションを回避 |
主要ファイル
| ファイル | 説明 |
|---|---|
ScriptableObjects/EntitySets/ArmyStateSet.asset | 軍隊 Entity Set |
ScriptableObjects/EntitySets/ProvinceStateSet.asset | 州 Entity Set |
ScriptableObjects/EntitySets/NationStateSet.asset | 国家 Entity Set |
Scripts/TinyHistorySimulation.cs | Orchestrator セットアップとジョブスケジューリング |
Scripts/HistoryManager.cs | スナップショットの取得と復元 |
Scripts/MapRenderer.cs | 州の GPU レンダリング |
Scripts/ArmyRenderer.cs | 軍隊の GPU レンダリング |
次のステップ
- システム全体の概要は アーキテクチャ をご覧ください
- Event Channels については Event Channels で学べます
さらに学ぶ
Reactive Entity Sets を自分のプロジェクトで使いたいですか?
- Reactive Entity Sets ガイド - 基本的な使い方、イベント、パターン、ベストプラクティスを網羅した完全ガイド
- Variables ガイド - Variables での GPU Sync の使い方