This document is about: QUANTUM 2
SWITCH TO

Components

概述

元件是特殊架構,其可被附加到實體,並且用於篩選他們(只迭代一個啟用中的實體的子集,且基於其被附加的元件來迭代)。

除了自訂元件以外,Quantum附有一些預先組建的元件:

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

元件

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

C#

component Action
{
    FP Cooldown;
    FP Power;
}

標記他們為元件(如同上述),而非架構,將生成適當的程式碼架構(標記介面、帳號屬性等等)。當被編譯之後,這些也將在Unity編輯器中可用,以與實體原型使用。在編輯器中,自訂元件被命名為實體元件元件名稱

在元件上工作的API透過 類別被呈現。
您可選擇在元件的複本上工作,或是透過指標在元件上工作。為了區分存取類型,在元件的複本上工作的API是可直接透過Frame來存取的,而對於存取指標的API在Frame.Unsafe下為可用的——因為後者修正了記憶體。

您將需要用來新增、取得及設定元件的最基本的功能,是同樣名稱的功能。

Add<T>用於新增一個元件到一個實體。各個實體只可以帶著一個特定元件的一個複本。為了在偵錯時輔助您,Add<T>傳回一個 新增結果 列舉。

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>在元件上儲存修正後的值。類似於 新增 方法,它傳回一個 設定結果,其可用於驗證操作的結果或回應它。

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 f, EntityRef entity, FP value){
    var health = f.Get<Health>(entity);
    health.Value = value;
    f.Set(entity, health);
}

這個表格回顧了已經呈現的方法,和其他提供給您的方法,以操控元件及他們的值:

方法 傳回 額外資訊
Add<T>(EntityRef實體參照) 新增結果列舉,請參見上述。 允許一個無效的實體參照。
Get<T>(EntityRef實體參照) T
一個T的複本,附有目前的值。
不允許一個無效的實體參照。
如果T元件沒有在實體上顯示,則擲回一個例外狀況。
Set<T>(EntityRef實體參照) 設定結果列舉,請參見上述。 允許一個無效的實體參照。
Has<T>(EntityRef實體參照) 布林值
真 = 實體存在並且元件被附加
偽 = 實體不存在,或是元件沒有被附加。
允許無效的實體參照,
並且元件可以不存在。
TryGet<T>(EntityRef實體參照,外T值) 布林值
真 = 實體存在並且元件被附加於其上。
偽 = 實體不存在,或是元件沒有被附加於其上。
允許一個無效的實體參照。
TryGetComponentSet(EntityRef實體參照,
外ComponentSet元件集)
布林值
真 = 實體存在並且所有元件的元件被附加
偽 = 實體不存在,或是集的一個或多個元件
沒有被附加。
允許一個無效的實體參照。
Remove<T>(EntityRef實體參照) 沒有傳回值。
如果實體存在且帶有元件,將移除元件。
否則沒有動作。
允許一個無效的實體參照。

為了協助直接在元件上工作,並且避免來自使用取得/設定的——小的——額外負荷,Frame.Unsafe提供取得及嘗試取得的不安全的版本(請參見下表)。

方法 傳回 額外資訊
GetPointer<T>(EntityRef實體參照) T* 不允許無效的實體參照。
如果T元件沒有在實體上顯示,則擲回一個例外狀況。
TryGetPointer<T>(EntityRef實體參照
外T*值)
布林值
真 = 實體存在並且元件被附加於其上。
偽 = 實體不存在,或是元件沒有被附加於其上。
允許一個無效的實體參照。

單一元件

一個 單一元件 是元件的一個特殊類型,其在任何時間點只能存在一個。在整個遊戲狀態中的 任何 實體上,只能有一個特定T單一元件的一個執行個體——這是在ECS資料緩衝區的核心深處被強制執行。這是由Quantum嚴格強制執行。

一個自訂的 單一元件 可在DSL中被定義,其使用singleton component

C#

singleton component MySingleton{
    FP Foo;
}

單一繼承一個稱為IComponentSingleton的介面,而其繼承於IComponent。因此它可以做所有您可以期待常規元件所能做的一般事情:

  • 它可被附加到任何實體。
  • 它可透過所有常規的安全及不安全的方法(例如取得、設定、嘗試取得指標等等)來被管理。
  • 它可透過Unity編輯器被放在實體原型上,或在一個實體上的程式碼中被具現化。

除了常規的元件相關的方法以外,也有一些針對單一的特殊方法。如同針對常規的元件,這些方法分為 安全不安全,其基於他們傳回一個值類型或一個指標。

方法 傳回 額外資訊
API——幀
SetSingleton<T> (T元件,
EntityRef optionalAddTarget = 預設)
無效 如果單一不存在,則設定一個單一。
-------
EntityRef(選擇性),特定出它要新增到的實體。
如果沒有給出任何實體,則一個新的實體將被建立,以將單一新增到該實體。
GetSingleton<T>() T 如果單一不存在,則擲回例外狀況。
不需要實體參照,它將自動地尋找實體參照。
TryGetSingleton<T>(外T元件) 布林值
真 = 單一存在
偽 = 單一不存在
如果單一不存在,不會擲回一個例外狀況。
不需要實體參照,它將自動地尋找實體參照。
GetOrAddSingleton<T>;(EntityRef optionalAddTarget = 預設) T 取得一個單一並且傳回它。
如果單一不存在,它將被建立,如同在設定單一中一樣。
-----
EntityRef(選擇性),如果單一必須被建立,則特定出它要新增到的實體。
如果未傳入實體參照,則一個新的實體將被建立,以將單一新增到該實體。
GetSingletonEntityRef<T>() 實體參照 傳回目前持有單一的實體。
如果單一不存在,則擲回。
TryGetSingletonEntityRef<T>(外EntityRef實體參照) 布林值
真 = 單一存在。
偽 = 單一不存在。
取得目前持有單一的實體。如果單一不存在,不會擲回。
API——幀.不安全
Unsafe.GetPointerSingleton<T>() T* 取得一個單一指標。
如果它不存在,則擲回例外狀況。
TryGetPointerSingleton<T>(外T*元件) 布林值
真 = 單一存在。
偽 = 單一不存在。
取得一個單一指標。
GetOrAddSingletonPointer<T>;(EntityRef optionalAddTarget = 預設) T* 取得或新增一個單一並且傳回它。
如果單一不存在,它將會被建立。
-----
EntityRef(選擇性),如果單一必須被建立,則特定出它要新增到的實體。
如果未傳入實體參照,則一個新的實體將被建立,以將單一新增到該實體。

新增功能性

因為元件是特殊架構,您可以透過在一個C#檔案中撰寫一個 部分 架構定義,以自訂方法擴展他們。
舉例而言,相較之前,如果我們可以擴展我們的動作元件如下:

C#

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

回應性回調

有兩個元件特定的回應性回調:

  • ISignalOnAdd<T>:當一個元件T類型被新增到一個實體時被調用。
  • ISignalOnRemove<T>:當一個元件T類型從一個實體被移除時被調用。

當一部分元件被新增/移除,您需要操控一部分元件的時候,這些特別有用——舉例而言,在一個自訂元件中配置或取消配置一個清單。

為了收到這些信號,只需簡單地在一個系統中執行他們。

元件迭代器

如果您只需要一個單一的元件,元件迭代器(安全)及元件區塊迭代器(不安全)最為合適。

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

篩選器

篩選器是一個方便的方式,以基於一個元件集來篩選實體,同時只捕捉系統所需要的必要的元件。篩選器可用於安全(取得/設定)及不安全(指標)的程式碼。

泛型

為了建立一個篩選器,請簡單地使用幀所提供的 篩選器() API。

C#

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

泛型篩選器可含有最多8個元件。
如果您需要更特定,可建立 不含任何 元件集 篩選器。

C#

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

一個 元件集 可以持有最多8個元件。
元件集 被傳送為 不含 參數時,將排除所有帶有至少一個在集之中特定的元件的實體。任何 集確保實體有至少一個或更多個被特定的元件;如果一個實體沒有被特定的元件,它將被篩選器所排除。

迭代篩選器,就如同以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;
}

在這個例子中,您正在直接地修正元件的資料。

篩選器架構

除了常規的篩選器以外,您可以使用 篩選器架構 方法。
為此,針對各個您希望收到的元件類型,您首先需要以 公開的 屬性來定義一個架構。

C#

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

如同一個 元件集,一個 篩選器架構 可以篩選最多8個不同的元件指標。

請注意: 一個作為 篩選器架構 的架構,是 必須 有一個 實體參照 欄位!

在一個 篩選器架構 中的 元件類型 成員 必須是 指標;只有他們將被篩選器填滿。除了元件指標以外,您也可以定義其他變數,然而,這些將被篩選器忽略,並且留給您來管理。

C#

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

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

Frame.Unsafe.FilterStruct<T>()有一個多載,其利用可選的元件集 任何不含 以進一步特定篩選器。

關於次數的注意事項

一個篩選器並不預先知道它將碰到及迭代多少實體。這是因為篩選器在 疏鬆集 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 f) {
        MyFilter result = default;

        if (f.Unsafe.ComponentGetter<MyFilter>().TryGet(f, f.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 f) {
      _myFilterGetter = f.Unsafe.ComponentGetter<MyFilter>();
    }

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

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

篩選策略

您將經常進入一個情況,就是您將有許多實體,但是您只要他們的一個子集。先前我們已經介紹了在Quantum中可用的元件和工具以篩選他們;在這個章節,我們將呈現利用他們的策略。
請注意: 最好 的方法將取決於您自己的遊戲及其系統。我們建議採取下列策略作為一個起點,以建立一個適合您的獨特情況的策略。

請注意:所有下列使用的術語都已被內部地建立,以封裝其他冗長的概念。

微元件

雖然許多實體可能使用相同的元件類型,只有少數實體使用相同的元件組合。一個進一步特製化他們的組合的方法是使用 微元件微元件 是高度特製化的元件,附有資料,以針對一個特定系統或行為。他們的獨特性將允許您來建立篩選器,其可以快速識別帶有它的實體。

旗標元件

一個常見的識別實體的方法是透過新增一個 旗標元件 到它們。在ECS中,旗標 的概念不是本質上存在的概念,Quantum也不支援 實體類型;所以 旗標元件 到底是什麼?他們是持有少數或沒有持有資料的元件,以識別實體為唯一目的而被建立。

舉例而言,在一個基於團隊的遊戲中,您可以有:

  1. 一個「團隊」元件,針對團隊A及團隊B附有一個列舉;或是
  2. 一個「團隊A」及「團隊B」元件。

選項1在主要目的是從檢視來輪詢資料時相當有用,而選項2將讓您享受到在相關模擬系統中的篩選性能所帶來的好處。

請注意: 有時候一個旗標元件也被稱為標籤元件,因為標籤實體和旗標實體使用上可以交換使用。

計數

在模擬中現存的T元件的數量可使用Frame.ComponentCount<T>()來擷取。舉例而言,當與旗標元件結合使用時,它啟用一個特定單位類型的一個快速計數。

新增/移除

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

全域清單

旗標元件的一個替代選項,雖然是一個「較不」ECS化的選項,是在FrameContext.User.cs中保留全域清單。如果您需要追蹤N個團隊,則這樣做並不一定能隨之縮放,但對於子集有限的集而言,這很方便。

如果您希望聚焦在所有少於50%健康值的玩家,您可以持有一個全域清單並且做下列動作:

  • 在模擬開始時有一個系統,其新增/移除entity_refs到清單;
  • 在所有後續系統中,使用該相同的清單。

請注意: 如果您只需要偶爾識別這些情況類型,我們建議在需要時動態地計算它,而非維持全域清單。

Back to top