シミュレーションにおけるアセット
データアセットクラス
Quantumアセットは、ランタイム中に不変データコンテナとして機能するC#クラスです。これらのアセットがどのように設計、実装、使用されるべきかを定義するいくつかのルールがあります。
以下は、いくつかのシンプルな決定論的プロパティを持つアセットクラス(キャラクター仕様)の最小限の定義です。
C#
public class CharacterSpec : AssetObject {
public FP Speed;
public FP MaxHealth;
}
アセットクラスのインスタンスをデータベースに作成して読み込むこと(Unityからの編集)は、この章の後半で説明します。
アセットの使用とリンク
アセットインスタンスは不変のオブジェクトであり、参照として保持される必要があります。通常のC#オブジェクト参照はメモリ整列されたECS構造体に含めることができないため、ゲーム状態(エンティティ、コンポーネント、またはその他の一時データ構造)内のプロパティを宣言するにはDSL内で特別なタイプ asset_ref を使用する必要があります:
C#
component CharacterData {
// CharacterSpecの不変インスタンスへの参照(Quantumアセットデータベースから)
asset_ref<CharacterSpec> Spec;
// 他のコンポーネントデータ
}
Characterエンティティを作成する際にアセット参照を割り当てる1つの方法は、フレームアセットデータベースからインスタンスを直接取得し、そのプロパティに設定することです:
C#
// cdはCharacterDataコンポーネントへのポインタであると仮定
// SLOW文字列パスオプションを使用(データ駆動型アセット参照の高速については次で説明)
cd->Spec = frame.FindAsset<CharacterSpec>("path-to-spec");
アセットの基本的な使用法は、ランタイムにデータを読み取り、それをシステム内の計算に適用することです。以下の例では、割り当てられた CharacterSpec の Speed 値を使用して、対応するキャラクタの速度(物理エンジン)を計算します:
C#
// cdはCharacterData*、bodyはPhysicsBody2D*(例えばコンポーネントフィルターから)と考えてください
var spec = frame.FindAsset(cd->Spec);
body->Velocity = FPVector2.Right * spec.Speed;
決定論に関する注意点
上記のコードは、ランタイム中にキャラクタの望ましい速度を計算するために Speed プロパティを読み取るだけですが、その値(スピード)は決して変更されないことに注意してください。
ゲーム状態アセット参照をランタイム中にUpdateから切り替えることは完全に安全で適切です(asset_ref
はロールバック可能なタイプであり、そのためゲーム状態の一部となることができます)。
しかし、データアセットのプロパティの値を変更することは決定論的ではありません(アセットの内部データはゲーム状態の一部と見なされないため、決してロールバックされることはありません)。
以下のスニペットは、ランタイム中に安全な操作(参照の切り替え)と安全でない操作(内部データの変更)を示しています:
C#
// cdはCharacterData*
// これは有効で安全です。CharacterSpecアセット参照はゲーム状態の一部です。
cd->Spec = frame.FindAsset<CharacterSpec>("anotherCharacterSpec-path");
// これは有効でも決定論的でもありません。アセットの内部データは一時的なゲーム状態の一部ではないため:
var spec = frame.FindAsset<CharacterSpec>("anotherCharacterSpec-path");
// (これを行ってはいけません)アセットオブジェクトインスタンス内の値を直接変更
spec.Speed = 10;
アセットの継承
データアセットにおいて継承を使用することが可能で、開発者にとって大きな柔軟性を提供します(特に多態的メソッドと組み合わせて使用する際に)。
継承の基本ステップは、抽象基本アセットクラスを作成することです(キャラクター仕様の例を続けます):
C#
public abstract class CharacterSpec : AssetObject {
public FP Speed;
public FP MaxHealth;
}
CharacterSpec
の具体的なサブクラスは独自のカスタムデータプロパティを追加でき、Serializable 型としてマークされる必要があります:
C#
public class MageSpec : CharacterSpec {
public FP HealthRegenerationFactor;
}
public class WarriorSpec : CharacterSpec {
public FP Armour;
}
データ駆動型ポリモーフィズム
具体的な CharacterSpec クラスを直接評価するゲームプレイロジック(if文やswitch文で処理すること)は非常に悪い設計となるため、アセットの継承はポリモーフィックメソッドと組み合わせて使用する方が理にかなっています。
データアセットにロジックを追加することは、quantum.stateプロジェクトでロジックを実装することを意味し、このロジックは次の制限を考慮する必要があります:
- 一時的ゲーム状態データに対して操作すること:データアセットのロジックメソッドは、一時的データ(エンティティのポインタまたはフレームオブジェクト自身)をパラメータとして受け取る必要があります。
- アセット自体のデータを変更することはない:アセットは 不変 な読み取り専用インスタンスとして扱われる必要があります。
以下の例では、基本クラスに仮想メソッドを追加し、サブクラスの1つにカスタム実装を追加します(ここで、文書の冒頭で定義された Health フィールドを使用しています):
C#
public unsafe abstract class CharacterSpec : AssetObject {
public FP Speed;
public FP MaxHealth;
public virtual void UpdateCharacter(Frame f, EntityRef e, CharacterData* cd) {
if (cd->Health < 0)
f.Destroy(e);
}
}
public unsafe class MageSpec : CharacterSpec {
public FP HealthRegenerationFactor;
// 自身のインスタンスからデータを読み込み、渡されたキャラクターポインタの一時的な健康を更新します
public override void UpdateCharacter(Frame f, EntityRef e, CharacterData* cd) {
cd->Health += HealthRegenerationFactor * f.DeltaTime;
base.UpdateCharacter(f, e, cd);
}
}
この柔軟なメソッド実装を具体的なアセットに依存せずに使用するためには、任意のシステムから実行できます:
C#
// cdが特定のエンティティのCharacterDataコンポーネントへのポインタであり、entityが対応するEntityRefと仮定します:
var spec = frame.FindAsset(cd->Spec);
// データ駆動型ポリモーフィズムを使用して健康を更新します(動作はデータアセットのタイプとキャラクターに割り当てられたインスタンスに依存します)
spec.UpdateCharacter(frame, entity, cd);
アセット内でのDSL生成構造体の使用
DSLで定義された Structs
はアセット内でも使用できます。DSL構造体は [Serializable]
属性を付加する必要があり、そうでない場合はUnityで検査できません。
[Serializable]
struct Foo {
int Bar;
}
Quantumアセット内でDSL struct
を使用する例:
C#
public class FooUser : AssetObject {
public Foo F;
}
構造体が [Serializable]
に対応していない場合(例えば、ユニオンまたはQuantumコレクションを含むため)、プロトタイプを代わりに使用できます:
C#
using Quantum.Prototypes;
public class FooUser : AssetObject {
public FooPrototype F;
}
プロトタイプは必要に応じてシミュレーション構造体に具現化できます:
C#
Foo f = new Foo();
fooUser.F.Materialize(frame, ref f, default);
実行時に静的アセットを追加する
Quantumシミュレーションが開始される前に、実行時にアセットデータベースに静的アセットを追加することが可能です。これは、バックエンドからマップをダウンロードしたり、手続き的にコンテンツを生成したりするシナリオで有用です。実行時にアセットを追加する際には、各アセットが決定論的なGUIDを持つことを確実にし、すべてのクライアント間での一貫性を維持することが重要です。
決定論的な AssetGuid
を生成する方法は2つあります:
定数を使用する:
- これは決定論的な
AssetGuid
を得る最も簡単な方法です。 - 他のアセットに同じ
AssetGuid
が割り当てられていないことを確認する必要があります。 - 固定数のアセットを追加し、同じアセットが常に同じ
AssetGuid
に割り当てられることが保証されている限り、これは問題ありません。
- これは決定論的な
生成:
QuantumUnityDB.CreateRuntimeDeterministicGuid
メソッドはAssetGuid
を生成するためのAPIを提供します。- アセットオブジェクトの名前をシードとして使用して、決定論的な
AssetGuid
を生成します。 - 追加するアセットの数が固定されていない場合、このアプローチを使用すべきです。
使用例:
C#
// 任意のアセットを作成
var assetObject = AssetObject.Create<MyAssetObjectType>();
// 名前を設定
assetObject.name = "My Unique Asset Object Name";
// 決定論的GUIDを取得
var guid = QuantumUnityDB.CreateRuntimeDeterministicGuid(assetObject);
// アセットをアセットデータベースに追加
QuantumUnityDB.Global.AddAsset(assetObject);
// GUIDを設定
assetObject.Guid = guid;
アセットがアセットデータベースに追加された後、それはエディタで作成されて追加されたアセットと実質的に同じです。ただし、これはゲームがまだ開始されていない場合にのみ有効です。一度ゲームが開始されると、静的アセットデータベースは変更されてはなりません。
ゲームプレイ中にアセットを追加または変更する必要がある場合は、代わりに DynamicDB
API を使用する必要があります。
動的アセット
アセットは、シミュレーションによってランタイムで作成されることができます。この機能は DynamicAssetDB と呼ばれます。
C#
var mageSpec = AssetObject.Create<MageSpec>();
mageSpec.Speed = 1;
mageSpec.MaxHealth = 100;
frame.AddAsset(mageSpec);
このようなアセットは、他のアセットと同様にロードおよび廃棄することができます:
C#
MageSpec asset = frame.FindAsset<MageSpec>(assetGuid);
frame.DisposeAsset(assetGuid);
動的アセットはピア間で同期されません。代わりに新しいアセットを作成するコードは決定論的であり、各ピアが同じ値を使用してアセットを生成できることを保証する必要があります。
上記のルールの唯一の例外は、遅延参加者がいる場合です - 新しいクライアントは、最新のフレームデータと共に DynamicAssetDB のスナップショットを受け取ります。フレームのシリアル化とは異なり、動的アセットのシリアル化とデシリアル化はシミュレーションの外で、IAssetSerializer
インターフェースに委譲されます。Unityで実行されると、デフォルトで QuantumUnityJsonSerializer
が使用され、Unityシリアライズ可能な任意の型をシリアル化/デシリアル化することができます。
DynamicAssetDBの初期化
シミュレーションは、既存の動的アセットで初期化できることがあります。シミュレーション中にアセットを追加するのと同様に、これらもクライアント間で一貫性を持たせる必要があります。
まず、DynamicAssetDB
のインスタンスを作成し、アセットを追加します:
C#
var initialAssets = new DynamicAssetDB();
initialAssets.AddAsset(mageSpec);
initialAssets.AddAsset(warriorSpec);
...
次に、QuantumGame.StartParameters.InitialDynamicAssets
を使用して、そのインスタンスを新しいシミュレーションに渡す必要があります。Unityでは、QuantumGame
を管理するのが QuantumRunner
の動作であるため、QuantumRunner.StartParameters.InitialDynamicAssets
が代わりに使用されます。
ビルトインアセット
Quantumには、以下のようないくつかのビルトインアセットが付属しています:
- SimulationConfig - Quantumシミュレーションの仕様を多く定義します。シーン管理の設定、ヒープ構成、スレッド数、物理/ナビゲーション設定など。
- DeterministicConfig - ゲームセッションの詳細を指定します。シミュレーションレート、チェックサムの間隔、クライアントとサーバーの両方に関する入力関連の多数の設定などがあります。
- QuantumEditorSettings - エディタ専用の詳細(DBが基づくフォルダー、Gizmosの色、自動ベイクオプションなど)の定義が含まれています。
- BinaryData - ユーザーが任意のバイナリ情報(
byte[]
の形式)を参照できるアセット。デフォルトでは、物理エンジンとナビゲーションエンジンは、静的三角形データなどの情報を格納するためにバイナリデータアセットを使用します。このアセットには、gzipを使用してデータを圧縮および展開するためのユーティリティもビルトインされています。 - CharacterController3DConfig - ビルトインの3D KCCに対する設定アセット。
- CharacterController2DConfig - ビルトインの2D KCCに対する設定アセット。
- PhysicsMaterial - Quantumの3D物理エンジン用の
Physics Material
を定義します。 - PolygonCollider - Quantumの2D物理エンジン用の
Polygon Collider
を定義します。 - NavMesh - Quantumのナビゲーションシステムで使用される
NavMesh
を定義します。 - NavMeshAgentConfig - Quantumのナビゲーションシステム用の
NavMesh Agent Config
を定義します。 - Map - 物理設定、コライダー、NavMesh設定、リンク、領域など、シーンごとの静的情報を多く格納します。また、そのマップ内のシーンエンティティプロトタイプも格納されます。各マップは単一のUnityシーンに関連付けられています。