2024年夏季技术解析:Hytale的实体组件系统
Hytale团队发布于2024年6月18日
大家好!今天我们将揭开Hytale引擎背后技术的神秘面纱,重点介绍我们最近宣布的一个框架:Flecs——一个轻量级且强大的实体组件系统(ECS)。
不久前,我们谈到了重启Hytale引擎的决定,从Java服务器和C#客户端转向使用C++构建两者。做出这一改变有很多原因:我们希望确保能够在多个平台上发布游戏;我们希望在针对低配置设备时提高性能;我们希望构建一个足够强大的核心引擎,以便我们能够在未来对游戏进行修补和维护。
ECS是我们依赖的众多工具之一,可以帮助我们实现这些目标。在这篇文章中,我们将讨论为什么选择Flecs作为Hytale新引擎的ECS基础,以及它究竟如何帮助我们实现这些目标。
但在深入探讨Flecs本身之前,我们需要先看看ECS——实体组件系统模式。
从一个三字母缩写到另一个
ECS并不是一个特别新的概念,也不像几年前那样在游戏开发中很少见。即便如此,第一次接触时仍然可能感到复杂和陌生。为了正确地讨论它,我们需要在游戏开发的范围内对ECS概念进行语境化。
传统的游戏开发很大程度上依赖于古老的面向对象编程(OOP)模型,或实体-组件(或角色-组件!)架构。面向对象编程在整个软件开发领域都很普遍,它将问题分解成可以作为对象进行推理的熟悉结构。你可能有一个总括性的Character对象类型,它提供所有Character通用的游戏逻辑,然后由Player对象、各种NPC对象以及任何其他可以被视为Character的类型继承或特化。
这个树状结构可能会变得非常广泛。
实体-组件让我们更接近ECS,是许多流行游戏引擎(如Unreal和Unity)使用的主要架构。在这种范式中,我们现在有了实体——个体单位,如"玩家"、"NPC"、"椅子"——和组件——可以附加到这些实体上的数据和功能的组合。每个实体由多个组件组成。如果你熟悉OOP,实体-组件范式严重依赖于"面向对象编程 "的原则。例如:玩家可能在世界中有一个位置,能够读取控制器输入,并拥有一个物品栏;NPC也有一个位置,可能有一个物品栏,并有某种形式的行为逻辑;我们的椅子,不幸的是,只有一个位置。
除非你为它添加行为逻辑,它就变成了一个生物[免责声明:这只是为了演示目的!]
立即就应该明显看出这种结构对mod制作有多么大的解放。它不仅通过资产配置促进了高度数据驱动的功能,仅仅改变附加到NPC或对象上的组件就会产生明显不同的行为,而且理论上允许创建全新的功能,而无需修改现有代码。使用脚本语言,你可以创建并添加自己的组件,并将其附加到你想要运行该行为的实体上。各种可能性就此打开
一个实体可以以多种不同方式组合!
但这仍然不是ECS。ECS——实体组件系统——将这些概念进一步推进。而在实体-组件模型中,功能(例如方法和函数)存在于组件本身内部,ECS将这种功能与它处理的数据和状态解耦。与其让每个组件都有自己的内部更新逻辑,我们有系统来匹配具有定义的组件集的实体并对其进行操作。这意味着使用ECS,我们仍然可以解锁从不同组件组合实体的能力,但解耦结果是一种数据和逻辑架构,对硬件运行更加高效,因此性能更好。关于ECS如何实现这些性能优势的许多细节都是高度技术性的,但足以说明它涉及利用CPU架构,以一种紧密打包的方式构建数据以受益于其访问模式的局部性,并使用这些访问模式来尽可能地并行化大量逻辑。
系统可以匹配任何组件组合。
遗留引擎中的ECS
即使在我们还在开发遗留引擎时,我们就知道想要切换到使用ECS架构,这是由于它将给我们带来性能和可扩展性的提升,以及它与我们构建和配置游戏实体和角色的数据驱动方法的自然契合。因此,我们开发了自己的Java实现这一概念,并开始将其集成到整个遗留服务器中。当时,我们没有C#客户端的等效实现,这意味着我们的实现严格限于服务器端。
这项工作的一部分涉及重构现有逻辑的某些方面以遵循ECS模式,即使我们开始与之并行开发新功能。在那段时间里我们学到了许多教训,其中最主要的是,从头开始实现一个健壮和高性能的ECS框架是一项极具挑战性且耗时的工作。ECS有无数不同的风格,每种都有自己的优点和缺点,但都需要深入的理解和技术专业知识才能达到高标准。Java也并不总是性能最好的编程语言,由于其独特的怪癖,我们做出了许多妥协和设计选择。
即便如此,我们初生的ECS实现提供了明显的性能优势,以及一种新的系统架构方法,体现了我们想要实现的数据驱动设计原则。如果做得正确,我们可以让mod制作者轻松提供影响游戏行为的数据,而几乎不需要技术知识。
新引擎的可靠基础
当我们重启引擎时,我们知道我们想继续使用ECS,但也希望将其扩展到客户端,确保我们能在每个可行的方面收获好处。我们也知道,转向C++意味着可能会有其他框架——我们不必自己构建和维护的框架,具有推动该范式边界的前沿功能。
在评估了所有可用选项后,我们选定了Flecs——一个由ECS专家Sander Mertens编写和维护的高度精炼的ECS框架。
开箱即用,它为我们提供了大多数ECS实现常见的各种功能,以及出色的性能和多平台兼容性。完全用C语言编写并具有C++ API意味着它比任何C#或Java实现都要快得多,让我们能够充分利用其智能的并行化和多线程实现。另一个明显的好处是我们不需要自己维护它——Flecs经过实战检验,经常收到更新和错误修复,而且凭借其全面的测试套件,我们可以相对确定其稳定性。
但也许最吸引人的方面是它广泛的功能集,这些功能超越了传统ECS框架所提供的,并提供了将ECS推向新高度的灵活性。这方面的一个例子是"关系"的概念。像组件一样,关系是你可以附加到实体的数据,但这些数据用于连接一个实体到另一个实体。父子关系就是一个很好的例子,其中玩家实体可能有一个摄像机实体作为子实体,跟随它移动。另一个例子可能更加字面化,即实体A喜欢实体B。通过使用这种结构,我们可以轻松运行查询,如"找出所有喜欢实体B的实体",或"找出实体B喜欢的所有实体"。
在许多方面,ECS类似于数据库,Flecs充分利用了这一事实。底层规则引擎是一个强大的工具,支持以各种方式查询数据,从系统的简单匹配(例如,基于多个附加组件更新作物生长的系统)到复杂的游戏逻辑或调试查找(例如,找出所有持剑对玩家具有攻击性的NPC)。除此之外,组件共享机制允许我们创建一个基本角色类型,如Character,并说明NPC或Player是Character,让我们获得OOP风格的继承,但构建为受益于ECS优化。
有时,其中一些功能可能很难理解,这在学习全新架构时经常发生。尽管如此,一旦理解并掌握,它们就会提供极其强大的游戏开发工具。
更快的思考
最后,我们来看一个这样的例子。过去我们为Hytale的NPC引入了一项名为战斗行动评估器的增强功能。这是一个框架,旨在让NPC能够更智能、更"模糊"地决定使用哪种攻击以及对哪个目标使用,基于许多高度可配置的输入。虽然最初是在遗留引擎中实现的,但它从概念上就被设计为数据驱动的,每个单独的输入都以类似于ECS中组件的方式附加。虽然它在遗留引擎中admirably完成了其目的,并提供了有时可能被误认为是人类玩家的战斗NPC,但由于其对游戏整体性能的潜在影响,不得不对其施加限制。毕竟,在OOP环境中允许NPC基于大量输入数据做出"模糊"决策意味着巨大的处理负担——我们基于Java的pre-ECS引擎并不具备处理这种负担的能力。因此,我们只会以不规则的间隔运行战斗行动评估器。这可能意味着我们会避免大量NPC处于活跃战斗状态时可能导致的任何潜在服务器减速,但这也意味着它们会做出更慢的决策——慢到玩家可以察觉的程度。
有了Flecs,我们的游戏玩法设计选择不再受到性能问题的限制。通过遵循ECS模式重新设计内部框架并大量使用Flecs功能,我们最终得到了一个等效物,不再需要以如此低效的方式处理这些数据。相反,我们可以智能地将所有检查特定信息的查询分组并并行化它们,从而大大缩短处理时间,并消除了对评估频率进行人为限制的任何需要。在遗留引擎中,我们会按顺序执行检查(优先处理昂贵的检查,这样如果它们失败,我们就可以更早退出!),现在这些检查可以同时进行,由Flecs的查询引擎处理繁重的工作。
本质上,这意味着NPC可以更快地思考——比它们在遗留引擎中所能做到的更快地对环境和周围的变化做出反应。
展望未来
最终,这只是触及了Flecs和ECS所能实现的可能性的表面。引擎的许多其他部分都为围绕ECS进行优化提供了有趣的机会,从资产数据库到分阶段的世界生成,随着Flecs和Hytale引擎的不断发展,我们只期待可能性的增长。