This document is about: QUANTUM 2
SWITCH TO

DSL (game state)

概述

Quantum需要元件和其他運行階段遊戲狀態資料類型,以透過其自己的DSL(特定領域語言)被宣告。

這些定義被寫入以.qtn為副檔名的文字檔案。Quantum編譯器將剖析他們為一個AST,並且針對各個類型來生成部分C#架構定義(定義可按照需要被分為多個檔案,編譯器將相應地合併他們)。

DSL的目標是為了從開發者那裡,抽象出Quantum的ECS疏鬆集記憶體模型所需要的,複雜記憶體對齊要求,這是支援針對模擬的確定性預測/復原方法所需要的。

這個程式碼生成方法也消除了針對類型序列化(用於快照集、遊戲儲存、擊殺鏡頭重播)、總和檢查碼以及其他功能,比如為了偵錯的目的來列印/傾印幀資料,而來撰寫「樣板」程式碼的需要。

元件

元件是特殊架構,可附加到實體,並用於篩選他們(按照使用中實體的附加元件,只逐一查看使用中實體的一個子集)。這是一個元件的基本的實例定義:

C#

component Action
{
    FP Cooldown;
    FP Power;
}

這些將被轉為一般的C#架構。標記他們為元件後(如同上述)將生成適當的程式碼架構(標記介面、帳號屬性等等)。

除了自訂元件以外,Quantum也帶來一些預先組建的元件:

  • 轉換2D/轉換3D:使用固定點(FP)值來定位及旋轉;
  • 物理碰撞器、物理主體、物理回調、物理接合(2D/3D):由Quantum的無狀態物理引擎所使用;
  • 導航器代理、方向控制代理、迴避代理、迴避障礙物:基於導航網格的導航及移動。

架構

架構可在DSL及C#中被定義。

DSL定義

Quantum DSL也允許一般架構(比如元件、記憶體對齊,以及協助程式功能都將被處理)的定義:

C#

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

在生成架構之後,欄位將根據字母順序進行排序。如果您需要/希望他們以特定順序顯示,您將需要在C#中定義架構(請參見下述章節)。

這讓您可以使用「資源」架構,在DSL的所有其他部分中作為一個類型,比如在一個元件定義中使用它:

C#

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

被生成的架構是部分的,並且可以按照需要在C#中被擴充。

CSharp定義

您也可以在C#中定義架構;然而,在這個情況下您將需要手動定義架構所對齊的記憶體。

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;
}

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

Unknown

import struct Foo(12);

請注意:匯入 並不支援常數大小;您每次將需要特定出確切的數值。

元件vs.架構

一個重要的問題是為何及何時應該使用元件而非一般的架構(元件最終而言也是架構)。

元件包含被生成的中繼資料,這些中繼資料將他們轉為一個特殊類型,並附有以下功能:

  • 可以被直接附加於實體;
  • 當周遊遊戲狀態時用於篩選實體(下一章節將會討論篩選器API);

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

動態集合

Quantum的自訂配置器公開了可直接複製的集合,作為可復原的遊戲狀態的一部分。集合只支援可直接複製的類型(比如基本定義及DSL定義的類型)。

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

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

請注意: 在釋放一個集合之後,它 必須 透過設定其為default來被設為空值。為了使遊戲狀態的序列化工作順利,這是必須的。省略設為空值的步驟將導致非確定性的行為及不同步。作為手動釋放一個集合及將指標設為空值的替代方案,可以使用問題欄位上的FreeOnComponentRemoved屬性。

重要注意事項

  • 多個元件可以參照同樣的集合執行個體。
  • 在元件及架構中,動態集合被儲存為參照。因此在初始化他們時,必須 配置 他們,並且更重要的是,當不再需要他們時 釋放 他們。如果集合是一個元件的一部分,將有兩個可用選項:
    • 執行被動回調ISignalOnAdd<T>ISignalOnRemove<T>,並且在那裡配置/釋放集合。(如需取得關於這些特定訊號的更多資訊,請參見操作手冊的ECS章節的元件頁面);或者,
    • 使用[AllocateOnComponentAdded][FreeOnComponentRemoved]屬性,以讓Quantum在元件被新增及移除時,分別地處理配置和取消配置。
    • Quantum 不會 從原型預先配置集合,除非至少有一個值。如果集合為空,則記憶體需要被手動配置。
  • 嘗試 釋放 一個集合超過一次,將會導致錯誤,並且內部地將堆積置於一個無效的狀態。

清單

動態清單可使用list<T> MyList在DSL中被定義。

Unknown

component Targets {
  list<EntityRef> Enemies;
}

處理這些清單的基本的API方法是:

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

當處理完成後,一個清單可以被所有預期的清單API方法來重複執行或操控,比如新增、移除、包含、取得索引、移除在、[]等等。

為了使用上述程式碼片段中定義的類型 目標 的元件中的清單,您可以建立以下系統:

C#

namespace Quantum
{
    public unsafe class HandleTargets : SystemMainThread, ISignalOnComponentAdded<Targets>, ISignalOnComponentRemoved<Targets>
    {
        public override void Update(Frame f)
        {
            var targets = f.GetComponentIterator<Targets>();

            for (int i = 0; i < targets.Count; i++)
            {
                var target = targets[i];

                // To use a list, you must first resolve its pointer via the frame
                var list = frame.ResolveList(target.Enemies);

                // Do stuff
            }
        }

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

        public void OnRemoved(Frame f, 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.
            f.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

C#

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

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

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

就如同任何其他動態集合,在使用它之前必須配置它,並且在不再使用字典時,必須從幀資料中對其取消配置並設為空值。請參見上述關於清單的章節中所提供的實例。

雜湊集

雜湊集可在DSL中被宣告,比如hash_set<T> MyHashSet

C#

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)

就如同任何其他動態集合,在使用它之前必須配置它,並且在不再使用雜湊集時,必須從幀資料中對其取消配置並設為空值。請參見上述關於清單的章節中所提供的實例。

聯集、列舉及位元集

也可以生成C-like語言的聯集及列舉。下述的實例展示了如何透過重疊一些互斥的資料類型/值成為一個聯集,來節省資料記憶體:

C#

struct DataA
{
    FPVector2 Something;
    FP Anything;
}

struct DataB
{
    FPVector3 SomethingElse;
    Int32 AnythingElse;
}

union Data
{
    DataA A;
    DataB B;
}

被生成的類型 資料 也將包含一個區分器屬性(以指出哪一個聯集類型已經被填入)。「觸碰」任何聯集子類型將會設定這個屬性為適當的值。

位元集可針對任何目的(比如戰爭迷霧、針對完美像素遊戲機制的網格狀架構等等),用於宣告固定大小的記憶體區塊:

C#

struct FOWData
{
    bitset[256] Map;
}

輸入

在Quantum中,在客戶端之間交換的運行階段輸入,也在DSL中被宣告。這個實例定義了針對一個遊戲的,作為輸入的一個簡單的移動向量:

C#

input
{
    FPVector2 Movement;
}

針對按鍵型的輸入,Quantum能夠安全地計算狀態更改(這將會在下一個章節——系統中被提及),所以應使用特殊類型「按鍵」:

C#

input
{
    FPVector2 Movement;
    button Fire;
}

對於預測/復原遊戲,按鍵經常是理想類型,因為它在網路協定上只使用一個單一的位元。

輸入架構在每次刷新時被輪詢,並且發送到伺服器(當線上遊戲時)。伺服器負責針對全刷新集(所有玩家的輸入)來批次處理及向下傳送輸入確認。為了這個原因,這個架構應該以一個盡可能最小的尺寸被保存。

確定性的指令(請參見相關章節)是Quantum的另一個輸入路徑,並且可以有任意資料及大小,這使得他們對於特殊輸入類型而言很理想,比如「購買這個物品」、「傳送到某處」等等。

請參見下一章節,以了解輸入是如何被使用(被系統使用)及從Unity導入Quantum(啟動程序Unity專案)

信號

信號是功能簽章,用於作為一個低耦合系統間通訊API(一個發行者/訂閱者API的表單)。以下將定義一個簡單的信號(請注意特殊類型 entity_ref——這些將在本章節結尾被表列):

C#

signal ApplyDamage(FP damage, entity_ref entity);

這將生成下述的介面(其可由任何系統執行):

C#

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

信號是唯一的允許在Quantum的DSL中,直接宣告一個指標的概念,所以透過參照來傳遞資料可用於在其具體實作中直接地修改原始資料:

C#

signal OnDamageBefore(FP Damage, Resources* resources);

請注意這允許一個元件指標(取代了實體參照類型)的傳遞。

事件

事件是一個更細緻的解決方案,以向轉譯引擎/檢視來溝通在模擬中發生的事情(他們不應該用於修改/更新遊戲狀態的一部分)。請使用「事件」關鍵字以定義其名稱及資料:

如需取得關於事件的詳細資訊,請參見遊戲事件操作手冊

使用Quantum DSL來定義一個事件

Unknown

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範圍來宣告全域。

C#

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的幀堆積)。只支援可直接複製的類型(基本類型+DSL定義的類型)。
  • array<Type>[size]:固定大小的「陣列」以代表資料集合。一個正常的C#陣列將是一個堆積配置的物件參照(它有屬性等等),這違反了Quantum的記憶體要求,所以特殊的陣列類型生成一個指標基礎的簡單的API,以保存遊戲狀態中的可復原的資料集合;

關於資產的注意事項

資產是Quantum的一個特殊功能,讓開發者定義由資料驅動的容器(正常類別,附有繼承、多型方法等等),這個容器最終成為一個在已建立索引的資料庫中的不可變執行個體。「資產」關鍵字用於指派一個(已存在的)類別作為一個資料資產,其在遊戲狀態內可以有被指派的參照(請參見資料資產章節以取得更多關於功能及限制的資訊):

C#

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

以下架構展示上述類型的一些有效的實例(有時候參照了預先已定義的類型):

C#

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

可用的類型

當在DSL中工作時,您可以使用各式各樣的類型。有些是由剖析器預先匯入,其他需要由手動匯入。

預設

Quantum的DSL剖析器有一個預先匯入的跨平台的確定性類型的清單,這些類型可用於遊戲狀態定義:

  • 布林值 / 布林——內部地被包裝於QBoolean,它一致性地工作(取得/設定、比較等等)
  • 位元組
  • SByte
  • UInt16 / Int16
  • UInt32 / Int32
  • UInt64 / Int64
  • FP
  • FPVector2
  • FPVector3
  • FPMatrix
  • FPQuaternion
  • PlayerRef / player_ref,在DSL中
  • EntityRef / entity_ref,在DSL中
  • 層遮罩
  • NullableFP / FP?,在DSL中
  • NullableFPVector2 / FPVector2?,在DSL中
  • NullableFPVector3 / FPVector3?,在DSL中
  • QString是針對UTF-16(也就是.NET中的Unicode)
  • QStringUtf8總是UTF-8
  • Hit
  • Hit3D
  • Shape2D
  • Shape3D
  • 接合、距離接合、彈簧接合及鉸鏈接合

針對QStrings的注意事項N代表字串的總大小,以位元為單位並減掉2位元,用於簿記。換句話說QString<64>將針對一個最大位元長度62位元的字串,使用64位元,也就是最多達31個UTF-16字元。

手動匯入

如果您需要一個沒有列在前述章節的類型,當在QTN檔案中使用它時,您需要手動地匯入它。

命名空間/Quantum之外的類型

為了匯入在其他命名空間中定義的類型,那您可以使用下述語法:

C#

import MyInterface;
or
import MyNameSpace.Utils;

針對一個列舉,語法將如下述:

C#

import enum MyEnum(underlying_type);

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

內建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;
}
}

C#

#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 f)
{
    Assert.Check(Constants.FOO_SIZE == Foo.SIZE);
}
}

如果內建架構的大小在升級時改變了,這個Assert將擲回並且允許您更新在DSL中的值。

屬性

Quantum支援一些屬性,以呈現在偵測器中的參數。

屬性被包含在Quantum.Inspector命名空間。

= 要顯示的標題文字。
屬性 參數 說明
DrawIf 字串 欄位名稱
long
CompareOperator 比較
HideType 隱藏
只有在條件評估為真時顯示屬性。

欄位名稱 = 要評估的屬性的名稱。
= 用於比較的值。
比較 = 要進行的比較操作 等於不等於小於小於等於大於等於,或大於
隱藏 = 當運算式評估為時欄位的行為:隱藏唯讀

如需更多比較及隱藏的資訊,請參見下述。
Header 字串標題 在屬性上新增一個標題。

header
HideInInspector - 將欄位序列化,並且在Unity偵測器中隱藏下列屬性。
Layer - 只能適用於int類型。
將會在欄位上調用EditorGUI.LayerField
Optional 字串已啟用的屬性路徑 允許開啟/關閉一個屬性的顯示。

已啟用的屬性路徑 = 到布林值的路徑,用於評估切換。
Space - 在屬性上新增一個空白
Tooltip 字串 工具訣竅 當停留在屬性上時,顯示一個工具訣竅。

tooltip = 要顯示的訣竅。
ArrayLength(從2.1開始)
FixedArray(在2.0中)
只適用於CSharp
整數 長度
--------
整數 最小長度
整數 最大長度
使用長度將允許定義一個陣列的大小。
------
使用最小長度最大長度將允許定義偵測器中的一個尺寸範圍。
在偵測器中的最終大小可以進而被設定。
最小長度最大長度也包含在內)
ExcludeFromPrototype - 可用於一個元件及元件欄位。
------
- 欄位:排除來自為了元件生成的原型的欄位。
- 元件:不會為這個元件而生成原型。
PreserveInPrototype - 被新增到一個類型時,將標記其在原型中為可用,並且防止原型類別被發出。
被新增到一個欄位時,只影響一個特定的欄位。對於簡單的[可序列化]架構而言很有用,因為它避免在Unity側上使用_原型類型。
AllocateOnComponentAdded - 可適用於動態集合。
這將針對該集合來配置記憶體,前提是在保留集合的元件被新增到實體時,記憶體還沒有被配置。
FreeOnComponentRemoved - 可用於動態集合及Ptrs
當元件被移除時,這將取消配置相關的記憶體,並且將保留在欄位中的Ptr設為空值。
------
重要事項:請不要與跨參照的集合一起使用這個屬性,因為它只將保留在該特定欄位中的Ptr設為空值,而其他將指向無效的記憶體。

在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#中的使用相同。

C#

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

當結合多個屬性時,他們 必須 被串連。

C#

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

編譯器選項

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

C#

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

// 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
Back to top