文章目录
- 1 ECS是什么
- 2 为什么用ECS
- 3 ECS优缺点
- 3.1 优点
- 3.2 缺点
- 4 什么是DOTS
- 5 Entity
- 5.1 Entity是什么
- 5.2 生成Entity
- 5.3 销毁Entity
- 6 Component
- 6.1 IComponentData
- 6.2 ISharedComponentData
- 6.3 IBufferElementData
- 6.4 添加组件
- 6.5 获取组件
- 6.6 更新组件
- 6.7 删除组件
- 7 System
- 8 World
- 9 小结
1 ECS是什么
ECS分别是Entity
,Component
,System
的缩写:
- Entity是实例,作为承载组件的载体,也是框架中维护对象的实体.;
- Component只包含数据,具备这个组件便具有这个功能.;
- System作为逻辑维护,维护对应的组件执行相关操作。
ECS是面向组件编程(也叫面向数据编程),和传统的OOP(面向对象编程)相比,更看重的是组件。
按照ECS的思路,功能拆分成组件,然后把这些细小的组件组合便组成了具备所需功能的对象。
2 为什么用ECS
提一下Unity中的Update方法,相信很多开发者都深有体会,引擎中所有的Update是每帧遍历执行的,引擎中的模块多,很多模块我们往往并不使用,此时带来了很大的消耗,可以考虑使用一下ECS框架,可以完美解决这个问题,《守望先锋》游戏正是使用该框架设计。
ECS在游戏里的运用,最初是用来解决预测和回放问题的,但是由于面向数据的编程结构,天然符合了现代CPU的编程思想,所以目前Unity ECS主要还是推动展现性能方面的优势。
3 ECS优缺点
3.1 优点
为性能而生
更容易写出高度优化和可重用的代码
更能充分利用硬件的性能
原型的数据被紧密地排列在内存中
享受 Burst 编译器带来的魔法
3.2 缺点
对 ECS 的常见观点是:ECS 需要编写很多代码。因此,实现想要的功能需要处理很多样板代码。
4 什么是DOTS
DOTS:高性能多线程式数据导向性技术堆栈。如下图
Unity 构建了名为 Burst 的代码生成器和编译器。
5 Entity
5.1 Entity是什么
实体只是一个 32 位的整数 key,除了实体的组件数据外,不必为实体保存或分配太多内存。实体可以实现游戏对象的所有功能,甚至更多功能,因为实体非常轻量。
实体本身不是对象,也不是一个容器,它的作用是把其组件的数据关联到一起。
在ECS中,实体基本上实现为简单ID。
Unity ECS遵循此,Entity结构定义总结如下。
public struct Entity
{
public int Index;
public int Version;
}
Index是表示实体的ID。由于实体ID可以在删除后重复使用,因此Version有必要区分已删除的实体和新实体。
5.2 生成Entity
Entity entity = EntityManager.CreateEntity();
没有组件的实体是没有意义的,因此组件会添加到此组件中,但如果您知道最初需要的组件,则通过在生成时指定原型来提前添加它们会更有效
EntityArchetype archetype = EntityManager.CreateArchetype(typeof(Position), typeof(Velocity));
Entity entity = EntityManager.CreateEntity(archetype);
// 也可以是缩写
Entity entity = EntityManager.CreateEntity(typeof(Position), typeof(Velocity));
此外,Instantiate它也可以通过复制组件组和现有的实体。
Entity copy = EntityManager.Instantiate(entity);
如果你想在同一时间,以产生大量的实体, NativeArray<Entity>
中使用CreateEntity
,Instantiate
是更有效地使用。
using (var entities = new NativeArray<Entity>(42, Allocator.Temp, NativeArrayOptions.UninitializedMemory))
{
EntityManager.CreateEntity(archetype, entities);
EntityManager.Instantiate(source, entities);
}
5.3 销毁Entity
EntityManager.DestroyEntity(entity);
6 Component
6.1 IComponentData
传统的Unity Component(包括MonoBehaviour)是面向对象的类,所以说它包含了数据和定义行为的方法。IComponentData是纯粹的ECS类型的组件,意味着他不定义行为,只包含数据。IComponentData是结构体(struct)而非类(class),所以他们进行值拷贝而不是引用拷贝。
IComponentData作为struct直接在栈里分配,它不像class一样分配在托管堆里,不进行垃圾回收,是实现所谓“连续紧凑的内存布局”的好结构。
纯粹ECS下,Component名为Component,但要抛弃过去的认识,实际上它就只是纯粹的数据而已,与逻辑完全无关。
IComponentData是最基本的接口。大多数组件实现此接口。
public struct Position : IComponentData
{
public float Value;
}
// 为简单包装器定义隐式类型转换很方便
public struct Velocity : IComponentData
{
public float Value;
public static implicit operator Velocity(float value) => new Velocity { Value = value };
public static implicit operator float(Velocity value) => value.Value;
}
6.2 ISharedComponentData
ISharedComponentData是表示由多个实体共享的数据的接口。
[System.Serializable]
public struct RenderMesh : ISharedComponentData
{
public Mesh mesh;
public Material material;
public ShadowCastingMode castShadows;
public bool receiveShadows;
}
ISharedComponentData由于实现的组件的值存储在块之外,并且只有引用信息存储在块中,因此它可以包含引用类型。但是,它必须是可序列化的,以允许保存和恢复。System.Serializable请务必添加属性以定义它。
6.3 IBufferElementData
IBufferElementData是一个表示数组元素的接口。
ECS无法将同一类型的多个组件与实体关联。因此,如果要拥有相同类型的多个数据,则需要在组件中包含数据数组,但由于基本组件不能使用引用类型,因此它不能包含可变长度数组。因此,Unity ECS提供了一种称为动态缓冲区组件的机制。
动态缓冲区组件是表示可变长度数组的组件,IBufferElementData并指定实现元素类型的类型。
[InternalBufferCapacity(8)]
public struct MyElement : IBufferElementData
{
public int Value;
}
IBufferElementData由于实现类型的数据必须在块内完成,因此它不能包含引用类型。
6.4 添加组件
接口 | 描述 | 初始值 |
AddComponent(Entity, ComponentType) | 任何组件 | - |
AddComponentData<T>(Entity, T) | 基本组件 | ○ |
AddSharedComponentData<T>(Entity, T) | 共享组件 | ○ |
AddBuffer<T>(Entity) | 动态缓冲组件 | - |
AddChunkComponentData<T>(Entity)*4 | 块组件 | - |
AddComponentObject(Entity, object) | 组件对象 | ○ |
EntityManager.AddComponent(entity, typeof(Position));
EntityManager.AddComponentData(entity, new Velocity() { Value = 42f });
6.5 获取组件
接口 | 描述 |
GetComponentData<T>(Entity) | 基本组件 |
GetSharedComponentData<T>(Entity) | 共享组件 |
GetBuffer<T>(Entity) | 动态缓冲组件 |
GetChunkComponentData<T>(ArchetypeChunk) | 块组件 |
GetComponentObject<T>(Entity) | 组件对象 |
Position position = EntityManager.GetComponentData<Position>(entity);
6.6 更新组件
接口 | 描述 |
SetComponentData<T>(Entity, T) | 基本组件 |
SetSharedComponentData<T>(Entity, T) | 共享组件 |
SetChunkComponentData<T>(ArchetypeChunk, T) | 块组件 |
EntityManager.SetComponentData(entity, new Position { Value = 42f });
6.7 删除组件
接口 | 描述 |
RemoveComponent(Entity, ComponentType) | 任何组件 |
RemoveComponent<T>(Entity) | 除了块组件 |
RemoveChunkComponent<T>(Entity) | 块组件 |
7 System
我们不必使用用户的 Update 方法搜索组件,然后在运行时对每个实例进行操作,使用 ECS 时我们只需静态地声明:我想对同时附带 Velocity 组件和 Rigidbody 组件的所有实体进行操作。为了找到所有实体,我们只需找到所有符合特定“组件搜索查询”的原型即可,而这个过程就是由系统(System)来完成的。
public class MySystem : ComponentSystem
{
protected override void OnUpdate() { /* ... */ }
}
8 World
世界本身就是包含所有存在的实体,即使有多个世界,也没有问题。但是,不同的世界是完全独立的,不能直接干涉。
9 小结
实体提供纯粹的数据给系统,系统根据自己所需要的组件来获得相应的满足条件的实体,最后系统再通过多线程来基于 Job System 来处理数据。