Components
Component用來表示你的資料。Entity隻是一個辨別,用來将Component收集到一起。System提供行為。
實際上,Component是個struct結構體,這些結構體繼承以下接口:
- IComponentData – 用于general purpose 及 chunk components。
- IBufferElementData – 用于為Entity建立dynamic buffer資料。
- ISharedComponentData – 用于在一個archetype内的entities根據值來分類或分組(共享同一個ISharedComponentData的Entity會被組織到一起)。
- ISystemStateComponentData – 用于辨別Entity的特定系統狀态,以及用來檢測Entity的建立,銷毀。
- ISharedSystemStatecomponentData – 組合了共享,及系統狀态資料。
以上元件類型的描述,比較抽象,不明白可以繼續,後面會針對每一種做介紹。
Entity Manager根據Entity上的元件的組合來定義Archetype,并按照Archetype組織entities。同一個Archetype的所有entities的components被存儲在一個叫做chunk的記憶體區域。一個chunk中的所有的entities擁有相同的component archetype(元件原型)。
這張圖檔展示了chunk是如何根據archetype來存儲元件的資料的。Shared components和chunk components不在這張圖中,因為他們存儲在chunk外。一個這種類型的資料對象,被所有适用的entities使用。此外,也可以在 chunk外存儲dynamic buffers。盡管這些類型的components不被存儲在chunk中,你依然可以用其它components的查詢周遊方式通路它們。
General Purpose Components
Unity中的ComponentData(ECS中的component)對象,是個僅存儲一個Entity資料的結構體。ComponentData不能包含方法,除了通路資料的函數。所有的遊戲邏輯都應該在System中實作。這相當于是老的Unity中的Component,隻是隻包含變量。
ECS提供了 IComponentData,可以實作該接口。
IComponentData
傳統的Unity components(包括MonoBehaviour)是面向對象的,包含了資料和方法。IComponentData是純粹的ECS類型的元件,隻有資料,沒有方法。同時它是結構體,是以預設指派是通過值拷貝,而不是引用。修改它的資料通常要像下面這樣:
var transform = group.transform[index]; // 讀取資料
transform.heading = playerInput.move; // 修改資料
transform.position += deltaTime * playerInput.move * settings.playerMoveSpeed;
group.transform[index] = transform; // 将資料寫回去
IComponentData結構體不能持有托管對象的引用,因為所有的ComponentData都是建立在無垃圾回收的chunk memory上(不需要)。
Shared Component Data
Shared components 是一種特殊的資料元件,可以根據shared componet的不同數值,來将entities進一步細分。當将一個shared component添加到一個entity,Entity Manager會将所有共享該shared component(值相同,則是一個),存儲到一個Chunk。Shared components允許你的系統一起處理處理相似的entities。例如,Rendering.RenderMesh,定義在Hybrid.rendering包中的shared component,還包括mesh, material, receiveShadows等。渲染時,同時處理擁有相同的該類元件的3D對象會大大提升效率。因為這些元件時shared components,是以Entity Manager會把比對的entities放到同一個chunk中,這樣進行周遊渲染時會更加有效率。
注意:過度使用shared component會導緻Chunk使用率降低。因為這引入了Archetype和每個值的shared component之間的組合數量的急劇擴大,而每種組合都要配置設定Chunk,導緻配置設定更多的記憶體。避免添加不是必須的shared components。可以利用Entity Debugger來檢視目前的Chunk使用率。
向一個Entity添加,删除component,或者改變SharedComponent的值,Entity Manage都會将該Entity移動到其它比對的Chunk,或者建立新的Chunk。
IComponentData通常适用于entities之間不同的資料,比如世界位置,打擊點,粒子存活時間,等。ISharedComponentData适用于多個entities共享的資料。例如RenderMesh,所有執行個體化自同一個模型的對象,共享同一個RenderMesh。
[System.Serializable]
public struct RenderMesh : ISharedComponentData
{
public Mesh mesh;
public Material material;
public ShadowCastingMode castShadows;
public bool receiveShadows;
}
ISharedComponentData的最大好處,是每個對象在共享資料上的記憶體占用為0。
使用ISharedComponentData将使用同樣的InstanceRenderer(渲染資料)的entities組織到一起,來更加高效地進行渲染。因為這些資料是線性排布的。
參考:RenderMeshSystemV2
Some important notes about SharedComponentData
使用SharedComponentData需要重點注意的幾點:
- 引用同一個SharedComponentData的entities被組織到同一個Chunk中。存儲SharedComponentData的索引,隻在Chunk中儲存,而不是在entities中儲存。是以SharedComponentData對于entities的記憶體占用為0.
- 使用EntityQuery可以周遊所有相同類型的entities。
- 此外,可以通過調用EntityQuery.SetFilter()來周遊指定SharedComponentData的值的entities。基于資料的排布,這種周遊消耗并不高。
- 使用EntityManager.GetAllUniqueSharedComponents可以得到所有添加到entities上的唯一的SharedComponentData(其值唯一,沒有變體)。(大概是這個意思,不太了解這句話)。
- SharedComponentData自動維護引用計數。
- SharedComponentData應該盡量少的改變。改變一個SharedComponentData會導緻調用memcpy來将引用它的entities的Component Data拷貝到其它Chunk。
System State Components
SystemStateComponentData的作用,是跟蹤資源在系統内部的狀态,以提供機會在适當的時機建立和銷毀資源,而不是依靠某個回調函數。
SystemStateComponentData和SystemStateSharedComponentData跟ComponentData以及SharedComponentData一樣,各自都有一個重要的概念:
SystemStateComponentData在銷毀entity時,不會被銷毀。
銷毀的簡單流程如下:
找到引用該entity ID的所有的Component Data
删除這些components
回收entity ID,以重複使用。
然而,如果entity有SystemStateComponentData,它不會被移除。這給system機會來清理該entity ID的資源以及相關狀态。隻有當SystemStateComponentData被移除後,entity ID才能被重複使用。
Motivation
目的
- System可能需要維持基于ComponentData的一個内部狀态。例如,資源是否配置設定。
- System需要能管理由其它系統對該值或狀态的更新。例如,值改變了,或者相關元件添加或删除。
- “無回調”,時ECS設計準則的重要概念。
Concept
一個用法是鏡像一個使用者的元件的内部狀态。
例如:
- FooComponent(ComponentData,使用者建立)
- FooStateComponent(SystemComponentData,系統建立)
Detecting Component Add
監測添加元件
當添加FooComponent時,FooStateComponent還不存在。Foo System查詢到添加了FooComponent但是沒有FooStateComponent,則可以推斷出該FooComponent是新添加的。同時Foo System會添加FooStateComponent及其它需要的内部狀态。
Detecting Component Remove
檢測删除元件
當删除FooComponent元件時,FooStateComponent依然存在。Foo System更新時發現有FooStateComponent但是沒有FooComponent,則可以推斷出FooComponent被删除了。這時Foo System會删除FooStateComponent并根據需要恢複其它内部狀态。
Detecting Destroy Entity
監測銷毀實體
實體的銷毀,可以簡化為步驟:
- 查找到引用該entity ID的所有的components
- 删除這些components
- 回收 entity ID
然而,調用Destroy Entity時,SystemStateComponentData沒有被移除,entity ID也不會被回收,直到最後一個元件被删除。這讓系統可以用與删除元件相同的方式,清理内部狀态。
SystemStateComponent
SystemStateComponent和ComponentData類似,用法也類似:
struct FooStateComponent : ISystemStateComponent
{
}
對于成員,也可以用public,private,protected來修飾可通路性。但是,我們最好在建立該元件的系統内更新,改變它的值,而在該系統之外,是隻讀的。
SystemStateSharedComponent
SystemStateSharedComponent與SharedComponentData用法類似:
struct FooStateSharedComponent : ISystemStateSharedComponentData
{
public int Value;
}
Example system using state components
下面的例子,用一個簡單的系統,展示了如何利用system state component來管理entities。例子定義了一個普通的IComponentData和它的ISystemStateComponentData執行個體,還定義了三個對該類entities的query查詢:
- m_newEntities 選擇有普通component但是沒有system state component的entities,這些entities是新建立的。系統執行job,為它們添加system state component。
- m_activeEneities選擇同時有component和system state component的entities。在實際應用中,其它系統也可能會處理或者銷毀這些entities。
- m_destroyedEntities選擇了有system state component但是沒有component的entities,這些實體是被本系統,或者其它系統删除的entities。該系統運作一個job将system state component從entities上删除,以便ECS可以回收該entity ID。
注意我們的這個簡化的例子,并沒有處理任何狀态,system state component的一個作用就是跟蹤資源的配置設定和清理。
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using UnityEngine;
public struct GeneralPurposeComponentA : IComponentData
{
public bool IsAlive;
}
public struct StateComponentB : ISystemStateComponentData
{
public int State;
}
public class StatefulSystem : JobComponentSystem
{
private EntityQuery m_newEntities;
private EntityQuery m_activeEntities;
private EntityQuery m_destroyedEntities;
private EntityCommandBufferSystem m_ECBSource;
protected override void OnCreate()
{
// Entities with GeneralPurposeComponentA but not StateComponentB
m_newEntities = GetEntityQuery(new EntityQueryDesc()
{
All = new ComponentType[] {ComponentType.ReadOnly<GeneralPurposeComponentA>()},
None = new ComponentType[] {ComponentType.ReadWrite<StateComponentB>()}
});
// Entities with both GeneralPurposeComponentA and StateComponentB
m_activeEntities = GetEntityQuery(new EntityQueryDesc()
{
All = new ComponentType[]
{
ComponentType.ReadWrite<GeneralPurposeComponentA>(),
ComponentType.ReadOnly<StateComponentB>()
}
});
// Entities with StateComponentB but not GeneralPurposeComponentA
m_destroyedEntities = GetEntityQuery(new EntityQueryDesc()
{
All = new ComponentType[] {ComponentType.ReadWrite<StateComponentB>()},
None = new ComponentType[] {ComponentType.ReadOnly<GeneralPurposeComponentA>()}
});
m_ECBSource = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
}
struct NewEntityJob : IJobForEachWithEntity<GeneralPurposeComponentA>
{
public EntityCommandBuffer.Concurrent ConcurrentECB;
public void Execute(Entity entity, int index, [ReadOnly] ref GeneralPurposeComponentA gpA)
{
// Add an ISystemStateComponentData instance
ConcurrentECB.AddComponent<StateComponentB>(index, entity, new StateComponentB() {State = 1});
}
}
struct ProcessEntityJob : IJobForEachWithEntity<GeneralPurposeComponentA>
{
public EntityCommandBuffer.Concurrent ConcurrentECB;
public void Execute(Entity entity, int index, ref GeneralPurposeComponentA gpA)
{
// Process entity, possibly setting IsAlive false --
// In which case, destroy the entity
if (!gpA.IsAlive)
{
ConcurrentECB.DestroyEntity(index, entity);
}
}
}
struct CleanupEntityJob : IJobForEachWithEntity<StateComponentB>
{
public EntityCommandBuffer.Concurrent ConcurrentECB;
public void Execute(Entity entity, int index, [ReadOnly] ref StateComponentB state)
{
// This system is responsible for removing any ISystemStateComponentData instances it adds
// Otherwise, the entity is never truly destroyed.
ConcurrentECB.RemoveComponent<StateComponentB>(index, entity);
}
}
protected override JobHandle OnUpdate(JobHandle inputDependencies)
{
var newEntityJob = new NewEntityJob()
{
ConcurrentECB = m_ECBSource.CreateCommandBuffer().ToConcurrent()
};
var newJobHandle = newEntityJob.ScheduleSingle(m_newEntities, inputDependencies);
m_ECBSource.AddJobHandleForProducer(newJobHandle);
var processEntityJob = new ProcessEntityJob()
{ConcurrentECB = m_ECBSource.CreateCommandBuffer().ToConcurrent()};
var processJobHandle = processEntityJob.Schedule(m_activeEntities, newJobHandle);
m_ECBSource.AddJobHandleForProducer(processJobHandle);
var cleanupEntityJob = new CleanupEntityJob()
{
ConcurrentECB = m_ECBSource.CreateCommandBuffer().ToConcurrent()
};
var cleanupJobHandle = cleanupEntityJob.ScheduleSingle(m_destroyedEntities, processJobHandle);
m_ECBSource.AddJobHandleForProducer(cleanupJobHandle);
return cleanupJobHandle;
}
protected override void OnDestroy()
{
// Implement OnDestroy to cleanup any resources allocated by this system.
// (This simplified example does not allocate any resources.)
}
}
Dynamic Buffer Components
Dynamic Buffers
DynamicBuffer是一種支援可變大小的彈性buffer component data,可以存放一定數量的元素資料。如果空間不足會配置設定堆記憶體塊來擴充。
該方法的記憶體管理是自動的。DynamicBuffer的記憶體是由EntityManager管理的,是以當DynamicBuffer component删除時,其記憶體也會被釋放。
Fixed Array 固定長度數組已經被Dynamicbuffer替代并移除。
Declaring Buffer Element Types
聲明元素類型
需要用指定的類型來聲明buffer:
// This describes the number of buffer elements that should be reserved
// in chunk data for each instance of a buffer. In this case, 8 integers
// will be reserved (32 bytes) along with the size of the buffer header
// (currently 16 bytes on 64-bit targets)
[InternalBufferCapacity(8)]
public struct MyBufferElement : IBufferElementData
{
// These implicit conversions are optional, but can help reduce typing.
public static implicit operator int(MyBufferElement e) { return e.Value; }
public static implicit operator MyBufferElement(int e) { return new MyBufferElement { Value = e }; }
// Actual value each buffer element will store.
public int Value;
}
以上看起來是定義了一個元素類型,而不是一個buffer,這種設計由2個好處:
- 通過派生IBufferElementData,我們可以支援更多類型的buffer。而且這些類型可以有更多的資料成員。
- 我們可以将buffer定義到EntityArchetype原型中,就像一個comopent。
Adding Buffer Types To Entities
添加buffer
調用方法AddBuffer():
entityManager.AddBuffer<MyBufferElement>(entity);
利用原型:
Entity e = entityManager.CreateEntity(typeof(MyBufferElement));
Accessing Buffers
有多種方法可以通路buffers,
直接在主線程通路:
DynamicBuffer<MyElementBuffer> buffer = entityManager.GetBuffer<MyElementBuffer>(entity);
基于Entity通路
可以在JobComponentSystem中基于每個entity進行通路
var lookup = GetBufferFromEntity<MyBufferElement>();
var buffer = lookup[myEntity];
buffer.Append(7);
buffer.RemoveAt(0);
Reinterpreting Buffers (Experimental)
Buffer類型強制轉換(實驗特性)
Buffer可以被強制轉換成長度相同的類型的Buffer:
var intBuffer = entityManager.GetBuffer<MyBufferElement>().Reinterpret<int>();
将MyBufferElement類型的Buffer,轉換成int類型。因為它們的長度一緻。
需要注意的是,因為沒有類型檢查,是以轉成float也不會報錯,但是操作資料會産生不可預料的結果。
Chunk Components
Chunk component data
可以使用chunk components将特定的chunk(内的entities)和data關聯起來。
Chunk components包含的資料,将應用到指定chunk中的所有entities上。例如,如果有一些表示3D對象的entities(它們在一個或者及個chunk裡),你可以将它們的bounding box,存儲到一個chunk component裡。
接口:IComponentData
Chunk components是chunk内的entities的archetype原型的一部分。是以當向一個entity添加,或者删除chunk component時,該entity會被移動到其它的chunk,因為它的archetype改變了。當然,該改變不會作用到該chunk的其它entity上。
如果在通路entity時改變了chunk component的值,那麼,該改變将應用到該entity chunk上的所有的entities(其實是因為chunk component data是共享的)。如果為一個entity添加了一個chunk component改變了它的archetype,導緻該entity被移動到一個已有的chunk中,不會改變新的chunk中的chunk component data的值。如果entity是被移動到了一個新建立的chunk總,則該新chunk中的chunk component data 保留第一個entity的值。
使用ComponentData 和Chunk Component Data之間,主要的差別,是添加,設定,移除時調用的接口不同。Chunk component也由相應的ComponentType函數,用來定義entity archetype和queries。
相關的APIs:
Purpose | Function |
Declaration | IComponentData |
ArchetypeChunk methods | |
Read | GetChunkComponentData(ArchetypeChunkComponentType) |
Check | HasChunkComponent(ArchetypeChunkComponentType) |
Write | SetChunkComponentData(ArchetypeChunkComponentType, T) |
EntityManager methods | |
Create | AddChunkComponentData(Entity) |
Create | AddChunkComponentData(EntityQuery, T) |
Create | AddComponents(Entity,ComponentTypes) |
Get type info | GetArchetypeChunkComponentType(Boolean) |
Read | GetChunkComponentData(ArchetypeChunk) |
Read | GetChunkComponentData(Entity) |
Check | HasChunkComponent(Entity) |
Delete | RemoveChunkComponent(Entity) |
Delete | RemoveChunkComponentData(EntityQuery) |
Write | EntityManager.SetChunkComponentData(ArchetypeChunk, T) |
Declaring a chunk component
聲明IComponentData來定義chunk components。
public struct ChunkComponentA : IComponentData
{
public float Value;
}
Creating a chunk component
可以直接添加chunk component,利用目标chunk裡的一個entity或者利用entity query選擇一組目标chunks。不能在Job内添加Chunk components,也不能用EntityCommandBuffer建立。
還可以将chunk component作為EntityArchetype的一部分,或者添加到ComponentType list中來建立entities的同時,為存儲該原型的entities chunk建立chunk component,類型參數定義為:ComponentType.ChunkComponent<T>或者ComponentType.ChunkComponentReadOnly<T>。
用目标chunk的一個entity建立:
EntityManager.AddChunkComponentDat<ChunkComponentA>(oneEntity);
用這種方法,不能馬上為chunk component設定值。
用EntityQuery建立:
用給定的entity query選擇你想要添加chunk component的所有的entity chunk,并用EntityManager.AddChunkComponentData<T>()方法
EntityQueryDesc ChunksWithoutComponentADesc = new EntityQueryDesc()
{
None = new ComponentType[] {ComponentType.ChunkComponent<ChunkComponentA>()}
};
ChunksWithoutChunkComponentA = GetEntityQuery(ChunksWithoutComponentADesc);
EntityManager.AddChunkComponentData<ChunkComponentA>(ChunksWithoutChunkComponentA,
new ChunkComponentA() {Value = 4});
用這種方法,可以為所有的chunk建立chunk components并用同樣的值進行初始化。
用EntityArchetype:
用archetype或者component type清單建立entities時,将chunk component類型添加到archetype中。
ArchetypeWithChunkComponent = EntityManager.CreateArchetype(
ComponentType.ChunkComponent(typeof(ChunkComponentA)),
ComponentType.ReadWrite<GeneralPurposeComponentA>());
var entity = EntityManager.CreateEntity(ArchetypeWithChunkComponent);
或者component type清單:
ComponentType[] compTypes = {ComponentType.ChunkComponent<ChunkComponentA>(),
ComponentType.ReadOnly<GeneralPurposeComponentA>()};
var entity = EntityManager.CreateEntity(compTypes);
用上面這些方法,如果建立新的entity時建立了新的chunk,則新建立的chunk component是預設值。如果是已經存在的chunk components(隻是改變了現有entity的archetype導緻移動到新的chunk時),其值不會改變。
Reading a chunk component
可以用目标chunk的一個entity,或者chunk的ArchetypeChunk對象來通路chunk component。
用chunk中的entity: EntityManager.GetChunkComponentData<T>:
if(EntityManager.HasChunkComponent<ChunkComponentA>(entity))
chunkComponentValue = EntityManager.GetChunkComponentData<MyChunkComponent>(entity);
可以用一下方法選擇所有特定的entities來通路:
Entities.WithAll(ComponentType.ChunkComponent<MyChunkComponent>().ForEach(
(Entity entity_=>
{
var compValue = EntityManger.GetChunkComponentData<MyChunkComponent>(entity);
}
需要注意的是,不能直接将chunk component傳遞給query的for-each邏輯,而應該傳遞Entity對象,并通過EntityManger來通路chunk component。
用ArchetypeChunk執行個體
給定chunk,可以通過調用EntityManger.GetChunkComponentData<T>來通路chunk component。下面的例子,周遊了所有比對query的chunks并通路它們的chunk component:ChunkComponentA
var chunks = ChunksWithChunkComponentA.CreateArchetypeChunkArray(Allocator.TempJob);
foreach (var chunk in chunks)
{
var compValue = EntityManager.GetChunkComponentData<ChunkComponentA>(chunk);
//..
}
chunks.Dispose();
Updating a chunk component
可以更新給定的chunk的chunk component。在IJobChunk Job裡,可以調用ArchetypeChunk.SetChunkComponentData。在主線程内,可以調用EntityManager.SetChunkComponentData。需要注意的,不能再IJobForEach内通路chunk components,因為不能通路ArchetypeChunk和EntityManager。
用ArchetypeChunk執行個體:
ArchetypeChunk chunk;
EntityManager.SetChunkComponentData<ChunkComponentA>(chunk,
new ChunkComponentA({Value=7});
用entity:
Entity entity;
EntityManger.SetChunkComponentData<ChunkComponentA>(entity,
new ChunkComponentA({Value=8});
Reading and writing in a JobComponentSystem
在JobComponentSystem内的IJobChunk,可以将chunk作為參數傳遞給IJobChunk的Execute方法,來通路chunk components。像IJobChunk Job的任何componen data一樣,需要将ArchetypeChunkComponentType<T>對象作為參數,傳遞給IJobChunk 的資料成員來通路component。
下面的系統定義了一個Query,來選擇包含ChunkComponentA的所有的entities和chunks。然後用一個IJobChunk來周遊chunks并通路每個chunk components。Job用ArchetypeChunk的GetChunkComponentData和SetChunkComponentData來讀寫chunk component data。
using Unity.Burst;
using Unity.Entities;
using Unity.Jobs;
public class ChunkComponentChecker : JobComponentSystem
{
private EntityQuery ChunksWithChunkComponentA;
protected override void OnCreate()
{
EntityQueryDesc ChunksWithComponentADesc = new EntityQueryDesc()
{
All = new ComponentType[]{ComponentType.ChunkComponent<ChunkComponentA>()}
};
ChunksWithChunkComponentA = GetEntityQuery(ChunksWithComponentADesc);
}
[BurstCompile]
struct ChunkComponentCheckerJob : IJobChunk
{
public ArchetypeChunkComponentType<ChunkComponentA> ChunkComponentATypeInfo;
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
{
var compValue = chunk.GetChunkComponentData(ChunkComponentATypeInfo);
//...
var squared = compValue.Value * compValue.Value;
chunk.SetChunkComponentData(ChunkComponentATypeInfo,
new ChunkComponentA(){Value= squared});
}
}
protected override JobHandle OnUpdate(JobHandle inputDependencies)
{
var job = new ChunkComponentCheckerJob()
{
ChunkComponentATypeInfo = GetArchetypeChunkComponentType<ChunkComponentA>()
};
return job.Schedule(ChunksWithChunkComponentA, inputDependencies);
}
}
如果隻需要讀取,而不寫資料,則用ComponentType.ChunkComponentReadOnly,可以提高效率。
Deleting a chunk component
調用EntityManager.RemoveChunkComponent方法來删除chunk component。可以移除指定entity的chunk component,或通過entity query來選擇chunks,并移除所有chunks的chunk components。如果删除一個entity的chunk component,該entity會被移動到其它chunk,因為它的archetype改變了。
Using a chunk component in a query
在query中使用chunk component,需要用ComponentType.ChunkComponent<T>或者ComponentType.ChunkComponentReadOnly<T>來指定類型。
EntityQueryDesc
下面的query描述,可以用來建立entity query 來選擇所有包含ChunkComponentA的chunks以及entities。
EntityQueryDesc ChunkWithChunkComponentADesc = new EntityQueryDesc()
{
All = new ComponentType[]{ComponentType.ChunkComponent<ChunkComponentA>()}
}
EntityQueryBuilder lambda函數:
下面的query周遊所有entities
Entities.WithAll(ComponentType.ChunkComponentReadOnly<ChunkCompA>())
.ForEach((Entity ent)=>
{
var chunkComponentA = EntityManager.GetChunkComponentData<ChunkCmpA>(ent);
}
);
注:不能将一個chunk component直接作為lambda的參數,隻能通過傳遞entity,用ComponentSystem.EntityManager來通路chunk components。改變chunk component的值,會改變該chunk的所有的entities的值,不會導緻entity移動。