找回密码
 立即注册
查看: 891|回复: 12

[笔记] UE5的ECS:MASS框架(一)

[复制链接]
发表于 2021-12-20 13:02 | 显示全部楼层 |阅读模式
最近官方更新了一个黑客帝国觉醒的试玩游戏,看了演示视频之后大为震撼,其中最后有提到街上的海量人群是使用MASS AI框架实现的。能做出这样的实机效果,这套框架也是功不可没的。而这个MASS代码虽然外发版还没有,但其实已经在github的ue5-main分支上存在了很久了,因为之前我也大概看过,最近这里的代码也在持续更新,所以想趁这个热度总结下内部实现原理。如果你之前有了解过ECS那你在阅读下面内容时就会很轻松,因为Mass其实就是UE5实现的ECS框架。
拉取最新代码后,编译完看启动闪屏版本号是5.1.0:


Mass分为几个库,都是以插件形式放在引擎源码里,所以低版本想要使用也会比较容易。默认是关掉的,需要手动打开。如下,在引擎插件目录手动打开。


其中MassEntity是基础库,还包括MassGameplay,MassAI,MassCrowd等,具体库的关系如下图:


其中MassEntity是整个Mass框架的基础。先看MassEntity里的代码文件


不看具体实现内容,就看这个代码的命名都能猜出这是一套ECS框架了。如果对Unity的ECS和UE的渲染框架比较熟悉的话,看到这套代码的结构会觉得非常熟悉和亲切。Archetype就对应的Unity的ECS的Archetype,这个实现和Unity的ECS非常像。而CommandBuffer,又很像UE渲染线程的CommandBuffer。
之前UE渲染管线效率非常高,很大的原因就是面向数据的设计,并且紧密结合TaskGraph利用起来多线程渲染的优势。现在在逻辑层也搭了一套这样的管线,就是为了让逻辑处理也能发挥出来UE5的性能优势。而且官方都把实机的演示成品给你玩了,我想你肯定可以感受到这非常优秀的运行效率了。
下面就具体来说说内部实现:
Entity和Archetype

和Unity的ECS除了名字不同,实现完全一致。Entity在mass里的名字是struct FMassEntityHandle,只有两个成员变量Index和SerialNumber,一共占8字节。看到这样的结构,肯定知道有一个全局的大数组,这个Index就是数组下标,而序列号就是用来做数据校验的,可以说和FWeakObjectPtr原理是一样的。


这个大数组保存在MassEntitySubsystem中,如下图所示。


这个大数组的类型是TChunkedArray,默认16K一个Chunk,每个元素类型是FEntityData。16K是因为大部分CPU的L1都是16K的倍数。参考我之前一篇,有具体说:UE4/UE5的LockFreeList - 知乎 (zhihu.com)


内部有个智能指针,指向的实际数据结构是FMassArchetypeData。除了Index外,还有个序列号SerialNumber,作用就是某个Index上的Entity被删除后,再创建个新的Entity,如果原来Index指向的EntityData和EntityHandle序列号不匹配,就可以明确EntityHandle指向的是老的Entity而不是新的,这样就避免了只用Index标记Entity导致的冲突问题。
FMassArchetypeData结构:




可以看到,这个其实定义的是Entity数据的Archetype,什么叫Archetype(原型)?可以简单这样理解,类就是对象的原型,结构体是结构体实例的原型,UClass里的CDO是对应UObject的原型,我们游戏要创建很多Entity,这里就需要先有Entity的原型定义,可以描述内存布局等信息。总之,创建Entity前肯定需要原型信息。
在定义原型的时候需要下面这4种信息作为参数:


一般情况使用FMassFragment就好了,这个就是定义每个Entity内部的数据结构,在传统的ECS里这个FMassFragment其实就是Component。而FMassTag的不能有实际的成员变量,只是作为ECS执行时候的标记,可以认为是传统ECS里额外的过滤器标签,而UE里的过滤器叫做Query。FMassChunkFragment是Chunk的额外内存数据,每个Chunk内共享一份。FMassSharedFragment是共享的布局,相当于Unity的ECS中的共享Component。如果FMassFragment可以理解为Entity的成员变量,那FMassSharedFragment就可以理解为Entity的static成员变量,而FMassChunkFragment可以理解为每个Chunk的static成员变量,Chunk具体是什么接下来会说。这4种结构,在创建原型的时候都会分别记录下来。保存在FMassArchetypeCompositionDescriptor中。


可以看到,这里提供了联合计算Hash的函数,也就是说通过这些原型,就可以得到一个唯一码。最终也会和Entity一样保存到MassEntitySubsystem中


在FMassArchetypeData初始化时,就会根据这个描述符来生成内存布局和相关变量,具体可以看FMassArchetypeData::Initialize函数。这里也会解释清楚上面4种基类的区别。
其中FragmentSizeTallyBytes是每个Entity的大小,是由sizeof(FMassEntityHandle)+每个FMassFragment子类的大小总和。而Tags本身不占Entity大小,只保存在Archetype中。FMassSharedFragment是多个同类型Entity共享的Fragment,所以也保存在Archetype中,不占用Entity内存。
实际的Entity数据保存在FMassArchetypeData的Chunks这个成员变量里


内部会一次创建一个固定64K大小的Chunk,给多个Entity使用。


为什么是64K,同样道理,因为大部分CPU的L2缓存是64K的倍数(Unity一个Chunk是16K),这里L1,L2都是CPU单核的独立缓存,所以都很快,如果到了L3因为涉及到多核共享,就会显著降速。所以根据Entity本身的大小不同,每个Chunk可以容纳64K / EntitySize个。当删除掉其中一个Entity时,内部的其他Entity并不会移动,所以这个Entity会在Chunk中空出来,这时如果再Add新的Entity会复用这个空出来的内存,当删除掉Chunk中所有Entity时,Chunk的内存会自动释放掉。整个数据结构实现,相当于是TSparseArray和TChunkedArray的结合,因为UE没有自带这种泛型容器,所以这里就单独实现了。
在定义ArchetypeData的时候,暴露给业务的其实是FMassEntityHandle。就像FMassEntityHandle一样。具体对应关系如下图所示:


这里要注意的一点是,EntityHandle中的Index,并不是Archetype中实际Entity的下标,而是System里Entities的下标。这两个下标会通过Achetype中的一个Map做映射,如下图所示。Entity中的Index是全局唯一的,而Archetype中的Index是在当前Archetype内唯一,多个Archetype中是会有相同的下标,所以需要这样一个映射才能确定实际的Entity数据是什么。


关于实际的Entity存储:


实际数据存储在RawMemory中,具体大小在AllocSize里。可以看到每个Chunk内还有一个ChunkFragmentData的。这个就是前面说的每个Chunk一份的static数据。
示例

上面这样描述对于不了解ECS的读者来说可能有些晕,下面用我具体写的这个例子说明更直观一些。我们先定义一下FMassFragment,这个就是Entity的内部数据结构。这里我准备创建3种类型的Entity,第一种内部数据是float的,第二种是int32的,第三种是float和int32组合在一起的。


Entity具体创建和删除示例如下:


可以看到上图,在创建前,我先定义了原型,原型其实就相当于是在运行时定义数据结构,这里需要强调的是运行时,自己手写一个struct其实是在编译期定义数据结构,因为都是使用的UScriptStruct,所以理论上可以使用蓝图定义的结构体。上图也可以看到,定义好了Entity的Archetype后,也可以随时修改,相当于ECS的ReplaceComponent。
我这里分别创建了100个,200个,150个。实际创建的就是下面这样的数据,在第一次创建的时候是连续的,连续创建和删除有可能产生空洞。


注意上图只是个示意,实际会分到Chunk里。借用一下Unity的ECS老图,具体结构是下面这样,我就不自己画了,原理和Unity的ECS是完全一样的。



本章主要介绍了Mass内部的内存布局,后续章节会继续讲解具体操作。

本帖子中包含更多资源

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

×
发表于 2021-12-20 13:04 | 显示全部楼层
和unity的ecs实在太像了。
发表于 2021-12-20 13:14 | 显示全部楼层
内存结构差不多,执行流程完全不一样,我这里还没写
发表于 2021-12-20 13:17 | 显示全部楼层
终于有自带的ecs了吗[飙泪笑]
发表于 2021-12-20 13:20 | 显示全部楼层
这样的ECS系统能和脚本系统结合起来吗?比如 JS Lua 脚本作为 system 执行的一部分,访问 entity 数据
发表于 2021-12-20 13:24 | 显示全部楼层
[捂脸]虚幻的ecs都出来了,unity的还在爬
发表于 2021-12-20 13:34 | 显示全部楼层
理论上完全可以,都是运行时的
发表于 2021-12-20 13:43 | 显示全部楼层
是的
发表于 2021-12-20 13:48 | 显示全部楼层
C#里可以用harmony反射让C#写的mod访问游戏里的任意内存(边缘世界),C++据说不方便反射,也就是说需要在system这边暴露数十个api给脚本侧使用吧?我常玩的一个c++写的开源游戏因为新加功能就要暴露api很繁琐,所以社区开发者一致决定不维护lua脚本系统了,所以我想了解下unreal这边是不是也会有这种困境
发表于 2021-12-20 13:52 | 显示全部楼层
是的 但是UE没有这么麻烦 标记UFUNCTION宏就可以了 UE自己的蓝图调用C++就是这样的 蓝图也属于脚本语言
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

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

GMT+8, 2024-11-22 01:41 , Processed in 0.100083 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

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