欢迎光临散文网 会员登陆 & 注册

RE: 从零开始的 Unity ECS 1.0.14 正式版 (2023 - 3年后的文章翻新)

2023-08-20 03:41 作者:ms2308  | 我要投稿

本文的目标是在尽可能短的时间内(15-30分钟)带你零基础用 Unity ECS 实现一个最简范例。

0. 前言

近几个月 Unity DOTS 终于迎来了官方声明的 “Fully Released”(存疑)的正式版,突然想最近重新捡起来看看与3年前有多大不同,果不其然资料仍然寥寥无几,大多是19年20年的尝鲜笔记。不过毕竟需求推动供应,ECS的“反人类”架构确实难以在业余开发爱好者中推广开来,严肃工作者只有追求极致优化的大厂,比如试图在移动端渲染数千实体的那种国战氪金手游。

尽管如此,我仍觉得同画面承载上千上万单位非常酷炫,在独立游戏也可一展拳脚,比如类吸血鬼幸存者?总之是块值得大家一起啃的骨头,而且虽然我做出好活的可能性比较低,万一读者中有高人呢?所以想趁最近没事重构一下3年前已经过时的ECS尝鲜文章,再次精简国内外的一些碎片资源、以及不太好懂的官方文档,以通俗易懂废话少说的方式解释基础并实现一个麻雀虽小五脏俱全的ECS Demo。

(另外,立志从臭猫区up主转回技术区)

关于ECS的概念、优劣不再赘述,简而概之就是适用CPU吃紧的超量单位的大场景,但不适用个体有复杂逻辑的情况,例如AI、状态机、物理、复杂动作等(未深入了解,根据社区反馈总结),因为实现起来繁琐且资料不足。此外,即便正式版已经出来,也请做好同样场景 ECS 要麻烦的多的心理准备!

本文基于稳定版Unity 2022.3.7f1,由于 Unity DOTS 改动很频繁,网络中绝大多数教程与最新版本有大量出入,直接照搬会有很多坑(Console 里会出现非常多的 "已废除"警告),所以最新版入门避坑也是本文的另一个目标。

1. 安装

注意:根据官方论坛讨论,当前且未来 DOTS 版本似乎仅支持 URP 或 HDRP,不再支持默认 RP。

Window -> Package Manager,安装Entities和Entities Graphics,如果内置的列表中找不到,在左上角+处URL添加:com.unity.entities、com.unity.entities.graphics。此外还须安装Burst(默认应该已安装)。


2. ECS 基础

本章将展示一系列 GameObject 与 ECS 实体的转换和交互方式:

  • 如何简单生成 ECS Entity(实体)

  • 如何为实体添加储存数据的 ECS Component(组件)

  • 如何通过脚本将 GameObject 转化为 ECS 实体的 Authoring(编写器)

  • 如何将 Authoring 组件转化为 ECS 组件的 Baker(烘焙器)

  • 如何通过 ECS System(系统)处理组件中的数据,使其进行简单运动

  • 如何使 GameObject 与 ECS 实体交互

2.1. 在场景中直接转化 ECS 实体

本段进行基本场景设置,并用简单方式将 GameObject 转化为 ECS 实体。

首先创建一个测试用新场景,再创建子场景(Subscene)进行隔离,即右键 -> New Sub Scene -> Empty Scene...,所有在子场景中创建的 Game Objects 将会被自动转换为 Entities。

关于为什么使用子场景(翻译自官方英文原文):

使用子场景(subscene)是因为Unity的核心场景系统与ECS不兼容,将 GameObjects 和 MonoBehaviour 组件添加进 subscene 并进行烘焙(baking),可将他们转化为实体(Entities)和 ECS 组件。

右键点击新创建的 SubScene ,创建一个测试用方块 Cube (或任意用户自己的 Prefab)。创建完注意 Inspector 的右上方有个小的空心圈,点击后选择 Runtime 便可显示 ECS 实体已绑定的组件:

以前需要手动绑定的基本组件,现在支持了自动生成

2.2. 添加 ECS Authoring、Component 、Baker

接下来为物体添加一个“移动速度”组件,使实体可以进行匀速运动。

创建一个 C# 文件(例如 Assets/Script/ECS/SpeedAuthoring.cs),Authoring 脚本用于引导 GameObject 转化为 ECS 实体,Baker 在运行时将 Authoring 组件的数据提取并转换为ECS所需的 Component 数据。

注意:虽然每个类或结构体一般单独创建一个脚本文件,但项目到达一定规模后,会有大量的 ECS C# 文件(Authoring、Component、Baker等等),挤在一起难以管理,可以考虑将部分高度相关的多个文件合并为一个文件。

以下为 SpeedAuthoring.cs 中的代码内容:

首先我们将 Inspector 右上的小圈选择回默认(Automatic),对 Cube 添加 Authoring 脚本,并写入一个速度值:

添加 Authoring 脚本

再次选择回 Runtime 模式,我们可以看到 Speed 组件及其数据成功被加入到了 Cube 的 ECS 组件里了:

2.3. 添加 ECS System

Unity DOTS 提供了两种处理 ECS 实体和组件逻辑的方式:“托管类型” SystemBase 和“非托管类型” ISystem。其中 SystemBase 继承自 ISystem ,加入了很多便于开发的特性,但硬伤是托管类型无法被 Burst 优化。ISystem 则兼容 Burst,相比 SystemBase 自然也快得多。

来自官方文档中两种模式的比较

详细比较请参考官方文档:https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/systems-comparison.html

总之虽然 SystemBase 方便,还是建议着重学习使用 ISystem。根据社区论坛开发者实践,一些场景会相差数毫秒的帧处理时间(例如可能直接从30帧提升至120帧)。


2.3.1 SystemBase

通过 SystemBase 可以相对简单地实现处理 ECS 实体中的数据。我们新建一个 C# 脚本,命名为 MovingSystemBase.cs:

该脚本无需添加至任何 GameObject 中,因为 ECS System 默认会将【所有】带有 Local Transform 和 Speed 组件的实体都进行移动(x轴),运行后应能看到方块会进行移动,如果复制多份会观察到所有都会同时移动。读者可尝试复制极多单位、修改移动模式等,观察与普通模式下的CPU负载差异,后续将进一步通过 ISystem 实现,一般最终都会达到数倍以上加速,故本文不再进行量化的性能比较:

当物体数量足够多、且运动逻辑更复杂时,性能提升会十分明显
注意当场景环境复杂且物体数过多时,渲染可能会成为瓶颈,这部分无法仅利用 ECS 架构解决

2.3.2 ISystem

接下来我们将上述逻辑通过 ISystem 重新实现,并补充引入两个新技术使代码逻辑更清晰:Aspect IJobEntity

我们先删除或注释上一段的 MovingBaseSystem.cs,新建一个 MovingISystem.cs。

首先 ISystem 无法直接使用 Entities.Foreach,我们需要另辟新径。在此之前,我们先引入一个 Aspect 去封装多个 ECS 组件数据与函数,否则当组件越来越多时,代码会十分难看。


2.3.2.1 创建一个 Aspect

我们可以新建一个 C# 脚本创建这个 Aspect,但因为在本 Demo 中,该 Aspect 仅服务于 MovingSystem,为了方便我们也可以把该类放在 MovingISystem.cs 中。

该 Aspect 将包括 实体编号(Entity)、本地位移(Local Transform)、速度(Speed)、移动函数 Move():

注意 RefRW 与 RefRO 标记组件是可读可写还是只读,尽可能使用只读有利于性能提升。


2.3.2.2 创建一个 ISystem

在 MovingISystem 中,利用刚刚创建的 MovingAspect,我们可以重构移动系统:

引入 Aspect 后,代码十分整洁,只需通过 SystemAPI 索引具备 MovingAspect 的实体即可。但目前所有逻辑都执行在主线程,接下来我们将 foreach 中的内容封装为 IJob,交给 Burst 去做大批量的函数调用优化。


2.3.2.3 创建一个 IJobEntity

因为我们要创建的 Job 需要遍历 Entity,所以继承 IJobEntity。将刚刚的 MovingISystem 更改如下:


2.3.2.4 Burst 编译

最后我们在 System 和 Job 前添加属性 [BurstCompile] 为 Burst 编译器进行标识(如想验证 Burst 编译前后的差异,可以在 Move 函数中将位移重复进行 1000 或 10000 次进行观察),System 的完整代码如下:

使用 Burst 编译时需要注意几点:

  1. 并行导致的一切问题这里都会出现,例如无法简单调用主线程或单例的函数等。

  2. 一切托管类型(Managed Type),即以传统的面向对象方式编写的类和结构体,在这里均无法调用(例如 state.World),因为托管类型的内存分配和释放由.NET运行时环境自动管理。相对的,非托管类型(Unmanaged Type)是手动管理内存,可被 Burst 利用进行优化。

  3. 以上会造成不少开发上的难度,但实际并非一切运算都要通过并行和 Burst 进行优化,可积极利用 Unity 自带的一系列 Profiler 去定位性能瓶颈,好钢用在刀刃上。

3. ECS 与传统 MonoBehaviour 交互

使用纯 ECS 构建一个完整的游戏难度极大,所以我们需要关心 ECS 实体如何与传统的 MonoBehaviour 类进行交互。

我们将实现一个基于 MonoBehaviour 的 Speed Manager,用来全局控制所有 ECS Entity 的速度。创建一个 SpeedManager.cs 并挂到主场景的一个空 GameObject 上:

我们向 ECS 实体管理者 EntityManager 获取所有带 Speed 组件的实体,并保存在一个临时实体数组中,然后我们遍历所有的 ECS 实体,Get 它们的组件并 Set 速度值。通过类似方法可以轻松从外部批量修改 ECS 实体的组件数值。

执行游戏时,我们可以拖动 Speed Manager 中的 Global speed,观察到所有的 ECS 实体会随之运动。


以上我们便完成了最简ECS范例,不得不说基本体验确实还是比三年前简洁优雅得多,可以尝试进一步做一点有实际意义的小项目了。本文篇幅较短,实际仍有很多未涵盖的点,例如:

  • 如何写 ECS 实体的 Spawner (发射大规模弹幕的需求)

  • 如何在实体内实现随机数生成器

  • 如何改变个体的材质

  • 如何实现碰撞检测与物理系统

  • 如何播放音效

  • 如何绑定 Animator

  • 如何实现多人模式

  • 与HDRP、粒子系统等的协同

未来一小段时间可能会进一步地进行尝试,或许可以接触到以上几个方面,并结合一些物联网技术开发一个小项目。如开发顺利将会继续补充 ECS 开发攻略。

RE: 从零开始的 Unity ECS 1.0.14 正式版 (2023 - 3年后的文章翻新)的评论 (共 条)

分享到微博请遵守国家法律