我们已经知道,继承了IComponentData的结构体是组件,而实际上,为了解决特定问题,ECS还提供了更多不同类型的组件。
我会简单地给大家介绍其他类型的组件,但是不做深入的讲解,因为在实际开发中可能需要根据不同的情况考虑使用哪种组件,而我还没实际使用ECS开发过,所以不想做过多的介绍,怕误导大家。
1.ISharedComponentData(共享组件)
想象一下,当我们创建一波同类型的怪物的时候,它们是不是拥有相同的材质?
并且,正常情况下,怪物的材质是不会发生变化的,比如史莱姆,它会一直是史莱姆的样子。
于是,我们所有的史莱姆也许可以共用一个材质对象,哪怕创建了1000个史莱姆,它们也只需要产生一个材质对象,节省了内存。
通过SharedComponentData(共享组件)就可以实现以上的想法。
我们都知道,继承了IComponentData就是组件:
public struct SomeOtherComponent : IComponentData{}
而现在,继承了ISharedComponentData就是共享组件:
public struct MeshSharedComponent : ISharedComponentData
{
public int mesh;
}
如果给某些实体添加共享组件,那么,它们将共用这个组件。
共享组件仍然是组件,所以组件的一些规则它也会遵守:
a.对于拥有相同类型组件的实体——原型(Archetype)相同,它们会保存在一个块(Chunk)中,不管是否包含了共享组件
b.一旦修改实体的共享组件的值,则该实体会被存放到一个新的块(Chunk)中,因为它的共享组件发生了变化,相当于使用了新的共享组件
c.一旦实体新增了其他任意组件,则该实体会被存放到一个新的块(Chunk),因为它的原型(Archetype)发生了改变
举个例子,比如下面的代码创建了3个实体:
通过AddComponentData可以添加组件,而通过AddSharedComponentData可以添加共享组件,三个实体的组件情况如下:
Entity1:添加了MeshSharedComponent(自定义的共享组件)
Entity2:添加了MeshSharedComponent(自定义的共享组件)
Entity3:添加了MeshSharedComponent(自定义的共享组件)、并且添加了SomeOtherComponent(自定义的普通组件)
则,
Entity1和Entity2属于同一个块(Chunk),并且它们拥有同一个共享组件。
Entity3自己在一个独立的块(Chunk),因为它的组件数量和类型和Entity1、Entity2都不一致。
Entity3的共享组件和Entity1、Entity2的共享组件不是同一个。
使用共享组件是要非常注意的,我们要尽量选择那些不会经常变动的组件作为共享组件,因为修改实体的共享组件的值,会导致这个实体被移动到新的块(Chunk)中,这种移动块的操作是比较消耗时间的。(修改普通组件的值是不会改变实体的块的)
共享实体可以节省内存,特别是要创建大量相同物体时,但也不要滥用,否则有可能降低效率。
2.ChunkComponent(块组件)
块组件其实也是普通组件(仍然继承IComponentData),只不过它有专门的函数来新增、修改、删除等。
比如,添加普通组件是调用EntityManager.AddComponentData(entity, normalComponent)而添加块组件是调用EntityManager.AddChunkComponentData<ChunkComponentA>(entity)
所以,块组件是什么?
块组件和共享组件有点相似,块组件也是所有实体共用的组件,块组件也遵守组件的一些基本规则。
但是,块组件和共享组件的最大区别是:修改块组件的值不会导致实体被移动到新的块(Chunk),而是将实体所在块的所有实体的块组件的值也一起修改。
官方原文:
If you change the value of a chunk component using an entity in that chunk, it changes the value of the chunk component common to all the entities in that chunk
我的理解是,块中的所有实体共用这个块组件,所以,当块组件的值发生变化时,所有实体都一样。至于这个块组件是整个块中只有一个,还是所有实体都有一个,我暂时没有从官方文档中找到描述。
总之,修改块组件的值,不会导致实体被移动到其他的块,而是块中所有的实体的块组件的值都改变了。
至于块组件的其他操作函数,大家需要用到的时候再看官方手册就好了,都很简单,这里不多说:https://docs.unity3d.com/Packages/com.unity.entities@0.3/manual/ecs_chunk_component.html
3.ISystemStateComponent(状态组件)
首先,有个很难受的消息:ECS是没有回调的。
我相信很多人和我一样,非常依赖回调,毕竟用起来很方便。
但ECS没有回调,怎么办?
比如一个很简单的需求,我怎么知道某个实体是不是被删除了?
可能在以前的话,是订阅某个实体的死亡消息,在实体死亡的时候回调。
现在的话,需要利用状态组件(SystemStateComponent)。
状态组件是一个很简单的东西,它就是很普通的组件(继承了ISystemStateComponent接口的结构体就是状态组件),只不过,在实体被销毁时,状态组件是不会被删除的。
比如,EntityA有三个组件:ComponentA、ComponentB、SystemStateComponentC。
当EntityA被销毁时,它的ComponentA和ComponentB都被删除了,但是,SystemStateComponentC仍然存在,而此时EntityA实际上是没被完全销毁的。
这有什么用呢?我们可以通过筛选实体组件来判断实体是否被"销毁"。
比如,我们筛选SystemStateComponentC组件(EntityQueryDesc的All,下一篇会介绍),并且排除ComponentA和ComponentB(EntityQueryDesc的None,下一篇会介绍)。
当实体只剩下SystemStateComponentC组件时,我们就成功筛选到数据,成功筛选到数据就代表实体已经被"销毁"了,它剩下一个状态组件。这就相当于实体死亡了,我们也能在实体死亡时做一些我们希望的操作。
再总结一下,状态组件就是一个不会因为实体销毁而被删除的组件,除非我们主动删除。
4.ISystemStateSharedComponentData(状态共享组件)
状态共享组件和共享组件的用法是一样的,只不过状态共享组件多了一个功能:
不会因为实体销毁而被删除的组件,除非我们主动删除。
继承了ISystemStateSharedComponentData接口的结构体就是状态共享组件。
5.IBufferElementData(动态队列/缓冲区/数组)
动态缓冲区是类似List集合的一种组件,可以给实体添加多个相同的组件。
我们来看个最简单的例子,先创建一个BufferElementData组件(继承IBufferElementData接口):
using Unity.Entities;
public struct BufferComponent : IBufferElementData
{
public int num;
}
然后是创建实体,我们用比较简单的方式创建,把下面这个类挂在一个空GameObject上即可:
using Unity.Entities;
using UnityEngine;
public class Spawner_BufferElementData : MonoBehaviour
{
public GameObject Prefab;
void Start ()
{
/* 创建实体时需要指定配置,这里涉及到World的概念,可以先不管,照抄就是了 */
var settings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, null);
/* 从我们的prefab中创建一个实体对象 */
var entityFromPrefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(Prefab, settings);
/* 实体管理器 */
var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
/* 新的实体1 */
var entity1 = entityManager.Instantiate(entityFromPrefab);
/* 添加一个Buffer组件到实体 */
DynamicBuffer<BufferComponent> buffer = entityManager.AddBuffer<BufferComponent>(entity1);
/* 给Buffer增加三个组件对象 */
buffer.Add(new BufferComponent { num = 1 });
buffer.Add(new BufferComponent { num = 2 });
buffer.Add(new BufferComponent { num = 3 });
entityManager.DestroyEntity(entityFromPrefab);
}
}
具体创建实体的代码是之前介绍过的,这里唯一不同是,我给实体添加了一个Buffer组件。通过调用EntityManager.AddBuffer来给实体添加Buffer组件(如果在Job中添加,则需要利用EntityCommandBuffer),AddBuffer会返回一个对象,这个对象就是新增后的Buffer。
然后我给新增的Buffer添加了3个元素,其实用法和一般的List是差不多的,理解起来也是类似的。
最后看看System的代码:
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using UnityEngine;
public class BufferSystem : JobComponentSystem
{
struct DataSpawnJob : IJobForEachWithEntity_EB<BufferComponent>
{
public void Execute (Entity entity,
int index,
DynamicBuffer<BufferComponent> buffer)
{
int sum = 0;
foreach (int number in buffer.Reinterpret<int>())
{
sum += number;
}
Debug.Log("Sum of all buffers: " + sum);
}
}
protected override JobHandle OnUpdate (JobHandle inputDeps)
{
return new DataSpawnJob().Schedule(this, inputDeps);
}
}
这次我们的Job继承了IJobForEachWithEntity_EB接口,IJobForEachWithEntity我们之前讲过了,那IJobForEachWithEntity_EB又是啥?其实和IJobForEachWithEntity差不多,只不过IJobForEachWithEntity_EB是用来筛选Buffer组件的。
然后我们通过调用buffer.Reinterpret<int>把这个Buffer转换为了int类型的Buffer,这是因为BufferComponent只有一个字段,且类型是int,所以可以转换为int类型的Buffer。
如果指定的转换类型错误,则会报错。比如,我们给BufferComponent再加一个int字段,那么它就有2个int字段,这种情况下直接将这个Buffer转换为int类型的Buffer是不行的,因为系统不知道应该如何转换。
剩余的用法和List没有差别了。
关于IBufferElementData的更多细节,可以查看官方手册,这里不介绍这么多了:https://docs.unity3d.com/Packages/com.unity.entities@0.2/manual/dynamic_buffers.html
注意,本系列教程基于DOTS相关预览版的Package包,是预览版,不代表正式版的时候也适用。
转发本系列文章的朋友请注意,带上原文链接和原文日期,避免误导未来使用正式版的开发者。