嗨,黑客努恩!今天继续讨论Unity开发中的ECS(Entity-Component-System)。在 中,我讨论了 ECS(实体组件系统)是什么、为什么需要 ECS、如何使用 ECS、Unity 中 ECS 的优点以及 中 ECS 的缺点。 Unity 中的 ECS(实体组件系统)指南:第 1 部分 Unity 在这一部分中,我将重点介绍初学者在 Unity 游戏中使用 ECS 的错误和良好实践。我还将稍微介绍一下 Unity/C# 的框架。 初学者在Unity游戏中使用ECS时的错误 在本节中,我将告诉您更有经验的人在我的旧代码中注意到的错误,这些错误阻碍了我的开发。我还将介绍许多初学者在开始掌握 ECS 时所犯的错误。我希望这能帮助您避免一些错误。 组件继承和接口 继承组件和使用接口是初学者普遍犯的错误。那么为什么它对 Unity 开发不利呢? 4个原因: 与通过数据进行的 ECS 抽象相比,组件级别的抽象绝对没有任何好处 它给您带来了限制:您会发现扩展此类组件并完善相关逻辑变得更加困难 它会导致组件拥有自己的逻辑的情况,正如我们所记得的,这违反了 ECS 原则,您不应该这样做。 这导致了继承系统或按类型进行各种 switch case 的不合理必要性。 我会立即指出,继承系统并不总是一个好主意,但与组件继承不同,它不会做任何可怕的事情。所以如果你想继承组件,就不要这样做。再想想如何用另一种方式解决问题。 无法正确使用ECS抽象 ECS 抽象是指您只需将通用数据(应在 OOP 中继承)放入单独的组件中。您只需添加包含所需数据的新组件并使用 和 过滤实体即可创建此类组件的“继承者”。一切都是初级的。如果组件/实体之间有一些公共数据,您几乎总是可以将它们放入单独的组件中。越早做越好。 BaseComponent InheritorComponent 启用/禁用系统更改逻辑 在 ECS 中,世界和处理世界的系统是静态的并且始终存在,但实体及其数据却非常动态。如果您需要禁用某些逻辑,禁用系统并不是正确的解决方案。通常无法访问其他系统(这是一件好事)。一个更实用的选择是创建一些标记组件。它会说系统逻辑不应该为带有标记的实体工作。 许多新人声称:“但是如果我没有系统的实体,系统为什么要工作?为了优化而关闭它不是更好吗?”。 不,这并没有更好。 如果你承认可能没有实体,那么添加 会更容易。到系统main方法的最开始处。在 的规模中,调用一个函数并比较两个整数只是沧海一粟,不会以任何方式影响你的性能。 if (entities.Length < 1) return; Unity游戏 在运行时禁用系统的唯一合法情况是 A/B 测试和特定系统的调试/测试。然而,大多数框架提供的工具不是通过代码而是通过编辑器窗口来完成此操作。 让 ECS 成为绝对 应该记住,ECS 拥护者并不禁止 OOP :D 使用 ECS 时,您不应该完全沉迷于 ECS 并将所有内容转移到它上面,因为这可能会适得其反。此外,正如我在 中提到的:并非所有数据结构都适合 ECS。因此,在这种情况下,最好创建一个单独的 OOP 类。 ECS 的缺点 一些 更进一步,不根据ECS制作项目的某些元素(例如UI),而是以任何其他方便的方式稍微放在一边,然后通过某种桥梁与ECS连接。此外,所有附加逻辑(加载配置、直接使用网络、保存到文件)更容易进行 OOP 并直接从所需的系统使用它。 Unity开发人员 您应该根据常识选择要做什么。 ECS 应该帮助发展,而不是阻碍发展。 尝试将现有代码移植到 ECS 而不进行任何更改 很多时候,初学者会尝试将现有代码逐字转移到 ECS。这不是一个好主意,因为 ECS 编写代码的方法与传统的架构模式不同。这种移植的结果通常是 ECS 一片混乱,最终代码也很糟糕。 如果您确实需要将旧代码移植到 ECS,最好的选择是在 ECS 上从头开始编写相同的逻辑。您所需要做的就是使用您的知识和现有代码作为指导。 在系统中使用委托/回调或反应逻辑 在ECS中,从系统中获取一些逻辑并将其存储在组件中以供以后使用或立即对某些更改做出反应(例如,系统对在另一个系统中添加组件做出反应)可能是危险的。 它不仅给系统增加了不必要的互连性(它们变得严重依赖于外部调用)。它还通过添加我们几乎无法控制调用的逻辑来破坏我们美丽的数据处理管道。 按类型将文件组织到文件夹中 当您开始使用 ECS 时,您首先要按类型放置新文件:组件放在 Components 文件夹中,系统放在 Systems 文件夹中。但根据经验,我意识到这种排序方式效率很低。使用它进行导航以了解哪些组件与哪些系统相关并不容易。 当与特定功能相关的所有内容都在一个文件夹中(可能具有组件/系统的内部层次结构)时,最好的选择是按功能排序。也就是说,所有与健康和损坏相关的组件和系统都将位于 Health 文件夹中。这将允许人们查看该文件夹以了解其中系统的基本数据上下文,并使项目导航变得更容易。 在Unity游戏中使用ECS的最佳实践 在上文中,您可能已经了解了在 ECS 上开发 Unity 游戏时不应该做什么。现在我们来谈谈有用的做法和技巧。 使用标记组件标记实体 在ECS中,有一个标签组件这样的概念。它是一个无字段组件,仅执行实体标记的作用。您可以将其视为类中的布尔标志:它要么存在(true),要么不存在(false)。 例如,我们有一千个单位,其中一个由玩家控制。您可以使用空的 组件来标记它。这将允许您仅获取过滤器中的玩家单位,并在一次浏览所有单位时了解您正在使用常规单位还是玩家控制的单位。 PlayerMarker 尽量减少组件变化的地方 组件发生变化的地方越少越好。总的来说,这只是遵循“不要重复自己”的有益原则(我推荐给大家)。练习这个有很多好处: 它可以让您更好地了解项目中更改数据的过程,并在出现问题时简化调试 更新数据变化逻辑时,需要更新的代码较少,最好只更新一处 允许数据错误即时发生的可能性更小 例如,与其更改每个有损坏的系统中的 HealthComponent,不如创建一个 DamageSystem,其目的是对具有 HealthComponent 的实体造成损坏。 忘记组件后缀 组件后缀对于初学者来说非常有用,因为它提醒他们“这里只有数据”。但随着时间的推移,提醒的需要消失了,代码仍然难以理解无处不在的组件。 这就是为什么我想建议:您可以放心地忘记组件后缀。除了可能对 IntelliSense 中的搜索进行一些简化之外,它没有提供任何有用的东西。这只是一个提示,甚至可能是一个品味问题,所以这取决于你如何处理它:) 例如, 变成了 ,并且代码变得更具可读性 。在这种情况下, 保持不变,因为后缀在这种情况下携带有用的信息,注意它不是一个简单的组件。 HealthComponent Health entity.Has<Health>() PlayerMarker 延迟反应性和单帧组件 ECS 中的反应性可能有害。但是当需要反应性时该怎么办呢?答案是反应迟缓。 延迟反应性是指您创建事件已发生的数据,而不是在事件发生时直接调用逻辑,并且每个人都会在他们想要的时间对事件做出反应。我可以与 OOP 中的脏标志进行类比,任何人都可以声明 事件,但逻辑会在认为合适时对该事件做出反应。 SetDirty(true) 在 ECS 中,您只需创建一个包含或不包含数据的组件(您只需向现有组件添加一个布尔标志),系统将在适当的时候处理该组件。此类组件在世界上存在的情况并不少见,仅用一帧来警告所有系统,但不在下一帧中重复逻辑。删除可以由生成事件的系统来处理,也可以由某个单独的系统来处理,该系统将在您想要的位置删除所有类型 X 的组件。 例如,您有一个 。为了告诉它要造成多少伤害,您可以声明 以及伤害量,并将其添加到应该承受伤害的实体中。 使用 和 遍历所有实体,损坏实体,删除 并创建一个 ,通知 之后的所有系统损坏的实体。在帧结束时,各个系统删除 ,以便系统在下一帧中不再处理该标记。 DamageSystem MakeDamageComponent DamageSystem HealthComponent MakeDamageComponent MakeDamageComponent DamagedEntityMarker DamageSystem DamagedEntityMarker 请求/事件作为系统的 API 发展单框架组件的思想,我们可以用它们来表达一种系统的API。我们应该区分用于外部请求的请求组件和用于向每个人通知事件的事件组件。系统本身可以控制两个组件的生命周期。可以在处理后立即删除请求并在启动新事件之前清理事件。如何准确命名它们以及是否添加请求/事件后缀取决于您。 例如,您有上一段中的 。您可以使用 组件向其表达损坏请求,并使用 组件通知其他系统。系统内部逻辑如下:清除上一帧中的所有 ,通过请求损坏所有实体,删除请求并添加 组件。 DamageSystem MakeDamageRequest DamagedEntityEvent DamagedEntityEvent DamagedEntityEvent 存储对组件内另一个实体的引用 您可能有一个问题“如何在ECS中建立实体之间的链接?是否需要用组件标记实体,然后循环搜索它们?”。 当然不是。一切都变得更加简单和普遍:我们只需保存引用即可。唯一的区别是,引用不是我们感兴趣的另一个实体的组件,而是实体本身。 因此,您可以将带有实体的字段(或框架存储实体的任何方式)添加到组件中。在使用它之前,您需要检查该实体是否处于活动状态并具有所需的组件。然后你就可以得到这个组件并按照你喜欢的方式使用它。 例如,您可以不直接在实体上创建 ,而是将其作为参考目标实体的单独实体事件运行。为此,您将 字段添加到 并重新设计 。现在它应该遍历所有请求,检查目标是否是具有 生命实体,获取 并造成伤害。 MakeDamageRequest Entity target MakeDamageRequest DamageSystem HealthComponent HealthComponent 运行 现在看起来也会有所不同。您无需将组件直接添加到实体,而是使用 创建一个新实体并指定 。这样,您可以以牺牲过滤便利性为代价,为单个 实体触发多个不同的损坏事件。 MakeDamageRequest MakeDamageRequest target target 将重复逻辑移至 StaticUtils/Extensions 随着时间的推移,您开始注意到您在不同的系统上运行相同的逻辑。通常,这是一个迹象,表明是时候创建一个新系统了:D 但碰巧重复的逻辑是次要的,与一两个特定的组件/实体相关,并且其结果用于不同的目的。比如说,对组件中数据的特殊解释。一些开发人员允许直接在组件中声明此类附加逻辑(例如,以 getter 的形式)。但为了避免违反 ECS,我建议另一种选择:我们从系统调用的静态实用程序(或 C# 中的扩展)。 例如,我们有一个 。里面有球队颜色。在多个系统中可能需要检查两个实体是否属于同一团队。因此,我们创建一个静态类 和其中的一个方法 ,描述比较两个实体的团队的重复逻辑。 InTeamComponent TeamUtils IsInSameTeam(Entity, Entity) 按执行时刻对系统进行分组 如您所知,系统调用的顺序在 ECS 中至关重要。因此,按照系统在框架中的调用顺序在顶层对系统进行分组是很方便的。 例如,框架中首先调用的系统可以是所有与输入相关的系统。他们将收集用户输入并以 ECS 格式准备。其次是一组具有游戏逻辑的系统,它们将以自己的方式解释输入数据并更新 ECS 世界。最后,我们可能有一组系统负责渲染或只是在所有游戏逻辑之后应该调用的各种附加事物。 将主要功能分离到单独的程序集中 这种方法将功能相互分离并控制它们的依赖关系。在理想的世界中,它们根本不应该重叠。功能之间的顺序应该不重要。 还应该有一个核心组件,所有功能需要工作的组件都应该位于其中。 我应该在 ECS 中将组件/系统拆分成更小的部分吗? 这个问题很有趣,不同经验的Unity开发者对此有不同的看法。但我会尽力涵盖这两个答案,以帮助您了解您的需求。 是的,总是需要拆分组件 ECS 组织中的这种方法可以称为原子方法。这种方法的最高境界是每个组件只有一个字段。这将使我们能够达到该项目组合学的最高点,并消除以 ECS 抽象的名义进行重构的需要。我们不能再思考“如何将具有X属性的实体组合起来”。 ECS中总是拆分组件的缺点: 类和文件的数量将会增加,如果您不适当注意项目组织,这可能会导致大型项目的混乱 组件的数量(一般或每个实体)会影响框架的性能 通过一堆属性来理解实体是什么比较困难(可以通过一个普通名称的标记来解决) 现在让我们转向第二种意见。 不,您应该仅在需要时拆分组件 这个原则被称为“在时间到来之前不要分裂”。这是我个人坚持的原则。当你需要拆分数据时,衡量标准很简单:这些数据是否已使用/计划在该组件之外的其他地方使用?如果没有,则没有理由花时间在它上面。你可以采用类似的方法将逻辑划分为系统。 仅在需要时拆分 ECS 中的组件的缺点: 仍然需要花时间在ECS抽象上 设计实体的自由度较低 您可以选择自己的立场:) Unity/C# 框架 如果您是初学者,我会假设您首先考虑学习 Unity DOTS。但我想警告你。 Unity DOTS 对于初学者来说不是一个好的选择,因为它庞大且复杂。没有我们想要的那么多文档和经验丰富的人员。而且,它与旧的 Unity 代码不太兼容(这一切可能会在本文发布后改变)。 如果您喜欢 Unity Editor 并且已经习惯将组件直接挂在 GameObjects 上,那么 是您的最佳选择。它提供简单的 API、与 Unity 编辑器的紧密集成(它可以在 Unity 之外工作)以及与 MonoBehaviour 的便捷配合。简单方便。使用 Unity 所需的一切都在其中。您只需安装它并使用它即可。它的主要缺点是需要付费的 才能在 Unity 中完全工作。 Morpeh Odin Inspector Entitas 和 DOTS (Unity ECS):您应该掌握它们吗? 现在简要回顾一下我个人遇到过的 Unity/C# 框架以及我注意到的优缺点。值得注意的是,自本文发布以来,下面描述的所有内容可能会发生变化,因此最好自己检查框架。 实体 它是最古老的 Unity/C# ECS 框架,并且仍然是最受欢迎的。这是 ECS 职位发布中最常见的一种。 实体优点: Unity 编辑器中出色的 WorldViewer 流畅的代码风格(得益于代码生成) 良好的文档 非常大的社区 许多成功的项目都在其上 开发商的强制支持(感谢 AssetStore) 可以在 Unity 之外的纯 C# 中使用 实体缺点: 相对于其他 ECS 框架性能较差(但仍优于 MonoBehaviour) 大量分配,这会对 GC 产生负面影响 需要为组件结构的每次更改生成代码 在大型项目中,API 由于代码生成而变得非常臃肿 Github 版本不允许在编译错误时调用代码生成,而 AssetStore 版本则允许 你应该掌握它,至少在基础水平上。 DOTS(统一 ECS) 我认为无需介绍。 DOTS 不是一个框架,而是一个成熟的平台(技术堆栈),内部有 Unity ECS 作为框架。 我想指出的是,下面的经验有些过时了。我承认最新的 DOTS 可以解决下面描述的问题。 DOTS (Unity ECS) 优点: ECS 上成熟的开发平台 由引擎开发人员构建,尽可能紧密地集成到编辑器中 支持作业和突发 它与 Jobs 和 Burst 一起实现了 ECS 框架中的最高性能。 拥有一个优秀的网络库,具有 NetCode 预测功能 子场景机制 DOTS(Unity ECS)缺点: 正在进行的工作,这会导致破坏旧代码更改和新错误 文档薄弱,往往跟不上变化,你必须阅读源代码 需要编写比类似代码更多的技术代码 代码难以阅读且不够简洁 工作速度并不比没有 Burst/Jobs 的开源解决方案快(在某些情况下更慢) 不太适合旧的 Unity 代码 DOTS 本质上是一个运行时,并非所有旧功能都已移植到 DOTS。这限制了使用的可能性。 Unity 中 ECS 的结论 您可能已经从大量的缺点中注意到,ECS 并不是灵丹妙药。与任何其他架构解决方案一样,该架构解决方案也有其优点和缺点,如果您选择使用此架构模式进行开发,则必须忍受这些优点和缺点。因此,在项目中是否使用 ECS 的选择完全取决于您。 我强烈建议你至少尝试在 ECS 上做一个小项目来了解你是否喜欢这种方法。 我还建议您查看此 ,其中有许多与 ECS 相关的问题的答案。您将找到报告链接、不同语言的框架列表以及使用 ECS 的游戏和程序示例。 存储库 从我个人的角度来看,ECS 看起来是创建 Unity 游戏的绝佳选择。对我个人来说,开发它是一种乐趣:你正在开发一个游戏,而不是试图弄清楚如何在不破坏任何东西的情况下将新代码集成到旧系统中。值得记住的是,ECS 开发的可用性很大程度上受框架选择的影响,因此请尝试不同的选项并谨慎选择。 根据我的经验,我倾向于认为 ECS(或其变体)是交互式游戏开发的未来。不仅仅是因为 (甚至可能是 Epic)选择它作为主要关注点,还因为 ECS 在游戏开发方面具有优势。 Unity Technologies 总的来说,这是一种实用的方法,一开始看起来很尴尬,但从长远来看是有回报的。