如何实现可热交换的C ++模块?


39

快速的迭代时间是开发游戏的关键,在我看来,这比花哨的图形和具有大量功能的引擎更重要。难怪许多小型开发人员选择脚本语言。

能够暂停游戏并修改资产和代码,然后继续并立即使更改立即生效的Unity 3D方式对此非常有用。我的问题是,有人在C ++游戏引擎上实现了类似的系统吗?

我听说有些真正的超高端引擎确实可以,但是我对找出是否有办法在本土引擎或游戏中做到这一点更感兴趣。

显然会有折衷方案,而且我无法想象您可以在游戏暂停期间重新编译代码,重新加载代码并在各种情况下或在每种平台上工作都可以。

但是,对于AI编程和简单级逻辑模块来说,这也许是可能的。我似乎并不想在短期(或长期)项目中做到这一点,但我很好奇。

Answers:


26

尽管没有典型的C ++代码,但绝对可以做到。您需要创建一个C风格的库,您可以在运行时动态链接并重新加载它。为了使之成为可能,库必须将所有状态包含在不透明指针中,该指针可以在重新加载时提供给库。

蒂莫西·法拉尔(Timothy Farrar)讨论了他的方法:

“为了进行开发,代码被编译为一个库,一个小的加载程序创建该库的副本,并加载该库副本以运行该程序。该程序在启动时分配所有数据,并使用一个指针来引用任何数据。除了只读数据外,我不使用任何全局变量。在程序运行时,可以重新编译原始库。然后一键按下,程序返回到加载器并返回单个数据指针。加载器复制新库,加载副本,然后将数据指针传递到新代码。然后引擎从中断的地方继续。” 原始资源,现在可以通过网络存档获得


在您实际回答问题时,我更喜欢布莱尔的回答。
乔纳森·迪金森

15

用二进制代码解决热交换是一个非常非常困难的问题。即使将代码放在单独的动态链接库中,代码也需要确保内存中没有对函数的直接引用(因为函数的地址可以通过重新编译来更改)。从本质上讲,这意味着不使用虚函数,而使用函数指针的任何东西都将通过某种调度表来使用。

毛茸茸的东西。对于需要快速迭代的代码,最好使用脚本语言。

在工作中,我们当前的代码库是C ++和Lua的混合体。Lua也不是我们项目的重要组成部分,几乎是50/50的比例。我们已经实现了Lua的动态重新加载,因此您可以更改一行代码,重新加载并继续进行。实际上,您可以执行此操作来修复Lua代码中发生的崩溃错误,而无需重新启动游戏!


3
另一个重要的一点是,即使您不打算将系统保留在脚本中,但是如果您希望在早期进行大量迭代,那么从Lua开始仍然是完全合理的,然后在需要时使用C ++。额外的性能,迭代速度变慢。这里的明显缺点是最终需要移植代码,但是很多时候使逻辑正确是最困难的部分-一旦弄清楚了逻辑部分,代码几乎会自行编写。
Logan Kincaid 2010年

15

(如果只是幽默的心理形象,您可能想知道“猴子打补丁”或“鸭打孔”一词。)

除此之外:如果您的目标是减少“行为”更改的迭代时间,请尝试一些可以最大限度地帮助您解决问题的方法,并很好地结合起来以在将来实现更多功能。

(这会有点切线,但是我保证它会回来的!)

  • 从数据开始,从小开始:从边界(“级别”等)重新加载,然后逐步使用OS功能来获取文件更改通知或定期进行轮询
  • (有关奖励积分和较低的加载时间(再次减少迭代时间),请查看数据烘焙。)
  • 脚本是data,它允许您迭代行为。如果您使用脚本语言,则现在具有通知/功能,可以重新加载这些脚本(已解释或已编译)。您还可以将解释器挂接到游戏中的控制台,网络套接字等上,以提高运行时的灵活性。
  • 代码也可能是数据:您的编译器可能支持overlays,共享库,DLL等。因此,您现在可以选择一个“安全”时间来卸载和重新加载手动或自动覆盖或DLL。其他答案在这里详细介绍。请注意,此方法的某些变体可能会与加密签名证明,NX(无执行)位或类似的安全性机制混淆。
  • 考虑一个深版本的保存/加载系统。如果即使面对代码更改也可以稳健地保存和恢复状态,则可以关闭游戏,并在同一时间使用新逻辑重新启动游戏。说起来容易做起来难,但是它是可行的,并且比通过调用内存来更改指令明显更容易,更可移植。
  • 根据游戏的结构和确定性,您可能可以进行录制和回放。如果该记录刚好在“游戏命令”上(例如,打牌游戏),则可以更改所需的所有渲染代码,然后重播该记录以查看所做的更改。对于某些游戏,这就像记录一些开始参数(例如随机种子)然后进行用户操作一样“容易”。对于某些人来说,要复杂得多。
  • 努力减少编译时间。与上述保存/加载或记录/回放系统结合使用,甚至与覆盖或DLL结合使用,与其他任何事情相比,这可能会减少周转时间。

即使您没有完全重新加载数据或代码的方式,这些观点中的许多观点也是有益的。

支持轶事:

在大型PC RTS(约120人的团队,主要是C ++)上,有一个非常深的状态保存系统,该系统至少用于三个目的:

  • “浅”保存不是馈送到磁盘,而是馈送到CRC引擎,以确保多人游戏保持锁步仿真状态,每10-30帧一个CRC。这样可以确保没有人作弊,并在几帧后捕获了异步错误
  • 如果并且当发生多人游戏同步错误时,每帧都会执行一次超深度保存,然后再次馈送到CRC引擎,但是这次CRC引擎将生成许多CRC,每个CRC用于较小的字节批处理。通过这种方式,它可以准确地告诉您状态的哪一部分已在最后一帧内开始偏离。我们发现使用此处理器的AMD和Intel处理器之间存在令人讨厌的“默认浮点模式”差异。
  • 常规的深度保存可能不会保存例如您的单元正在播放的动画的确切帧,但会获取所有单元的位置,运行状况等,从而使您可以在游戏过程中随时进行保存和恢复。

从那以后,我就在DS的C ++和Lua纸牌游戏上使用了确定性的记录/播放。我们加入了为AI设计的API(在C ++方面),并记录了所有用户和AI动作。我们在游戏中使用了此功能(为玩家提供重播),但也可以诊断问题:当发生崩溃或异常行为时,我们要做的就是获取保存文件并在调试版本中进行播放。

此后,我还多次使用了叠加层,并将其与“自动抓取该目录并将新内容上传到手持设备”系统结合使用。我们要做的就是离开过场动画/关卡/所有内容,然后再返回,不仅将加载新数据(子画面,关卡布局等),而且覆盖层中还将包含任何新代码。不幸的是,由于复制保护和专门处理代码的反黑客机制,对于最新的手持设备而言,这变得越来越难。不过,对于lua脚本,我们仍然这样做。

最后但并非最不重要的一点:您可以(而且在各种非常小的特定情况下,我也可以)通过直接修补指令操作码来进行一些麻烦。但是,如果您使用的是固定平台和编译器,则此方法效果最佳,并且由于它几乎无法维护,非常容易出错,并且难以快速完成工作,因此我大多只在调试时使用它来重新路由代码。但是,它确实使您很快了解了很多有关指令集体系结构的知识。


6

您可以执行一些操作,例如在运行时热交换模块,将模块实现为动态链接库(或UNIX上的共享库),并使用dlopen()和dlsym()从库中动态加载函数。

对于Windows,等效项是LoadLibrary和GetProcAddress。

这是一种C方法,在C ++中使用它会带来一些陷阱,您可以在此处阅读有关内容。


该指南是制作可热交换库的关键,谢谢
JqueryToAddNumbers

4

如果您使用的是Visual Studio C ++,则实际上您可以在某些情况下暂停并重新编译您的代码。Visual Studio支持编辑并继续。在调试器中附加游戏,使其在断点处停止,然后在断点后修改代码。如果要保存然后继续,Visual Studio将尝试重新编译代码,将其重新插入到正在运行的可执行文件中,然后继续。如果一切顺利,所做的更改将实时生效,而无需执行整个编译-构建-测试周期。但是,以下将阻止其工作:

  1. 更改回调函数将无法正常工作。E&C的工作原理是添加新的代码块并更改调用表以指向新的块。对于回调和使用函数指针的任何操作,它仍将执行旧的未修改的调用。要解决此问题,您可以使用包装器回调函数来调用静态函数
  2. 修改头文件几乎永远不会起作用。设计用于修改实际的函数调用。
  3. 各种语言构造都会导致它神秘地失败。根据我的个人经验,通常会先声明枚举之类的东西。

我已经使用“编辑并继续”来显着提高UI迭代之类的速度。假设您有一个完全用代码构建的工作UI,只是您不小心交换了两个框的绘制顺序,所以看不到任何东西。通过实时更改两行代码,您可以节省20分钟的编译/构建/测试周期,以检查琐碎的UI修复程序。

这不是针对生产环境的完整解决方案,我发现最好的解决方案是将尽可能多的逻辑移到数据文件中,然后使这些数据文件可重载。


哪个编译周期需要20分钟?!?我宁愿开枪
自杀

3

正如其他人所说,动态链接C ++是一个难题。但这是一个已解决的问题-您可能听说过COM或多年来使用的一种营销名称:ActiveX。

从开发人员的角度来看,COM的名称有点不好,因为要实现使用C ++组件公开其功能的C ++组件可能会花费很多精力(尽管使用ATL(ActiveX模板库)可以更轻松地实现)。从使用者的角度来看,它的名字不好,因为使用它的应用程序(例如,将Excel电子表格嵌入到Word文档中或将Visio图表嵌入Excel电子表格中)在当日往往会崩溃。这归结为相同的问题-即使有Microsoft提供的所有指导,COM / ActiveX / OLE仍然很难实现。

我将强调COM技术本身并不是天生就糟糕的。首先,DirectX使用COM接口公开它的功能,并且运行良好,许多使用ActiveX控件嵌入Internet Explorer的应用程序也是如此。其次,它是动态链接C ++代码的最简单方法之一-COM接口本质上只是一个纯虚拟类。尽管它确实具有像CORBA这样的IDL,但您并没有被迫使用它,尤其是当您定义的接口仅在项目中使用时。

如果您不是为Windows编写的,请不要认为COM不值得考虑。Mozilla在其代码库(用于Firefox浏览器)中重新实现了它,因为他们需要一种将其C ++代码进行组件化的方法。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.