在游戏编程中全局变量不好吗?

时间:2010-05-14 06:31:50

标签: c++ global-variables

我知道我对全局变量的直觉反应“糟糕!”但是在我的大学全球比赛中使用的两个游戏开发课程被广泛使用,现在我正在使用的DirectX 9游戏编程教程(www.directxtutorial.com)我被告知全局游戏编程是可以的。 ..?该网站还建议只使用结构,如果你可以在进行游戏编程时帮助保持简单。

我在这个问题上真的很困惑,我一直在努力做的所有研究都很混乱。我意识到使用全局变量存在问题(线程问题,它们使代码难以维护,它们的状态难以跟踪等)但是还有与不使用全局变量相关的成本,我必须通过一个loooot虽然我觉得指针会加快这个过程(这是我第一次用C ++编写游戏。)无论如何,我意识到可能没有“正确”或“错误”回答这里,因为两种方式工作,但我希望我的代码尽可能正确,所以任何输入都会很好,非常感谢你!

12 个答案:

答案 0 :(得分:37)

游戏和全局游戏的问题在于游戏(现在)是在引擎级别进行线程化的。使用引擎的游戏开发人员使用引擎的抽象而不是直接编程并发(IIRC)。在许多高级语言(如C ++)中,线程共享状态很复杂。当许多并发进程共享一个共同的资源时,他们必须确保它们不会踩在彼此的脚趾上。

要解决此问题,请使用并发控制,例如互斥锁和各种锁。这实际上使得代码的异步关键部分以同步方式访问共享状态以进行写入。并发控制的主题太多了,无法在此完全解释。

可以说,如果线程使用全局变量运行,它会使调试变得非常困难,因为并发错误是一场噩梦(想想,“哪个线程写的那个?谁持有那个锁?”)。

游戏编程API(例如OpenGL和DX)中的是例外。如果您的共享数据/全局变量是指向DX或OpenGL图形上下文的指针,那么通常会映射到GPU操作,这些操作不会因同样的问题而受到太大影响。

小心点。保持代表“玩家”或“僵尸”或其他任何东西的对象,并在线程之间共享它们可能会很棘手。产生'玩家'线程和'僵尸组'线程,并且基于消息传递在它们之间具有强大的并发抽象,而不是在线程/临界区边界上访问那些对象的状态。

说了这些,我同意下面提到的“对全局性说”点。

有关线程和共享状态的复杂性的更多信息,请参阅:

1 POSIX Threads API - I know it is POSIX, but provides a good idea that translates to other API
2 Wikipedia's excellent range of articles on concurrency control mechanisms
3 The Dining Philosopher's problem (and many others)
4 ThreadMentor tutorials and articles on threading
5 Another Intel article, but more of a marketing thing.
6 An ACM article on building multi-threaded game engines

答案 1 :(得分:24)

已经参与过AAA游戏,我可以告诉你,全球化应该在它们像癌症一样传播之前立即根除。我已经看到它们完全损坏了I / O子系统,因此必须完全抛弃它才能被重写。

对全局变量说不。总是

答案 2 :(得分:7)

在这方面,游戏和其他程序之间没有区别。虽然在小学课程中给出的小例子中可以说是好的,但在实际课程中强烈反对全球变量。

因此,如果您想编写正确,可读和可维护的代码,请尽可能远离全局变量。

答案 3 :(得分:5)

直到现在所有答案都处理全局/线程问题,但我会将2美分添加到struct / class(理解为所有公共属性/私有属性+方法)讨论。基于更简单的方法而优先选择类的结构与使用汇编语言而不是C ++相同,因为它是一种更简单的语言。

您必须考虑如何使用实体并为其提供方法,这使得具体实体变得更加复杂,但却极大地简化了代码的其余部分和可维护性。封装的重点在于它通过提供清晰的方式简化程序,以便在维护对象不变量的同时修改数据。您可以控制入口点以及可能发生的情况。公开所有属性意味着代码的任何部分都可能有一个小的无辜错误(忘记检查条件X)并完全打破你的不变量(健康低于0,但没有'死'处理被触发)

另一个常见的讨论是性能:如果我只需要更新数据,那么必须调用方法将影响我的性能。并不是的。如果方法很简单,并且您在标题中将它们作为内联提供(在类体内部或在外部使用inline关键字),编译器将能够将这些指令复制到每个使用位置。您可以保证编译器不会错误地省略任何检查,并且不会影响性能。

答案 4 :(得分:4)

阅读了更多你发布的内容:

  

我被告知全局都没问题   游戏编程......?该网站也   建议如果你只使用结构   可以在做游戏编程时   帮助保持简单。

游戏代码与其他代码没有什么不同。无论如何,无条件使用全局变量都很糟糕。至于“只使用结构”,这只是一堆垃圾。以与任何其他软件相同的原则进行游戏开发 - 您可能会找到需要弯曲它的地方,但它们应该是例外,通常是在处理低级硬件问题时。

答案 5 :(得分:3)

我会说关于全局变量和“保持简单”的建议可能是更容易学习和老式思考性能的混合物。当我接受关于游戏编程的教学时,我记得被告知C ++不建议用于游戏,因为它太慢但是我使用C ++的每个方面都参与了多个游戏,证明这不是真的。

我想在这里添加每个人的答案,尽可能避免使用全局变量,我不会害怕使用C ++中的任何内容来使代码易于理解和使用。如果您遇到性能问题然后描述该特定问题,我敢打赌,大多数情况下您不需要删除C ++功能的使用,而只是更好地考虑您的问题。可能仍然有一些平台需要纯C,但我并没有真正的经验,即使Gameboy Advance似乎很好地处理C ++。

答案 6 :(得分:3)

这里的元素是国家的元素。任何给定的函数依赖多少状态,它有多少变化?然后考虑隐式与显式有多少状态,并与检查与变化交叉。

如果你有一个函数/方法/任何叫做DoStuff()的东西,你根本不知道它依赖什么,它需要什么,以及共享状态会发生什么。如果这是一个类成员,你也不知道该对象的状态将如何变异。这很糟糕。

与cosf(2)这样的东西相比,这个函数被理解为不会更改任何全局状态,并且它所需的任何状态(例如查找表)都被隐藏起来并且没有效果在你的程序上。这是一个根据您给出的值计算值的函数,它返回该值。它没有改变任何状态。

然后

类成员函数有机会加强一些问题。

之间存在巨大的差异
myObject.hitpoints -= 4;
myObject.UpdateHealth();

myObject.TakeDamage(4);

在第一个示例中,外部操作正在改变其成员函数之一隐式依赖的某些状态。事实是,这两行代码可以被许多其他代码行分开,这使得在UpdateHealth调用中会发生什么变得不明显,即使在减法之外它与TakeDamage调用相同。封装状态变化(在第二个例子中)意味着状态变化的细节对外部世界并不重要,希望它们不是。在第一个示例中,状态更改显式对外部世界很重要,这与设置一些全局变量并调用使用这些全局变量的函数没有什么不同。例如。希望你永远不会看到

extern float value_to_sqrt;
value_to_sqrt = 2.0f;
sqrt();  // reads the global value_to_sqrt
extern float sqrt_value; // has the results of the sqrt.

然而,有多少人在其他情况下完成了这类事情? (特别考虑到类实例状态对于该特定实例是“全局的”。)

所以更喜欢给函数调用提供显式指令,并且更喜欢直接返回结果,而不是在调用函数之前必须显式设置状态,然后在返回后检查其他状态。

一些代码拥有的状态依赖性越多,使多线程安全越困难,但上面已经介绍过了。我想说的是,问题不在于全局变量,而在于一些代码运行所需的状态集合的可见性(以及随后其他代码还取决于该状态的程度)。 / p>

答案 7 :(得分:2)

大多数游戏都不是多线程的,虽然较新的游戏正在沿着这条路走下去,所以他们到目前为止已经成功逃脱了它。 说全局游戏在游戏中没问题就好像不打扰你的汽车刹车,因为你只能以10英里每小时的速度行驶! 无论你怎么看待它都是不好的做法。 您只需要查看游戏中的错误数量即可查看此示例。

答案 8 :(得分:2)

如果在A类中你需要访问数据D,而不是设置D global,你最好把A引用到D.

答案 9 :(得分:2)

全球不是本质上不好的。例如,在C语言中,它们是语言正常使用的一部分......而且由于C ++构建在C语言之上,它们仍然占有一席之地。

在美学层面上,最好避开它们,你可以明智地让它们成为一个类的一部分,但如果你所做的只是将一堆全局变量包装成一个单独的,你就会变得更糟,因为至少对于全局变量它是< em>明显重点是什么。

要小心,但对某些事情而言,强迫OO概念实际上是一个全球价值是没有意义的。

答案 10 :(得分:2)

我过去遇到的两个具体问题:

首先:如果你试图分开例如无论出于何种原因,渲染阶段(const访问大多数游戏状态)从逻辑阶段(非常规访问到大多数游戏状态)(将渲染速率与逻辑速率分离,在网络中同步游戏状态,在固定点记录和回放游戏玩法)在框架等)中,全局变量使得执行它变得非常困难。

问题倾向于蔓延并变得难以调试和根除。这也会对与游戏逻辑等分离的线程渲染器产生影响(其他答案将彻底涵盖此主题)。

第二:许多全局变量的存在往往会使literal pool膨胀,编译器通常会在每个函数之后放置它。

如果你通过一个“struct GlobalTable”到达你的状态,它持有全局变量或对象之类的方法集合,你的文字池往往要小得多,减少了{{1您的可执行文件中的部分。

这主要是指令集体系结构的一个问题,它不能将负载目标直接嵌入到指令中(例如,参见ARM处理器上的固定宽度ARM或Thumb版本1指令编码)。即使在现代处理器上,我也会打赌你会获得稍微小一些的代码。

当你的指令和数据缓存是分开的时,它也会倍受伤害(同样,在某些ARM处理器上);您将倾向于加载两个缓存行,而您可能只需要一个具有统一缓存的缓存行。由于文字池可能被视为数据,并且不会始终在高速缓存行边界上启动,因此可能必须将相同的高速缓存行加载到i-cache和d-cache中。

这可能算作“微优化”,但它是一个相对容易应用于整个代码库的转换(例如.text,然后用extern struct GlobalTable { /* ... */ } the;替换所有g_foo)...在某些情况下,我们已经看到代码大小减少了5%到10%,并且由于保持了缓存更加清晰,因此性能也相应提升。

答案 11 :(得分:0)

我将承担风险,并说这取决于我的项目范围/规模。因为如果你试图用一大堆代码来编写一个史诗般的游戏,那么全局变量可能会在后见之明中以最痛苦的方式轻松地花费你比他们节省更多的时间。

但是如果你想编写一些像Super Mario或Metroid或Contra这样简单的东西,甚至更简单的东西,比如Pac-Man,那么这些游戏最初是用6502程序集编码的,并且几乎用于所有的全局变量。几乎所有的游戏数据和状态都存储在数据段中,这并没有阻止开发人员运送一个非常称职和受欢迎的产品,尽管他们使用了绝对劣等的工具和工程标准,这可能会让人们感到震惊。

因此,如果你只是在编写这种小而简单的游戏,其范围非常有限,并且不是为了扩展和扩展而远远超出其原始设计,那么它的设计不能保持数年和数年,几千行简单的C ++代码,然后我看不到在这里使用全局或单例的大量内容。有人试图用SOLID和DI框架中最健全的工程技术来设计超级马里奥,最终可能会比在6502 asm中编写它的开发人员花费更长的时间来运送。

当我看到这些旧的简单游戏以及它们是如何被编码的时候,我已经老去了,那里有一些东西,而且几乎看起来开发人员正在做一些正确的事情,尽管有硬编码的魔法数字和当我在职业生涯中摸索并尝试找出最好的工程设计方法的时候,满满的全球各地。这说明这可能是一个非常不受欢迎的观点,而不是十年或两年前我会喜欢的,但它有一些东西。我不看银河战士的6502主义并认为,“这些开发人员不会重新设计他们的产品,如果他们做到这一点或那样,他们的生活将变得如此简单。”似乎他们做的事情只是关于正确。

但这又是针对小规模的东西,可能是按照今天的标准在独立游戏类别中,以及在其中较小的独立游戏中,并且远远没有在数据量方面做任何突破性的事情可以处理或使用尖端的硬件技术。如果有疑问,我肯定建议在避免全局变量方面犯错误。与C语言相比,它在C ++中也有点棘手,因为你可以拥有带有构造函数和析构函数的对象,并且初始化和销毁​​顺序没有明确定义,并且很容易被全局对象预测。在那里我会说更倾向于避免使用全局变量,因为当你没有以可预测的顺序明确地初始化和销毁​​它们时,它们会以全新的方式绊倒你。当然,如果你想在这里和那里之外多线程多线程,那么如果你不能将你的游戏状态的范围/可见性降到最低,那么你对任何特定代码的线程安全性进行推理的能力将会大大降低。地方。