システム (ゲームロジック)
イントロ
システムは、Quantumのすべてのゲームプレイロジックのエントリポイントです。
これらは通常のC#クラスとして実装されますが、システムが予測/ロールバックモデルに準拠するには以下のいくつかの制限が適用されます。
- ステートレスである(Frameクラスのインスタンスであるゲームプレイデータは、Quantumのシミュレーターによってパラメーターとしてすべてのシステムアップデートに渡されます)。
- 確定的なライブラリとアルゴリズムのみを実装または使用している(固定小数点演算、ベクトル演算、物理学、乱数生成、経路探索などのライブラリを提供しています)。
- Quantum名前空間に常駐している。
継承できる基本システムクラスは3つあります。
- SystemMainThread: 単純なゲームプレイの実装(initおよびupdateコールバック+シグナル)。
- SystemSignalsOnly: シグナルを実装するだけのアップデート不要のシステム(タスクをスケジュールしないことでオーバーヘッドを削減)。
- SystemBase: 高度な使用のみ。タスクグラフへの並列ジョブのスケジュール(この基本的なマニュアルでは説明していません)。
コアシステム
デフォルトでSystemSetupにQuantum SDK のすべての_Core_システムが含まれています。
- Core.CullingSystem2D(): 予測されたフレーム内で、- Transform2Dコンポーネントを持つエンティティをカリングします。
- Core.CullingSystem3D(): 予測されたフレーム内で、- Transform3Dコンポーネントを持つエンティティをカリングします。
- Core.PhysicsSystem2D():- Transform2Dと- PhysicsCollider2Dコンポーネントを持つすべてのエンティティに対して物理演算を実行します。
- Core.PhysicsSystem3D():- Transform3Dと- PhysicsCollider3Dコンポーネントを持つ全てのエンティティに対して、物理演算を実行します。
- Core.NavigationSystem(): 全てのNavMesh関連コンポーネントに使用されます。
- Core.EntityPrototypeSystem():- EntityPrototypeの作成、マテリアライズ、初期化を行います。
- Core.PlayerConnectedSystem():- ISignalOnPlayerConnectedと- ISignalOnPlayerDisconnectedのシグナルをトリガーするために使用されます。
- Core.DebugCommand.CreateSystem(): ステートインスペクタにより、データの送信を送信してその場でエンティティをインスタンス化、削除し、変更するのに使用されます (Editorでのみ使用可能)。
ユーザーの利便性を考慮して、すべてのシステムがデフォルトで含まれています。コアシステムは、ゲームに必要な機能に応じて選択的に追加/削除することができます。例えば、ゲームが2Dなのか3Dなのかに応じて、PhysicsSystem2DやPhysicsSystem3Dだけを残すことができます。
基本システム
Quantumの最も基本的なシステムは、 SystemMainThreadから継承するC#クラスです。
スケルトンの実装では、少なくともUpdateコールバックを定義する必要があります。
C#
namespace Quantum
{
  public unsafe class MySystem : SystemMainThread
  {
    public override void Update(Frame f)
    {
    }
  }
}
Systemクラスでオーバーライドできるコールバックは次のとおりです:
- OnInit(Frame f): ゲームプレイが初期化されているときに1回だけ呼び出されます(ゲームコントロールデータをセットアップするのに適した場所など)。
- Update(Frame f): ゲームの状態(ゲームループのエントリポイント)を進めるために使用されます。
- OnDisabled(Frame f)と- OnEnabled(Frame f): システムが別のシステムによって無効化/有効化されたときに呼び出されます。
使用可能なすべてのコールバックに同じパラメーター(Frameのインスタンス)が含まれていることに注意してください。Frameクラスは、エンティティ、物理、ナビゲーション、および不変のアセットオブジェクト(別の章で説明)等を含む、すべての一時的および静的なゲーム状態データのコンテナーです。
これは、Quantumの予測/ロールバックモデルに準拠するために、システムがステートレスである必要があるためです。Quantumは、すべての(変更可能な)ゲーム状態データがFrameインスタンスに完全に含まれている場合にのみ確定性を保証します。
読み取り専用の定数またはプライベートメソッド(すべての必要なデータをパラメーターとして受け取るでき)を作成すると有効です。
次のコードスニペットは、システムで有効および無効(ステートレス要件に違反)のいくつかの基本的な例を示しています。
C#
namespace Quantum
{
  public unsafe class MySystem : SystemMainThread
  {
    // this is ok
    private const int _readOnlyData = 10;
    // this is NOT ok (this data will not be rolled back, so it would lead to instant drifts between game clients during rollbacks)
    private int _transientData = 10;
    public override void Update(Frame f)
    {
        // ok to use a constant to compute something here
        var temporaryData = _readOnlyData + 5;
        // NOT ok to modify transient data that lives outside of the Frame object:
        _transientData = 5;
    }
  }
}
システム設定
具体的なシステムクラスは、ゲームプレイの初期化中にQuantumのシミュレーターに注入する必要があります。
これは SystemSetup.csファイルで行われます:
C#
namespace Quantum
{
  public static class SystemSetup
  {
    public static SystemBase[] CreateSystems(RuntimeConfig gameConfig, SimulationConfig simulationConfig)
    {
      return new SystemBase[]
      {
        // pre-defined core systems
        new Core.PhysicsSystem(),
        new Core.NavMeshAgentSystem(),
        new Core.EntityPrototypeSystem(),
        // user systems go here
        new MySystem(),
      };
    }
  }
}
Quantumにはいくつかのビルド済みシステムが含まれています(物理エンジンの更新、navmesh、およびエンティティプロトタイプのインスタンス化のエントリポイント)。
確定性を保証するために、システムが挿入される順序は、すべてのコールバックがすべてのクライアントのシミュレーターによって実行される順序になります。したがって、更新が発生する順序を制御するには、カスタムシステムを目的の順序で挿入するだけです。
システムのアクティブ化と非アクティブ化
注入されたすべてのシステムはデフォルトで有効になっていますが、シミュレーションの任意の場所からこれらの汎用関数を呼び出すことにより、実行時にそれらのステータスを制御することができます(Frameオブジェクトで使用できます)。
C#
public override void OnInit(Frame f)
{
  // deactivates MySystem, so no updates (or signals) are called in it
  f.SystemDisable<MySystem>();
  // (re)activates MySystem
  f.SystemEnable<MySystem>();
  // possible to query if a System is currently enabled
  var enabled = f.SystemIsEnabled<MySystem>();
}
どのシステムでも別のシステムを非アクティブ化(および再アクティブ化)できるため、1つの一般的なパターンは、シンプルなステートマシンを使用してより専門的なシステムのアクティブ/非アクティブライフサイクルを管理するメインコントローラーシステムを持つことです(例:ゲーム内に最初にロビーを設置、ゲームプレイへのカウントダウンを行い、次に通常のゲームプレイ、そして最後にスコアの状態)。
デフォルトでシステム起動を無効化するには、このプロパティを上書きします:
C#
public override bool StartEnabled => false;
特別なシステムタイプ
ほとんどのシステムではデフォルトの SystemMainThreadタイプを使用する可能性が高いですが、Quantumは特殊なシステム用にいくつかの代替オプションを提供しています。
| システム | 説明 | 
|---|---|
| SystemMainThread | 最も一般的なシステムタイプ。 通常の機能をすべて備えた通常のUpdate()を実装します。 | 
| SystemSignalsOnly | Update()関数がありません。これは、他のシステムからの信号の実装と受信にのみ焦点を当てたシステムを対象としています。更新ループを回避することで、オーバーヘッドを節約できます | 
| SystemMainThreadFilter | このタイプのシステムは、T型のFilterStructを使用して、それに基づいてエンティティのセットをフィルタリングし、それらをループしてメソッドを呼び出します。より複雑な方法が必要な場合は、SystemMainThreadを継承し、自分でフィルタを繰り返し実行することをお勧めします(FilterStructsとFilterの詳細については、コンポーネントのページを参照してください)。。 | 
システムグループ
システムはグループとしてセットアップし、処理することができます。
一つ目のステップは、SystemMainThreadGroupから継承したクラスを作成することです。
C#
namespace Quantum
{
  public class MySystemGroup : SystemMainThreadGroup
  {
    public MySystemGroup(string update, params SystemMainThread[] children) : base(update, children)
    {
    }
  }
}
The MySystemGroupシステムは、SystemSetup.csでシステムをまとめるために使用できるようになりました。システムグループは通常のシステムと混ぜたりマッチさせたりして使用することができます。
C#
namespace Quantum {
  public static class SystemSetup {
    public static SystemBase[] CreateSystems(RuntimeConfig gameConfig, SimulationConfig simulationConfig) {
      return new SystemBase[] {
        new MyRegularSystem(),
        new MySystemGroup("Gameplay Systems", new MyMovementSystem(), new MyOrbitScanSystem()),
      };
    }
  }
}
これにより、コード1行で一連のシステムの有効化・無効化を行うことができます。システムグループを有効化・無効化すると、そのグループを構成するすべてのシステムを有効化・無効化することになります。注意: Frame.SystemEnable<T>()メソッドおよびFrame.SystemDisable<T>()メソッドはシステムをタイプ別で特定します。そのため、複数のシステムが存在すべき場合は、複数のシステムグループをそれぞれ有効化・無効化できるように各自にこれらのメソッドを実装する必要があります。
エンティティライフサイクルAPI
このセクションでは、エンティティの作成と構成に直接APIメソッドを使用します。 データ駆動型アプローチについては、エンティティプロトタイプの章を参照してください。
新しいエンティティインスタンスを作成するには、次のコードを使用します(メソッドはEntityRefを返します)。
C#
var e = frame.Create();
エンティティには事前定義されたコンポーネントはありません。Transform3DとPhysicsCollider3Dをこのエンティティに追加するには、次のように入力します。
C#
var t = Transform3D.Create();
frame.Set(e, t);
var c =  PhysicsCollider3D.Create(f, Shape3D.CreateSphere(1));
frame.Set(e, c);
これらの2つの方法も役立ちます。
C#
// destroys the entity, including any component that was added to it.
frame.Destroy(e);
// checks if an EntityRef is still valid (good for when you store it as a reference inside other components):
if (frame.Exists(e)) {
  // safe to do stuff, Get/Set components, etc
}
エンティティに特定のコンポーネントタイプが含まれているかどうかを動的にチェックし、コンポーネントデータへのポインタをフレームから直接取得することもできます。
C#
if (frame.Has<Transform3D>(e)) {
    var t = frame.Unsafe.GetPointer<Transform3D>(e);
}
ComponentSetを使用すると、エンティティに複数のコンポーネントがあるかどうかを1回チェックできます。
C#
var components = ComponentSet.Create<CharacterController3D, PhysicsBody3D>();
if (frame.Has(e, components)) {
  // do something
}
次の通り、コンポーネントを動的に削除するのは簡単です:
C#
frame.Remove<Transform3D>(e);
EntityRef タイプ
Quantumのロールバックモデルは、可変サイズのフレームバッファーを維持します。つまり、ゲームの状態データ(DSLから定義)のいくつかのコピーが、別々の場所にあるメモリブロックに保持されます。エンティティ、コンポーネント、または構造体へのポインターは、単一のFrameオブジェクト内でのみ有効です(更新など)。
エンティティ参照は、問題のエンティティがまだ存在している限り、フレーム全体で機能するエンティティへの安全な参照(一時的にポインタを置き換える)です。エンティティ参照には、次のデータが含まれています。
- エンティティインデックス:エンティティスロット。特定のタイプのDSL定義の最大数から。
- エンティティのバージョン番号:エンティティインスタンスが破棄され、スロットを新しいインスタンスに再利用できる場合に、古いエンティティ参照を廃止するために使用されます。
フィルター
Quantum v2にはエンティティタイプはありません。 スパースセットECSメモリモデルでは、エンティティはコンポーネントのコレクションへのインデックスです。 EntityRef タイプは、バージョン管理などの追加情報を保持します。 これらのコレクションは、動的に割り当てられたスパースセットに保持されます。
したがって、エンティティーのコレクションを反復処理する代わりに、フィルターを使用して、システムが機能する一連のコンポーネントを作成します。
C#
public unsafe class MySystem : SystemMainThread
{
    public override void Update(Frame f)
    {
        var filtered = frame.Filter<Transform3D, PhysicsBody3D>();
        while (filtered.Next(out var e, out var t, out var b)) {
          t.Position += FPVector3.Forward * frame.DeltaTime;
          frame.Set(e, t);
        }
    }
}
フィルターの使用方法に関しては、Components ページを参照してください。
ビルド済みのアセットと構成クラス
Quantumには、常にFrameオブジェクトを介してシステムに渡される、事前に構築されたいくつかのデータアセットが含まれています。
これらが最も重要なビルド済みのアセットオブジェクトです(QuantumのAsset DBからの)。
- Mapと- NavMesh: プレイ可能領域、静的物理コライダー、ナビゲーションメッシュなどに関するデータ。 カスタムプレーヤーデータは、データアセットスロットから追加できます(データアセットの章で説明します)。
- SimulationConfig: 物理エンジン、navmeshシステムなどの一般的な構成データ
- デフォルト PhysicsMaterialとagent configs(KCC, navmesh, 等):
次のスニペットは、Frameオブジェクトから現在のMapおよびNavMeshインスタンスにアクセスする方法を示しています。
C#
// Map is the container for several static data, such as navmeshes, etc
Map map = f.Map;
var navmesh = map.NavMeshes["MyNavmesh"];
アセットデータベース
すべてのクアンタムデータアセットは、動的アセットデータベースAPIを介してシステム内で利用できます。次のスニペット(DSLとシステムのC#コード)は、データベースからデータからアセットを取得し、それをasset_refスロットに割り当ててキャラクターに割り当てる方法を示しています。最初に、アセットをqtnファイルで宣言し、それを保持できるコンポーネントを作成します。
C#
asset CharacterSpec;
component CharacterData
{
    asset_ref<CharacterSpec> Spec;
    // other data
}
参照を保持するアセットとコンポーネントが宣言されたら、次のようにシステムで参照を設定できます:
C#
// C# code from inside a System
// grabing the data asset from the database, using the unique GUID (long) or path (string)
var spec = frame.FindAsset<CharacterData>("path-to-spec");
// assigning the asset reference assuming you have a pointer to CharacterData component
data->Spec = spec;
データアセットについては、それぞれの章で詳細に説明しています(Unityのスクリプト可能なオブジェクト(デフォルト、カスタムシリアライザーまたは手順で生成されたコンテンツ)を介してデータアセットを設定する方法のオプションを含む)。
シグナル
前の章で説明したように、シグナルは、システム間通信用のパブリッシャー/サブスクライバーAPIを生成するために使用される関数シグニチャーです。
次の例は、DSLファイルです(前の章の)。
C#
signal OnDamage(FP damage, entity_ref entity);
このトリガー信号はFrameクラス(f変数)で生成され、「パブリッシャー」システムから呼び出すことができます。
C#
// any System can trigger the generated signal, not leading to coupling with a specific implementation
f.Signals.OnDamage(10, entity)
「サブスクライバー」システムは、次のように生成された「ISignalOnDamage」インターフェースを実装します。
C#
namespace Quantum
{
  class CallbacksSystem : SystemSignalsOnly, ISignalOnDamage
  {
    public void OnDamage(Frame f, FP damage, EntityRef entity)
    {
      // this will be called everytime any other system calls the OnDamage signal
    }
  }
}
シグナルには常に最初のパラメーターとしてFrameオブジェクトが含まれます。これは通常、ゲームの状態に役立つ処理を行うために必要です。
生成および事前構築されたシグナル
DSLで直接定義された明示的なシグナルに加えて、Quantumには、事前に作成された(「生の」物理衝突コールバックなど)およびエンティティ定義に基づいて生成されたもの(エンティティタイプ固有の作成/破棄コールバック)も含まれています。
衝突コールバックシグナルついては、物理エンジンに関する特定の章で説明します。ここでは他の事前作成されたシグナルについて簡単に説明します。
- ISignalOnPlayerDataSet: ゲームクライアントがRuntimePlayerのインスタンスをサーバーに送信すると呼び出されます(データは1ティックに確認/アタッチされます)。
- ISignalOnAdd<T>,- ISignalOnRemove<T>: コンポーネントタイプTがエンティティに追加/エンティティから削除されるときに呼び出されます。
イベントのトリガー
シグナルの場合と同様に、イベントをトリガーするための入り口はFrameオブジェクトであり、各(具体的な)イベントは特定の生成された関数(イベントデータをパラメーターとして)になります。
C#
// taking this DSL event definition as a basis
event TriggerSound
{
    FPVector2 Position;
    FP Volume;
}
これをシステムから呼び出して、このイベントのインスタンスをトリガーできます(Unityからの処理については、ブートストラッププロジェクトに関する章で説明します)。
C#
// any System can trigger the generated events (FP._0_5 means fixed point value for 0.5)
f.Events.TriggerSound(FPVector2.Zero, FP._0_5);
イベントは、ゲームプレイ自体の実装に使用してはなりません(Unity側のコールバックは確定的ではないため)。 イベントは、詳細なゲーム状態の更新のレンダリングエンジンと通信するための一方向なAPIで、ビジュアル、サウンド、UI関連のオブジェクトをUnityで更新します。
追加フレームAPIアイテム
Frameクラスには、APIの他のいくつかの確定的な部分のエントリポイントも含まれています。これらは一時的なデータとして処理する必要があります(そのため、必要に応じてロールバックされます)。
次のスニペットは、最も重要なものを示しています。
C#
// RNG is a pointer.
// Next gives a random FP between 0 and 1.
// There are also bound options for both FP and int
f.RNG->Next();
// any property defined in the global {} scope in the DSL files is accessed through the Global pointer
var d = f.Global->DeltaTime;
// input from a player is referenced by its index (i is a pointer to the DSL defined Input struct)
var i = f.GetPlayerInput(0);