This document is about: QUANTUM 3
SWITCH TO

Components

簡介

元件是一種特殊的結構體,可以附加到實體上,並用於過濾實體(根據其附加的元件僅迭代一部分活動實體)。

除了自訂元件外,Quantum 還提供了多種預先建置的元件:

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

元件

這是一個在 DSL 中定義元件的基本範例:

Qtn

component Action
{
    FP Cooldown;
    FP Power;
}

將它們標記為元件(如上所示),而不是結構體,將生成適當的程式碼結構(標記介面、ID 屬性等)。編譯後,這些元件也將在 Unity 編輯器中可用於實體原型。在編輯器中,自訂元件命名為 Entity Component ComponentName

操作元件的 API 通過 Frame 類別提供。
您可以選擇使用元件的副本或通過指標來操作元件。為了區分存取類型,使用副本的 API 可直接通過Frame存取,而使用指標的 API 則在Frame.Unsafe下提供,因為後者會直接修改記憶體。

您將需要的最基本功能是名稱相同的函數:新增、取得和設定元件。

Add<T>用於將元件附加到實體上。每個實體只能攜帶某個元件的一個副本。為了幫助除錯,Add<T>返回一個 AddResult 枚舉。

C#

public enum AddResult {
    EntityDoesNotExist     = 0, // The EntityRef passed in is invalid.
    ComponentAlreadyExists = 1, // The Entity in question already has this component attached to it.
    ComponentAdded         = 2  // The component was successfully added to the entity.
}

一旦實體擁有元件,您可以使用Get<T>來取得它。這將返回元件值的副本。由於您操作的是副本,因此需要使用Set<T>將修改後的值保存回元件。類似於 Add 方法,它返回一個 SetResult,可用於驗證操作的結果或對其作出反應。

C#

public enum SetResult {
    EntityDoesNotExist = 0, // The EntityRef passed in is invalid.
    ComponentUpdated   = 1, // The component values were successfully updated.
    ComponentAdded     = 2  // The Entity did not have a component of this type yet, so it was added with the new values.
}

例如,如果您要設定生命值元件的初始值,可以這樣做:

C#

private void SetHealth(Frame frame, EntityRef entity, FP value){
    var health = frame.Get<Health>(entity);
    health.Value = value;
    frame.Set(entity, health);
}

以下表格總結了已經介紹的方法以及其他用於操作元件及其值的方法:

方法 返回 附加資訊
Add<T>(EntityRef entityRef) AddResult枚舉,見上文。 允許無效的EntityRef
Get<T>(EntityRef entityRef) 帶有當前值的 T複本。 允許無效EntityRef
如果元件T不存在於實體上,則拋出異常。
Set<T>(EntityRef entityRef) SetResult枚舉,見上文。 允許無效EntityRef
Has<T>(EntityRef entityRef) true 如果實體存在且元件已附加。

false 如果實體不存在或元件未附加。
允許無效的EntityRef且元件可以不存在。
TryGet<T>(EntityRef entityRef, 外 T 值) true 如果實體存在且元件已附加。

false 如果實體不存在或元件未附加。
允許無效的 EntityRef
TryGetComponentSet(EntityRef entityRef,
out ComponentSet componentSet)
true = 實體存在且所有元件集中的元件都已附加。

false = 實體不存在,或元件集中的一個或多個元件未附加。
允許無效的 EntityRef
Remove<T>(EntityRef entityRef) 無返回值。
如果實體存在且攜帶該元件,則移除元件。
否則不執行任何操作。
允許無效的 EntityRef

為了方便直接操作元件並避免使用 Get/Set 的微小開銷,Frame.Unsafe提供了 Get 和 TryGet 的不安全版本(見下表)。

方法 返回 附加資訊
GetPointer<T>(EntityRef entityRef) T* 不允許無效的實體引用。
如果元件T不存在於實體上,則拋出異常。
TryGetPointer<T>(EntityRef entityRef
外 T* 值)
true 如果實體存在且元件已附加。

false 如果實體不存在或元件未附加。
允許無效的 EntityRef
AddOrGet<T>(EntityRef entityRef, 外 <T>* 結果) true 如果實體存在且元件已附加或已被附加。

false 如果實體不存在。
允許無效的 EntityRef

應避免使用單一結構體,並將其拆分為多個結構體。它們在編譯 IL2CPP 時可能導致 托架巢狀層級超過最大值 錯誤。

單例元件

單例元件 是一種特殊類型的元件,任何時候只能存在一個。在整個遊戲狀態中,任何 實體上只能有一個特定 T 單例元件的實例——這是 ECS 數據緩衝區核心嚴格執行的。Quantum嚴格執行的。

可以在 DSL 中使用singleton component來定義自訂的 單例元件

C#

singleton component MySingleton{
    FP Foo;
}

單例元件繼承了一個名為IComponentSingleton的介面,該介面本身繼承自IComponent。因此,它可以執行您期望常規元件執行的所有常見操作:

  • 可以附加到任何實體上。
  • 可以使用所有常規的安全和不安全方法進行管理(例如 Get、Set、TryGetPointer 等)。
  • 可以通過 Unity 編輯器放置在實體原型上,或在程式碼中實例化到實體上。

除了常規的元件相關方法外,還有幾個專門用於單例的特殊方法。與常規元件一樣,這些方法分為 SafeUnsafe,取決於它們返回的是值類型還是指標。

方法 返回 附加資訊
API - 幀
SetSingleton<T> (T component,
EntityRef optionalAddTarget = default)
void 設定一個單例,如果該單例不存在。
-------
EntityRef(可選),指定要將其附加到的實體。
如果未提供,則會創建一個新實體來附加該單例。
GetSingleton<T>() T 如果單例不存在,則拋出異常。
不需要實體引用,它會自動找到。
TryGetSingleton<T>(out T component) bool
true = 單例存在
false = 單例不存在
如果單例不存在,則不拋出異常。
不需要實體引用,它會自動找到。
GetOrAddSingleton<T>(EntityRef optionalAddTarget = default) T 取得單例並返回它。
如果單例不存在,則會像 SetSingleton 一樣創建它。
-----
EntityRef(可選),指定在需要創建時要附加到的實體。
如果未傳入 EntityRef,則會創建一個新實體來附加該單例。
GetSingletonEntityRef<T>() EntityRef 返回當前持有該單例的實體。
如果單例不存在,則拋出異常。
TryGetSingletonEntityRef<T>(out EntityRef entityRef) bool
true = 單例存在。
false = 單例不存在。
取得當前持有該單例的實體。如果單例不存在,則不拋出異常。
API - Frame.Unsafe
Unsafe.GetPointerSingleton<T>() T* 取得單例指標。
如果不存在,則拋出異常。
TryGetPointerSingleton<T>(out T* component) bool
true = 單例存在。
false = 單例不存在。
取得單例指標。
GetOrAddSingletonPointer<T>(EntityRef optionalAddTarget = default) T* 取得或新增單例並返回它。
如果單例不存在,則會創建它。
-----
EntityRef(可選),指定在需要創建時要附加到的實體。
如果未傳入 EntityRef,則會創建一個新實體來附加該單例。

ComponentTypeRef

ComponentTypeRef結構體提供了一種在運行時通過類型引用元件的方式。這在您需要通過多態動態新增元件時非常有用。

C#

// set in an asset or prototype for example
ComponentTypeRef componentTypeRef;

var componentIndex = ComponentTypeId.GetComponentIndex(componentTypeRef);

frame.Add(entityRef, componentIndex);

擴展功能

由於元件是特殊的結構體,您可以通過在 C# 檔案中編寫 partial 結構體定義來擴展它們。
例如,我們可以像這樣擴展之前的 Action 元件:

C#

namespace Quantum
{
    public partial struct Action
    {
        public void UpdateCooldown(FP deltaTime){
            Cooldown -= deltaTime;
        }
    }
}

反應式回調

有兩個與元件相關的反應式回調:

  • ISignalOnComponentAdd<T>:當元件類型 T 被附加到實體時調用。
  • ISignalOnComponentRemove<T>:當元件類型 T 從實體上移除時調用。

這些回調在您需要在新增/移除元件時操作元件的某部分時特別有用——例如在自訂元件中分配和釋放列表。

要接收這些信號,只需在系統中實現它們即可。

元件迭代器

如果您只需要單個元件,ComponentIterator(安全)和ComponentBlockIterator(不安全)是最適合的選擇。

C#

foreach (var pair in frame.GetComponentIterator<Transform3D>())
{
    var component = pair.Component;
    component.Position += FPVector3.Forward * frame.DeltaTime;
    frame.Set(pair.Entity, component);
}

元件塊迭代器通過指標提供最快的存取方式。

C#

// This syntax returns an EntityComponentPointerPair struct
// which holds the EntityRef of the entity and the requested Component of type T.
foreach (var pair in frame.Unsafe.GetComponentBlockIterator<Transform3D>())
{
    pair.Component->Position += FPVector3.Forward * frame.DeltaTime;
}

// Alternatively, it is possible to use the following syntax to deconstruct the struct
// and get direct access to the EntityRef and the component
foreach (var (entityRef, transform) in frame.Unsafe.GetComponentBlockIterator<Transform3D>())
{
    transform->Position += FPVector3.Forward * frame.DeltaTime;
}

過濾器

過濾器是一種方便的方式,可以根據一組元件來過濾實體,並且僅抓取系統所需的必要元件。過濾器可以用於安全(Get/Set)和不安全(指標)程式碼。

通用

要創建過濾器,只需使用框架提供的 Filter() API。

C#

var filtered = frame.Filter<Transform3D, PhysicsBody3D>();

通用過濾器最多可以包含 8 個元件。
如果需要更精確的過濾,可以通過創建 withoutany ComponentSet 過濾器來實現。

C#

var without = ComponentSet.Create<CharacterController3D>();
var any = ComponentSet.Create<NavMeshPathFinder, NavMeshSteeringAgent>();
var filtered = frame.Filter<Transform3D, PhysicsBody3D>(without, any);

ComponentSet 最多可以包含 8 個元件。
作為 without 參數傳入的 ComponentSet 將排除所有攜帶該集合中至少一個元件的實體。any 集合確保實體擁有至少一個或多個指定的元件;如果實體沒有指定集合中的任何元件,則將被過濾器排除。

通過使用 filter.Next() 的 while 循環來迭代過濾器非常簡單。這將填充所有元件的副本以及它們所附加的實體的 EntityRef

C#

while (filtered.Next(out var e, out var t, out var b)) {
  t.Position += FPVector3.Forward * frame.DeltaTime;
  frame.Set(e, t);
}

注意: 您正在迭代並操作元件的 副本。因此,您需要將新數據設置回它們各自的實體上。

通用過濾器還提供了使用元件指標的功能。

C#

while (filtered.UnsafeNext(out var e, out var t, out var b)) {
  t->Position += FPVector3.Forward * frame.DeltaTime;
}

在這種情況下,您直接修改元件的數據。

FilterStruct

除了常規過濾器外,您還可以使用 FilterStruct 方法。
為此,您首先需要為每種您希望接收的元件類型定義一個帶有 公共 屬性的結構體。

Qtn

struct PlayerFilter
{
    public EntityRef Entity;
    public CharacterController3D* KCC;
    public Health* Health;
    public FP AccumulatedDamage;
}

ComponentSet 類似,FilterStruct 可以過濾最多 8 種不同的元件指標。

注意: 用作 FilterStruct 的結構體 必須 有一個 EntityRef 欄位!

FilterStruct 中的 元件類型 成員 必須 是指標;只有這些指標才會被過濾器填充。除了元件指標外,您還可以定義其他變量,但這些變量將被過濾器忽略,由您自行管理。

C#

var players = f.Unsafe.FilterStruct<PlayerFilter>();
var playerStruct = default(PlayerFilter);

while (players.Next(&playerStruct))
{
    // Do stuff
}

Frame.Unsafe.FilterStruct<T>()有一個重載,使用可選的 ComponentSets anywithout 來進一步指定過濾器。

關於計數的說明

過濾器無法預先知道它將觸及和迭代多少實體。這是因為過濾器在 Sparse-Set ECS 中的工作方式:

  1. 過濾器首先找到提供的元件中關聯實體最少的元件(較小的集合以檢查交集);
  2. 然後遍歷該集合并丟棄任何不具備其他查詢元件的實體。

預先知道確切數量需要遍歷過濾器一次;由於這是一個 (O(n) 操作,效率不高。

元件獲取器

如果您想從一個 已知 實體中取得一組特定的元件,可以使用過濾器結構體結合Frame.Unsafe.ComponentGetter注意: 這僅在不安全的上下文中可用!

C#

public unsafe class MySpecificEntitySystem : SystemMainThread

    struct MyFilter {
        public EntityRef      Entity; // Mandatory member!
        public Transform2D*   Transform2D;
        public PhysicsBody2D* Body;
    }

    public override void Update(Frame frame) {
        MyFilter result = default;

        if (frame.Unsafe.ComponentGetter<MyFilter>().TryGet(frame, frame.Global->MyEntity, &result)) {
            // Do Stuff
        }
    }

如果此操作需要頻繁執行,您可以在系統中快取查找結構體,如下所示(100% 安全)。

C#

public unsafe class MySpecificEntitySystem : SystemMainThread

    struct MyFilter {
        public EntityRef      Entity; // Mandatory member!
        public Transform2D*   Transform2D;
        public PhysicsBody2D* Body;
    }

    ComponentGetter<MyFilter> _myFilterGetter;

    public override void OnInit(Frame frame) {
      _myFilterGetter = frame.Unsafe.ComponentGetter<MyFilter>();
    }

    public override void Update(Frame frame) {
      MyFilter result = default;

      if (_myFilterGetter.TryGet(frame, frame.Global->MyEntity, &result)) {
        // Do Stuff
      }
    }

過濾策略

通常,您會遇到一種情況,即您有許多實體,但只需要其中的一部分。之前我們介紹了 Quantum 中可用的元件和工具來過濾它們;在本節中,我們將介紹一些利用這些工具的策略。
注意: 最佳 方法取決於您自己的遊戲及其系統。我們建議將以下策略作為起點,以創建適合您獨特情況的策略。

注意:以下使用的所有術語均為內部創建,以封裝其他冗長的概念。

微型元件

儘管許多實體可能使用相同的元件類型,但很少有實體使用相同的元件組合。進一步專業化其組合的一種方法是使用 微型元件微型元件 是高度專業化的元件,具有特定系統或行為的數據。它們的唯一性使您能夠創建可以快速識別攜帶它們的實體的過濾器。

標記元件

識別實體的一種常見方法是向它們添加 標記元件。在 ECS 中,標記 的概念本身並不存在,Quantum 也不支持 實體類型;那麼什麼是 標記元件 呢?它們是攜帶很少或沒有數據的元件,專門用於識別實體。

例如,在一個基於團隊的遊戲中,您可以有以下兩種方式:

  1. 一個帶有 TeamA 和 TeamB 的枚舉的「Team」元件;或
  2. 一個「TeamA」和「TeamB」元件。

選項 1. 在主要目的是從視圖中輪詢數據時很有幫助,而選項 2. 將使您能夠在相關的模擬系統中受益於過濾性能。

注意: 有時標記元件也被稱為標籤元件,因為標記和標籤實體可以互換使用。

計數

可以使用Frame.ComponentCount<T>()取得模擬中當前存在的元件 T 的數量。與標記元件結合使用時,可以快速計算某種類型的單位數量。

新增 / 移除

如果您只需要 暫時 附加標記元件或微型元件到實體上,它們仍然是一個合適的選擇,因為AddRemove操作都是 O(1)。

全局列表

標記元件的替代方案是將全局列表保存在FrameContext.User.cs中,儘管這是一種「較少」 ECS 風格的方法。雖然如果您需要跟踪 N 個團隊時這不一定能擴展,但對於子集有限的情況來說很方便。

如果您想突出顯示所有生命值低於 50% 的玩家,可以維護一個全局列表並執行以下操作:

  • 在模擬開始時有一個系統新增/移除實體引用到列表中;
  • 在後續的所有系統中使用該列表。

注意: 如果您只需要偶爾識別這些類型的條件,我們建議在需要時動態計算,而不是維護全局列表。

最大元件數量

默認情況下,Quantum 解決方案支持最多 256 種不同的元件類型定義。
對於用戶定義的元件,這個數字較小(236),因為核心 DLL 已經預定義了 20 種元件類型(Transforms、Colliders 等)。

儘管這對大多數遊戲來說已經足夠,但可以通過在 QTN 檔案中添加以下編譯器定義將最大數量增加到 512:

#pragma max_components 512

增加元件數量可能會導致依賴於基於元件集過濾實體的高實體數量遊戲的平均模擬時間增加,因此建議進行性能分析測試,以測量對您特定場景的性能影響,使用分析頁面中共享的說明。

導入自定義元件

Quantum 3 允許在 DSL 之外定義元件並手動導入它們。這在您需要在外部 DLL 中定義元件時非常有用。通常這 完全不需要,因為在 DSL 本身中定義元件的常規路徑更安全。

要導入元件,請在任何 DSL 檔案中添加import FooComponent;import singleton FooComponent;

元件定義本身必須遵循一些準則才能正確導入。
PS: 請非常小心此定義。正確實現它對於 SDK 的功能非常重要。聲明正確的元件 SIZE 和 FieldOffsets 非常重要。

要求如下:

  1. 實現IComponent介面;
  2. 具有const int SIZE欄位,定義元件的大小;
  3. 具有Serialize方法,用於序列化元件(見下面的簽名);
  4. 具有ComponentChangedDelegate OnAdded靜態屬性(可以返回 null)或符合委派簽名的靜態OnAdded方法。
  5. 具有ComponentChangedDelegate OnRemoved靜態屬性(可以返回 null)或符合委派簽名的靜態OnRemoved方法。

一種 更安全 的替代方法是首先在 DSL 中定義元件,複製其生成的程式碼,然後再次刪除它,以便處理所有重要細節。

以下是元件定義所需的基本結構範例:

C#

[StructLayout(LayoutKind.Explicit)]
public unsafe struct Example : IComponent {
  public const int SIZE = sizeof(int);

  public static ComponentChangedDelegate OnAdded;
  public static ComponentChangedDelegate OnRemoved;

  [FieldOffset(0)]
  public int _number;

  public static void Serialize(void* ptr, IDeterministicFrameSerializer serializer) {
    serializer.Stream.Serialize(&((Example*)ptr)->_number);
  }

  public override int GetHashCode() {
    return _number;
  }
}
Back to top