This document is about: QUANTUM 3
SWITCH TO

DSL (game state)

概述

Quantum 要求元件和其他運行時遊戲狀態資料類型必須使用其專屬的 DSL (領域特定語言) 來宣告。

這些定義會寫入副檔名為.qtn的文字檔案中。Quantum 編譯器會將其解析為 AST,並為每種類型生成部分的 C# 結構定義(定義可以分散在多個檔案中,編譯器會自動合併它們)。

DSL 的目的是為了讓開發者無需處理 Quantum ECS 稀疏集記憶體模型所要求的複雜記憶體對齊問題,這些問題是為了支援確定性的預測/復原模擬方法而產生的。

這種程式碼生成方式也消除了編寫「樣板」程式碼的需求,例如類型序列化(用於快照、遊戲存檔、擊殺鏡頭重播)、校驗和計算以及其他功能,例如用於除錯目的的資料印出/傾印。

若要為專案建立新的.qtn檔案,請在 Unity 的 Project 標籤上開啟上下文選單,點擊Create/Quantum/Qtn,或直接建立一個副檔名為.qtn的新檔案。

元件

元件是一種特殊的結構體,可以附加到實體上,並用於篩選實體(根據附加的元件來迭代僅部分的活躍實體)。以下是一個元件的基礎定義範例:

Qtn

component Action
{
  FP Cooldown;
  FP Power;
}

這些定義會被轉換為普通的 C# 結構體。將其標記為元件(如上所示)會生成適當的程式碼結構(標記介面、ID 屬性等)。

除了自訂元件外,Quantum 還內建了多種預設元件:

  • Transform2D/Transform3D:使用固定點 (FP) 值來表示位置和旋轉;
  • PhysicsCollider、PhysicsBody、PhysicsCallbacks、PhysicsJoints (2D/3D):用於 Quantum 的無狀態物理引擎;
  • PathFinderAgent、SteeringAgent、AvoidanceAgent、AvoidanceObstacle:基於導航網格的路徑尋找和移動。

結構體

結構體可以在 DSL 和 C# 中定義。

DSL定義

Quantum DSL 也允許定義常規結構體(與元件類似,記憶體對齊和輔助函式會被處理):

Qtn

struct ResourceItem
{
  FP Value;
  FP MaxValue;
  FP RegenRate;
}

生成的結構體會以相同的順序宣告欄位,但記憶體偏移會調整以避免冗餘並提供最佳化的記憶體排列。

這讓您可以在 DSL 的其他部分使用「Resources」結構體作為類型,例如在元件定義中使用它:

Qtn

component Resources
{
  ResourceItem Health;
  ResourceItem Strength;
  ResourceItem Mana;
}

生成的結構體是部分的,可以根據需要在 C# 中擴展。

CSharp定義

您也可以在 C# 中定義結構體;然而,這種情況下您需要手動定義:

  • 結構體的記憶體佈局(使用 LayoutKind.Explicit);
  • 在結構體中新增一個 const int SIZE,包含其大小;
  • 實作Serialize函式。

C#

[StructLayout(LayoutKind.Explicit)]
public struct Foo {
  public const int SIZE = 12; // the size in bytes of all members in bytes.

  [FieldOffset(0)]
  public int A;

  [FieldOffset(4)]
  public int B;

  [FieldOffset(8)]
  public int C;

  public static unsafe void Serialize(void* ptr, FrameSerializer serializer)
  {
    var foo = (Foo*)ptr;
    serializer.Stream.Serialize(&foo->A);
    serializer.Stream.Serialize(&foo->B);
    serializer.Stream.Serialize(&foo->C);
  }
}

當在 DSL 中使用 C# 定義的結構體(例如在元件中)時,您需要手動匯入結構體定義。

import struct Foo(12);

注意: 匯入 不支援在大小中使用常數;每次都需要明確指定數值。

元件 vs. 結構體

一個重要的問題是何時以及為什麼應該使用元件而不是常規結構體(元件本質上也是結構體)。

元件包含生成的元資料,使其成為具有以下特性的特殊類型:

  • 可以直接附加到實體上;
  • 用於在遍歷遊戲狀態時篩選實體(下一章將深入介紹篩選 API);

元件可以作為指標或值類型來存取、使用或傳遞參數,就像任何其他結構體一樣。

動態集合

Quantum 的自訂分配器將 Blittable 的集合作為可復原的遊戲狀態的一部分公開。集合僅支援 Blittable 的類型(即基本類型和 DSL 定義的類型)。

為了管理集合,幀 API 為每種集合提供了 3 種方法:

  • Frame.AllocateXXX:在堆積上為集合分配空間。
  • Frame.FreeXXX:釋放/回收集合的記憶體。
  • Frame.ResolveXXX:透過解析指標來存取集合。
將集合設為空

釋放集合後,必須 透過將其設為default來將其清空。這是為了確保遊戲狀態的序列化能正常運作。忽略此清空步驟將導致非確定性行為和不同步問題。作為手動釋放和清空集合指標的替代方案,可以在對應的欄位上使用FreeOnComponentRemoved屬性。

重要注意事項

  • 多個元件可以引用同一個集合實例。
  • 動態集合在元件和結構體中以參考形式儲存。因此,在初始化時 必須 進行 分配,更重要的是,在不再需要時必須進行 釋放。如果集合是元件的一部分,有兩種選擇:
    • 實作反應式回調ISignalOnAdd<T>ISignalOnRemove<T>,並在其中分配/釋放集合(有關這些特定信號的更多資訊,請參閱手冊的 ECS 章節中的元件頁面);或,
    • 使用[AllocateOnComponentAdded][FreeOnComponentRemoved]屬性,讓 Quantum 在元件新增和移除時自動處理分配和釋放。
  • Quantum 不會 從原型預分配集合,除非至少有一個值。如果集合為空,則必須手動分配記憶體。
  • 嘗試多次 釋放 同一個集合會導致錯誤,並使堆積內部處於無效狀態。

列表

動態列表可以在 DSL 中使用list<T> MyList來定義。

Qtn

component Targets {
  list<EntityRef> Enemies;
}

處理這些列表的基本 API 方法包括:

  • Frame.AllocateList<T>()
  • Frame.FreeList(QListPtr<T> ptr)
  • Frame.ResolveList(QListPtr<T> ptr)

解析後,可以使用列表的所有預期 API 方法來迭代或操作,例如 Add、Remove、Contains、IndexOf、RemoveAt、[] 等。

若要使用上述程式碼片段中定義的 Targets 元件中的列表,可以建立以下系統:

C#

namespace Quantum
{
  public unsafe class HandleTargets : SystemMainThread, ISignalOnComponentAdded<Targets>, ISignalOnComponentRemoved<Targets>
  {
    public override void Update(Frame frame)
    {
      foreach (var (entity, component) in frame.GetComponentIterator<Targets>()) {
        // To use a list, you must first resolve its pointer via the frame
        var list = frame.ResolveList(component.Enemies);

        // Do stuff
      }    
    }

    public void OnAdded(Frame frame, EntityRef entity, Targets* component)
    {
      // allocating a new List (returns the blittable reference type - QListPtr)
        component->Enemies = frame.AllocateList<EntityRef>();
    }

    public void OnRemoved(Frame frame, EntityRef entity, Targets* component)
    {
      // A component HAS TO de-allocate all collection it owns from the frame data, otherwise it will lead to a memory leak.
      // receives the list QListPtr reference.
      frame.FreeList(component->Enemies);

      // All dynamic collections a component points to HAVE TO be nullified in a component's OnRemoved
      // EVEN IF is only referencing an external one!
      // This is to prevent serialization issues that otherwise lead to a desynchronisation.
      component->Enemies = default;
    }
  }
}

字典

字典可以在 DSL 中使用dictionary<key, value> MyDictionary來宣告。

Qtn

component Hazard {
  dictionary<EntityRef, Int32> DamageDealt;
}

處理這些字典的基本 API 方法包括:

  • Frame.AllocateDictionary<K,V>()
  • Frame.FreeDictionary(QDictionaryPtr<K,V> ptr)
  • Frame.ResolveDictionary(QDictionaryPtr<K,V> ptr)

與其他動態集合一樣,必須在使用前分配記憶體,並在不再使用時從幀資料中釋放並清空。請參閱上方列表部分的範例。

HashSet

雜湊集可以在 DSL 中使用hash_set<T> MyHashSet來宣告。

Qtn

component Nodes {
  hash_set<FP> ProcessedNodes;
}

處理這些字典的基本 API 方法包括:

  • Frame.AllocateHashSet(QHashSetPtr<T> ptr, int capacity = 8)
  • Frame.FreeHashSet(QHashSetPtr<T> ptr)
  • Frame.ResolveHashSet(QHashSetPtr<T> ptr)

與其他動態集合一樣,必須在使用前分配記憶體,並在不再使用時從幀資料中釋放並清空。請參閱上方列表部分的範例。

枚舉、聯合和位元集合

枚舉

枚舉可以用於定義一組具名的常數值。使用 "enum" 關鍵字來定義其名稱和資料。以下範例定義了一個簡單的枚舉EDamageType,並展示如何在結構體中作為欄位使用:

Qtn

enum EDamageType {
    None, Physical, Magic
}

struct StatsEffect {
    EDamageType DamageType;
}

預設情況下,枚舉在CodeGen中被視為從 0 開始的整數常數。然而,如果需要,可以明確為枚舉成員指派整數值。此外,在記憶體使用需要優化的情況下,可以透過使用不同的底層類型(例如byte)來減少枚舉值的記憶體佔用。以下是一個範例:

Qtn

enum EModifierOperation : Byte
{
  None = 0,
  Add = 1,
  Subtract = 2
}

flags關鍵字用於枚舉類型,表示枚舉中的每個值代表一個獨立的位元標記。這允許您使用位元運算來組合多個枚舉值,這是一種表示相關選項或狀態集合的方式。

Qtn

flags ETeamStatus : Byte
{
  None,
  Winning,
  SafelyWinning,
  LowHealth,
  MidHealth,
  HighHealth,
}

使用flags關鍵字還會為枚舉類型生成程式碼實用方法,這些方法比常規的System.Enum方法(例如IsFlagSet(),比System.Enum.HasFlag()更高效)更高效,因為它避免了值類型裝箱的需求。

聯合

也可以生成類似 C 的聯合。聯合類型在記憶體中重疊所有相關結構體的資料佈局。以下是在 DSL 中宣告聯合的範例:

Qtn

struct DataA
{
  FPVector2 Foo;
}

struct DataB
{
  FP Bar;
}

union Data
{
  DataA A;
  DataB B;
}

聯合可以作為元件的一部分宣告:

Qtn

component ComponentWithUnion {
  Data ComponentData;
}

在內部,聯合類型Data包含必要的邏輯,以便在存取時切換聯合類型。以下是一些使用範例:

C#

private void UseWarriorAttack(Frame frame)
{
    var character = frame.Unsafe.GetPointer<Character>(entity);
    character->Data.Warrior->ImpulseDirection = FPVector3.Forward;
}

private void ResetSpellcasterMana(Frame frame)
{
    var character = frame.Unsafe.GetPointer<Character>(entity);
    character->Data.Spellcaster->Mana = FP._10;
}

使用聯合時,可以僅使用其包含的多種內部類型之一,也可以在運行時動態切換,只需存取特定的內部類型即可。在上方的程式碼片段中,存取 Warrior 和 Spellcaster 指標已經在內部更換了聯合類型。

也可以透過使用Field屬性和一些程式碼生成的常數來檢查當前使用的聯合類型:

C#

private bool IsWarrior(CharacterData data)
{
    return data.Field == CharacterData.WARRIOR;
}

位元集合

位元集合可以用於宣告固定大小的記憶體區塊,用於任何需要的用途(例如戰爭迷霧、像素完美遊戲機制的網格結構等):

Qtn

struct FOWData
{
  bitset[256] Map;
}

輸入

在 Quantum 中,客戶端之間交換的運行階段輸入也在 DSL 中宣告。以下範例定義了一個簡單的移動向量和一個開火按鈕作為遊戲的輸入:

Qtn

input
{
  FPVector2 Movement;
    button Fire;
}

輸入結構體會在每個刷新被輪詢並發送到伺服器(在線上遊戲時)。

有關輸入的更多資訊,例如最佳實踐和推薦的優化方法,請參閱此頁面:輸入

信號

信號是用於解耦系統間通訊的函式簽章(一種發布/訂閱 API 的形式)。以下是一個簡單信號的定義(請注意特殊類型 entity_ref - 這些類型將在本章末尾列出):

Qtn

signal OnDamage(FP damage, entity_ref entity);

這會生成以下介面(可以由任何系統實作):

C#

public interface ISignalOnDamage
{
  public void OnDamage(Frame frame, FP damage, EntityRef entity);
}

信號是唯一允許在 Quantum DSL 中直接宣告指標的概念,因此可以透過參考傳遞資料,以便在具體實作中直接修改原始資料:請注意,這允許傳遞元件指標(而不是實體參考類型)。

Qtn

signal OnBeforeDamage(FP damage, Resources* resources);

請注意,這允許傳遞元件指標(而不是實體參考類型)。

事件

事件是一種細緻的解決方案,用於將模擬中發生的事情傳達給渲染引擎/視圖(它們絕不應用於修改/更新遊戲狀態的部分)。使用 "event" 關鍵字來定義其名稱和資料:

詳細資訊請參閱幀事件手冊

使用 Quantum DSL 定義事件

Qtn

event MyEvent{
  int Foo;
}

從模擬中觸發事件

C#

f.Events.MyEvent(2022);

在 Unity 中訂閱和耗用事件

C#

QuantumEvent.Subscribe(listener: this, handler: (MyEvent e) => Debug.Log($"MyEvent {e.Foo}"));

全域

可以在 DSL 檔案中定義全域存取的變數。全域可以在任何 .qtn 檔案中使用global作用域來宣告。

Qtn

global {
  // Any type that is valid in the DSL can also be used.
  FP MyGlobalValue;
}

與所有 DSL 定義的內容一樣,全域變數是狀態的一部分,並完全相容於預測-復原系統。

在全域作用域中宣告的變數可以透過幀 API 存取。它們可以從任何可以存取幀的地方讀取/寫入 - 請參閱 ECS 章節中的 系統 文件。

注意: 全域變數的另一種替代方案是單例元件;更多資訊請參閱手冊的 ECS 章節中的 元件 頁面。

特殊類型

Quantum 有一些特殊類型,用於抽象複雜概念(實體參考、玩家索引等),或防止未管理程式碼中的常見錯誤,或兩者兼具。以下特殊類型可用於其他資料類型中(包括元件、事件、信號等):

  • player_ref:表示運行階段玩家索引(也可以轉換為 Int32 或從 Int32 轉換)。在元件中定義時,可用於儲存控制該相關實體的玩家(結合 Quantum 基於玩家索引的輸入)。
  • entity_ref:由於 Quantum 中每一幀/刷新的資料都位於獨立的記憶體區域/區塊(Quantum 會保留多個副本以支援復原),指標無法在幀之間快取(無論是在遊戲狀態中還是在 Unity 指令碼中)。實體參考抽象了實體的索引和版本屬性(防止開發者透過舊參考意外存取已銷毀或重用的實體槽中的已棄用資料)。
  • asset_ref<AssetType>:可復原的參考,指向 Quantum 資產資料庫中的資料資產實例(請參閱資料資產章節)。
  • list<T>, dictionary<K,T>:動態集合參考(儲存在 Quantum 的幀堆積中)。僅支援 Blittable 的類型(基本類型 + DSL 定義的類型)。
  • array<Type>[size]:固定大小的「陣列」,用於表示資料集合。普通的 C# 陣列是堆積分配的物件參考(它具有屬性等),這違反了 Quantum 的記憶體要求,因此特殊陣列類型生成了一個基於指標的簡單 API,以在遊戲狀態中保存可復原的資料集合;

關於資產的注意事項

資產是 Quantum 的一個特殊功能,允許開發者定義資料驅動的容器(普通類別,具有繼承、多型方法等),這些容器最終會成為索引資料庫中的不可變實例。"asset" 關鍵字用於將(現有的)類別指定為資料資產,可以在遊戲狀態中分配參考(有關功能和限制的更多資訊,請參閱資料資產章節):

Qtn

asset CharacterData; // the CharacterData class is partially defined in a normal C# file by the developer

以下結構體展示了一些上述類型的有效範例(有時引用先前定義的類型):

Qtn

struct SpecialData
{
  player_ref Player;
  entity_ref Character;
  entity_ref AnotherEntity;
  asset_ref<CharacterData> CharacterData;
  array<FP>[10] TenNumbers;
}

可用類型

在 DSL 中工作時,可以使用多種類型。有些是由解析器預設匯入的,而其他則需要手動匯入。

預設類型

Quantum 的 DSL 解析器有一組預設匯入的跨平台確定性類型清單,可用於遊戲狀態定義:

  • Boolean / bool - 內部會包裝在 QBoolean 中,功能相同(獲取/設定、比較等)
  • Byte
  • SByte
  • UInt16 / Int16
  • UInt32 / Int32
  • UInt64 / Int64
  • FP
  • FPVector2
  • FPVector3
  • FPMatrix
  • FPQuaternion
  • DSL中的PlayerRef / player_ref
  • DSL中的EntityRef / entity_ref
  • LayerMask
  • NullableFP / FP? in the DSL
  • DSL中的NullableFPVector2 / FPVector2?
  • DSL中的NullableFPVector3 / FPVector3?
  • QString 用於 UTF-16(即 .NET 中的 Unicode)
  • QStringUtf8 始終為 UTF-8
  • Hit
  • Hit3D
  • Shape2D
  • Shape3D
  • Joint、DistanceJoint、SpringJoint和HingeJoint

關於 QStrings 的注意事項N表示字串的總大小(以位元組為單位)減去用於簿記的 2 位元組。換句話說,QString<64>將使用 64 位元組來儲存最大長度為 62 位元組的字串,即最多 31 個 UTF-16 字元。

手動匯入

如果需要使用前一節未列出的類型,則必須在使用時在 QTN 檔案中手動匯入。

命名空間 / Quantum 外部的類型

匯入特定類型

若要匯入其他命名空間中定義的類型並直接在 DSL 的元件或全域中使用,請使用以下語法:

Qtn

import MyInterface;
or
import MyNameSpace.Utils;

對於枚舉,語法如下:

Qtn

import enum MyEnum(underlying_type);

// This syntax is identical for Quantum specific enums
import enum Shape3DType(byte);

包含命名空間

在某些情況下,可能還需要將using MyNamespace;新增到任何 QTN 檔案中,以便將該命名空間包含在生成的類別中。

內建的 Quantum 類型和自訂類型

當匯入 Quantum 內建類型或自訂類型時,結構體大小在其 C# 宣告中預先定義。因此,新增一些安全措施非常重要。

C#

namespace Quantum {
  [StructLayout(LayoutKind.Explicit)]
  public struct Foo {
    public const int SIZE = sizeof(Int32) * 2;
    [FieldOffset(0)]
    public Int32 A;
    [FieldOffset(sizeof(Int32))]
    public Int32 B;
  }
}

Qtn

#define FOO_SIZE 8 // Define a constant value with the known size of the struct
import struct Foo(8);

為了確保結構體的預期大小與實際大小一致,建議在您的某個系統中新增一個Assert,如下所示。

C#

public unsafe class MyStructSizeCheckingSystem : SystemMainThread{  
  public override void OnInit(Frame frame)
  {
    Assert.Check(Constants.FOO_SIZE == Foo.SIZE);
  }
}

如果內建結構體的大小在升級過程中發生變化,此Assert會拋出異常,並允許您在 DSL 中更新值。

屬性

Quantum 支援多種屬性,用於在檢查器中呈現參數。

這些屬性包含在Quantum.Inspector命名空間中。



欄位:從元件的原型生成中排除欄位。

元件:不會為此元件生成原型。
屬性 參數 描述
AllocateOnComponentAdded 可應用於動態集合。
當持有集合的元件新增到實體時,如果集合尚未分配記憶體,則會分配記憶體。
ArrayLength
僅限CSharp
int 長度 使用 長度 可以定義陣列的大小。
ArrayLength
僅限CSharp
int minLength
int maxLength
使用 minLengthmaxLength 可以在檢查器中定義大小的範圍。
最終大小可以在檢查器中設定。
(minLengthmaxLength 是包含的)
DrawIf string 欄位名稱

long

CompareOperator 比較

HideType 隱藏
僅在條件評估為真時顯示屬性。

欄位名稱 = 要評估的屬性名稱。
= 用於比較的值。
比較 = 要執行的比較操作:EqualNotEqualLessLessOrEqualGreaterOrEqualGreater
隱藏 = 當表達式評估為 False 時欄位的行為:HideReadOnly

有關比較和隱藏的更多資訊,請參閱下方。
FreeOnComponentRemoved 可應用於動態集合和Ptrs
當元件被移除時,這將釋放相關的記憶體並清空欄位中持有的Ptr

重要:請將此屬性與交叉引用的集合一起使用,因為它僅清空該特定欄位中持有的Ptr,其他欄位將指向無效的記憶體。
ExcludeFromPrototype 可應用於元件或元件欄位。
Header string標題 在屬性上方新增標題。

標題 = 要顯示的標題文字。
HideInInspector 序列化欄位並在 Unity 檢查器中隱藏以下屬性。
Layer 僅能應用於類型int
將在欄位上調用EditorGUI.LayerField
OnlyInPrototype C可應用於欄位,該欄位將在物件狀態中被忽略,僅新增到原型中。
OnlyInPrototype string fieldName
string fieldType
可應用於元件,該元件將欄位新增到原型中,但在物件狀態中忽略它(類似於上方)。
PreserveInPrototype 新增到類型上標記其可用於原型,並防止原型類別被發出。
新增到欄位上僅影響特定欄位。對於簡單的 [Serializable] 結構體很有用,因為它避免了在 Unity 端使用 _Prototype 類型。
Optional string enabledPropertyPath 允許開啟/關閉屬性的顯示。

enabledPropertyPath = 用於評估開關的 布林值 的路徑。
Range (0, 10) 為檢查器新增 Unity 預設的範圍滑桿,適用於 int
RangeEx (0.1, 9.9) 為檢查器新增範圍滑桿,適用於FPlong

這些值將由編輯器檢查器使用 float 進行鉗制,並且與 Quantum 地圖類似,僅在構建或匯出時具有確定性。
Space 在屬性上方新增空格
Tooltip string tooltip 當滑鼠懸停在屬性上時顯示工具提示。

tooltip = 要顯示的提示。

除非另有說明,否則 屬性 可以在 C# 和 qtn 檔案中使用;然而,有一些語法差異。### 在 CSharp 中使用

在 C# 檔案中,屬性可以像任何其他屬性一樣使用和串聯。

C#

// Multiple single attributes
[Header("Example Array")][Tooltip("min = 1\nmax = 20")] public FP[] TestArray = new FP[20];

// Multiple concatenated attributes
[Header("Example Array"), Tooltip("min = 1\nmax = 20")] public FP[] TestArray = new FP[20];

在 qtn 中使用

在 qtn 檔案中,單一屬性的使用方式與 C# 相同。

Qtn

[Header("Example Array")] array<FP>[20] TestArray;

當組合多個屬性時,它們 必須 被串聯起來。

Qtn

[Header("Example Array"), Tooltip("min = 1\nmax = 20")] array<FP>[20] TestArray;

編譯器選項

以下編譯器選項目前可在 Quantum 的 DSL 檔案中使用(未來會新增更多):

Qtn

// pre defining max number of players (default is 6, absolute max is 64)
#pragma max_players 16

// increase the component count from 256 to 512
#pragma max_components 512

// numeric constants (useable inside the DSL by MY_NUMBER and useable in code by Constants.MY_NUMBER)
#define MY_NUMBER 10

// overriding the base class name for the generated constants (default is "Constants")
#pragma constants_class_name MyFancyConstants

自訂 FP 常數

您也可以在 Quantum 的 DSL 檔案中定義自訂的FP常數。例如:

Qtn

// in a DSL file
#define Pi 3.14

然後,Quantum 程式碼生成會將對應的常數生成到FP結構體中:

Qtn

// 3.14
FP constant = Constants.Pi;

它還會生成對應的原始值:

Qtn

// 3.14 Raw
var rawConstant = Constants.Raw.Pi;
Back to top