シミュレーション内のアセット
データアセットクラス
QuantumアセットはC#クラスで、実行時に不変データコンテナとして機能します。これらのアセットには、どのように設計・実装・使用すべきかを定義するルールがいくつかあります。
以下はアセットクラス(キャラクターの仕様)の最小限の定義で、いくつかの単純な決定論的プロパティを持ちます。
C#
public class CharacterSpec : AssetObject {
public FP Speed;
public FP MaxHealth;
}
アセットクラスのインスタンスを、データベース(Unityから編集する)に作成してロードする方法は、このチャプターの後半で説明します。
アセットの使用とリンク
アセットのインスタンスは不変オブジェクトで、参照として保持される必要があります。通常のC#オブジェクト参照は、メモリアライメントされたECS構造体に含めることができないため、ゲームステート(エンティティ・コンポーネント・その他の一時的データ構造)内プロパティを宣言するには、DSLで特別な型asset_ref
を使用する必要があります。
Qtn
component CharacterData {
// reference to an immutable instance of CharacterSpec (from the Quantum asset database)
asset_ref<CharacterSpec> Spec;
// other component data
}
キャラクターエンティティ作成時にアセット参照を代入する方法の1つは、アセットデータベースから直接インスタンスを取得してプロパティに設定することです。
C#
// cdはCharacterDataコンポーネントのポインタ
// 低速な文字列パスオプションを使用(高速なデータ駆動アセット参照は後で説明します)
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のAssetRefはゲームステートの一部であるため、「有効」かつ「安全」
cd->Spec = frame.FindAsset<CharacterSpec>("anotherCharacterSpec-path");
// アセット内のデータはゲームステートの一部ではないため、「有効」でも「決定論的」でもない
var spec = frame.FindAsset<CharacterSpec>("anotherCharacterSpec-path");
// (これは絶対にしないでください)アセットオブジェクトインスタンスの値を直接変更する
spec.Speed = 10;
アセットの継承
データアセットは継承可能なため、大きな柔軟性を持たせることができます(特に多態メソッドを組み合わせて使用する場合)。
継承の基本ステップは、抽象基底アセットクラスを作成することです(CharacterSpec
の例を引き続き使用します)。
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;
}
データ駆動ポリモーフィズム
具象CharacterSpect
クラスを直接評価する(if文やswitch文で分岐する)ゲームプレイロジックは非常に悪い設計で、アセットの継承と多態メソッドを組み合わせる方が合理的です。
データアセットにロジックを追加する場合は、以下の制限を考慮する必要があることに注意してください。
- 一時的ゲームステートデータに対する操作:データアセットのロジックメソッドは、一時的データ(エンティティのポインタ、またはフレームオブジェクト自身)をパラメーターとして受け取る必要があります。
- アセット自体は読み取り専用で決して変更しない:アセットは「不変」な読み取り専用インスタンスとして扱う必要があります。
以下の例では、基底クラスに仮想メソッドを追加し、サブクラスの1つに独自実装を追加しています(ここで冒頭で定義したHealth
フィールドを使用します)。
C#
public unsafe abstract class CharacterSpec : AssetObject {
public FP Speed;
public FP MaxHealth;
public virtual void UpdateCharacter(Frame frame, EntityRef entity, CharacterData* data) {
if (data->Health < 0)
frame.Destroy(entity);
}
}
public unsafe class MageSpec : CharacterSpec {
public FP HealthRegenerationFactor;
// 自身のインスタンスからデータを読み込み、パラメーターからポインタで渡されたキャラクターのヘルスを更新する
public override void UpdateCharacter(Frame frame, EntityRef entity, CharacterData* data) {
data->Health += HealthRegenerationFactor * frame.DeltaTime;
base.UpdateCharacter(frame, entity, data);
}
}
この柔軟なメソッド実装を、CharacterData
に代入された具象アセットに依存せずに使用するため、任意のシステムから以下のコードが実行できます。
C#
// dataは対象エンティティのCharacterDataコンポーネントのポインタ
// entityは対象エンティティのEntityRef
var spec = frame.FindAsset(data->Spec);
// データ駆動ポリモーフィズム(データアセット型とキャラクターに代入されたインスタンスに依存した振る舞い)を使用してヘルスを更新する
spec.UpdateCharacter(frame, entity, data);
DSLで生成された構造体をアセットで使用
DSLで定義されたstruct
は、アセット内でも使用できます。DSL構造体は[Serializable]
属性を付加しないと、Unity側で確認できません。
Qtn
[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が基づくフォルダー・ギズモの色・自動ビルド・自動マップベイク・ナビメッシュなど)が含まれます。
- 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 - シーンごとの様々な静的情報(物理設定・コライダー・ナビメッシュ設定・リンク・領域・マップのシーン上に存在するエンティティプロトタイプなど)を格納します。各マップは、単一のUnityシーンに関連付けられます。