使用Unity的实体组件系统的Survival Shooter(修订版)

这篇文章是由 Gamevanilla的 创始人兼代码管道工 David Pol 撰写的

几个月前,就在宣布Unity的新实体组件系统(即现在的ECS)并在GDC上发布其第一版预览之后,我决定使用此新系统编写官方的Survival Shooter教程项目,以供学习。

从那时起,ECS有了很大的发展,所以我决定花一些圣诞节的时间来使该项目与图书馆的最新技术保持同步。

因此,事不宜迟,我们该怎么做才能改善我们的原始项目?

摆脱注射

在原始代码库中,注入是用于迭代实体的主要机制。 当时,这是最推广的方法,但今天它被认为是不理想的设计(Aria Bonczek的Unite LA演示文稿很好地说明了为什么如此),并将在未来。 这些是注射的主要问题:

  • 它在幕后执行了大量非显而易见的工作。
  • 它完成这项工作的方式效果不佳(主要是由于内部使用了反射和GC分配)。

注入既简单又方便,但是事后看来,仅编译器会警告您有关未使用/初始化的相关结构和变量(因为所有内容都是隐式发生的)这一事实实际上在API方面本身就是一个很大的危险信号。

让注入自动为我们自动创建实体查询的第一种方法是直接自己完成并通过方便的ForEach抽象对其进行迭代。 例如,我们曾经有过这样的地方:

我们现在有这个:

不是特别糟糕,不是吗? 我们使用GetEntityQuery方法手动创建一个实体查询,该方法需要在OnCreate内部调用,并通过ComponentType.ReadOnly 传递我们感兴趣的必需组件类型(我们可以使用typeof ,但是ReadOnly效率更高且对称以及ReadWriteExclude变体)。

OnUpdate内部,我们通过ForEach抽象对组内的实体进行迭代。 请注意,系统如何为我们提供此方法的许多不同重载,以便我们可以轻松检索适当实体,经典组件和ECS组件的不同组合。

非常简单! 它看起来也比注射剂清洁得多。 但是,我们还能释放一些其他性能吗?

介绍作业系统

尽管我们在原始项目中专门使用了通过ComponentSystem在主线程上运行的系统(并且仍然需要与与经典基于MonoBehaviour的组件进行交互的许多系统这样做),但我们已经可以将某些仅处理ECS实体的系统移到作业中系统,它们以多线程方式执行工作。 特别是,对于大多数用例,推荐使用IJobForEach作业(及其IJobForEachWithEntity播广告)的作业系统。

我们还有一种称为块迭代的方法,它是对实体进行迭代的最终,最强大的机制,但是在ECS入门项目的背景下进行讨论要稍微复杂一些,因此我们在这里不使用它(我仍然建议您了解它,因为它是其余ECS的基础。

那么,这种类型的系统在实际中看起来如何? 让我们看一个名为EnemyHealthSystem的特定示例,该示例负责更新玩家所击中的敌人的生命值:

化的系统不是从ComponentSystem派生,而是从JobComponentSystem派生。 与它们的主线程类似,我们需要重写其OnUpdate方法。 仅这次使用JobHandle参数(可用于在不同作业之间建立依赖关系链)。 在此方法内部,我们调度了一个名为EnemyHealthJob的自定义作业,该自定义作业是从IJobForEachWithEntity派生的,该作业执行系统的实际工作。

此作业类型采用许多要用作实体“过滤器”的组件类型。 在此特定示例中,作业将遍历已损坏的敌方实体。 当前,您可以为作业指定的组件类型的最大数量为4,但是在以后的库版本中将添加更多的组件类型。

作业的Execute方法将实体,作业索引和指定的组件类型作为参数,并执行以下逻辑:

  • 它通过更新其HealthData组件来解决对敌人实体的实际损害。
  • 它删除了DamagedData组件,以避免多次处理敌方实体。
  • 如果敌人的生命值达到0或更低,它将向其添加DeadData组件以杀死该实体。 不同的系统将处理死亡的敌人。

请注意如何通过并发EntityCommandBuffer而不是通常的EntityManager (不适用于作业)完成所有操作。

一个有趣的提示是,为了防止将DeadData组件多次添加到已死的敌方实体中,我们通过ComponentDataFromEntity 数组检查相关实体是否已经具有该组件,该数组通过传递到作业中GetComponentDataFromEntity方法。

这里更酷的是,您可以向EnemyHealthJob作业添加[BurstCompile]属性,并且您的代码将由Burst编译器自动编译,Burst编译器是Unity构建的自定义编译器,它接受面向C#的高性能子集并利用自动矢量化,优化调度和更好地使用指令级并行性来减少数据依赖性和所生成机器代码中流水线中的停顿。 到目前为止,我们还不能真正在特定的工作中使用它,因为Burst仍然不支持使用命令缓冲区的工作(除非您专门使用它们来破坏实体),但是我们将在不久的将来实现这一目标。

我无法充分强调Unity的ECS,工作系统和Burst如何改变整个游戏开发方法。 在不经历实际使用该语言的痛苦的情况下,达到类似C ++的性能水平(甚至有可能做得更好)? 在实际上同时享受高性能代码的同时编写高性能代码? 好吧,签约我!

FixedUpdate系统

需要注意的重要一件事是,在Unity的原始项目中, FixedUpdate中运行了一些代码(即,播放器移动和摄像机跟随代码)。 为了也可以在FixedUpdate中运行等效的ECS系统,我们需要向它们添加DisableAutoCreation属性,然后从FixedUpdate手动调用其更新方法。 一种简单的方法是通过辅助行为MonoBehaviour:

预计随着ECS的成熟,我们将拥有一种更具表现力的方式来定义如何更新系统。

您可以在此GitHub存储库中找到完整的,更新的Survival Shooter项目。 该项目需要Unity 2019.1.0(当前为Beta),并且ECS特定的代码位于ECS文件夹中。

接下来是什么?

即使进行了所有这些更新,项目中仍有大量代码依赖于基于MonoBehaviour的经典组件(主要是与渲染,物理和动画相关的所有组件)。 虽然我们当然可以为这些区域编写我们自己的定制系统,但是在此演示的上下文中,我想保留所有使用内置ECS功能的内容。

展望未来,ECS将可以使用越来越多的核心引擎系统。 2019年才刚刚开始,随着这一进展的发生,我迫不及待地继续更新我们的Survival Shooter项目。