シミュレーション上のアセット
データアセットクラス
Quantumのアセットは実行時に不変のデータコンテナとして機能する通常のC#クラスです。Quantumのアセットをどのように設計し、実装し、使用しなければならないかは、いくつかのルールで定義されています。
ここでは、単純な決定論的特性を持つアセットクラス(キャラクター仕様のもの)の最小限の定義を示します。
C#
namespace Quantum {
partial class CharacterSpec {
public FP Speed;
public FP MaxHealth;
}
}
アセットクラスの定義は 部分的 であり、** 明確に、Quantum 名前空間に** 含まれなければならないことに注意してください。
Notice that the asset class definition must be partial and it has to be contained specifically in the Quantum namespace.
アセットクラスのインスタンスの作成とデータベースへの読み込み(Unityからの編集)については、本章の後半で説明します。
アセットの使用とリンク
Quantumにこれがアセットクラスであることを伝える(基本的なAssetObjectクラスを継承させることで内部メタデータを追加し、そのインスタンスを格納するデータベースを準備する):
C#
// this goes into a DSL file
asset CharacterSpec;
アセットインスタンスは不変のオブジェクトであり、参照として扱われなければなりません。通常のC#オブジェクトの参照は、メモリ上に配置されたECS構造体に含めることができないため、ゲーム状態内のプロパティ(エンティティ、コンポーネント、その他の一時的なデータ構造)を宣言するには、DSL内でasset_ref特殊型を使用する必要があります。
C#
component CharacterData {
// reference to an immutable instance of CharacterSpec (from the Quantum asset database)
asset_ref<CharacterSpec> Spec;
// other component data
}
Characterエンティティの作成時にアセット参照を割り当てるには、フレームアセットデータベースから直接インスタンスを取得し、プロパティに設定する方法があります:
C#
// assuming cd is a pointer to the CharacterData component
// using the SLOW string path option (fast data driven asset refs will be explained next)
cd->Spec = frame.FindAsset<CharacterSpec>("path-to-spec");
アセットの基本的な使い方は、実行時にデータを読み込んで、システム内の任意の計算に適用することです。次の例では、割り当てられたCharacterSpecからSpeed値を使用して、対応するキャラクターの速度(物理エンジン)を計算します:
C#
// consider cd a CharacterData*, and body a PhysicsBody2D* (from a component filter, for example)
var spec = frame.FindAsset<CharacterSpec>(cd->Spec.Id);
body->Velocity = FPVector2.Right * spec.Speed;
決定論について
上記のコードでは、実行時にキャラクターの速度を計算するために Speed プロパティを読み取るだけで、その値(速度)は決して変更されないことに注意してください。
ゲーム状態のアセット参照をアップデート内から実行時に切り替えることは、完全に安全かつ有効です(asset_refはロールバック可能な型であり、したがってゲーム状態の一部となることができるため)。
しかし、データアセットのプロパティの**値を変更することは、決定的ではありません(アセットの内部データはゲーム状態の一部と見なされないため、ロールバックされることはありません)。
次のスニペットは、実行時に安全なもの(参照先を切り替える)と安全でないもの(内部データを変更する)の例を示しています。
C#
// cd is a CharacterData*
// this is VALID and SAFE, as the CharacterSpec asset ref is part of the game state
cd->Spec = frame.FindAsset<CharacterSpec>("anotherCharacterSpec-path");
// this is NOR valid NEITHER deterministic, as the internal data from an asset is NOT part of the transient game state:
var spec = frame.FindAsset<CharacterSpec>("anotherCharacterSpec-path");
// (DO NOT do this) changing a value directly in the asset object instance
spec.Speed = 10;
AssetObjectConfig 属性
AssetObjectConfig
属性で、アセットリンクスクリプトの生成を細かく設定することができます。
C#
[AssetObjectConfig(GenerateLinkingScripts = false)]
partial class CharacterSpec {
// ...
}
- GenerateLinkingScripts (default=true) - Unityでアセットを編集可能にするスクリプトが生成されないようにします。
- GenerateAssetCreateMenu (default=true) - このアセットに対して Unity の
CreateAssetMenu
属性を生成しないようにします。 - GenerateAssetResetMethod (default=true) - Unity Scriptable Object の
Reset()
メソッド (アセット作成時に Guid が自動生成される) の生成を防止します。 - CustomCreateAssetMenuName (default=null) -
CreateAssetMenu
の名前を上書きします。null
に設定すると、メニューのパスは、継承グラフを使用して自動的に生成されます。 - CustomCreateAssetMenuOrder (default=-1) -
CreateAssetMenu
の並び順を上書きします。-1を指定すると、アルファベット順で表示されます。
Asset Script Location を上書きし、AOT ファイルを生成を無効にする
ファイル toolscodegen_unity_quantum.codegen.unity.dll.config
を作成します。警告: アップグレードの際には、このファイルが失われる可能性があるため、注意が必要です。
XML
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="RelativeLinkDrawerFilePath" value="Quantum/Editor/PropertyDrawers/Generated/AssetLinkDrawers.cs"/>
<add key="RelativeAssetScriptFolderPath" value="Quantum/AssetTypes/Generated"/>
<add key="RelativeGeneratedFolderPath" value="Quantum/Generated"/>
<add key="GenerateAOTFile" value="true"/>
<add key="EndOfLineCharacters" value=" "/>
</appSettings>
</configuration>
アセット継承
データアセットで継承を使用することができます。これは開発者により多くの柔軟性を与えます(特にポリモーフィックメソッドと一緒に使用した場合)。
継承の基本的な手順は、抽象的なベースアセットクラスを作成することです(CharacterSpec の例で説明します)。
C#
namespace Quantum {
partial abstract class CharacterSpec {
public FP Speed;
public FP MaxHealth;
}
}
CharacterSpec の具体的なサブクラスは、独自のカスタムデータプロパティを追加することができ、Serializable 型としてマークする必要があります。
C#
namespace Quantum {
[Serializable]
public partial class MageSpec : CharacterSpec {
public FP HealthRegenerationFactor;
}
[Serializable]
public partial class WarriorSpec : CharacterSpec {
public FP Armour;
}
}
DSL において
DSL でアセットを宣言すると、DSL で asset_ref<T>
型を使用して、ベースクラスとそのサブクラスへの参照を保持することができるようになります。
C#
component CharacterData {
// Accepts asset references to CharacterSpec base class and its sub-classes(MageSpec and WarriorSpec).
asset_ref<CharacterSpec> ClassDefinition;
FP CooldownTimer;
}
サブクラス固有の参照を保持したい場合、派生アセットはまず DSL で asset import
を使って宣言する必要があります。
C#
asset CharacterSpec;
asset import MageSpec;
派生アセットがすでにDSLで宣言されている場合は、ベースクラスと同様に asset_ref<T>
を使用するだけです。例えば、CharacterSpec
の代わりにMageSpec
をDSLで直接使用するには、以下のように記述する必要があります。
C#
component MageData {
// Only accepts asset references to MageSpec class.
asset_ref<MageSpec> ClassDefinition;
FP CooldownTimer;
}
データ駆動型ポリモーフィズム
ゲームプレイのロジックが具象的な CharacterSpec クラスを直接評価することは、設計上非常に悪いことです。アセット継承は、ポリモーフィックメソッドと組み合わせるべきです。
データアセットにロジックを追加するということは、quantum.stateプロジェクトにロジックを実装するということであり、このロジックは以下の制約を考慮しなければなりません。
- ゲーム状態の一時的なデータで操作する:つまり、データ アセットのロジック メソッドは、パラメータとして一時的なデータ(エンティティ ポインタまたは Frame オブジェクト自体)を受け取る必要があります。
- アセット自体のデータを読み取るだけで、決して変更しない:アセットは依然として、不変 の読み取り専用インスタンスとして扱う必要があります。
次の例では、ベースクラスに仮想メソッドを追加し、サブクラスの1つにカスタム実装を追加しています(このドキュメントの先頭にある Character エンティティ用に定義された Health フィールドを使用していることに注意してください)。
C#
namespace Quantum {
partial unsafe abstract class CharacterSpec {
public FP Speed;
public FP MaxHealth;
public virtual void Update(Frame f, EntityRef e, CharacterData* cd) {
if (cd->Health < 0)
f.Destroy(e);
}
}
[Serializable]
public partial unsafe class MageSpec : CharacterSpec {
public FP HealthRegenerationFactor;
// reads data from own instance and uses it to update transient health of Character pointer passed as param
public override void Update(Frame f, EntityRef e, CharacterData* cd) {
cd->Health += HealthRegenerationFactor * f.DeltaTime;
base.Update(f, e, cd);
}
}
}
この柔軟なメソッド実装を、各 CharacterData に割り当てられた具象アセットとは別に使用する場合、このメソッドは、任意のSystemから実行することができます:
C#
// Assuming cd is the pointer to a specific entity's CharacterData component, and entity is the corresponding EntityRef:
var spec = frame.FindAsset<CharacterSpec>(cd->Spec.Id);
// Updating Health using data-driven polymorphism (behavior depends on the data asset type and instance assigned to character
spec.Update(frame, entity, cd);
DSLで生成された構造体をアセットで使用する
DSL で定義された Structs
は、アセットでも使用することができます。DSLの構造体は、[Serializable]
属性で注釈する必要があり、そうしないとUnityでデータを閲覧することができません。
[Serializable]
struct Foo {
int Bar;
}
asset FooUser;
QuantumアセットでDSL struct
を使用する。
C#
namespace Quantum {
public partial class FooUser {
public Foo F;
}
}
もし、構造体が [Serializable]
に適さない場合(例えば、ユニオンであったり Quantumコレクションを含んでいるなど)、代わりにprototypeを使用することができます。
C#
using Quantum.Prototypes;
namespace Quantum {
public partial class FooUser {
public Foo_Prototype F;
}
}
プロトタイプは、必要なときにシミュレーション構造体に実体化することができます。
C#
Foo f = new Foo();
fooUser.F.Materialize(frame, ref f, default);
動的アセット
アセットは、シミュレーションによって実行時に作成することができます。この機能は DynamicAssetDB と呼ばれます。
C#
var assetGuid = frame.AddAsset(new MageSpec() {
Speed = 1,
MaxHealth = 100,
HealthRegenerationFactor = 1
});
このようなアセットは、他のアセットと同じように読み込み、処分することができます。
C#
MageSpec asset = frame.FindAsset<MageSpec>(assetGuid);
frame.DisposeAsset(assetGuid);
動的なアセットはピア間で同期されません。その代わり、新しいアセットを作成するコードは決定論的である必要があり、各ピアが同じ値を使用してアセットを生成することを保証する必要があります。
上記のルールの唯一の例外は、後発の参加者がいる場合です。新しいクライアントは、最新のフレームデータとともに DynamicAssetDB のスナップショットを受け取ります。フレームのシリアライズとは異なり、ダイナミックアセットのシリアライズとデシリアライズはシミュレーションの外側、 IAssetSerializer
インターフェースに委ねられます。Unity で実行する場合、デフォルトで QuantumUnityJsonSerializer
が使用されます。これは Unity でシリアライズ可能なあらゆる型をシリアライズ/デシリアライズすることができます。
DynamicAssetDBの初期化
シミュレーションは、既存の動的アセットで初期化することができます。シミュレーション中にアセットを追加するのと同様に、アセットもクライアント間で決定論的である必要があります。
まず、DynamicAssetDB
のインスタンスを作成し、アセットで埋める必要があります。
C#
var initialAssets = new DynamicAssetDB();
initialAssets.AddAsset(new MageSpec() {
HealthRegenerationFactor = 10
});
initialAssets.AddAsset(new WarriorSpec() {
Armour = 100
});
...
次に、新しいシミュレーションにインスタンスを渡すために、 QuantumGame.StartParameters.InitialDynamicAssets
を使用する必要があります。Unityでは、QuantumGame
を管理するのはQuantumRunner
動作なので、代わりにQuantumRunner.StartParamters.InitialDynamicAssets
が使用されます。