C ++和模块化:我应该在哪里划线?

时间:2011-06-26 17:49:39

标签: c++ class interface game-engine modularity

根据广泛传播的建议,我应该注意尽可能保持我的大型软件项目的模块化。当然有各种方法可以实现这一点,但我认为没有办法使用更多或更少的接口类

以C ++中2D游戏引擎的开发为例。

现在,人们当然可以通过几乎所有东西使用接口来实现一个非常模块化的系统:从渲染器(接口渲染器类 - > Dummy,OpenGL,DirectX,SDL等),通过音频到输入管理。 / p>

然后,可以选择广泛使用消息传递系统。但从逻辑上讲,这些再次伴随着高性价比。

我应该如何构建这样的工作引擎?

我不想在性能方面(实体,粒子等的最大可行数量)降低我的引擎的限制,只是为了让一个完美的模块化系统在后台工作。这很重要,因为我还想针对CPU功耗和内存有限的移动平台。

例如,拥有渲染器类的接口将涉及用于时间关键绘制操作的虚函数调用。仅这一点就会使发动机减速很多。

以下是我的主要问题:

  • 我应该在哪里通过模块化编程在一致性和性能之间划清界线?

  • 有哪些方法可以保持项目的模块化,同时保持良好的时间关键操作性能?

4 个答案:

答案 0 :(得分:3)

有许多方法可以在不使用单个“接口”类的情况下保持代码模块化。

  • 您已经提到过消息传递
  • 然后有一些简单的旧回调。如果对象需要能够在系统中的其他位置触发某个事件,请为其提供一个可以调用以触发该事件的回调函数。它不需要知道任何关于你的架构的其余部分
  • 并使用模板和静态多态,您可以实现与接口类相同的大多数目标 - 但是性能开销为零。 (例如,模拟游戏引擎,以便在编译时选择基于Direct3D或OpenGL的渲染器

此外,模块化是棘手的,并不仅仅是通过隐藏接口后面的所有内容。对于它是模块化的,无论该接口实现什么应该可替换。您必须有一个策略来将一个实现替换为另一个实现。并且必须可能才能创建多个不同的实现。

如果您只是盲目地隐藏接口后面的所有,那么您的代码将不会是模块化的 。替换任何任何实现的任何东西将是一个巨大的痛苦,因为你必须挖掘无数层接口才能这样做。您必须遍历代码中的数百个位置,并确保选择并实例化并传递正确的实现。并且您的界面将非常通用,无法表达您需要的功能,或者具体说明无法实现其他实现。

如果你想要一个俗气的比喻,砖块是模块化的。砖可以很容易地取出并用另一块砖代替。但是你也可以将它们磨成细小的烤粘土颗粒。这是模块化的吗?你当然创造了更多,更小的“模块”。但唯一的影响是更换任何给定的组件要困难得多。我不能再拿起一块有形的砖块扔掉,把它换成砖块大小的东西。相反,我必须经历数千个小颗粒,为每个小颗粒寻找合适的替代品。而且由于被更换的部件不再被更大结构中的几块砖包围,但是有数十或数十万个颗粒,现在还有一些荒谬的其他“模块”受到影响,因为我换掉了他们与之接口的邻居。

将所有内容磨成更精细和更小的位并不会使任何更加模块化。它只是从您的应用程序中删除所有结构。编写模块化软件的方法是实际思考并确定哪些组件在逻辑上是如此独立且不同,以便可以在不影响应用程序其余部分的情况下替换它们。然后编写应用程序和组件,以保持这种隔离。

答案 1 :(得分:2)

首先是

原型,然后让界面边界出现。

抢占式界面设计可以使编码成为拖拽

在编写代码之前尝试设计抽象障碍可能会非常棘手,因为您遇到了两个风险。一个是你不可避免地会在错误的地方画出一些抽象障碍,当你开始编写工作代码(而不是接口代码)时,你会发现你的接口很难为你的问题服务,尽管用自然语言描述时听起来不错。另一个问题是它使编码更容易受到阻碍,因为你必须在头脑中而不是一个问题处理两个问题:编写一个你还不完全理解的问题的工作代码,并坚持可能转向的界面好坏。

界面边界出现在工作代码中。

我当然不是说界面不好,但是如果没有先编写工作代码就很难正确设计。一旦你有了一个工作程序,很明显哪些部分应该是同一个虚函数的不同实例,哪些函数需要共享资源(因此应该放在同一个类中)等等。

原型,然后只绘制您需要的界面边界。

因此,我同意@ jdv-Jan de Vaan的建议,即首先要做的是推出最有效的可读程序。 (这与最短的程序有所不同。当然,即使在最开始时,也有一些最小量的界面设计。)我的补充就是说接口设计就在那之后。也就是说,一旦你拥有尽可能简单的代码,就可以将它重构为接口,使其更短,更易读。如果您希望接口具有可移植性,那么在您实际拥有两个或更多平台的代码之前,我不会启动它。然后界面边界将以自然(和可测试)的方式出现,因为很明显哪些函数可以按原样使用,并且需要在接口后面隐藏多个实现。

答案 2 :(得分:1)

我不同意这个建议(或者你的解释)。 “尽可能模块化”:这应该在哪里结束?您是否要为3d矢量编写虚拟接口,以便切换实现?我不这么认为,但它会“尽可能模块化”。

如果您正在销售游戏引擎,则模块化可以帮助您缩短构建时间,减少潜在客户端所需的头文件数量,以及切换特定问题域的实现的能力(例如directx vs OpenGL的)。它还可以通过对代码进行分区来帮助您维护代码。但在这种情况下,您不需要将模块与接口分离。

我的建议是始终写最短的可读程序。如果您可以编写20行代码来解决本地问题,或者将函数分散到五个不同的类中,后者将更加模块化,但通常结果不太可靠,可读性较差且维护较少。

答案 3 :(得分:1)

请记住,虚函数调用主要用于处理(指针/引用)对象的集合,这些对象不一定都是相同的实际类型。

你当然不应该考虑通过OpenGL绘制正方形的东西,而是通过DirectX或者该顺序上的任何东西。在构建代码时,通过模板甚至文件选择来处理此级别的模块化是完全合理的,但是针对这种情况的虚拟函数没有任何意义。

这可能会提出有关从C ++中获取性能的相关建议:使用模板实现灵活性和模块化,同时仍保持最佳性能。模板如此受欢迎的主要原因之一是它们在不牺牲性能的情况下为您提供模块化。 CRTP与最初看起来需要虚函数的代码特别相关。

至于在一致性和性能之间划分界限,确实没有一个答案 - 这在很大程度上取决于您需要多少性能。对于您的情况(移动设备的3D游戏引擎),性能显然比许多(大多数)其他情况更重要。