Quantum システム(ゲームロジック)
Systemは、Quantumのすべてのゲームプレイロジックのエントリーポイントです。
これらは、通常のC#クラスとして実装されていますが、システムが予測/ロールバックモデルに準拠するためのいくつかの制限が適用されます。
- ステートレスでなければなりません(ゲームプレイデータはすべてのゲームループコールバックにQuantumのシミュレータによってパラメータとして渡されます)。
- C#ポインタを使用する(したがって、システムはunsafeキーワードを使用する必要があります)。
- 確定的なライブラリとアルゴリズムのみを実装し、使用します(固定小数点演算、ベクター演算、物理、乱数生成、パス検索などのライブラリを提供します)。
- Quantum名前空間に存在する。
基本のシステム
Quantumの基本のシステムは、 "SystemBase"を継承するC#クラスです。
スケルトンの実装では、少なくともUpdateコールバックを定義する必要があります。
C#
namespace Quantum
{
public unsafe class MySystem : SystemBase
{
public override void Update(Frame f)
{
}
}
}
これらは、システムクラスでオーバーライドできるコールバックです。
- OnInit(Frame f):ゲームプレイが初期化されているとき(初期エンティティの作成、ゲームデータの設定などに適した場所)、一度だけ呼び出されます。
- Update(Frame f):ゲームの状態を前進させるために使用される(ゲームループエントリーポイント)。
- OnDisabled(Frame f)およびOnEnabled(Frame f):システムが別のシステムによって無効/有効にされたときに呼び出されます(これらについては次の章で説明します)。
利用可能なすべてのコールバックには同じパラメータ(Frame)が含まれます。Frameクラスは、生成されたエンティティ、物理などの生成されたAPI(それぞれは別に説明されます)を含む、すべての一時的および静的なゲーム状態データのコンテナです。
この理由は、システムがQuantumの予測/ロールバックモデルに準拠するためにはステートレスでなければならないためです。
Quantumは、すべてのゲーム状態データが(DSL定義から生成された)Frameクラスに完全に含まれている場合にのみ、決定論を保証します。
読み取り専用の定数やプライベートメソッド(すべての必要なデータをパラメータとして受け取る必要がある)を作成することは有効です。
次のコードスニペットは、システム内の有効及び有効でない(ステートレス要件に違反する)基本的な例を示しています。
C#
namespace Quantum
{
public unsafe class MySystem : SystemBase
{
// 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;
}
}
}
システムの設定
具体的なSystemクラスをゲームプレイの初期化中にQuantumのシミュレータに注入する必要があります。
これは、quantum.systemsプロジェクトに含まれている "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.PhysicsSystemPre(),
// user systems go here
new MySystem(),
// pre-defined core systems
new Core.AnimatorSystem(),
};
}
}
}
Quantumにはいくつかの事前構築されたシステム(物理エンジンの更新と確定的なアニメータ・マネージャのエントリーポイント)が含まれています。
決定論を保証するためには、システムが挿入される順序と、すべてのコールバックがすべてのクライアントのシミュレータによって実行される順序が同じでないといけません。
したがって、更新が行われる順序を制御するには、カスタムシステムを必要な順序で挿入します。
システムの有効化と無効化
注入されたすべてのシステムはデフォルトで有効ですが、シミュレーションの任意の場所からこれらの汎用関数を呼び出すことで、実行時の状態を制御することができます(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>();
}
どのシステムでも、別のシステムを停止(および再起動)することができます。そのため、単純な状態マシンを使用して、より特殊化されたシステムの有効/無効なライフサイクルを管理するメインコントローラシステムのパターンを使用することが基本的な方法です(例:最初にゲーム内ロビーを用意し、ゲームプレイ、通常のゲームプレイ、最後にスコア状態にカウントダウンします)。
エンティティ生成API
システムはQuantumゲームにおけるゲームプレイロジックのエントリーポイントなので、実用的なものはエンティティのライフサイクルを中心とします:作成、更新、破壊。
エンティティタイプがDSLファイルで定義されると、インスタンスライフサイクルを管理するために必要なすべてのAPI呼び出しが自動的に生成されます。
そのため、前章(DSL)で取り上げたトピックに基づいて、次の「文字」エンティティを例に挙げてみましょう。
C#
// this goes in a Quantum DSL file
entity Character[8]
{
use Transform2D;
use DynamicBody;
}
Frameクラスでは、次の関数を使用できます。
C#
// finds the next free Character slot (from 8 pre-allocated ones) and returns a pointer to it.
Returns null if there are no free slots;
var c = f.CreateCharacter();
// entity refs (explained in the next section) are rollbackable and safe to store across frame both in the game state and from Unity
var reference = c->EntityRef;
// entity references can be used to retrive back the pointer from the Frame object (returns null if ref is obsolete/invalid)
var ca = f.GetCharacter(reference);
// optional check to avoid the null return:
var exists = f.CharacterExists(reference);
// to destroy an entity (also making all refs to it obsolete), one can use either the pointer or a ref.
// This frees up a Character entity slow as well:
f.DestroyCharacter(c);
f.DestroyCharacter(reference);
EntityRefタイプ
Quantumのロールバックモデルは、可変サイズのフレームバッファを維持することを意味します。つまり、DSLから定義されたゲーム状態データの複数のコピーが別々の場所にあるメモリブロックに保持されます。
エンティティ、コンポーネント、または構造体へのポインタは、単一のFrameオブジェクト(更新など)内でのみ有効です。
エンティティの参照は、対象のエンティティが存在する限り、フレームをまたいで動作するエンティティ(一時的にポインタを置き換える)への安全な参照を保持します。
エンティティの参照には以下のデータが内部的に含まれます:
- Entity Type:DSL定義タイプから
- Entity Index:特定のタイプのDSL定義の最大数からのエンティティスロット。
- Entity version number:エンティティインスタンスが破棄され、新しいインスタンスのためにスロットを再利用できるときに古いエンティティの参照を無効にするために使用されます。
エンティティデータの更新
特定のエンティティタイプのすべての有効な(作成された)インスタンスにアクセスするには、生成されたイテレータアクセサを使用する必要があります。
C#
// this returns an iterator with pointers to all currently active Character entities
var all = f.GetAllCharacters();
// looping through the iterator
while (all.Next())
{
// retrieving the Character pointer
var c = all.Current;
// Updating data in the entity:
c->Transform2D.Position += FPVector2.Up * f.DeltaTime;
}
コンポーネントの直接更新
ゲームプレイの更新に関する興味深いアプローチとして、エンティティの代わりにコンポーネントタイプを通じてゲームの状態をトラバースする機能があります。
これは、コンポーネントをどのエンティティタイプが使用しているかを知らなくても、システムがデータを更新する可能性のある場所でコンポーネント再利用の余地を生み出します(この例では、有効なエンティティのすべてのDynamicBodyコンポーネントに力を加えます):
C#
// acquiring the component buffer based on the component type (generated function).
Buffers are pooled, so the using keyword must be used to safely return them to the pool.
using (var bodies = f.GetAllDynamicBodies())
{
// a component buffer is traversed with a for loop
for (int i = 0; i < bodies.Count; i++)
{
// each entry includes the Component pointer (in this case DynamicBody)
var b = bodies[i];
// applying a force, no need to know which entity type is this
b.DynamicBody->AddForce(FPVector2.Up / 2);
// entity pointer is also available if needed (type information can be queried, etc)
var e = b.Entity;
}
}
データ駆動型アセット
データ駆動型アプローチは、ゲームを実装する際に非常に有力な方法なので、Quantumではシミュレーションに読み取り専用データを注入するための非常に柔軟なアセット・リンキング・システムを備えています(実行時の初期化またはパラメータ化されたデータ用)。
データ駆動型アセットについては、別の章(リンク)で詳しく説明しますが、それらを使用するためのエントリポイントがシステムのため、いくつかの例がこちらに含まれています。
事前構築されたアセット
Quantumには、Frameオブジェクトを介してシステムに渡される、事前に構築されたいくつかのデータアセットが含まれています。
最も重要なものは次のとおりです:
- RuntimeConfig:"RuntimeConfig.User.cs"ファイルで拡張できる、一般的なゲーム構成データ(以下に説明するRuntimePlayersの配列を含む) 。
- RuntimePlayer:プレイヤー固有のランタイムデータ(たとえば、プレイヤーが選んだキャラクター仕様)を注入するのに適しています。
カスタムデータを"RuntimePlayer.User.cs"に追加することができます。 - Map:再生可能領域、静的物理コライダに関するデータ。
カスタムプレイヤーデータはデータアセットスロットから追加できます(データアセットの章で説明します)。
次のスニペットは、Frameオブジェクトからこのデータにアクセスする方法を示しています。
C#
RuntimeConfig config = f.RuntimeConfig;
// basic data in runtime config is the players array
for (int i = 0; i < config.Players.Length; i++)
{
RuntimePlayer p = config.Players[i];
}
// Map is the container for several static data, such as navmeshes, etc
Map map = f.Map;
var navmesh = map.NavMeshes["MyNavmesh"];
アセットデータベース
すべてのQuantumデータアセットは、 "DB"静的クラスを介してシステム内で使用できます。
次のスニペット(DSL、システムからのC#コード)は、データベースからデータアセットを取得し、それをasset_refスロットに割り当てる方法を示しています。
C#
// this goes in a Quantum DSL file
asset CharacterSpec;
entity Character[8]
{
use Transform2D;
use DynamicBody;
fields
{
asset_ref<CharacterSpec> Spec;
}
}
C#
// C# code from inside a System
// grabing the data asset from the database, using a unique string ID
var spec = DB.FindAsset<CharacterData>("spec-guid");
// assigning the asset reference to the first Character entity
f.GetAllCharacters().Current->Spec = spec;
データアセットは、別の章で詳細に説明されています(Unityスクリプト可能オブジェクト(デフォルト、カスタムシリアライザまたは手続き的に生成されたコンテンツ)を使用してデータを設定する方法に関するオプションを含む)。
シグナル
前の章で説明したように、シグナルはシステム間通信用のパブリッシャ/サブスクライバAPIを生成するために使用される関数シグネチャです。
DSLファイルを使用した例(前の章から):
C#
signal OnDamage(FP Damage);
"publisher"システムから呼び出すことができるFrameクラス(f変数)で生成されるこのトリガシグナルにつながります:
C#
// any System can trigger the generated signal, not leading to coupling with a specific implementation
f.Signals.OnDamage(10)
「サブスクライバ」システムは、生成された「ISignalOnDamage」インタフェースを実装します。このインタフェースは次のようになります。
C#
namespace Quantum.Systems.Example
{
class CallbacksSystem : SystemBase, ISignalOnDamage
{
public void OnDamage(Frame f, Damage dmg)
{
// this will be called everytime any other system calls the OnDamage signal
}
public override void Update(Frame f)
{
}
}
}
通知シグナルには、Frameオブジェクトが常に最初のパラメータとして含まれています。これは通常gamestateに何かを行うために必要です。
生成され、事前構築されたシグナル
Quantumには、DSLで直接定義された明示的なシグナルのほかに、事前定義されたシグナル(「ローの」物理的コリジョンコールバックなど)や、エンティティ定義に基づいて生成されるシグナル(エンティティタイプ固有の作成/破棄コールバック)が含まれています。
コリジョンコールバックシグナルについては物理エンジンに関する章で説明されるので、ここでは作成/破壊シグナルについて簡単に説明します。
- ISignalOnEntityCreated/ISignalOnEntityDestroyed:エンティティが作成/破棄されるたびに呼び出される既成のシグナル(パラメータはFrameと該当エンティティポインタです)。
フレームの更新が完了するまで(常にすべてのシステムの更新が呼び出されるまで)、Quantumでのエンティティの破棄は常に遅延されるため、破壊シグナルは特に便利です。 - ISignalOnENTITYTYPECreated/ISignalOnENTITYTYPEDestroyed(たとえば、ISignalOnCharacterCreated):上記と同様ですが、型固有のポインタを渡します。
イベントのトリガー
シグナルと同様に、トリガイベントのエントリポイントは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);
重要:ゲームプレイ自体を実装するためにイベントを使用しないよう注意してください。
イベントは、詳細なゲーム状態の更新をレンダリングエンジンに伝え、ビジュアル、サウンド、UI関連のオブジェクトをUnity上で更新するために使用するAPIです。
その他のFrame 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);
Back to top