Unity ECS 1.0.14 真・正式版的探索(一)
本篇将尝试基于最新版 Unity ECS 1.0.14 解决以下的游戏开发需求:
ECS 实体生成器
ECS 系统中的随机数
ECS 实体的材质

0. 前言
前篇介绍了 Unity ECS 1.0.14 正式版(2023/08)的 Demo 最简实现 ,涵盖了 ECS 的核心基础概念:

后续将尝试逐一攻破游戏开发中会遇到的一系列需求。

1. ECS 实体生成器
第一个目标是实现一个 ECS 实体生成器,对应情景例如一个工厂(MonoBehavior)持续生产机器人(Entity),或机器人(Entity)持续发射大量子弹(Entity)。
1.1. 在运行时(Runtime)创建 ECS 实体
在上一篇的基础范例中,ECS 实体是由子场景(Subscene)中已存在的 GameObject 通过 Authoring 和 Baker 转化而成,而现在我们希望实现在游戏中实时生成 ECS 实体。
参考官网文档,基于 MonoBehavior 和 ECS Entity 的 Spawner 可参考该流程:
首先写 Spawner 的 ECS 组件(IComponentData),用于储存 Prefab 的 ECS 实体,并且可根据游戏设计自由调整加入固定的生成坐标、生成时间等等
在子场景放置空 GameObject 转为 ECS Entity 生成器,赋予携带 GameObject Prefab 的 Spawner Authoring 脚本和 Baker
实现该 Spawner 的 ISystem,使用实体管理器(Entity Manager)或实体命令缓冲器(ECB)新实体
接下来将按照此流程实现一个范例:在场景中放置一个大方块(GameObject),每次被鼠标点击后生成一个新方块(Entity)。
1.2. 实现 CubeSpawner 的组件、Authoring、Baker
我们让 CubeSpawner 的组件包括一个方块的 Prefab、一帧生成的方块数和位置:
接着在子场景创建一个 CubeSpawner 的空 GameObject,将 Authoring 脚本挂上去,并赋予 Prefab。
注意 Prefab 也需挂载 Authoring 和 Baker ,否则要额外向新生成的空实体逐一添加新组件或 Aspect。
1.3. 实现 CubeSpawner 的 Aspect 和 System
虽然目前只有 CubeSpawner 组件,我们还是可以创建 Aspect 并将 Spawn 函数写入(而非 Job 中),个人感觉这样逻辑更清晰一些:
代码中有一点需要特别注意,虽然实体管理者(Entity Manager)可用于生成新实体,但如果想利用 Job 以及并行处理则强烈建议不使用此方案,因为创建实体、添加组件等部分行为会造成结构改变,导致内存块重构。如强行使用必须手动设置同步点(Sync Point),既麻烦也不利于优化。
所以 Unity 引入了线程安全的实体命令缓冲器 Entity Command Buffer (ECB) 来对应这种情况。简单来说 ECB 把所有改变结构的命令缓存后统一在主线程执行 ,并且也允许对新创建实体进行添加组件。
更多关于 ECB 的信息,可参考:https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/systems-entity-command-buffers.html
1.4 实现“在点击处生成方块”
为方便测试,我们可以用传统方法基于 Raycast 做一个点击触发器,在点击的坐标生成 Prefab 的 ECS 实体。

挂在大方块的 MonoBehaviour 点击检测脚本供参考:
2. ECS 中的随机数生成器
本阶段的目标是给 ECS 系统增加一个可用的随机数生成器,并实现将X轴上匀速移动方块改为每3秒随机改变方向的匀速移动方块。
一般来说,在主线程生成随机数只需随便用一个种子初始化,然后持续从序列中获取即可;但多线程并发时显然要麻烦的多。在这里我们需解决两个问题:一是在哪初始化随机数生成器,二是如何设置种子避免所有生成器产生一样的序列。
文档和官方论坛并没有找到明确的最佳实践,调查比较后个人认为添加一个泛用的随机生成器组件比较符合 ECS 的设计哲学,并且可以利用初始 Spawner 中的随机数生成器去给生成的实体赋予新的随机种子。
2.1. 随机生成器组件
我们创建一个泛用的随机生成器组件:
代码中有三点需要注意。
当 seed 设为0时, 我们用 UnityEngine.Random 为它初始化一个随机的种子,然后赋予 Rnd 组件一个 Unity.Mathmatics.Random。这两个类来自不同的命名空间,UnityEngine的是全局单例,所以我们不需要手动初始化,在主线程用非常方便,但无法在以外的情况用,所以组件中我们需要用 Unity.Mathmatics.Random 创建随机数生成器。
Spawner 和 Prefab 都需要添加该 Rnd 组件,Spawner 利用它为生成的方块 Entity 里的随机数生成器赋予随机种子,方块的 Prefab 则用它来实现随机移动。
为了方便为方块寄存一个随机方向,我在组件中添加了一个 random_vector。但 Spawner 实际用不上这个变量,严格来说应该再创建一个组件专门用来储存它,但为了简洁于是和随机数生成器放在了一起。
2.2. 随机方块的移动方向
接下来我们需要分别实现:
修改 Spawner 与 Prefab Moving 的 Aspect 使其包括 Rnd 组件
修改 Spawner 的 System 使其生成方块时赋予新随机生成器
修改 Prefab 的运动 System 使其定时随机运动方向。
修改 Spawner 的 Aspect,不要忘记顺便初始化随机方向(可利用 NextFloat3Direction 随机长度为1的三维向量),否则一开始所有方块都会向同一方向移动。
修改 Prefab 的 Moving Aspect,每3秒变换一次移动方向。注意 Move 中额外引入了度过时间(Elapsed Time),用于判断是否过了3秒。
2.3. 修改后执行游戏后的效果如下

3. 更改 Prefab 材质
本阶段的目标是为每个 Prefab 生成的 Entity 赋予不同的材质参数,例如可以用于发射大量彩色子弹,或生成外形不同的机器人。
注意:目前还未发现网上关于这方面的教程,我的主要信息来源是官网 Entities Graphics 关于 C# 材质覆写的文档:https://docs.unity3d.com/Packages/com.unity.entities.graphics@1.0/manual/material-overrides-code.html
更改 Entity Material 或 Mesh 的方案有两个方案。最简单的方式是用一个 Prefab 列表,在创建时进行选择,利用前文的方法可简单实现。在这里我们更关心的是如何在运行时通过 ECS System 对实体进行修改。
原本实现这块是件比较麻烦的事,所幸无意间发现 Unity 已经为我们准备好了内置的 Authoring 脚本和 System。在 Project 的 Packges/Entities Graphics/HDRPMaterialProperties (或 URP),我们可以找到一系列针对不同渲染属性的脚本,仅仅只要挂在 Prefab 上就可生效。
接下来将展示将一个机器方块的两个灯点亮并变化颜色,步骤如下:
为 Prefab 创建一个 Bot Component,分别将前灯、后灯、镜头灯的 Entity 保存,用于更改材质参数
为 3 个灯的 Game Object 添加 HDRPMaterialPropertyEmissiveColor.cs (如需更改其他属性亦或基于 URP 可自行替换),在 Unity 编辑器通过 Add Component 也可搜索到该脚本。
3.1. 用于储存 Prefab 中的 Child GameObject
在 System 中,获取 Entity 后修改对应的 <HDRPMaterialPropertyEmissiveColor> 组件即可(具体实现将不再在后篇展示)。
3.2. 用于储存 Prefab 中的 Child GameObject
float3 的 X,Y,Z 分别对应 Emissive Map 的 R、G、B
结果如下:


在下一篇将探索关于 ECS 实体的 批量行动 和 生命周期。