This document is about: QUANTUM 3
SWITCH TO

シミュレーション内のアセット

データアセットクラス

QuantumアセットはC#クラスで、実行時に不変データコンテナとして機能します。これらのアセットには、どのように設計・実装・使用すべきかを定義するルールがいくつかあります。

以下はアセットクラス(キャラクターの仕様)の最小限の定義で、いくつかの単純な決定論的プロパティを持ちます。

C#

  public class CharacterSpec : AssetObject {
    public FP Speed;
    public FP MaxHealth;
  }
Unityでは、「AssetObject」は「UnityEngine.ScriptableObject」のサブクラスなので、定義はクラス名に対応するファイル(例:CharacterSpec.cs)に保存する必要があります。また、「Update」や「Start」メソッドを追加すると、Unityのバージョンによってはコンパイルエラーが発生することがあります。

アセットクラスのインスタンスを、データベース(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");

アセットの基本的な使用方法は、実行時にデータを読み取り、システム内の計算に適用することです。以下の例では、CharacterSpecSpeed値を使用して、キャラクターの速度(物理エンジン)を計算しています。

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つあります。

  1. 定数を使用する:
    • 決定論的なAssetGuidを得る最も簡単な方法です。
    • 他のアセットに同じAssetGuidが割り当てないようにする必要があります。
    • 一定数のアセットを追加して、同じアセットに常に同じAssetGuidを割り当てるなら、これで問題ありません。
  2. 生成する:
    • 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;

アセットがアセットデータベースに追加された後は、エディターで作成/追加されたアセットと実質的に同じになります。ただし、これはゲームがまだ開始していない場合にのみ有効です。ゲームが開始された後に、静的アセットデータベースを変更してはいけません。

ゲームプレイ中にアセットを追加/変更する必要がある場合は、かわりにDynamicDBAPIを使用してください。

動的アセット

シミュレーションから実行時にアセットを作成できます。この機能は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シーンに関連付けられます。
Back to top