This document is about: QUANTUM 3
SWITCH TO

システム (ゲームロジック)

はじめに

システムは、Quantum におけるすべてのゲームプレイロジックの入り口です。

システムは通常の C# クラスとして実装されていますが、予測/ロールバックモデルに準拠するためのいくつかの制限があります。システムは以下の条件を満たす必要があります:

  • ステートレスであること:システム内で変更可能なフィールドを宣言してはいけません。すべての変更可能なゲームデータは .qtn ファイルに宣言され、それが Frame クラス内のロールバック可能なゲーム状態の一部になります;
  • 決定論的ライブラリとアルゴリズムのみを実装および使用すること(Quantum には、固定小数点算術、ベクトル算術、物理学、ランダム数生成、経路探索などのライブラリが付属しています)。

継承できるいくつかの基本システムクラスがあります:

  • SystemMainThreadOnInit および Update コールバックを持っています。Update はシステムごとに一度実行され、エンティティやそのコンポーネントを反復処理する必要がある場合、ユーザーは独自のフィルタを作成しなければなりません。また、Quantum シグナルにサブスクライブし、反応するためにも使用できます;
  • SystemMainThreadFilter<Filter>SystemMainThread と同様に機能しますが、コンポーネントレイアウトを定義するフィルタを受け取り、Update はフィルタで定義されたすべてのコンポーネントを持つエンティティごとに一度呼ばれます;
  • SystemSignalsOnlyUpdate コールバックを提供せず、通常は Quantum シグナルに反応するためだけに使用されます。タスクスケジューリングがないため、オーバーヘッドが減少します;
  • SystemBase:高度な使用のみで、タスクグラフへの並列ジョブをスケジュールするためのもの(この基本マニュアルではカバーされていません)。

コアシステム

Quantum SDK は、デフォルトの SystemsConfig にすべての Core システムを含んでいます。

  • Core.CullingSystem2D():予測フレーム内で Transform2D コンポーネントを持つエンティティをカリングします。
  • Core.CullingSystem3D():予測フレーム内で Transform3D コンポーネントを持つエンティティをカリングします。
  • Core.PhysicsSystem2D()Transform2D および PhysicsCollider2D コンポーネントを持つすべてのエンティティに対して物理を実行します。
  • Core.PhysicsSystem3D()Transform3D および PhysicsCollider3D コンポーネントを持つすべてのエンティティに対して物理を実行します。
  • Core.NavigationSystem():すべての NavMesh 関連コンポーネントに使用されます。
  • Core.EntityPrototypeSystem()EntityPrototypes を作成、具現化、および初期化します。
  • Core.PlayerConnectedSystem()ISignalOnPlayerConnected および ISignalOnPlayerDisconnected シグナルをトリガーするために使用されます。
  • Core.DebugCommand.CreateSystem():状態インスペクターによって、エンティティをインスタンス化/削除/修正するためのデータを動的に送信するために使用されます(エディタ内でのみ利用可能!)。

すべてのシステムは、ユーザーの利便性のためにデフォルトで含まれています。コアシステムは、ゲームの必要な機能に基づいて選択的に追加/削除できます。例えば、ゲームに必要に応じて PhysicsSystem2D または PhysicsSystem3D のみを保持することができます。

基本システム

Unity では、右クリックメニューからテンプレートを使用して Quantum システムを作成することができます。

System Templates

生成される対応するコードスニペットは次のとおりです:

システム

C#

namespace Quantum {
  using Photon.Deterministic;
  using UnityEngine.Scripting;

  [Preserve]
  public unsafe class NewQuantumSystem : SystemMainThread {
    public override void Update(Frame f) {
    }
  }
}

オーバーライド可能な API:

  • OnInit(Frame f)
  • Update(Frame f)
  • OnDisabled(Frame f)/OnEnabled(Frame f)
  • StartEnabled;

システムフィルタ

C#

namespace Quantum {
  using Photon.Deterministic;
  using UnityEngine.Scripting;

  [Preserve]
  public unsafe class NewQuantumSystem : SystemMainThreadFilter<NewQuantumSystem.Filter> {
    public override void Update(Frame f, ref Filter filter) {
    }

    public struct Filter {
      public EntityRef Entity;
    }
  }
}

オーバーライド可能な API は SystemMainThread と同じですが、さらに以下があります:

  • Any
  • Without

システムシグナル専用

C#

namespace Quantum {
  using Photon.Deterministic;
  using UnityEngine.Scripting;

  [Preserve]
  public unsafe class NewQuantumSystem : SystemSignalsOnly {
  }
}

オーバーライド可能な API は SystemMainThread と同じですが、Update は含まれていません。

システムクラスでオーバーライドできる主なコールバックには次のものがあります:

  • OnInit:ゲームスタート時に一度だけ実行される。初期ゲームデータのセットアップに一般的に使用されます;
  • Update:ゲーム状態を進めるために使用されます;
  • OnDisabled(Frame f) および OnEnabled(Frame f):システムが直接無効化/有効化されたときや、親システムの状態が切り替わったときに呼び出されます;
  • UseCulling:システムがカリングされたエンティティを除外すべきかどうかを定義します。

PS: すべての Quantum システムは、属性 [UnityEngine.Scripting.Preserve] を使用することが義務付けられています。

使用可能なすべてのコールバックには Frame のインスタンスが含まれていることに注意してください。Frame クラスは、エンティティ、物理、ナビゲーション、そして不変のアセットオブジェクトを含むすべての変更可能および静的なゲーム状態データのコンテナです(不変のアセットオブジェクトについては別の章で説明されます)。

これを行う理由は、システムが Quantum の予測/ロールバックモデルに準拠するために ステートレス である必要があるからです。Quantum は、すべての(変更可能な)ゲーム状態データが、Frame インスタンスに完全に含まれている場合にのみ決定性を保証します。

読み取り専用の定数や、必要なデータをパラメータとして受け取るプライベートメソッドを作成することは有効です。

以下のコードスニペットは、システム内の有効な例と無効な例(ステートレス要件に違反している)の基本的な例を示しています。

C#

namespace Quantum
{

  public unsafe class MySystem : SystemMainThread
  {
    // これは OK
    private const int _readOnlyData = 10;
    // これは OK ではない(このデータはロールバックされないため、ロールバック中にゲームクライアント間で即座にずれが生じることになります)
    private int _mutableData = 10;

    public override void Update(Frame f)
    {
        // ここで定数を使用して何かを計算するのは OK
        var temporaryData = _readOnlyData + 5;

        // Frame オブジェクトの外部に存在する一時的データを修正するのは OK ではありません:
        _transientData = 5;
    }
  }
}

SystemsConfig

Quantum 3 では、システム構成の処理方法が変更されました。構成をコードに直接埋め込むのではなく、SystemsConfig というアセット内にカプセル化されます。

この設定は RuntimeConfig に渡され、Quantum は要求されたシステムを自動的にインスタンス化します。

Quantum には、いくつかの事前構築されたシステム(物理エンジンの更新、ナビメッシュ、エンティティプロトタイプのインスタンス化の入口)が含まれています。

決定性を保証するために、システムが挿入される順序は、シミュレーターがすべてのクライアントでコールバックを実行する順序と同じになります。したがって、アップデートが発生する順序を制御するには、カスタムシステムを望ましい順序で挿入します。

新しい SystemsConfig の作成

SystemsConfig は通常の Quantum アセットです。つまり、プロジェクトウィンドウで右クリックして -> Quantum -> SystemsConfig を選択することで新しいものを作成できます。

アセットにはシステムのシリアライズされたリストがあります。これは通常の Unity リストのように操作できます。

Systems Config

システムの有効化と無効化

すべての注入されたシステムはデフォルトで有効ですが、これらの汎用関数をシミュレーション内の任意の場所から呼び出すことで、ランタイムでそのステータスを制御できます(これらは Frame オブジェクト内で利用可能です):

C#

public override void OnInit(Frame f)
{
  // MySystem を無効化するので、更新(またはシグナル)は呼ばれない
  f.SystemDisable<MySystem>();
  // MySystem を(再)有効化する
  f.SystemEnable<MySystem>();
  // システムが現在有効かどうかを確認する
  var enabled = f.SystemIsEnabled<MySystem>();
}

任意のシステムは他のシステムを無効化(および再有効化)できます。そのため、一般的なパターンとして、より専門的なシステムのアクティブ/非アクティブライフサイクルを簡単な状態マシンを用いて管理するメインコントローラーシステムを持つことがあります(例えば、ゲームプレイの前にカウントダウンを持つゲーム内ロビーがあり、その後通常のゲームプレイ、最後にスコア状態が続くなど)。

デフォルトで無効の状態でシステムを開始させるには、このプロパティをオーバーライドします:

C#

public override bool StartEnabled => false;

システムグループ

システムはグループとして設定および処理できます。

最初のステップは、SystemMainThreadGroup を継承するクラスを作成することです。

C#

namespace Quantum
{
  public class MySystemGroup : SystemMainThreadGroup
  {
    public MySystemGroup(string update, params SystemMainThread[] children) : base(update, children)
    {
    }
  }
}

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#

// エンティティを破棄し、それに追加されたすべてのコンポーネントを含む。
frame.Destroy(e);

// EntityRef がまだ有効かどうかを確認する(他のコンポーネント内に参照として保存する際に便利):
if (frame.Exists(e)) {
  // 安全に処理を実行し、コンポーネントの取得/設定などを行う
}

特定のコンポーネントタイプがエンティティに含まれているかどうかを動的に確認し、フレームから直接コンポーネントデータへのポインタを取得することも可能です:

C#

if (frame.Has<Transform3D>(e)) {
    var t = frame.Unsafe.GetPointer<Transform3D>(e);
}

ComponentSet を使用すると、エンティティが複数のコンポーネントを持っているかどうかを一度に確認できます:

C#

var components = ComponentSet.Create<CharacterController3D, PhysicsBody3D>();
if (frame.Has(e, components)) {
  // 何かを実行
}

コンポーネントを動的に削除するのも簡単です:

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 には、常にフレームを介してシステムに渡されるいくつかの事前構築されたデータアセットが含まれています。

これらは、Quantum のアセット DB からの最も重要なプリビルトアセットオブジェクトです:

  • MapNavMesh:プレイ可能エリアに関するデータ、静的物理コライダー、ナビゲーションメッシュなど。カスタムプレイヤーデータは、データアセットスロットから追加できます(データアセットに関する章で説明します);
  • SimulationConfig:物理エンジン、ナビメッシュシステムなどの一般的な構成データ。
  • デフォルトの PhysicsMaterialagent configs(KCC、ナビメッシュなど):

次のスニペットは、フレームオブジェクトから現在の Map と NavMesh インスタンスにアクセスする方法を示しています:

C#

// Map はナビメッシュなどの静的データのコンテナです
Map map = f.Map;
var navmesh = map.NavMeshes["MyNavmesh"];

アセットデータベース

すべての Quantum データアセットは、システム内で動的アセットデータベース API を介して利用可能です。以下のスニペット(DSL から始まり、システム内の C# コード)は、データベースからデータアセットを取得し、Character 内の asset_ref スロットに割り当てる方法を示しています。まず、qtn ファイルでアセットを宣言し、それを保持できるコンポーネントを作成します:

C#

asset CharacterSpec;

component CharacterData
{
    asset_ref<CharacterSpec> Spec;
    // その他のデータ
}

アセットとそれに対する参照を保持するコンポーネントが宣言されると、次のようにシステム内で参照を設定できます:

C#

// システム内の C# コード

// ユニーク GUID(long)またはパス(string)を使用してデータアセットをデータベースから取得
var spec = frame.FindAsset<CharacterData>("path-to-spec");
// CharacterData コンポーネントへのポインタがあると仮定してアセット参照を割り当てる
data->Spec = spec;

データアセットについては、デフォルトで Unity スクリプタブルオブジェクト、カスタムシリアライザー、または手続き的に生成されたコンテンツを使用してポピュレートするオプションを含む詳細が自分の章で説明されています。

シグナル

前の章で説明したように、シグナルはインターシステム通信用のパブリッシャー/サブスクライバー API を生成するための関数シグネチャです。

次の例はDSLファイル内のものです(前の章から):

C#

signal OnDamage(FP damage, entity_ref entity);

これは、フレームクラス(f 変数)上でこのトリガーシグナルが生成され、"パブリッシャー" システムから呼び出すことができます:

C#

// どのシステムでも生成されたシグナルをトリガーでき、特定の実装との結合を招かない
f.Signals.OnDamage(10, entity)

"サブスクライバー" システムは、生成された "ISignalOnDamage" インターフェースを実装します。これを次のように記述できます:

C#

namespace Quantum
{
  class CallbacksSystem : SystemSignalsOnly, ISignalOnDamage
  {
    public void OnDamage(Frame f, FP damage, EntityRef entity)
    {
      // 他のシステムが OnDamage シグナルを呼び出すたびにこのメソッドが呼ばれます
    }
  }
}

シグナルには常にフレームオブジェクトが最初のパラメータとして含まれていることに注意してください。これは通常、ゲーム状態に対して有用な操作を行うために必要です。

生成されたシグナルとプリビルトシグナル

DSL で明示的に定義されたシグナルに加えて、Quantum には事前構築されたシグナル(たとえば、生の物理的衝突コールバック)や、エンティティ定義に基づいて生成されたシグナル(エンティティタイプ固有の作成/破棄コールバック)が含まれています。

衝突コールバックシグナルは、物理エンジンに関する特定の章でカバーされますので、他のプリビルトシグナルの簡単な説明を行います:

  • ISignalOnPlayerDataSet:ゲームクライアントが RuntimePlayer のインスタンスをサーバーに送信したときに呼び出され(データが確認されて1ティックにアタッチされます)。
  • ISignalOnAdd<T>, ISignalOnRemove<T>:コンポーネントタイプ T がエンティティに追加または削除されたときに呼び出されます。

イベントのトリガー

シグナルと同様に、イベントをトリガーするためのエントリーポイントはフレームオブジェクトであり、各(具体的な)イベントは特定の生成関数(イベントデータをパラメータとして持つ)を生成します。

C#

// このDSLイベント定義を基に
event TriggerSound
{
    FPVector2 Position;
    FP Volume;
}

これは、システムからこのイベントのインスタンスをトリガーするために呼び出すことができます(Unity 側の処理はブートストラッププロジェクトに関する章で説明します):

C#

// どのシステムでも生成されたイベントをトリガーできる(FP._0_5 は 0.5 の固定小数点値を意味する)
f.Events.TriggerSound(FPVector2.Zero, FP._0_5);

重要な点は、イベントはゲームプレイの実装を行うために使用すべきではなく(Unity 側のコールバックが決定的でないため)、イベントはレンダリングエンジンに詳細なゲーム状態の更新を通信するための一方向の細かい API であるため、ビジュアル、サウンド、UI 関連オブジェクトを Unity で更新するために使用されるべきです。

追加のフレーム API アイテム

Frame クラスには、トランジェントデータとして扱う必要がある他の決定論的 API のためのエントリポイントも含まれています(必要に応じてロールバックされる)。以下のスニペットは、最も重要なものを示しています:

C#

// RNG はポインタです。
// Next は 0 から 1 の間のランダムな FP を返します。
// FP と int の両方のバウンドオプションもあります
f.RNG->Next();

// DSL ファイルのグローバル {} スコープで定義されたプロパティは、グローバルポインタを介してアクセスされます
var d = f.Global->DeltaTime;

// プレイヤーからの入力は、そのインデックスによって参照されます(i は DSL で定義された Input 構造体へのポインタです)
var i = f.GetPlayerInput(0);

スケジューリングによる最適化

パフォーマンスのホットスポットとして特定されたシステムを最適化するために、単純なモジュロベースのエンティティスケジューリングが役立ちます。これを使用することにより、各ティックごとにエンティティのサブセットだけが更新されます。

C#

public override void Update(Frame frame) {
  foreach (var (entity, c) in f.GetComponentIterator<Component>()) {
    const int schedulePeriod = 5;
    if (frame.Number % entity.Index == frame.Number % schedulePeriod) {
      // このエンティティを更新する時間です
    }
}

schedulePeriod5 に設定すると、エンティティは5ティックごとにのみ更新されます。2 を選択すれば、毎回のティックで更新されます。

このようにすることで、更新の合計数が大幅に削減されます。すべてのエンティティを 一度 のティックで更新することを避けるために、entity.Index を追加することで、負荷が複数のフレームに分散されます。

このようにしてエンティティの更新を遅延させることには、ユーザーコードに対する要件があります:

  • 遅延更新コードは、異なるデルタタイムを処理できる必要があります。
  • エンティティの遅延 "応答性" は視覚的に顕著になる可能性があります。
  • entity.Index を使用すると、はるかに早くまたは遅く新しい情報が処理されるため、怠慢がさらに増す可能性があります。

Quantum ナビゲーションシステム には、この機能が組み込まれています。

Back to top