【问题标题】:In game programming are global variables bad?在游戏编程中,全局变量不好吗?
【发布时间】:2010-05-14 06:31:49
【问题描述】:

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

我对这个问题真的很困惑,而且我一直在尝试做的所有研究都非常令人困惑。我意识到使用全局变量时存在问题(线程问题,它们使代码更难维护,它们的状态难以跟踪等)但也有与不使用全局变量相关的成本,我必须通过 loooot周围的信息经常令人困惑,我想会耗费时间,尽管我猜指针会加快这个过程(这是我第一次用 C++ 编写游戏。)无论如何,我意识到可能没有“正确”或“错误”的答案在这里,因为两种方式工作,但我希望我的代码尽可能正确,所以任何输入都会很好,非常感谢!

【问题讨论】:

  • 嗯...被滥用的全局变量不好吗?
  • 每次使用全局变量,上帝都会杀死一只小猫。
  • @ereOn 但耶稣复活了它,所以没关系。
  • @Lo'oris - 恕我直言,指针是关于 C 的最好的东西,我不会为了这个世界而放弃它们;)
  • 针对 C++ 程序员的互联网教程的问题在于,其中 90% 都是垃圾。

标签: c++ global-variables


【解决方案1】:

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

要解决这个问题,您可以使用并发控制,例如互斥锁和各种锁。这实际上使代码的异步关键部分以同步的方式访问共享状态以进行写入。并发控制这个话题太多了,这里就不一一解释了。

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

在 OpenGL 和 DX 等游戏编程 API 中有例外。如果您的共享数据/全局变量是指向 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.
6An ACM article on building multi-threaded game engines

【讨论】:

  • 谢谢。我需要一些时间来阅读所有这些链接,但你的信息是有道理的,而且它似乎是一个比我最初计划的更强大的系统。所以,再次感谢!
  • 例如,关于 OpenGL 中的共享状态,这正是“坏”状态。您不想随时随地更改 GL 状态。出于一个原因,您不希望“其他人”在您正要绘制某些东西时改变状态。另一个原因是随机状态更改会破坏您的运行时性能。
【解决方案2】:

我曾开发过 AAA 级游戏,我可以告诉你,应该在全局变量像癌症一样扩散之前立即将其根除。我已经看到他们彻底破坏了一个 I/O 子系统,以至于必须将其完全丢弃才能重写。

对全局说不。总是。

【讨论】:

  • @John,我在这里看不到相关性 - 您不需要 OO 来删除全局变量。您可以编写一个 C 应用程序,其中包含函数和结构中的所有内容,而不是全局。
  • @Necrolis:将全局变量包装在单例中只是假装您解决了问题。强迫它 OO 并不能让它变得更好。
  • @John 我同意,单身人士至少和全球人一样糟糕。
  • 哼...但是单例和全局之间有什么区别???全局变量不错,因为它们被标记为“全局变量”,它们很糟糕,因为它们引入了全局共享状态,我看不出单例为该特定问题带来了什么(忽略初始化/破坏问题)。
  • @Matthieu M. 两者之间没有太大区别。一个只是有一个花哨的名字。
【解决方案3】:

在这方面,游戏和其他程序没有区别。虽然在基础课程中给出的小例子中可以说是可以的,但在实际程序中强烈建议不要使用全局变量。

所以如果你想编写正确、可读和可维护的代码,请尽可能远离全局变量。

【讨论】:

    【解决方案4】:

    到目前为止,所有答案都涉及全局/线程问题,但我会将我的 2 美分添加到结构/类(理解为所有公共属性/私有属性+方法)讨论中。由于更简单而优先使用结构而不是类与基于更简单的语言优先使用汇编程序而不是 C++ 的思路相同。

    您必须考虑如何使用您的实体并为其提供方法这一事实使具体实体稍微复杂一些,但大大简化了其余代码和可维护性。封装的全部意义在于它通过提供清晰的方法来简化程序,使您的数据可以在保持对象不变性的同时进行修改。您可以控制入口点以及那里可能发生的事情。公开所有属性意味着代码的任何部分都可能有一个小的无辜错误(忘记检查条件 X)并完全破坏您的不变量(健康低于 0,但不会触发“死亡”处理)

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

    【讨论】:

    • 使用内联仍然会影响性能。如果内联太多以至于代码的关键部分不再适合缓存,您可能会发现由于缓存未命中而导致速度损失。
    • @Shaggy Frog:+1(如果可以的话,+10):这也是一个可怕的陷阱;很难将自己挖掘出来,尤其是当您摆脱困境时,同事们往往会尝试将事物“重新优化”回局部最优值。有时,别无选择,只能阅读该死的反汇编代码,意识到自己遇到了麻烦,然后开始告诉编译器停止内联。
    • @David 结构和类在 C++ 中基本等价
    • @FredOverflow:我知道它们是相同的,这就是括号中的注释的意思:在答案的其余部分中,结构将用作“所有公共属性”,而类为“私有属性+方法”。我想这还不够清楚。
    • @Shaggy Frog:如果您手动复制该方法而不是让编译器内联它,那么您不会在性能方面获得任何收益,但在维护方面会受到很大的伤害.. . 无论如何inline 是一个提示,地狱甚至 VS __forceinline 是一个提示 ;)
    【解决方案5】:

    阅读更多您发布的内容:

    有人告诉我,全局变量在 游戏编程...?该网站还 如果您建议仅使用结构 可以在做游戏编程的时候 帮助保持简单。

    游戏代码与其他代码真的没有什么不同。 无偿使用全局变量是不好的。至于“只使用结构”,那只是一堆废话。按照与任何其他软件相同的原则进行游戏开发 - 您可能会发现需要改进的地方,但它们应该是例外,通常是在处理低级硬件问题时。

    【讨论】:

    • 这只是游戏程序员给的借口 a) 让自己感觉特别 b) 为糟糕的设计找借口。我应该知道,我曾经是一个
    • 似乎所有软件都适用相同的原则:首先关注软件需要做什么,并且更喜欢最易读的版本。如果你发现你需要提高性能(专注于软件是如何做到的),你可以衡量什么是花费最多的时间并改进它。无论您是在编写游戏、文本编辑器、VCR 还是核电站的控件,这似乎都是最好的做法。
    【解决方案6】:

    我会说关于全局变量和“保持简单”的建议可能是更容易学习和老式的性能思维的混合体。当我被教导游戏编程时,我记得有人告诉我不建议将 C++ 用于游戏,因为它太慢了,但我已经使用 C++ 的各个方面开发了多个游戏,这证明这不是真的。

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

    【讨论】:

      【解决方案7】:

      这里的元问题是国家问题。任何给定函数依赖于多少状态,它改变了多少?然后考虑多少状态是隐式与显式的,并将其与检查与更改相交叉。

      如果您有一个名为 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.
      

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

      因此,更愿意为您的函数调用提供显式指令,并更愿意它们直接返回结果,而不是在调用函数之前显式设置状态,然后在返回后检查其他状态。

      一段代码的状态依赖越多,就越难使其成为多线程安全的,但这已经在上面介绍过。我想说的一点是,问题不在于全局变量,而在于状态集合的可见性,这是​​运行一些代码所需的(以及随后有多少其他代码也取决于该状态)。

      【讨论】:

      • 这无疑是最好的答案。我试图在 cmets 中解释的所有内容都做得更好、更清楚。
      【解决方案8】:

      大多数游戏都不是多线程的,尽管较新的游戏正在走这条路,所以到目前为止他们已经设法摆脱了它。 说全局变量在游戏中没问题,就像因为你只以 10 英里/小时的速度行驶,所以不用费心修理汽车的刹车! 无论您以何种方式看待它,都是不好的做法。 您只需查看游戏中的错误数量即可查看示例。

      【讨论】:

      • Xbox360有3个双核处理器,PS3有一个主处理器和6个矢量处理器。两者都有额外的专用图形处理器。在什么意义上这些平台上的“大多数”游戏不是多线程的?
      • @dash-tom-bang:说句公道话,他没有说“最新一代硬件上的大多数游戏”,甚至也没有说“大多数 AAA 游戏”,他只是说“大多数游戏” ,因此我认为这个说法是正确的,即使你只计算过去 10 年的商业游戏。
      【解决方案9】:

      如果在A类中你需要访问数据D,而不是设置D全局,你最好在A中放入对D的引用。

      【讨论】:

        【解决方案10】:

        全局变量本质上并不坏。例如,在 C 中,它们是该语言正常使用的一部分……而且由于 C++ 是基于 C 构建的,它们仍然占有一席之地。

        在美学层面上,最好避免它们,因为你可以明智地将它们作为类的一部分,但如果你所做的只是将一堆全局变量包装到一个单例中,你会让事情变得更糟,因为至少使用全局变量它是 很明显重点是什么。

        要小心,但在某些情况下,将 OO 概念强加于实际的全局值是没有意义的。

        【讨论】:

        • 特别是在 C++ 中,gobals 是一个巨大 问题(想想初始化和销毁​​的顺序!)。此外,C++ 中存在更好的解决方案。有一些值得注意的例外(cincout),但除此之外,在 C++ 中应该完全避免使用全局变量。
        • C++ 不是纯 OO 语言,您在尝试强制使用这种方法时使用它是错误的。它提供了程序/OO的混合。如果您想要一种合适的 OO 语言,请改用一种。
        • C++ 还支持函数式编程(例如通过 boost::bindboost::lambda 以及现在 C++0x 中的匿名函数和闭包)和模板元编程等:)
        【解决方案11】:

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

        首先:如果您尝试分离,例如渲染阶段(对大多数游戏状态的常量访问)从逻辑阶段(对大多数游戏状态的非常量访问)无论出于何种原因(将渲染速率与逻辑速率解耦,通过网络同步游戏状态,在固定点记录和播放游戏玩法在框架等),全局变量很难强制执行。

        问题往往会蔓延并变得难以调试和消除。这也对独立于游戏逻辑的线程渲染器等产生影响(其他答案彻底涵盖了这个主题)。

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

        如果您通过保存全局变量的单个“struct GlobalTable”或对象上的方法集合等来达到您的状态,那么您的文字池往往会小很多,从而减小 .text 的大小可执行文件中的部分。

        对于无法将加载目标直接嵌入指令的指令集架构来说,这主要是一个问题(请参阅 ARM 处理器上的固定宽度 ARM 或 Thumb 版本 1 指令编码)。即使在现代处理器上,我敢打赌你会得到略小的代码生成。

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

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

        【讨论】:

        • 我对此一无所知,非常感谢您的分享!我一直对学习如何优化代码很感兴趣,这听起来是个绝妙的技巧:D
        • 我的荣幸。但是请记住,就像任何看起来像“技巧”的东西一样,您应该始终确保在应用它之前需要它——使用分析器,或在前后检查 disasm。尝试这样的事情作为实验是值得的(如果你没有在截止日期前),以获得更好的理解,但并不总是值得保留结果。 =) 这样的东西往往会因编译器和 CPU 与 cpu 的不同而有很大差异,因此“向我展示配置文件”的口头禅始终值得牢记。
        • 关于文字池,我想知道为什么 ARM ABI 没有像旧的 Mac OS 那样指定使用全局变量基址寄存器?这将允许直接访问多达 4K 的全局变量,并通过一个额外的(非加载)指令访问额外的 1024K [例如使用 ARM 指令集时添加 r0,r4,#(93
        • @supercat:如果我不得不猜测,我会说两件事。专用寄存器是 RISC 和功耗意识架构的一种诅咒;你不希望你必须保持点亮的东西不会被积极使用。 (我不确定这里的确切权衡,前面更多只是在黑暗中拍摄。)其次,全球位置是链接时间的事情;如何根据生成的指令访问全局是编译时的事情。除非您有链接时代码生成,否则您必须悲观地假设您的全局最终超出了该范围。
        • @leander:可以肯定的是,嵌入式系统通常比其他应用程序在更大程度上使用全局变量,因此在“计算机”应用程序中专用寄存器用于全局寻址的相对好处将小于在嵌入式系统,但即使在计算机应用程序中,如果针对不同的目的,该工具仍然很有用:线程局部变量。
        【解决方案12】:

        我会冒险说这取决于我的项目范围/规模。因为如果您尝试使用大量代码编写史诗般的游戏,那么全局变量很容易以事后最痛苦的方式花费您的时间,而不是节省的时间。

        但是,如果您尝试编写诸如超级马里奥、银河战士或魂斗罗之类的简单游戏,或者甚至更简单的游戏(例如吃豆人),那么这些游戏最初是在 6502 汇编中编写的,并且几乎所有内容都使用了全局变量。几乎所有的游戏数据和状态都存储在数据段中,这并没有阻止开发人员推出一款非常称职和受欢迎的产品,尽管使用绝对劣质的工具和工程标准,这可能会让今天的人们感到恐惧。

        因此,如果您只是在编写这种范围非常有限的小而​​简单的游戏,并且其设计目的不是为了远远超出其原始设计的增长和扩展,也不是为了维护多年和多年,几千行简单的 C++ 代码,然后我看不出在这里使用全局或在那里使用单例有什么大不了的。如果有人痴迷于尝试使用 SOLID 和 DI 框架中最完善的工程技术来设计超级马里奥,那么最终的交付时间可能会比使用 6502 asm 编写它的开发人员要长得多。

        当我看到这些古老的简单游戏以及它们的编码方式时,我已经老了,而且其中有些东西,尽管有硬编码的幻数和全球各地的人,而我的职业生涯都在摸索并试图找出设计事物的最佳方法。也就是说,这可能是一个非常不受欢迎的观点,而且在十年或两年前我都不会喜欢,但它有一些东西。我不会看 Metroid 的 6502 asm 并认为,“这些开发人员对他们的产品设计不足,如果他们这样做或那样做,他们的生活会容易得多。” 似乎他们只是做了一些事情差不多吧。

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2012-01-24
          • 2010-10-15
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-12-04
          • 1970-01-01
          相关资源
          最近更新 更多