笨木头  2019-12-05 14:31     ECS入门     阅读(8238)     评论(0)
转载请注明,原文地址: http://www.benmutou.com/archives/2840
文章来源:笨木头与游戏开发
我们已经知道,继承了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包,是预览版,不代表正式版的时候也适用。

转发本系列文章的朋友请注意,带上原文链接和原文日期,避免误导未来使用正式版的开发者。

0 条评论
发表评论
粤ICP备16043700号

本博客基于 BlazorAnt Design Blazor 开发