This document is about: QUANTUM 3
SWITCH TO

Systems (game logic)

概述

系統是 Quantum 中所有遊戲邏輯的入口點。

它們以普通的 C# 類別實現,但為了符合預測/復原模型,系統需要遵守一些限制。系統必須:

  • 無狀態:系統中不應宣告可變欄位。所有可變的遊戲資料必須在 .qtn 檔案中宣告,並成為Frame類別中可復原的遊戲狀態的一部分;
  • 僅實作和/或使用確定性函式庫和演算法(Quantum 提供了固定點數學、向量數學、物理、亂數生成、路徑尋找等函式庫);

有幾種基礎系統類別可供繼承:

  • SystemMainThread:包含OnInitUpdate回調。更新在每個系統執行一次,當需要迭代實體及其元件時,使用者需自行創建篩選器。也可用於訂閱和響應 Quantum 信號;
  • SystemMainThreadFilter<Filter>:工作與SystemMainThread類似,但它接受一個定義元件佈局的篩選器,且會為每個擁有篩選器中定義的所有元件的實體調用一次Update
  • SystemSignalsOnly 提供Update回調,通常僅用於響應 Quantum 信號。由於沒有排程任務給它,其開銷較低);
  • SystemBase:僅用於高級用途,用於將平行任務排程到任務圖中(本基礎手冊不涵蓋)。

核心系統

Quantum SDK 在預設的SystemsConfig中包含了所有 核心 系統。

  • Core.CullingSystem2D():在預測幀中剔除帶有Transform2D元件的實體。
  • Core.CullingSystem3D():在預測幀中剔除帶有Transform3D元件的實體。
  • Core.PhysicsSystem2D():對所有帶有Transform2DPhysicsCollider2D元件的實體執行物理運算。
  • Core.PhysicsSystem3D():對所有帶有Transform3DPhysicsCollider3D元件的實體執行物理運算。
  • Core.NavigationSystem():用於所有導航網格相關元件。
  • Core.EntityPrototypeSystem():創建、實例化和初始化EntityPrototypes
  • Core.PlayerConnectedSystem():用於觸發ISignalOnPlayerConnectedISignalOnPlayerDisconnected信號。
  • Core.DebugCommand.CreateSystem():由狀態檢查器用於發送資料,以即時實例化/移除/修改實體(僅在編輯器中可用!)。

預設包含所有系統,以方便使用者。可以根據遊戲所需功能選擇性新增/移除核心系統;例如,僅保留PhysicsSystem2DPhysicsSystem3D

基礎系統

在 Unity 中,可以透過右鍵選單使用指令碼模板創建 Quantum 系統:

System Templates

生成的對應程式碼片段如下:

系統

C#

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

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

可覆寫的 API:

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

系統篩選器

C#

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

  [Preserve]
  public unsafe class NewQuantumSystem : SystemMainThreadFilter<NewQuantumSystem.Filter> {
    public override void Update(Frame frame, 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 frame)OnEnabled(Frame frame):當系統直接啟用/停用,或父系統狀態切換時調用;
  • UseCulling定義系統是否應排除被剔除的實體。

注意: 任何 Quantum 系統都必須使用屬性[UnityEngine.Scripting.Preserve]

所有可用的回調都包含一個Frame實例。幀類別是所有可變和靜態遊戲狀態資料的容器,包括實體、物理、導航以及其他不可變的資產物件(將在後續章節中介紹)。

這樣設計的原因是,系統必須是 無狀態的,以符合 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 _mutableData = 10;

    public override void Update(Frame frame)
    {
        // it is ok to use a constant to compute something here
        var temporaryData = _readOnlyData + 5;

        // it is NOT ok to modify transient data that lives outside of the Frame object:
        _transientData = 5;
    }
  }
}

SystemsConfig

在 Quantum 3 中,系統配置的處理方式有所改變。不再將配置直接嵌入程式碼中,而是將其封裝在名為SystemsConfig的資產中。

此配置被傳遞到RuntimeConfig中,Quantum 會自動實例化請求的系統。

注意,Quantum 包含一些預構建的系統(物理引擎更新、導航網格和實體原型實例化的入口點)。

為了保證確定性,系統的插入順序將是模擬器在所有客戶端上執行所有回調的順序。因此,若要控制更新的順序,只需按所需順序插入自訂系統即可。

創建新的SystemsConfig

SystemsConfig是一個普通的 Quantum 資產。可以在專案視窗中右鍵點擊 -> Quantum -> SystemsConfig 來創建新的資產。

該資產包含一個序列化的系統列表。可以像操作普通的 Unity 列表一樣與其互動。

Systems Config

啟用和停用系統

所有插入的系統預設為啟用狀態,但可以透過從模擬中的任何位置調用這些通用函式來控制其運行時狀態(這些函式可在幀物件中使用):

C#

public override void OnInit(Frame frame)
{
  // deactivates MySystem, so no updates (or signals) are called in it
  frame.SystemDisable<MySystem>();

  // (re)activates MySystem
  frame.SystemEnable<MySystem>();

  // possible to query if a System is currently enabled
  var enabled = frame.SystemIsEnabled<MySystem>();
}

任何系統都可以停用(或重新啟用)另一個系統,因此常見的模式是擁有一個主控制器系統,使用簡單的狀態機來管理更專業化系統的啟用/停用生命週期(例如,先有一個遊戲內大廳,倒數計時進入遊戲,然後是正常遊戲,最後是分數狀態)。

若要讓系統預設為停用狀態,覆寫以下屬性:

C#

public override bool StartEnabled => false;

系統群組

系統可以分組,這使得它們可以一起啟用和停用。

選擇SystemsConfig,新增一個類型為SystemGroup的新系統,然後將子系統附加到其中。

System Group

注意: Frame.SystemEnable<T>()Frame.SystemDisable<T>()方法透過類型識別系統;因此,如果需要多個系統群組,每個群組都需要自己的實現,以允許獨立啟用/停用多個系統群組。在這種情況下,可以如下宣告新的系統群組類型,然後在系統配置資產中使用。

C#

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

實體生命週期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);

以下兩個方法也很有用:

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,可以一次性檢查實體是否擁有多個元件:

C#

var components = ComponentSet.Create<CharacterController3D, PhysicsBody3D>();
if (frame.Has(e, components)) {
  // do something
}

動態移除元件也很簡單:

C#

frame.Remove<Transform3D>(e);

EntityRef 類型

Quantum的復原模型維護了一個可變大小的幀緩衝區;換句話說,遊戲狀態資料(從 DSL 定義)的多個副本儲存在不同位置的記憶體區塊中。這意味著任何指向實體、元件或結構的指標僅在單個幀物件(更新等)內有效。

實體參照是對實體的安全引用(暫時取代指標),只要相關實體仍然存在,就可以跨幀工作。實體參照內部包含以下資料:

  • 實體索引:實體槽位,來自 DSL 定義的特定類型的最大數量;
  • 實體版本號:當實體實例被銷毀且槽位可重新用於新實體時,用於使舊的實體參照失效。

篩選器

Quantum v2 沒有 實體類型。在稀疏集 ECS 記憶體模型中,實體是元件集合的索引;EntityRef 類型包含一些額外資訊,例如版本控制。這些集合儲存在動態分配的稀疏集中。
因此,與其迭代實體集合,不如使用篩選器來創建系統將處理的元件集合。

C#

public unsafe class MySystem : SystemMainThread
{
    public override void Update(Frame frame)
    {
        var filtered = rame.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 資產資料庫中最重要的預構建資產物件:

  • MapNavMesh:關於可玩區域、靜態物理碰撞器、導航網格等的資料。可以從資料資產槽中新增自訂玩家資料(將在資料資產章節中介紹);
  • SimulationConfig:物理引擎、導航網格系統等的通用配置資料。
  • 預設的PhysicsMaterialagent configs(KCC、導航網格等):

以下程式碼片段展示如何從幀物件中存取當前的 Map 和 NavMesh 實例:

C#

// Map is the container for several static data, such as navmeshes, etc
Map map = f.Map;
var navmesh = map.NavMeshes["MyNavmesh"];

資產資料庫

所有 Quantum 資料資產都可以透過幀中的資料庫 API 在系統中存取。有關模擬中的資產的更多資訊,請參見這裡。有關在視圖(Unity 編輯器)中處理 Quantum 資產的更多資訊,請參見這裡,以及有關使用視圖特定資料擴展資產的詳細資訊,請參見這裡

信號

如前一章所述,信號是用於生成發布者/訂閱者 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 frame, FP damage, EntityRef entity)
    {
      // this will be called everytime any other system calls the OnDamage signal
    }

  }
}

注意,信號總是包含幀物件作為第一個參數,因為這通常是對遊戲狀態進行任何有用操作所需的。

生成和預構建的信號

除了直接在 DSL 中定義的顯式信號外,Quantum 還包含一些預構建(例如「原始」物理碰撞回調)和基於實體定義生成的信號(特定於實體類型的創建/銷毀回調)。

碰撞回調信號將在物理引擎的特定章節中介紹,以下是其他預構建信號的簡要說明:

  • ISignalOnPlayerDataSet:當遊戲客戶端將 RuntimePlayer 的實例發送到伺服器(並且資料被確認/附加到一個刷新)時調用。
  • 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,用於向渲染引擎傳達詳細的遊戲狀態更新,以便視覺效果、聲音和任何 UI 相關物件可以在 Unity 中更新。

額外的幀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 (entity.Index % schedulePeriod == frame.Number % schedulePeriod) {
      // it is time to update this entity
    }
}

選擇schedulePeriod5將使實體每 5 個刷新才更新一次。選擇2則表示每隔一個刷新更新一次。

這樣可以顯著減少總更新次數。為了避免在 一個 刷新中更新所有實體,新增entity.Index會使負載分散到多個幀中。

像這樣延遲實體更新對使用者代碼有以下要求:

  • 延遲更新代碼必須能夠處理不同的差量時間。
  • 實體的延遲「響應」可能在視覺上明顯。
  • 使用entity.Index可能會增加延遲,因為不同實體的新資訊會在不同時間被處理。

Quantum導航系統內建了此功能。

Back to top