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(组件原型)。

 

这张图片展示了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移动。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值