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;
}
篩選器
篩選器是一個方便的方式,以基於一個元件集來篩選實體,同時只捕捉系統所需要的必要的元件。篩選器可用於安全(取得/設定)及不安全(指標)的程式碼。
泛型
為了建立一個篩選器,請簡單地使用幀所提供的 篩選器
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中的工作方式:
- 篩選器尋找提供給它的元件中,與它相關聯的實體最少的元件(較小的集用於檢查交集);然後,
- 它檢視集並且捨棄任何沒有帶有其他被查詢的元件的實體。
預先知道確切的數字將需要周遊一次篩選器;因為這是一個(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也不支援 實體類型;所以 旗標元件 到底是什麼?他們是持有少數或沒有持有資料的元件,以識別實體為唯一目的而被建立。
舉例而言,在一個基於團隊的遊戲中,您可以有:
- 一個「團隊」元件,針對團隊A及團隊B附有一個列舉;或是
- 一個「團隊A」及「團隊B」元件。
選項1在主要目的是從檢視來輪詢資料時相當有用,而選項2將讓您享受到在相關模擬系統中的篩選性能所帶來的好處。
請注意: 有時候一個旗標元件也被稱為標籤元件,因為標籤實體和旗標實體使用上可以交換使用。
計數
在模擬中現存的T元件的數量可使用Frame.ComponentCount<T>()
來擷取。舉例而言,當與旗標元件結合使用時,它啟用一個特定單位類型的一個快速計數。
新增/移除
如果您只需要 暫時地 附加一個旗標元件或微元件到一個實體,它們仍然是一個合適的選項,因為Add
及Remove
操作都是O(1)。
全域清單
旗標元件的一個替代選項,雖然是一個「較不」ECS化的選項,是在FrameContext.User.cs
中保留全域清單。如果您需要追蹤N個團隊,則這樣做並不一定能隨之縮放,但對於子集有限的集而言,這很方便。
如果您希望聚焦在所有少於50%健康值的玩家,您可以持有一個全域清單並且做下列動作:
- 在模擬開始時有一個系統,其新增/移除entity_refs到清單;
- 在所有後續系統中,使用該相同的清單。
請注意: 如果您只需要偶爾識別這些情況類型,我們建議在需要時動態地計算它,而非維持全域清單。
Back to top