文章目录

  • 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分别是EntityComponentSystem的缩写:

  • 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 ecs混用 unity ecs原理_unity ecs混用


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>中使用CreateEntityInstantiate是更有效地使用。

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

unity ecs混用 unity ecs原理_数据_02


我们不必使用用户的 Update 方法搜索组件,然后在运行时对每个实例进行操作,使用 ECS 时我们只需静态地声明:我想对同时附带 Velocity 组件和 Rigidbody 组件的所有实体进行操作。为了找到所有实体,我们只需找到所有符合特定“组件搜索查询”的原型即可,而这个过程就是由系统(System)来完成的。

public class MySystem : ComponentSystem
{
    protected override void OnUpdate() { /* ... */ }
}

8 World

世界本身就是包含所有存在的实体,即使有多个世界,也没有问题。但是,不同的世界是完全独立的,不能直接干涉。

unity ecs混用 unity ecs原理_ci_03

9 小结

实体提供纯粹的数据给系统,系统根据自己所需要的组件来获得相应的满足条件的实体,最后系统再通过多线程来基于 Job System 来处理数据。

unity ecs混用 unity ecs原理_ci_04