16

我理解使用数据驱动的实体组件系统进行游戏开发的吸引力。自然,我正在尝试寻找其他领域来应用这种范式。当我即将着手开发一个小型企业应用程序时,我一直想知道 Entity-Component 是否适合它。但是,除了游戏之外,我找不到任何关于在任何东西中使用实体组件的示例或讨论。有原因吗?除了游戏之外,在软件中使用实体组件会有什么优势吗?

4

2 回答 2

22

我最终冒险尝试在游戏领域之外使用 ECS(现在作为独立开发者,以前是公司员工),结果令我震惊。我现在不会以任何其他方式做事,并且拥有比以往任何时候都更易于维护的系统(并不完美,但比我们过去在我的行业中使用的 COM 风格的架构要好得多)。我之所以冒险,主要是因为它似乎为我和我的团队过去在使用 COM 架构时遇到的所有问题提供了答案,尽管我想象着这样一个冒险的举动,以至于我最终可能会交换一组问题另一个(现在我一个人愿意冒险)。原来我没有用一罐蠕虫换另一罐。ECS 几乎解决了所有这些问题,同时几乎没有引入任何新问题。

也就是说,我在 VFX 领域,它与游戏没有什么不同。我们仍然需要为角色设置动画、发射粒子、与网格、纹理交互、播放声音片段、渲染结果、允许人们编写插件、脚本等。

尝试在业务领域应用 ECS 更加大胆。也就是说,我想如果你有相对较少的系统处理大量的实体组合,它真的可以帮助创建一个可维护的系统。

可维护性

我发现,与以前的面向对象方法相比,甚至在我的个人项目中,ECS 对我来说更容易维护,因为以前的方法经常将维护开销从使用类的客户端转移到类本身。但是,会有几十个接口,数百个子类,它们都继承了不同的东西并实现了不同的接口来单独维护。由于有如此多的细化类和进行模拟测试的需要,测试也变得困难。

我的大脑只能处理这么多,数百个相互交互的子类远远超出了极限。很快我发现自己不再能够推理发生了什么,更不用说何时何地,被复杂的交互所淹没,导致复杂的副作用,并且从来没有自信我可以在其中的某个地方夹入新代码而不会造成不必要的副作用.

计算科学家的主要挑战是不要被自己制造的复杂性所迷惑。——EW迪克斯特拉

这甚至适用于我自己独创的项目。出现了一个突破点,通常在几十万 LOC 左右之后,我什至无法理解自己的创作。我会在这里和那里重构,获得一点动力,只是去度假,回来,然后再次迷失。

ECS 消除了这个挑战,我并不是说我可以休两周假,回到代码库,查看一些代码,并获得我在编写它时所拥有的水晶般清晰的愿景首先。ECS 在这方面并没有太大的改进,我仍然需要一些时间来重新熟悉我很久没有看过的代码。ECS 帮助很大的原因是我不需要回忆我为扩展和更改软件而编写的所有内容。这些系统彼此如此分离,以至于如果我忘记了一个系统是如何工作的,这并不是什么大不了的事。我可以只专注于我需要做的事情,而不必担心通过控制流的复杂交互触发复杂的副作用交互。我可以只专注于我需要做的事情,而不必考虑其他任何事情。

即使将全新的核心级功能集成到产品中,这一点也适用。这些天来,当我在产品中引入一个新的核心功能,比如一个全新的产品核心音频系统时,我唯一需要考虑的就是如何将它集成到用户界面中。与我以前工作过的架构相比,将其集成到架构中相对容易。

同时使用 ECS,我只需要维护几十个系统即可提供不亚于上述功能的功能。它们内部确实有一些复杂的逻辑,但我不必维护数百种不同的实体组合,因为它们只存储组件,而且我不必维护组件类型,因为它们只存储原始数据而且我很少曾经发现需要回去改变它们(非常接近永远)。

可扩展性

事后能够使用中心概念扩展 ECS 架构是我迄今为止遇到的最简单的事情,并且需要对现有代码库如何工作的最少知识。

作为一个非常新鲜的例子,我最近遇到了一个强烈的愿望,即脚本编写者使用我的软件能够使用简单的全局名称访问场景中的实体。在他们必须指定一个完整的场景路径之前,Scene.Lights.World.Sunlight而不是简单地,Sunlight.

通常,在我以前工作过的架构中,变化范围从高度侵入到适度侵入。围绕纯接口旋转的 COM 风格系统可能需要引入新接口,或者更糟糕的是,更改现有接口并更新数百个子类型以实现新功能。如果我们有一个所有东西都已经继承的中央抽象基类,我们也许可以集中修改它以实现这个新接口(或现有接口的新部分),但如果有一个中央基类,那可能会很可怕类为所有可能需要这样的名称,并需要涉足大量微妙的代码。

使用 ECS,我所要做的就是引入一个新的组件,GlobalName它具有一个处理GlobalName组件并可以通过指定名称快速找到实体的系统。它还确保没有两个GlobalName组件具有匹配的名称。GlobalName由于 ECS 的性质,当该组件因实体被破坏或组件被从中删除而被破坏时也很容易拾取,以保留用于加速按名称搜索的数据结构(特里)同步中。

之后,我就可以将此GlobalName组件附加到脚本编写者想要通过全局名称引用的任何内容上。他们也可以自己附加它,然后稍后通过该名称引用给定的实体。组件还以在大多数情况下保持向后兼容性的方式对自身进行序列化(例如:以前版本的软件不知道是什么GlobalName,在加载引用它的场景数据时会简单地忽略它)。

考虑到这是事后很晚才添加到一个 4 年的旧软件中,而这个软件并没有预料到需要这样做,所以它几乎是我可以改变想象的那样轻松和非侵入性的更改。我设法让它在第一次尝试时工作得很好。作为奖励,新添加的所有重要代码使这项工作隔离在自己的空间中;它不会与其他任何东西混在一起,也不会增加其他任何东西的复杂性,如果我使用抽象接口或基类,就不可避免地会出现这种情况。除了几行简单的脚本和一些简单的 GUI 代码来在可用时显示这些全局名称外,我不需要修改任何核心来使这项工作正常工作。

《随处继承》

您是否曾经希望您可以从代码中的任何位置扩展类的功能,而无需实际修改其代码?例如:

// In some part of the system exists a complex beast of a class
// which is tricky modify:
class Foo {...};

// In some other part of the system is a simple class that offers
// new behavior we'd like to have in 'Foo', with abstract functionality
// (virtual functions, i.e.) open to substitution:
class Bar {...};

// In some totally different part of the system, maybe even a script,
// make Foo inherit Bar's behavior on the fly, including its default
// constructor, copy constructor, and destructor behavior for Bar's state.
Foo.inherit(Bar);
  • 上面留下了一个问题:抽象功能将在哪里Bar实现,因为Foo不提供这样的实现?这就是系统类比地为 ECS 发挥作用的地方。

我认为对于我们中的大多数人来说,诱惑将会存在,他们不得不涉足某些现有类的复杂代码以使其做一些新的事情,同时冒着导致不必要的副作用/故障/脚趾踩踏的风险,或者我们甚至可能面临过我们无法控制的第三方库只是提供更多功能的诱惑,如果它只是提供“这件事”,我们会发现在使用该第三方库的整个代码中非常有用,或者我们可能只是讨厌这个想法即使我们的任务是提供新的中心行为,也必须更改我们同事的现有代码(不想踩到脚趾)。

ECS 为您提供了这种灵活性,尽管方式与上述示例非常不同(但为您提供了类比的好处)。它允许您从任何地方扩展任何事物的行为/功能/状态。与上面的可扩展性示例一样,我不必修改任何现有的东西来提供全局名称搜索功能和状态。我可以从外部甚至从脚本扩展这些实体的行为,只需将一种新类型的组件添加到我想要的任何实体,此时我编写的对此类组件感兴趣的任何系统都可以使用鸭子类型方法(“如果它有一个GlobalName组件,可以提供一个全局名称,可以用来快速找到匹配的组件”)。

关联数据

与上述类似,您是否曾经面临过将数据与代码中的现有对象相关联的诱惑?在这种情况下,我们可能必须维护并行数组或关联容器(如字典/地图),并且此类代码可能很难正确编写,因为它必须在添加和删除新对象时保持同步。

ECS 在中央层面解决了这个问题,因为现在您可以非常有效地将组件附加到您想要的任何实体或从任何实体移除组件。这成为您动态关联新数据的方式。您不再需要手动同步关联数据结构。

测试

就我个人而言,另一个问题可能是因为我从未掌握过单元测试的艺术(尽管我确实与一位真正研究过该主题的同事一起工作),它从未让我确信系统是相对错误的-自由。集成测试让我在这方面更有信心。我的问题是:即使单元测试通过了,你怎么知道客户端不会滥用接口?如果他们在错误的时间使用它怎么办?如果他们在故意不设计为线程安全的情况下尝试从多个线程中使用它怎么办?

看到单元测试通过,我并没有感到多么宽慰,因为遇到的大多数错误都与正在测试的接口之间发生的事情有关,尽管我们编写了数百个单元测试,但我们还是收到了很多通过。我喜欢测试驱动的开发,我确实在单元测试中发现了价值,它告诉我这个单元正在做它应该做的事情,这让我可以在整个代码库中更自信地使用它,但是单元测试从来没有给我对整个代码库的正确性感到非常宽慰。

ECS 为我解决了这个问题并使单元测试更有价值,即使对于像我这样从未掌握过测试艺术的人来说,因为有少数系统,他们每个人都做了大量的工作(而不是细粒度的小对象),而且他们'是具体的。如果我们必须做任何类似于模拟测试的事情,只需插入运行和测试它们所需的组件/实体。开始感觉测试系统比单元测试更接近集成测试,即使系统是最小的可测试单元。

均质处理

要应用 ECS,需要采用一种更循环的逻辑,其中更均匀的循环一次只做一件事。许多 OOP 倾向于鼓励非同质的控制流和复杂的交互,导致在系统的任何给定阶段/状态中发生许多事情。这是我最初发现的最困难的部分,因为我想一次将不同的任务应用于给定的实体/组件集,而我的诱惑不能如此直接地满足,因为解耦系统一次只执行一项任务。所以我必须学习如何延迟处理,存储一些状态以供下一个系统使用,并且我还使用(至少)一个事件队列,以便系统可以触发由其他系统处理的事件。

尽管如此,由于一系列简单的循环一次做一件事,我找到了编写复杂交互等价物的方法。强迫自己以这种方式工作,一次对一组实体应用一个统一的任务,从来没有像我想象的那么困难。在被迫这样做一段时间并保持结果之后——哇!我应该一直这样做。回想起十年来维护架构的过程,在呼吸到 ECS 架构的新鲜空气之后,这些架构比他们需要的维护难度大得多,这实际上有点令人沮丧。

互动

这是一个简化的“交互”图(不一定表示直接耦合,因为耦合版本将从具体对象到抽象接口)比较我采用 ECS 前后的差异。这是之前的:

在此处输入图像描述

除了那只是在少数类型之间(我懒得画数百个)。这就是为什么我一直在努力维护这些东西并且感觉在代码中纠缠不清。这是因为代码之间的交互实际上是一团糟,导致您在系统中使用各种远程功能,从而导致副作用。之后(现在组件只是原始数据,它们不包含自己的功能):

在此处输入图像描述

第二个版本是如此,更容易理解,更容易扩展,更容易维护,更容易推理正确性,更容易测试等等。如果你的业务架构能够有效地适合第二种类型的模型,我不能夸大它可以简化一切。

不变量

当我开始开发 ECS 引擎时,对我来说最可怕的部分之一是缺乏信息隐藏。当组件只是原始数据时,它们会悬空我认为应该是它们的隐私的东西,任何人都可以触摸。在本质上可能对任务更为关键的业务领域中,这可能是双重可怕的。

然而,由于访问任何给定组件的系统数量有限(而且通常如果数据被修改,整个代码库中的一个系统才有意义),所以我发现不变量同样易于维护,甚至更多。 ,极其简单的控制流程,以及由此产生的极其可预测的副作用。当您只需要担心一些系统的功能时,测试代码库的正确性非常容易。

结论

因此,如果您愿意冒险,我认为它可能会非常有效地应用于某些业务领域。我认为首先值得考虑的主要事情是,您是否可以将软件的全部需求建模为少数处理存储在组件中的数据的系统,每个系统仍然执行庞大但单一的职责(类似于 a RenderingSystemGuiSystem, PhysicsSystem,InputSystem等)。如果您发现需要数百个不同的系统来捕获业务逻辑,ECS 的好处自然会减少。

如果您有兴趣,我可以在以后的一些迭代中扩展我的答案,并尝试回顾一下我在 ECS 完全湿透时遇到的一些小问题。

于 2017-12-21T10:05:09.203 回答
7

(为死灵道歉)

来自企业背景,我最近一直在考虑这个问题。实体组件系统相对较新,代表了与大多数业务开发人员所经历的完全不同的设计范式。

考虑到我自己公司的例子,我已经看到了一些实体组件系统可以提供好处的场景。

例如,在我们的主要应用程序中,地址与联系人和组织相关联。(我们的数据库中有 ContactAddress 和 OrganisationAddress 连接表。)一位客户也希望将项目与地址相关联。有很多方法可以实现这一点,但基于实体组件的方法对我来说似乎很优雅——只需将一个 Addressable 组件添加到 Project 实体,GUI 就会自行解决。

相反,我们可能会添加一个新的连接表和新的数据输入页面(尽管重新使用通用控件)。

我认为,主要的缺点是(最初)开发人员缺乏将这种范式应用于商业软件的最佳方法的意识,正是因为以前似乎没有这样做过。一旦你开始采用这种方法,你就会致力于它——如果一旦你的项目达到一定的复杂性就证明它令人沮丧,那么没有重大的重写就没有出路。

于 2015-02-20T11:33:58.040 回答