介绍我们的新电子书:面向高级开发人员的 Unity 面向数据的技术栈 (DOTS)

Unity 的 Data-Oriented Technology Stack (DOTS)提供了一整套性能增强工具,可帮助您充分利用目标硬件,从而大规模创建复杂的游戏。
这本 50 多页的电子书、 面向数据的技术栈简介(面向高级 Unity 开发人员现在可免费下载。将其作为入门指南,可以更好地了解面向数据的编程,并评估 DOTS 是否是您下一个项目的正确选择。无论您是要启动一个基于 DOTS 的新项目,还是要为基于 Monobehaviour 的游戏中性能关键部分实施 DOTS,本指南都会以结构化和清晰的方式涵盖所有必要的内容。
Unity 6已进入预览阶段,DOTS 1.0 也已准备就绪,现在正是探索 DOTS 带来的机遇的大好时机。这本电子书由 Unity 高级软件工程师 Brian Will 撰写,与更新的 Unity Learn 样本、最近的 DOTS Bootcamp 和 GitHub 样本一起,为想要学习如何使用 DOTS 的开发人员提供了大量资源。

我们编写这本电子书的目的是帮助您做出明智的决定,即实施部分或全部 DOTS 软件包和技术是否是您现有或即将实施的 Unity 项目的正确决定。堆栈的每个部分都在提高游戏执行速度和效率方面发挥着作用。本指南旨在解释这些部分中的每一部分、它们如何一起使用,以及它们的共同基础--Unity 实体组件系统(ECS)。
使用 DOTS 的一个主要原因是要从目标硬件中获得最高性能,这就需要了解多线程和内存分配。此外,要充分利用 DOTS,您需要以不同于基于 C# 的 Monobehaviour 项目的方式构建面向数据的代码和项目,因为后者的抽象级别更高。
让我们来详细了解一下电子书中的内容。
CTA:下载 面向高级 Unity 开发人员的数据导向技术栈介绍。

指南的第一部分(我们将其包含在下文中)介绍了导致游戏 CPU 性能低下的一些因素,如垃圾回收开销、不适合缓存的数据和代码、编译器生成的次优机器代码等。
下一节将解释 DOTS 软件包和功能如何帮助编写代码,避免 CPU 性能缺陷。您会发现以下方面的有用解释
- C# 工作系统
- 突发编译器
- 收藏品
- 数学
- 实体
- 实体图形
- 统一物理
- 实体的网络代码
在简要介绍了堆栈的各个部分后,我们将向您介绍EntityComponentSystemSamplesGitHub repo,其中包含许多介绍基本和高级 DOTS 功能的示例。Github repo 中的部分示例在关于 DOTS 的新 Unity Learn 课程 "熟悉 DOTS"中重现。
DOTS 指南的另一个关键部分是附录。在这里,Brian Will 详细解释了与 Unity ECS 相关的概念,包括内存分配和垃圾回收、内存和 CPU 缓存、多线程编程、面向对象编程的局限性以及面向数据编程。

如果您是一位经验丰富的游戏开发人员,您就会知道目标平台的性能优化是贯穿整个开发周期的一项任务。也许你的游戏在高端 PC 上表现出色,但你的目标是低端移动平台呢?帧的时间是否比其他帧长很多,造成明显的停顿?加载时间是否长得令人讨厌,每次玩家穿过一扇门时,游戏是否会冻结整整几秒钟?在这种情况下,不仅当前的体验不尽如人意,而且您实际上无法添加更多功能:更多环境细节和规模、机械、角色和行为、物理和平台。
罪魁祸首是什么?在许多项目中,它都是渲染:纹理太大,网格太复杂,着色器太昂贵,或者批处理、剔除和 LOD 的使用效果不佳。
另一个常见的缺陷是过度使用复杂的网格碰撞器,从而增加了物理模拟的成本。或者,游戏模拟本身很慢。您编写的 C# 代码定义了游戏的独特之处,可能会导致每帧耗费过多毫秒的 CPU 时间。
那么,如何编写速度快或至少不慢的游戏代码呢?
在过去的几十年里,PC 游戏开发商往往只需等待就能解决这个问题。从 20 世纪 70 年代到 21 世纪,CPU 的单线程性能一般每隔几年就会翻一番(这一现象被称为摩尔定律),因此 PC 游戏在其生命周期内会 "神奇 "地变得更快。然而,在过去二十年中,CPU 单线程性能的提升幅度相对较小。相反,CPU 内核的数量一直在增长,如今即使是像智能手机这样的小型手持设备也配备了多个内核。此外,高端和低端游戏设备之间的差距已经拉大,很大一部分玩家使用的硬件都是几年前的产品。等待更快的硬件似乎不再是可行的策略。
那么,我们要问的问题是:"为什么我的 CPU 代码速度这么慢?有几个常见的陷阱:
- 垃圾收集会造成明显的开销和停顿:出现这种情况是因为垃圾回收器充当了自动内存管理器的角色,负责管理应用程序的内存分配和释放。垃圾回收不仅会产生 CPU 和内存开销,有时还会使代码的执行暂停数毫秒。用户可能会感觉到这些停顿是小打小闹,或者是干扰性更强的卡顿。
- 编译器生成的机器码是次优的:有些编译器生成的代码优化程度远低于其他编译器,不同平台的结果也不尽相同。
- CPU 内核利用率不足:尽管如今的最低端设备都配备了多核 CPU,但由于编写多线程代码通常比较困难,而且容易出错,因此许多游戏只是将大部分逻辑保留在主线程上。
- 数据对缓存不友好:从高速缓存访问数据比从主存储器获取数据要快得多。不过,访问系统内存可能需要 CPU 坐着等待数百个 CPU 周期;相反,你希望 CPU 尽可能多地从缓存中读写数据。 最简单的方法就是按顺序读写内存,因此最适合缓存的数据存储方式就是紧密堆叠的连续数组。相反,如果你的数据是非连续地散布在整个内存中,访问这些数据通常会引发许多昂贵的缓存未命中;CPU 请求的数据并不存在于缓存内存中,而是需要从速度较慢的主内存中获取。
- 代码对缓存不友好:在执行代码时,如果缓存中还没有代码,就必须从系统内存中加载代码。一种策略是尽可能少地调用函数,以减少从系统内存加载函数的频率。例如,与其在整个帧的不同位置调用某个函数,不如在一个循环中调用,这样每帧最多只需加载一次代码。
- 代码过于抽象:除其他问题外,抽象往往会造成数据和代码的复杂性,从而加剧上述问题:在没有垃圾回收的情况下管理分配变得更加困难;编译器可能无法进行有效优化;安全高效的多线程变得更加困难,而且数据和代码对缓存的友好程度也会降低。除此之外,抽象往往会分散性能成本,从而导致整个代码速度变慢,使你没有明显的瓶颈需要优化。
上述所有问题在 Unity 项目中都很常见。让我们更具体地了解一下:
- 虽然 C# 允许您创建手动分配的对象(即不会被垃圾回收的对象),但 C# 和大多数 Unity 项目的默认规范是使用C# 类实例,它们会被垃圾回收。在实践中,Unity 用户早已通过一种名为 "池化"(pooling)的技术缓解了这一问题(尽管池化可以说违背了使用垃圾收集语言的初衷)。对象池的主要优点是可以高效地重复使用预分配池中的对象,从而无需频繁地创建和删除对象。
- 在 Unity 编辑器中,C# 代码通常使用Mono 编译器编译为机器代码。对于单机版,使用IL2CPP(C# 中间语言交叉编译为 C++)可以获得更好的效果,但这也带来了一些弊端,比如构建时间更长,MOD 支持更困难。
- Unity 项目通常在主线程上运行所有代码,部分原因是这样做让 Unity 变得简单:
- Unity 事件函数,如 MonoBehaviours 的 Update() 方法,都是在主线程上运行的。
- 大多数 Unity API 只能在主线程中安全调用。
- 典型 Unity 项目中的数据结构往往是散落在内存中的随机对象,导致缓存利用率低下。部分原因还是因为 Unity 让这一切变得简单:
- 一个 GameObject 及其组件都是单独分配的,因此它们通常会出现在内存的不同部分。
- 典型 Unity 项目中的代码往往对缓存不友好:
- 传统的 C# 和 Unity 的 API 鼓励使用面向对象的代码风格,这种风格倾向于使用大量的小方法和复杂的调用链。与面向数据的方法不同,这种方法对硬件不太友好。
- 每个 MonoBehaviour 的事件函数都是单独调用的,调用不一定按 MonoBehaviour 类型分组。例如,如果您有 1000 个怪兽MonoBehaviours,每个怪兽都会单独更新,而不一定与其他怪兽一起更新。
传统 C# 和许多 Unity API 面向对象的风格通常会导致重抽象的解决方案。这样产生的代码往往存在效率低下的问题,很难将其分离和隔离。

这本电子书对所有人免费开放,但专为那些在基于单行为、面向对象的游戏开发方面经验丰富,但对 Unity DOTS 和面向数据的设计开发比较陌生的 Unity 开发人员量身定制。
我们希望本指南能帮助您了解 DOTS 以及这些功能如何为您的下一个 Unity 项目带来益处,并让您更轻松地从我们的 GitHub repo 上提供的示例中获得全部价值。
