如何避免游戏引擎中的硬编码


22

我的问题不是编码问题;它通常适用于所有游戏引擎设计。

您如何避免硬编码?

这个问题比看起来要深得多。假设,如果您要运行一个加载操作所需文件的游戏,如何避免load specificfile.wad在引擎代码中说些类似的话?另外,加载文件时,如何避免load aspecificmap in specificfile.wad说呢?

这个问题几乎适用于所有发动机设计,并且应该对尽可能少的发动机进行硬编码。做到这一点的最佳方法是什么?

Answers:


42

数据驱动编码

您提到的每件事都是可以在数据中指定的。你为什么要装载aspecificmap?因为游戏配置说这是玩家开始新游戏时的第一级,或者那是他们刚刚加载的玩家保存文件中当前保存点的名称,等等。

您如何找到aspecificmap?因为它在列出映射ID及其磁盘资源的数据文件中。

仅需要一小组特别少的“核心”资源,这些资源在合理程度上难以避免或无法避免进行硬编码。通过一些工作,可以将其限制为单个硬编码的默认资产名称,例如main.wad或类似名称。通过将命令行参数传递给游戏aka,可以在运行时更改此文件game.exe -wad mymain.wad

编写数据驱动的代码还依赖其他一些原则。例如,可以避免让系统或模块请求特定的资源,而取而代之的是反转那些依赖关系。也就是说,不要在其初始化代码中DebugDrawer增加负载debug.font;而是DebugDrawer在其初始化代码中使用资源句柄。该手柄可能是从主游戏配置文件加载的。

作为代码库中的一个具体示例,我们有一个从资源数据库中加载的“全局数据”对象(该对象本身默认为./resources文件夹,但可以通过命令行参数重载)。全局数据的资源数据库ID是代码库中唯一必需的硬编码资源名称(我们还有其他名称,因为有时程序员会变得懒惰,但最终最终还是要修复/删除它们)。该全局数据对象充满了其唯一目的是提供配置数据的组件。组件之一是UI全局数据组件,它包含许多其他配置项中所有主要UI资源(字体,Flash文件,图标,本地化数据等)的资源句柄。当UI开发人员决定主界面的资产从命名/ui/mainmenu.swf/ui/lobby.swf他们只是更新该全局数据引用;根本不需要更改引擎代码。

我们将此全局数据用于所有内容。所有可玩角色,所有级别,UI,音频,核心资产,网络配置等所有内容。(嗯,不是所有的东西,但是其他的东西都是需要修复的错误。)

这种方法还有很多其他优点。首先,它使资源打包和捆绑成为整个过程的组成部分。引擎中的硬编码路径也往往意味着必须用任何脚本或工具将这些相同的路径硬编码到打包游戏资产中,然后这些路径可能会不同步。取而代之的是,我们依靠单个核心资产和参考链来构建资产捆绑,例如,bundle.exe -root config.data -out main.wad并且知道它将包含我们需要的所有资产。此外,由于捆绑程序将仅遵循资源引用,因此我们知道它将包含我们需要的资产,并跳过在项目生命周期中不可避免地积累的所有剩余绒毛(加上我们可以自动生成该列表)修剪的绒毛)。

整个过程的棘手案例是脚本。从概念上讲,使引擎由数据驱动很容易,但是我看到了很多项目(爱好AAA),在这些项目中,脚本被视为数据,因此“被允许”随意使用资源路径。不要那样做 如果Lua文件需要资源并且只是调用一个函数,例如textures.lua("/path/to/texture.png")资产管道,那么知道脚本需要/path/to/texture.png正确操作并且可能会认为该纹理是未使用的和不必要的,就会遇到很多麻烦。脚本应像其他任何代码一样对待:它们需要的任何数据(包括资源或表)都应在配置条目中指定,引擎和资源管道可以检查其依赖性。foo.lua改为“加载脚本” 的数据应改为“foo.lua并提供这些参数”,其中参数包括任何必要的资源。例如,如果脚本随机产生了敌人,则将可能的敌人列表从该配置文件传递到脚本中。然后,引擎可以向敌人预先加载等级(因为它知道可能产生的完整列表)并且资源管道知道将所有敌人与游戏捆绑在一起(因为它们已被配置数据明确引用)。如果脚本生成路径名字符串并仅调用load函数,则不会引擎或资源管道都可以通过任何方式专门了解脚本可能尝试加载的资产。


好的答案,非常实用,并且还解释了实现此方法时人们所犯的陷阱和错误!+1
WHN

+1。还要补充一点,如果要启用修改,遵循指向包含配置数据的资源的模式也非常有帮助。修改要求您更改原始数据文件而不是创建自己的数据文件并指向它们的游戏变得更加困难和危险。如果您可以按定义的优先级顺序指向多个文件,那就更好了。
尤特纳格'17

12

避免在常规函数中进行硬编码的方式相同。

您传递参数,并将信息保留在配置文件中。

在这种情况下,编写引擎和编写类在软件工程上绝对没有区别。

MgrAssets
public:
  errorCode loadAssetFromDisk( filePath )
  errorCode getMap( mapName, map& )

private:
  maps[name, map]

然后,您的客户端代码读取一个“主”配置文件(文件是硬编码的或作为命令行参数传递的),其中包含指示资产文件在何处以及它们所包含的映射的信息。

从那里,一切都由“主”配置文件驱动。


1
是的,再加上某种引入自定义逻辑的机制。可能是通过嵌入C#,python等语言来通过用户定义的功能扩展引擎的核心功能
qCring

3

我喜欢其他答案,所以我会有点相反。;)

您无法避免将有关数据的知识编码到引擎中。无论信息来自何处,引擎都必须知道如何寻找它。但是,您可以避免将实际信息本身编码到引擎中。

一种“纯”数据驱动的方法是让您使用加载初始配置所需的命令行参数来启动可执行文件,但是必须对引擎进行编码才能知道如何解释该信息。例如,如果您的配置文件是JSON,你需要硬编码你看,例如变量引擎必须知道寻找"intro_movies""level_list"等等。

但是,“结构良好”的引擎只需换出配置数据及其引用的数据,便可以为许多不同的游戏工作。

因此,我们的口头禅不是要避免硬编码,而是要确保您可以用最少的精力进行更改。

与数据文件方法(我全心全意支持)形成对比,可能是您可以将数据编译到引擎中。如果这样做的“成本”较低,那么就没有真正的危害。如果您是唯一从事此工作的人,则可以将文件处理推迟到以后再进行,而不必搞砸自己。我的前几个游戏项目都将大数据表硬编码到游戏本身中,例如武器列表及其分类数据:

struct Weapon
{
    enum IconID icon;
    enum ModelID model;
    int damage;
    int rateOfFire;
    // etc...
};

const struct Weapon g_weapons[] =
{
    { ICON_PISTOL, MODEL_PISTOL, 5, 6 },
    { ICON_RIFLE, MODEL_RIFLE, 10, 20 },
    // etc...
};

因此,您可以将这些数据放在易于引用的位置,并且可以根据需要轻松进行编辑。理想的做法是将这些内容放入某种配置文件中,但随后您需要进行解析和转换,所有这些爵士乐,加上连接结构间引用可能会成为您确实不想承受的额外痛苦。处理。


解析json并不是很困难。唯一涉及的“成本”是学习。 (特别是,学习使用适当的模块或库。例如,Go具有良好的json支持。)
通配符

这并不困难,但它不仅需要学习,还需要做。例如,我知道如何从技术上解析JSON,我已经为许多其他文件格式编写了解析器,但我要么需要找到并安装第三方解决方案(并弄清依赖关系和构建方式),要么自行开发。比不做需要更多的时间。
dash-tom-bang

4
一切都花了比不做更多的时间。 但是您所需的工具已经编写好了。就像您不必设计编译器来编写游戏或玩弄机器代码一样,但是您必须为所使用的平台学习一种语言。因此,还要学习使用json解析器。
通配符

我不确定您的论点是什么。在这个答案中,我提倡YAGNI;如果您不需要花费/浪费时间做对您没有帮助的事情,那就不要。如果您想花时间在那上面,那就太好了。也许您以后需要花费时间,也许您不需要,但是提前进行操作只会使您分心于实际制作游戏的任务。游戏开发是微不足道的;制作游戏的每个任务都很简单。只是大多数游戏都有一百万个简单的任务,负责任的开发人员会选择最快实现目标的游戏。
dash-tom-bang

2
实际上,我赞成你的回答。没有真正的理由。我只是想指出,JSON 并不难解析。再读一遍,我想我主要是对片段做出回应,“但是那您需要进行解析和翻译以及所有这些爵士乐。” 但我同意,对于个人项目游戏,例如YAGNI。:)
通配符
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.