Systems (game logic)
概述
系統是Quantum中所有遊戲遊玩邏輯的入口。
他們作為正常C#類別而被執行,不過對於一個系統而言有一些限制,以符合預測/復原模型的要求:
- 必須是無狀態(遊戲遊玩資料——幀類別的一個執行個體——將作為一個參數,透過Quantum的模擬器被傳遞到每個系統更新);
- 執行和/或只使用確定性程式庫及演算法(我們提供針對固定點數學、向量數學、物理、隨機數字生成,及路徑尋找等等的程式庫);
- 位於Quantum命名空間之中;
可從三個基本系統類別來繼承:
SystemMainThread
:針對簡單的遊戲遊玩執行(初始及更新回調+信號)。SystemSignalsOnly
:無更新系統,只為了執行信號(透過不針對它來排程一個工作,來減少額外負荷)。SystemBase
:只用於進階使用,為了排程平行作業到工作圖表(在這份基本操作手冊中沒有包含)。
核心系統
Quantum SDK在SystemSetup
中預設包含所有 核心 系統。
Core.CullingSystem2D()
:在被預測的幀中挑出附有一個Transform2D
元件的實體。Core.CullingSystem3D()
:在被預測的幀中挑出附有一個Transform3D
元件的實體。Core.PhysicsSystem2D()
:在所有附有一個Transform2D
及一個PhysicsCollider2D
元件的實體上運行物理。Core.PhysicsSystem3D()
:在所有附有一個Transform3D
及一個PhysicsCollider3D
元件的實體上運行物理。Core.NavigationSystem()
:用於所有與導航網格相關的元件。Core.EntityPrototypeSystem()
:建立、具體化及初始化EntityPrototypes
。Core.PlayerConnectedSystem()
:用於觸發ISignalOnPlayerConnected
及ISignalOnPlayerDisconnected
信號。Core.DebugCommand.CreateSystem()
:被狀態偵測器使用,以發送資料來即時地具現化/移除/修正實體(只在編輯器中可用!)。
為了使用者的便利性,已預設包含所有系統。核心系統可基於遊戲所需要的功能來被選擇性地新增/移除;例如,基於遊戲是2D或3D遊戲,而只保留PhysicsSystem2D
或PhysicsSystem3D
。
基本系統
在Quantum中最基本的系統是一個C#類別,其繼承於SystemMainThread
。
基本架構執行方式至少需要更新回調,以被定義:
C#
namespace Quantum
{
public unsafe class MySystem : SystemMainThread
{
public override void Update(Frame f)
{
}
}
}
這些是在一個系統類別中可以被覆寫的回調:
OnInit(Frame f)
:當遊戲遊玩被初始化的時候(設定遊戲控制資料等等的好地方),只被調用一次;Update(Frame f)
:用於推進遊戲狀態(遊戲迴圈入口);OnDisabled(Frame f)
及OnEnabled(Frame f)
:當一個系統被另一個系統停用/啟用時被調用;
請注意,所有可用的回調都包含同樣的參數(幀的一個執行個體)。針對所有暫時性及靜態遊戲狀態資料,包含實體、物理、導航及其他像是不可變動的資產物件(這將在另一個單獨的章節中被提及)而言,幀類別是容器。
這樣做的理由是,系統必須是 無狀態 ,以符合Quantum的預測/復原模型的要求。Quantum只在幀執行個體含有所有(可變動的)遊戲狀態資料時,才確保確定性。
建立唯讀的常數或私用方法(應該以參數來收到所有需要的資料)是可行的。
以下的程式碼片段顯示了在一個系統中的一些基本的有效的及無效(違反無狀態的要求)的實例:
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包含一些預先組建的系統(針對物理引擎更新、導航網格及實體原型具現化的入口)。
為了確保確定性,系統被插入的順序將是在所有客戶端上模擬器將執行的所有回調的順序。所以為了控制您的更新發生的序列,請以您希望的順序來插入您的自訂系統。
啟用及停用系統
所有被插入的系統都是預設為啟用的,但是可以在運行階段控制他們的狀態,方法是從模擬中的任何地方調用這些一般功能(他們在幀物件中為可用):
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>();
}
任何系統都可以停用(及重新啟用)另一個系統,所以一個常見的模式是有一個主要的控制器系統,其使用一個簡單的狀態機器來管理更特製化的系統的啟用中/停用中的週期(一個實例是先有一個遊戲中的大廳,附有一個遊戲的倒數計時器,然後是正常的遊戲遊玩,並且最後是一個得分狀態)。
為了預設一個系統在開始時停用,請覆寫這個屬性:
C#
public override bool StartEnabled => false;
特殊系統類型
雖然您可能針對您大多數的系統,使用預設的SystemMainThread
類型,不過Quantum針對特製化的系統,提供幾種替代性選項。
系統 | 說明 |
---|---|
SystemMainThread | 最常見的系統類型。執行一個常規的更新(),附有所有常見的功能。 |
SystemSignalsOnly | 沒有一個更新()功能。它適用於只專注於執行和接收來自其他系統的信號的系統。透過避免更新迴圈,它可幫助您節省一些額外負荷 |
SystemMainThreadFilter | 這個系統類型使用一個T類型的篩選器架構,以篩選基於它的一個實體集,在它們中執行迴圈,並且調用一個方法。請注意:它不支援任何及不含的參數,如果您需要一個更複雜的選項,我們建議您從系統主要執行緒繼承,並且親自迭代篩選器(請參見元件頁面,以取得更多關於篩選器架構及篩選器的資訊)。 |
系統群組
系統可以作為一個群組而被設定及處理。
第一步包含了建立一個繼承自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()),
};
}
}
}
這允許以一個單行的程式碼來啟用/停用一個系統集。啟用/停用一個系統群組將啟用/停用所有屬於其一部分的系統。請注意: Frame.SystemEnable<T>()
及Frame.SystemDisable<T>()
方法透過類型來識別系統;因此如果將有好幾個系統群組,它們各自需要它們自己的執行方式,以允許獨立地啟用/停用多個系統群組。
實體週期API
此章節使用直接API方法來進行實體建立及轉譯。請參見實體原型的章節以了解由資料驅動的方法。
為了建立一個新的實體執行個體,請使用這個(方法將傳回一個實體參照):
C#
var e = frame.Create();
實體再也沒有預先定義的元件,為了新增一個轉換3D及一個物理碰撞器3D到這個實體,請輸入:
C#
var t = Transform3D.Create();
frame.Set(e, t);
var c = PhysicsCollider3D.Create(f, Shape3D.CreateSphere(1));
frame.Set(e, c);
這兩個方法也有用:
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);
}
有了元件集,如果一個實體有多個元件,則可以執行一個單一的檢查:
C#
var components = ComponentSet.Create<CharacterController3D, PhysicsBody3D>();
if (frame.Has(e, components)) {
// do something
}
動態地移除元件就如下述一樣簡單:
C#
frame.Remove<Transform3D>(e);
實體參照類型
Quantum的復原模型維持一個可變大小的幀緩衝區;換句話說,一些遊戲狀態資料(從DSL被定義)的複本被保存在另外的位置的記憶體區塊之中。這意味著任何指向一個實體、元件或架構的指標,都只在一個單一的幀物件內有效(更新等等)。
只要實體仍然存在,實體參照是到該跨幀工作的實體的可安全保存的參照(暫時地取代指標)。實體參照內部地含有以下資料:
- 實體索引:實體槽,來自DSL定義的針對特定類型的最大數字;
- 實體版本號碼:當一個實體執行個體被消除並且槽可被重新用於一個新的實體執行個體時,用於轉譯已過時的舊的實體參照。
篩選器
Quantum v2沒有 實體類型。在疏鬆集ECS記憶體模型中,實體是一個元件的集合的索引; 實體參照 類型保有一些額外的資訊,比如版本。這些集合被保存在動態地配置的疏鬆集之中。
因此,篩選器不是用於迭代一個實體集合,而是用於建立一個元件集且系統將在其上作業。
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);
}
}
}
為了了解篩選器是如何被使用的全貌,請參見 元件 頁面。
由資料驅動的資產
一般而言,在執行遊戲時,一個由資料驅動的方法可以充滿力量,因此Quantum包含了一個非常有彈性的資產連結系統,以將唯讀的資料插入模擬(為了初始化,或是參數化資料以供運行階段使用)。
雖然由資料驅動的資產將在下一個章節被更詳細的說明,不過使用他們的入口是系統,所以這裡包含了一些實例。
預先組建的資產及組態類別
Quantum含有一些預先組建的資料資產,其總是透過幀物件被傳送到系統之中。
這些是最重要的預先組建的資產物件(從Quantum的資產DB):
Map
及NavMesh
:關於可遊玩的地區、靜態物理碰撞器、導航網格等等的資料。自訂玩家資料可從一個資料資產槽被新增(將在資料資產章節中說明);SimulationConfig
;一般性的組態資料,針對物理引擎、導航網格系統等等。- 預設
PhysicsMaterial
及agent configs
(KCC、導航網格等等):
以下程式碼片段顯示了如何從幀物件存取目前的地圖及導航網格執行個體:
C#
// Map is the container for several static data, such as navmeshes, etc
Map map = f.Map;
var navmesh = map.NavMeshes["MyNavmesh"];
資產資料庫
所有Quantum資產資料庫透過動態資產資料庫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);
將導致這個觸發信號在幀類別上(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
}
}
}
請注意,信號永遠包含幀物件作為第一參數,這是做任何對遊戲狀態有用的事情所正常需要的。
被生成的及預先組建的信號
除了在DSL中直接定義的明確的信號以外,Quantum也包含一些預先組建的(例如,「原始」物理碰撞回調)及被生成的信號,其基於實體定義(實體——類型——特定的建立/消除的回調)。
碰撞回調信號將在關於物理引擎的特定章節中說明,所以這裡是其他預先組建的信號的簡短的說明:
ISignalOnPlayerDataSet
:當一個遊戲客戶端發送一個運行階段玩家的執行個體到伺服器(並且資料被確認/附加到一個刷新)時被調用。ISignalOnAdd<T>
、ISignalOnRemove<T>
:當一個元件T類型被新增到一個實體/從一個實體移除時被調用。
觸發事件
類似於信號所發生的事情,觸發事件的入口是幀物件,並且各個(實體)事件將導致一個特定的被生成的功能(以事件資料作為參數)。
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,以與細節的遊戲狀態更新的轉譯引擎通信,所以視覺效果、聲音及任何與使用者介面相關的物件可以在Unity上被更新。
額外的幀API項目
幀類別也含有針對一些其他API的確定性部分的入口,這些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);
透過排程來最佳化
為了最佳化被識別為性能焦點的系統,一個簡單的基於模數的實體排程可以有幫助。使用這個方法,只有一個實體子集被更新,而在每個刷新時迭代它們。
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) {
// it is time to update this entity
}
}
選擇一個5
的schedulePeriod
將使實體只在每個第五個刷新時被更新。選擇2
將意味著每隔一次刷新時被更新。
這個方式下,總更新次數將顯著降低。為了避免在一個刷新中更新所有實體,新增entity.Index
將使載入被分散到多個幀。
以此方法延遲實體更新,將對於使用者程式碼有所要求:
- 被延遲更新的程式碼必須能夠處理不同的差量時間。
- 實體延遲「回應性」可能是視覺可見的。
entity.Index
的使用可新增到延遲,因為針對不同實體,新的資訊遲早會被處理。
Quantum導航系統有這個內建的功能。
Back to top