天天看點

Unity Entity Component System 3 --- Components 元件

Components

Component用來表示你的資料。Entity隻是一個辨別,用來将Component收集到一起。System提供行為。

實際上,Component是個struct結構體,這些結構體繼承以下接口:

  1. IComponentData – 用于general purpose 及 chunk components。
  2. IBufferElementData – 用于為Entity建立dynamic buffer資料。
  3. ISharedComponentData – 用于在一個archetype内的entities根據值來分類或分組(共享同一個ISharedComponentData的Entity會被組織到一起)。
  4. ISystemStateComponentData – 用于辨別Entity的特定系統狀态,以及用來檢測Entity的建立,銷毀。
  5. ISharedSystemStatecomponentData – 組合了共享,及系統狀态資料。

以上元件類型的描述,比較抽象,不明白可以繼續,後面會針對每一種做介紹。

Entity Manager根據Entity上的元件的組合來定義Archetype,并按照Archetype組織entities。同一個Archetype的所有entities的components被存儲在一個叫做chunk的記憶體區域。一個chunk中的所有的entities擁有相同的component archetype(元件原型)。

Unity Entity Component System 3 --- Components 元件

這張圖檔展示了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需要重點注意的幾點:

  1. 引用同一個SharedComponentData的entities被組織到同一個Chunk中。存儲SharedComponentData的索引,隻在Chunk中儲存,而不是在entities中儲存。是以SharedComponentData對于entities的記憶體占用為0.
  2. 使用EntityQuery可以周遊所有相同類型的entities。
  3. 此外,可以通過調用EntityQuery.SetFilter()來周遊指定SharedComponentData的值的entities。基于資料的排布,這種周遊消耗并不高。
  4. 使用EntityManager.GetAllUniqueSharedComponents可以得到所有添加到entities上的唯一的SharedComponentData(其值唯一,沒有變體)。(大概是這個意思,不太了解這句話)。
  5. SharedComponentData自動維護引用計數。
  6. 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

目的

  1. System可能需要維持基于ComponentData的一個内部狀态。例如,資源是否配置設定。
  2. System需要能管理由其它系統對該值或狀态的更新。例如,值改變了,或者相關元件添加或删除。
  3. “無回調”,時ECS設計準則的重要概念。

Concept

一個用法是鏡像一個使用者的元件的内部狀态。

例如:

  1. FooComponent(ComponentData,使用者建立)
  2. 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

監測銷毀實體

實體的銷毀,可以簡化為步驟:

  1. 查找到引用該entity ID的所有的components
  2. 删除這些components
  3. 回收 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查詢:

  1. m_newEntities 選擇有普通component但是沒有system state component的entities,這些entities是新建立的。系統執行job,為它們添加system state component。
  2. m_activeEneities選擇同時有component和system state component的entities。在實際應用中,其它系統也可能會處理或者銷毀這些entities。
  3. 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個好處:

  1. 通過派生IBufferElementData,我們可以支援更多類型的buffer。而且這些類型可以有更多的資料成員。
  2. 我們可以将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移動。