使用实体组件系统构建业务应用程序有什么优势吗?

Jef*_*eff 10 components entity

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

Dra*_*rgy 9

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

这就是说,我在VFX领域,它不是说从游戏不同。我们仍然必须为诸如角色,发出粒子,与网格,纹理交互,播放声音片段,渲染结果,允许人们编写插件,脚本等之类的动画。

尝试将ECS应用于业务领域要困难得多。就是说,我想如果您有相对较少的系统来处理大量实体组合,那么它确实可以帮助创建可维护的系统。

可维护性

与以前的面向对象的方法相比,甚至在我的个人项目中,我发现ECS使我的维护变得如此容易,是因为以前的方法通常将维护开销从使用类的客户端转移到了类本身。但是,将有数十个接口,数百个子类,它们都继承不同的东西并实现不同的接口以分别维护。如此众多的粒度类以及进行模拟测试的需求也使测试变得困难。

我的大脑只能处理这么多,数百个子类之间的交互远远超出了限制。很快,我发现自己再也无法推理发生了什么,更不用说何时何地,复杂的交互作用导致复杂的副作用,而且再也没有自信我可以将新代码夹在其中而不会引起不必要的副作用。

计算科学家的主要挑战是不要因自己制作的复杂性而感到困惑。-EW Dijkstra

这甚至适用于我自己编写的项目。有一个断点,通常在几十万个LOC之后,我什至无法理解自己的创作。我会在这里和那里进行重构,获得一点动力,然后去度假,回来,然后又一次迷失了方向。

ECS消除了这一挑战,我的意思不是说我可以度过两个星期的假期,回到代码库,看一些代码,并获得我编写时清晰的视觉效果首先。ECS在这方面并没有改善太多,我仍然需要一些时间来重新认识我已经好一阵子没看过的代码了。ECS帮了大忙的原因是我不需要回忆起我为扩展和更改软件而写的所有内容。这些系统彼此分离,如果我忘记了它们的工作原理,这并不是什么大不了的事情。我可以专注于我需要做的事情,而不必担心由于控制流的复杂交互而触发副作用的复杂交互。我可以专注于我需要做的事情,而不必考虑其他任何事情。

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

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

可扩展性

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

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

通常,在我工作过的以前的体系结构中,范围从高度侵入性到中等侵入性的变化。围绕纯接口的COM样式的系统可能需要引入新接口,或者更糟的是,更改现有接口,并更新数百个子类型以实现新功能。如果我们有一个已经继承了所有内容的中央抽象基类,我们也许可以对它进行集中修改以实现这个新接口(或现有接口的新部分),但是如果有一个中央基类,那可能会很可怕。类,以获取可能需要这样一个名称的所有内容,并且需要花很多时间编写精致的代码。

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

之后,我就可以将此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);
Run Code Online (Sandbox Code Playgroud)
  • 上面的问题是:Bar由于Foo不提供这种实现,抽象的功能将在哪里实现?这就是系统以类似方式加入ECS的地方。

我认为,对于我们大多数不得不经历一些现有类的复杂代码以使其进行一些新操作而又冒着引起不必要的副作用/毛刺/脚趾踩踏的风险的人来说,诱惑总是存在的,否则我们甚至可能面临诱惑我们控制之外的第三方库提供一些更多的功能,如果该第三方库仅提供了“这件事”,我们会发现在整个代码中使用该功能非常有用,否则我们可能会讨厌这个想法即使我们的任务是提供新的中心行为,也必须更改同事的现有代码(不想踩脚趾)。

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

关联数据

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

ECS在中央级别解决了该问题,因为现在您可以非常高效地将组件连接到任何想要的实体或从中删除组件。这成为您动态关联新数据的方法。您不再需要手动同步关联数据结构。

测试中

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

我对看到单元测试通过感到无比宽慰,因为遇到的大多数错误都与被测试的接口之间发生的事情有关,尽管编写了数百个单元测试,但尽管如此,我们还是有很多缺陷通过。我喜欢测试驱动的开发,并且确实在单元测试中发现了价值,告诉我这个单元正在执行应做的事情,这使我可以在整个代码库中更加自信地使用它,但是单元测试从未给我带来好处。对整个代码库的正确性感到宽慰。

ECS为我解决了这个问题,并使单元测试变得更加有价值,即使对于像我这样从未掌握过测试技术的人也是如此,因为那里有少数几个系统,每个系统都承担着大量的工作(而不是细小的对象),而且是具体的。如果我们需要做类似于模拟测试的任何事情,只需插入运行它们并测试它们所需的组件/实体。尽管系统是可测试的最小单元,但开始感觉像测试系统比单元测试更接近集成测试。

均质加工

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

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

互动互动

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

在此处输入图片说明

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

在此处输入图片说明

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

不变量

当我开始开发ECS引擎时,对我来说最可怕的部分之一就是缺乏信息隐藏功能。当组件仅仅是原始数据时,它们悬而未决,我认为应该是它们的私有空间,任何人都可以接触。在本质上可能更关键的业务领域中,这可能会倍加吓人。

但是我发现不变式同样易于维护,即使不是更多,因为访问任何给定组件的系统数量有限(通常,如果修改了数据,则只有整个代码库中的一个系统才有意义) ,极其简单的控制流程以及由此产生的非常可预测的副作用。当您只有少数几个系统需要考虑功能性时,测试代码库的正确性非常容易。

结论

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

如果您有兴趣,我可以在以后的迭代中扩展我的答案,并尝试解决我完全不了解ECS时遇到的一些小难题。


FTW*_*ton 5

(为死灵法术道歉)

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

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

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

相反,我们可能会添加一个新的连接表和新的数据输入页面(虽然重新使用常用控件).

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