找回密码
 立即注册
查看: 708|回复: 10

浅谈Unity ECS(一)Uniy ECS基础概念介绍:面向未来的 ...

[复制链接]
发表于 2021-11-22 16:57 | 显示全部楼层 |阅读模式
系列内容规划

ECS是新的面向数据的游戏架构,从暴雪在GDC2017放出了Overwatch Gameplay Architecture and Netcode演讲后(需要科学上网,原本需要GDC Vault订阅才能看,目前已经免费开放,推荐大家观看),开始越来越多地进入人们的视野。目前已经有很多研发中的MOBA及FPS在研发过程中使用了ECS架构,云风大大目前也在开发自己的ECS架构引擎。各大常见商业游戏引擎也在往这个方向跟进:CryEngine5已经内嵌了ECS架构在其中;Unity在2018中已经有了ECS相关的Package可以导入使用,且给出了一些很优秀的示例,目前正在快速进行迭代,计划在未来将ECS功能直接放入正式版中,和正常的OO框架并存同步更新,且提到了未来的开发重心将会向ECS转移(On DOTS: Entity Component System中后面提到)。
可以说,ECS已经拥有成为未来主流游戏架构的潜力,作为一个游戏开发人员,有必要对相关的知识进行一个系统的了解。本系列以Unity Entites包的相关源码为基础,对ECS中有关的相关概念和设计思想进行一个简单的剖析。这个系列文章从开始构思,到动笔开写大约用了三周时间,这部分时间主要用在理解Unity Entities包的相关代码,整体来说相关的代码量较大(数十万行),且由于代码涉及到底层的内存管理相关逻辑(C# Unsafe代码),对于平时较少接触这部分逻辑的我来说还是有一定理解难度,在完整理解了几个我认为比较关键的知识点后,我才能安心地下笔开始写这个系列文章。由于本人水平有限,文章中难免存在不足之处,对于文章的不准确和其他建议想法,也欢迎在评论指出。
本系列预期一共由四篇文章组成,分别为:

  • 浅谈Unity ECS(一)Uniy ECS基础概念介绍:面向未来的ECS
  • 浅谈Unity ECS(二)Uniy ECS内存管理详解:ECS因何而快
  • 浅谈Unity ECS(三)Uniy ECS项目结构拆解:开发思路及案例分析
  • 浅谈Unity ECS(四)Uniy ECS项目实战:从零开始构建简单示例
本篇为系列第一章。
ECS基础介绍

基础概念

ECS的全称为Entity Component System,是最早由暴雪在GDC2017上提出的一个新的游戏设计框架,数据的组织和对象的处理上都和目前主流的OO模型有很大的差距,这里引用一下云风在浅谈《守望先锋》中的 ECS 构架中的总结:
Entity Component System (ECS) 是一个 gameplay 层面的框架,它是建立在渲染引擎、物理引擎之上的,主要解决的问题是如何建立一个模型来处理游戏对象 (Game Object) 的更新操作。



ECS结构示意,图片来自 Overwatch Gameplay Architecture and Netcode[1]

简单来讲,这里的Entity是一个实体,类似于Unity中的GameObject,可以表示游戏中的单个对象。Component是Entity的一个属性,或者说一个Entity是由多个Component组合而成的,类似Unity中的Component,但一般定义为一个结构体,不包含任何函数。System来处理含有某些特定Component的Entity的某一类行为,如一个MoveSystem处理所有包含Position和Velocity组件的Entity的移动过程。
这边最主要区别就是把对象上的数据和行为剥离,由专门的System来处理某一种行为,而不是每个对象在自己的一个更新函数中处理所有和自己相关的操作。当然我们在平时面向对象设计过程中,也会有类似的管理器来处理对象的一部分行为,但ECS在架构设计的时候就将需要将这部分操作和需要的数据提前考虑清楚,使得每一个系统的都能符合ECS的要求。
ECS代码查看

本系列的参考对象是Unity的ECS源码,因此当务之急是获取ECS包相关的代码,并将其放入项目中处于可以调试的状态,对于一些疑难之处可以直接查看源码调试理解。
幸运的是,Unity中可以很方便的获取到ECS包的代码,不需要去其他额外的地方下载,具体操作步骤如下:

  • Unity创建新项目(笔者目前使用Unity2018.3.6f1)
  • 打开Window->Package Manager窗口,导入最新的Entities包,以及依赖的Mathematics、Hybrid Renderer和Burst包。
  • Entities依赖的Performance Test包面板无法直接添加,找到项目目录/Packages/manifest.json文件,在entities包下面加入一行"com.unity.test-framework.performance": "0.1.50-preview",
  • 在项目目录/Library/PackageCache/目录中,找到entities和test-framework两个目录复制到项目目录/Packages/目录下
  • 修改manifest文件中的entities和test-framework两行,让其直接加载Packages文件夹中的本地文件:"com.unity.entities": "file:./com.unity.entities", "com.unity.test-framework.performance":"flie:./com.unity.test-framework.performance",
  • 关闭Unity,删除项目目录/Library/PackageCache/路径下的所有文件,并重新打开刚刚的项目
  • Unity打开C# Project,即可看到所有ECS相关的代码,可以添加简单的System代码进行调试。
如果觉得比较麻烦也可以直接下载我github上已经创建好的测试工程,已经按上述流程操作过并添加了简单的测试代码:
Unity ECS基本概念

在开始分析Unity ECS相关的源码之前,有必要先从实现层面对于Unity ECS的一些重要概念进行一个简要介绍
Entity的概念

Entity为ECS中基本的成员,实际上只是由一个Index和一个Version组成(Version只有在Entity被回收后会加1),其实际的Component数据存储在一个Chunk上(Unity ECS特有的数据类型,后面会讲到),需要操作其Component数据时,根据其index到EntityDataManager中找到其所在的Chunk和IndexInChunk,取到对应的Component数据后进行操作。
public struct Entity : IEquatable<Entity>
    {
        public int Index;
        public int Version;
        ......   
    }Component的概念

Component是Entity的一个属性,通常是一个继承了IComponentData或ISharedComponentData接口的结构体(两个接口都为空接口,仅标记类型)。一个Entity可以包含多个Component,继承了ISharedComponentData的数据会在多个Entity之间共享,同时可以使用托管类型的成员,一般用来存放GameObject或RenderMesh等渲染相关的成员。
一个Entity的Component可以在CreateEntity时指定,也可以使用一个ArcheType创建或从已有Entity复制来创建。同时已经创建的Entity还可以通过AddComponent和RemoveComponent来动态进行Component的添加或删除(由于效率问题不推荐)。
Tips:Component可以用Proxy包装后直接挂在GameObject上,挂载多个Proxy的GameObject可以作为Prefab直接传入EntityManager.Instantiate来生成新的Entity,如:
// Create an entity from the prefab set on the spawner component.
    var prefab = spawnerData.prefab;
    var entity = EntityManager.Instantiate(prefab);ArcheType的概念

ArcheType是Unity ECS中特有的概念,也是Unity ECS内存管理中的一个核心部分,许多重要操作都与此相关,也是下一篇文章中的重点。ArcheType是由某几个固定Component组成的Entity原型,有以下几个特点:

  • ArcheType管理所有属于它的Entity Component数据,对应数据存放在归属于它的chunk上
  • 可以通过ArcheType快速访问所有该类型的Entity Component数据
  • 拥有Component的Entity一定处在某个ArcheType的管理之下
  • ArcheType拥有缓存机制,第二次创建相同的ArcheType时会自动将现有的返回
我们可以使用EntityManager.CreateArchetype(params ComponentType[] types)来主动创建一个ArcheType,通过ArcheType可以直接调用EntityManager.CreateEntity(EntityArchetype archetype)来快速创建具有某一类特征的Entity。同时如果使用直接传入Components的方式来创建Entity时也会自动生成含有对应Component的ArcheType。关于ArcheType的相关问题会在下一篇文章中重点分析。
ComponentSystem的概念

ComponentSystem为System在Unity ECS中的实现。一个ComponentSystem会对含有某些Component的Entity执行一些特定的操作,通常继承自ComponentSystem或JobComponentSystem。区别是继承ComponentSystem只会在主线程执行,而继承自JobComponentSystem的则可以利用JobSystem来进行多线程并发处理,但同时对应操作过程中的限制也更严格。在大部分情况下应当尽量使用JobComponentSystem来完成相关的操作,从而提升性能。
多个不同的ComponentSystem可以在定义时通过UpdateBefore、UpdateAfter、UpdateBefored等标签来控制其执行顺序,这会在一定程度上影响并发执行,通常只在必要时使用。
一个ComponentSystem通常关注一个包含特定的Component组合的Entity集合(称为ComponentGroup,下一篇重点讲)。这个ComponentGroup集合可以通过GetComponentGroup主动获取,
ComponentGroup m_Spawners;
//获取包含ObjectSpawner和Position两个Component的ComponentGroup
protected override void OnCreateManager()
{
    m_Spawners = GetComponentGroup(typeof(ObjectSpawner), typeof(Position));
}也可以使用IJobProcessComponentData中的定义和RequireSubtractiveComponentAttribute等标签自动注入(Inject),同样也会生成一个ComponentGroup
//通过RequireComponentTagAttribute为JobComponentSystem添加额外的依赖项
//[RequireComponentTagAttribute(typeof(Object))]
//通过RequireSubtractiveComponentAttribute为JobComponentSystem添加额外的排除项
[RequireSubtractiveComponentAttribute(typeof(ObjectSpawner))]  
struct ObjectMove : IJobProcessComponentData<Position>
{
    ......
    public void Execute(ref Position position)
    {
        ......
    }
}数据存储相关

Chunk的概念

Chunk是Unity ECS中特有的一个数据结构,在ECS部分代码中有大量使用,通常是指用来存放Component信息的与ArchetypeChunk,此外还有更一般的Chunk通过ChunkAllocator进行开辟,可以存放ArcheType中的各类型信息,大小和存储结构都与ArchetypeChunk不同,此处的Chunk特指存放ArcheType中Component信息的ArchetypeChunk。Chunk有以下几个特点:

  • EntityManager会将Component数据存放在固定的16kb大小的Chunk中(可以在Chunk定义中找到指定大小kChunkSize)
  • 每个Chunk结构包含了这个区块中内容的相关信息
  • 每个EntityArchetype都包括了一个Chunk的独特集合
  • 一个chunk只能存在于一个archetype中
  • 一个ArchetypeChunk结构是一个到具体Chunk的指针
每一个Chunk中包含的内容有:

  • 一个指向其所归属的Archetype的指针
  • 一个可以容纳多少个该Archetype类型Component数据的容量(Capacity)以及当前存放个数(Count),容量由Chunk的总大小除以单个Archetype所有Component大小之和得到
  • 一个关于SharedComponentData值的索引(indices)数组,每个Chunk对于每种SharedComponentData类型只会存放一份具体值,所有在该Chunk上的Entity数据共享该值。
  • 一个ChangeVersion表明该Chunk上每一种Component上一次被修改的时机
  • Entity中各ComponentData的具体数据,这边相同的类型的ComponentData会被放在一起,一种类型结束才是下一种
Chunk中正式ComponentData数据内容从Buffer字段开始,ComponentData的具体存储排列如图:



Chunk中的数据排列,图来自 组件与Chunk的内存布局[2]

常用管理器介绍

Unity ECS中对于不同类型的对象由不同的Manager进行管理,这边对常用的Manager进行一个归纳。
EntityManager

ECS日常使用中使用最频繁的管理器,每个World都会有一个EnitityManager,不直接储存数据,但封装了常用的Enitity、Archetype、Component操作,包括了CreateEntity、CreateArchetype、Instantiate、AddComponent等操作,对应接口后面的章节会讲到,这里不作详细说明。
同时EntityManager提供了其他多个重要数据管理器的访问接口,包括Entity数据管理器EntityDataManager、原型管理器ArchetypeManager、Component集合管理器EntityGroupManager、以及共享数据管理器SharedComponentDataManager。可以说以EntityManager为入口,可以延伸到ECS系统中90%以上的内容,可谓包罗万象。
ArchetypeManager

管理所有的Archetype的创建、更新、删除等操作,管理了所有当前已创建的Archetype。当需要建立新的ComponentGroup时会到ArchetypeManager中读取所有Archetype的列表m_Archetypes,来筛选需要关注哪些Archetype,同时在Archetype发生变化时也会同时更新对应的ComponentGroup数据。
EntityDataManager

管理所有Entity相关的数据,可以通过m_Entities->ChunkData快速根据Entity Index获得Entity所在的Chunk和在该Chunk上的Index,从m_Entities->Archetype获取其Archetype,从而快速访问对应的Component数据。
EntityGroupManager

管理与ComponentSystem运作息息相关的ComponentGroup。从每个ComponentSystem中收集其需要关注的Component集合,同时在System的每次Update前更新这个集合,同样的集合只会存在一个。ComponentGroup的相关机制也是下一章内容的重点
总结

在本篇文章中对于Unity ECS系统的常用名词、基础结构进行了一个简单的介绍,旨在建立一个对于ECS系统的初步认识,在后续篇章中将会结合本篇的一些概念,针对Unity ECS内存管理部分进行一个系统的分析。
Reference


  • Overwatch Gameplay Architecture and Netcode
  • 组件与Chunk的内存布局
  • Unity ECS 架构与交通模拟的实现
  • 浅谈《守望先锋》中的 ECS 构架
  • 题图来自《Devil May Cry 5》

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2021-11-22 17:04 | 显示全部楼层
科普的挺详细的!加油,期待你的后续文章!
发表于 2021-11-22 17:08 | 显示全部楼层
感谢支持!
发表于 2021-11-22 17:15 | 显示全部楼层
期待第二篇~
发表于 2021-11-22 17:19 | 显示全部楼层
哥们那一届的, 校友啊
发表于 2021-11-22 17:28 | 显示全部楼层
2014届的
发表于 2021-11-22 17:37 | 显示全部楼层
哈哈 讲的够相信细节也讲到了,能不能分享下,学习方法和步骤
发表于 2021-11-22 17:45 | 显示全部楼层
够详细
发表于 2021-11-22 17:49 | 显示全部楼层
good
发表于 2021-11-22 17:49 | 显示全部楼层
ecs概念挺早的,只是由于守望先锋的演讲又火了一把
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-23 03:12 , Processed in 0.095658 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表