关于现代C ++和游戏开发的思考

对于游戏开发人员来说,新的一年始于在Aras Prankevichus发表的《关于现代C ++的投诉》之后,引起了C ++标准化委员会的批评。 引起了一个严重的问题:标准委员会真的失去了与现实的联系,还是反过来了?游戏开发人员是否已从其他C ++社区中分离出来?

我们为您提供本游戏界资深人士本·迪恩 Ben Dean)的热门职位翻译,本·迪恩曾在Blizzard,Electronic Arts和Bullfrog作为C ++开发人员和团队负责人工作了很长时间,他根据自己的经验回应批评。
TL; DR:C ++标准化委员会没有忽略游戏开发人员需求的隐藏目标,并且“现代” C ++不会成为“调试”语言。
在过去的一周中,Twitter上进行了积极的讨论 ,在此期间,许多程序员,尤其是从事游戏开发领域的程序员,都表示当前的“现代C ++”开发载体无法满足他们的需求 。 特别是,从普通游戏开发人员的角度来看,一切似乎都忽略了该语言的调试性能,并且代码优化已成为预期和必要的事情。

由于我在2019年设法在游戏行业工作了23年以上,基于对游戏开发相关主题的观察,我有自己的看法,我想与大家分享一下。 调试对游戏开发者重要吗?为什么? 与之相关的问题是什么?

首先-稍微偏离历史。

许多C ++游戏开发人员都在Microsoft Visual C ++中工作。 从历史上看,围绕Microsoft平台形成了巨大的游戏市场,这影响了普通游戏程序员的典型体验。 在90年代和2000年代,大多数游戏都是考虑到这些情况而编写的。 即使来自其他制造商的游戏机的出现和手机游戏的日益普及,当今许多AAA工作室和众多游戏程序员的资产仍是Microsoft制作的工具。

Visual Studio可以说是世界上最好的C ++调试器。 此外,在程序调试方面,Visual Studio确实是最突出的-比其前端,后端,STL实现或其他任何方式都更为出色。 在过去的五年中,Microsoft在开发C ++开发工具方面取得了长足的进步,但在此之前,Visual Studio中的调试器始终非常出色。 因此,当您在Windows PC上进行开发时,总是会拥有世界一流的调试器。

鉴于以上所述,让我们看一下没有错误的代码获取过程。 从不处理游戏的程序员的角度来看,我们拥有机会; 以及游戏开发人员面临的局限性。 如果您改用支持“现代C ++开发的向量”的主要论点,那么它将被简化为类型,工具和测试。 按照这种想法,调试器应该是防御最后一道防线 。 在实现它之前,我们有以下选择。

机会1:类型


我们可以根据需要使用尽可能多的强类型化,以在编译时消除整个类的bug。 毫无疑问,强类型化是C ++的最新发展为我们提供的机会。 例如,从C ++ 11开始,我们设法获得:

  • type traits显着延伸;
  • nullptr和有scoped enum创新与C语言传统作斗争-弱类型;
  • GSL和辅助工具;
  • C ++ 20中的概念

你们中有些人可能不喜欢模板元编程。 其他人可能不喜欢几乎普遍使用auto的编码风格。 无论这些首选项如何,都可以在这里清楚地了解使用C ++中列出的样式的主要动机-这是希望帮助编译器,以便它反过来可以在使用它最了解的同时对我们有所帮助的:类型系统。

如果我们谈论游戏编程,这里的强类型是一个广泛的研究领域,而我熟悉的游戏程序员正在积极使用它,他们对提高他们在实践中使用C ++的技能感兴趣。 这里有两个重要的问题:对编译时间的影响和对代码可读性的影响。

坦率地说,您可以轻松地忽略编译时间-但前提是您是一家非常大型的公司的程序员,该公司不玩游戏,并且拥有成熟的内部基础结构和无穷的计算能力,可以编译可编写的任何代码。 这样的大公司担心编译的成本-因此他们使用模块-但是,通常,这不会给单个开发人员带来痛苦。 同时,对于大多数游戏程序员而言,情况并非如此。 独立开发商没有要建设的农场; AAA游戏开发人员经常使用类似Incredibuild的工具 ,但是鉴于他们可以轻松使用10年或更长时间的代码库,因此构建过程仍可能需要15-20分钟。

我们可以争论增加硬件的相对成本与程序员花费的时间,我同意硬件便宜的观点,但是:

  • 硬件是真正的一次性支出,将被分配到当前季度的预算中,而时间/租用/等方面的实际支出则没有那么明显,后者将被分配更长的时间。 人们对这种妥协的决定不能很好地应付,而公司的建立是为了优化短期利润。
  • 基础架构需要支持,几乎没有人进入游戏行业才能成为发行工程师。 与使用C ++的其他地区相比,游戏开发人员的薪水不是很高-而且非游戏工程师的薪水甚至更低。

人们还可以推测这样一个事实,即编译时间永远不会达到这样的状态。 我再次同意你的看法。 这样做的代价是始终保持警惕-再次由发布工程师提供-理想情况下,某些自动化工具可让您跟踪构建构建所需时间的变化。 幸运的是,随着CI系统的出现,今天可以轻松实现这一目标。

机会2:工具


我们应该使用可用的最大工具-警告,静态分析,消毒剂,动态分析工具,分析器等。

我的经验是,游戏开发人员会尽可能使用这些工具,但是整个行业在这方面存在几个问题:

  • 这些工具在非Microsoft平台上通常会更好地工作-并且,如前所述,这不是典型的游戏开发方案。
  • 这些工具大多数都是针对“标准” C ++的。 他们开箱即CStaticVector支持std::vector ,但不支持我从假设引擎自写的类CStaticVector 。 当然,指责工具是没有用的,但这仍然是开发人员必须克服的使用障碍之一。
  • 创建和维护将运行所有这些工具的CI链需要发行工程师的存在-并且,如前所述,雇用人员从事与游戏没有直接关系的工程工作是游戏行业的系统性问题。

那么,既然这些工具与标准C ++配合得很好,那么为什么游戏开发人员不使用STL?

从哪里开始这个问题的答案? 也许,从下一次游览到游戏开发的历史中:

  • 直到90年代初,我们都不信任C编译器,因此我们用汇编器编写游戏。
  • 从开始到90年代中期,我们开始信任C编译器,但我们仍然不信任C ++编译器。 我们的代码是C,它使用C ++样式的注释,并且我们不再需要一直为结构编写typedef。
  • 在2000年左右,C ++革命在游戏开发领域发生了。 那是一个设计模式大类层次结构的时代。 那时,控制台上对STL的支持尚需时日,而控制台一度占据统治地位。 在PS2上,我们永远被GCC 2.95所困扰。
  • 在2010年左右,又发起了两次革命。 使用大型类层次结构的痛苦刺激了组件编码方法的发展。 今天,这种变化以实体组件系统体系结构的形式继续其发展。 与之并行的是第二次革命-尝试利用多处理器体系结构。

在这些范式转换期间,游戏开发平台本身也在不断变化,并且正在发生严重变化。 分段存储器已经让位于平坦的地址空间。 该平台已成为多处理器,对称且不是很对称。 习惯于使用Intel体系结构的游戏开发人员必须习惯于MIPS(Playstation),然后习惯于具有异构CPU(PS2)的特殊硬件,然后适应PowerPC(XBox 360),然后适应更大的异构性(PS3)...每个新平台具有针对处理器,内存和驱动器的新性能功能。 如果您想获得最佳性能,那么您将不得不重写旧的代码,而且很多时候而且很多次。 我什至不会提及互联网的兴起和增长以及平台持有者对开发人员的限制对游戏的影响。

从历史上看,游戏平台上的STL实现并不令人满意。 STL容器不适用于游戏并不是什么秘密。 如果您将游戏开发人员推上墙,那么他也许会承认std::string很不错,而std::vector是一个合理的默认选项。 但是STL中包含的所有容器都存在分配和初始化控制的问题。 在许多游戏中,您必须担心各种任务的内存限制-对于那些在游戏过程中最有可能必须动态分配内存的对象,经常使用平板竞技场分配器。 摊销固定时间并不是一个好结果,因为分配可能是程序执行期间可能发生的最“昂贵”的事情之一,我不想跳过一个帧,因为它只是在我没想到的时候发生的。 作为游戏开发人员,我必须提前管理我的内存需求。

通常,对于其他依赖项也有类似的情况。 游戏开发人员想知道每个处理器周期所需要的时间,何时何地以及每个内存字节负责什么,以及每个执行线程在何时何地受到控制。 直到最近,Microsoft编译器在每次更新时都更改了ABI-因此,如果您有很多依赖项,则重建所有依赖项可能是一个痛苦的过程。 游戏开发人员通常更喜欢小的依赖关系,这些依赖关系易于集成,只做一件事情并且做得很好(最好使用C风格的API),并且被许多公司所使用,可公开获得或具有免费的许可证需要作者的指示。 SQLitezlib是游戏开发人员更喜欢使用的示例。

此外,C ++游戏行业对患有“此处未发明”综合症的患者有着丰富的历史。 这应该是业界的期望,该行业始于单身发烧友,他们使用全新的设备制作自己的东西,没有其他选择。 除其他事项外,游戏行业是唯一在游戏中以无特定顺序显示程序员的游戏行业。 编写各种东西很有趣,并且对您的职业生涯有帮助! 建造自己的东西比买现成的东西好得多! 而且,由于我们非常担心性能,因此我们可以采用适合于我们项目的方式来调整我们的解决方案-而不是采用会无意识地浪费可用资源的通用解决方案。 对Boost的敌视是这种思维如何在游戏开发中体现出来的主要例证。 我从事的项目按以下方式进行:

  • 首先,为了解决特定问题,我们将Boost的库连接到项目。
  • 一切都很好。 当您需要更新时,会发生一点痛苦,但不超过更新任何其他依赖项时的痛苦。
  • 另一个游戏想使用我们的代码,但绊脚石是我们使用Boost-尽管我们在Boost方面的经验非常不错。
  • 我们使用Boost删除了代码,但是现在我们面临一个新问题:我们必须解决Boost的库而不是以前解决的问题。
  • 本质上,我们将需要的Boost代码的一部分复制到我们自己的名称空间中。
  • 后来,我们不可避免地会不时地遇到这样一个事实,即如果我们没有扔掉它,我们将需要原始代码中已经存在的其他功能。 但是现在我们是此代码的所有者,因此我们必须继续支持它。

我们不喜欢尝试同时做太多事情或会影响编译时间的巨大事物-这是很合理的。 人们一次又一次地犯错是因为他们反对今天接受所谓的痛苦-而由于这一决定,在某些东西的支持下,他们将面对一个非常现实的,更大得多的痛苦,而这是以牺牲某人为代价的-他们未来三年必须经历的预算。 las,以游戏形式存在的证据成功地使用了STL和Boost的菜品,绝不会影响人类的心理并说服游戏开发者。

由于所有这些原因,许多游戏公司都创建了自己的库,这些库涵盖STL的功能,甚至更多,同时还支持特定于游戏的用例。 一些大型游戏公司甚至能够掌握自己的,完全成熟的,几乎完全兼容API的STL替代品的开发 ,这随之而来的是用于支持该项目的巨大成本。

找到 std::map 的改进替代方法 ,或者在std::vector应用小缓冲区优化 是明智的 。 注定要支持自己的algorithmstype traits实现实在是太不可接受了,这没有什么用。 对于我来说,令人遗憾的是,对于大多数开发人员而言,STL只是容器。 自从一开始学习STL以来,他们就被教导说,就STL而言,最隐含的是std::find_if尽管实际上他们应该考虑std::find_if

机会3:测试


有人认为应该进行广泛的测试,TDD和/或BDD应该覆盖所有可以覆盖的代码,并且应该通过编写新的测试来解决错误。

因此,让我们讨论测试主题。

从我的经验来看,游戏行业几乎不使用自动测试。 怎么了

1.因为正确性不是那么重要,但是没有真正的规范


作为游戏行业的年轻程序员,我很快就摆脱了我应该努力模拟现实的想法。 游戏充满了烟雾和镜子,并在寻找短路。 没有人会在乎您的仿真有多现实。 最主要的是,它很有趣。 当您除了“游戏应该感觉正确”之外没有其他规格时,就缺少测试的主题。 多亏了错误,游戏玩法甚至可以变得更好。 经常有一个bug出现在发行版中,甚至赢得了用户的喜爱( 记住来自Civilization的同一个Gandhi )。 游戏与使用C ++的其他领域有所不同; 这里缺乏正确性并不能导致某人最终失去了积蓄。

2.因为很难


当然,您想尽可能地进行自动化测试。 对于某些子系统有明确说明的最终结果,可以这样做。 当然,存在游戏行业中的单元测试,但通常它限于低级代码-前面提到的STL类似物,字符串转换过程,物理引擎方法等。 那些代码的可执行部分具有可预测结果的情况通常通过单元测试进行测试,尽管此处未使用TDD,因为游戏程序员更喜欢简化生活,反之亦然。 但是,您如何测试游戏代码(请参阅第一点)? 一旦您超出了单元测试的范围,就会立即遇到测试游戏如此困难的另一个原因。

3.因为涉及到内容


测试非平凡的系统可能会包括提供内容以实现它。 大多数工程师不是很擅长自己制作此内容,因此,要进行有意义的测试,您需要吸引具有适当技能的人员来创建内容。 之后,您将遇到测量输出结果的问题-毕竟,它不再是一行或一个数字,而是屏幕上的图像或声音随时间变化。

4.因为我们不练习


我知道单元测试的功能是可能的输入和输出。 但是,游戏玩法是不可预测的,动态变化的行为,我不知道如何正确测试这种现象。 我可以测试的内容-当然,如果我得到经理的许可,可以花足够的时间对此进行测试-例如,性能或我可以分析的高级功能(例如配对)。 这样的基础结构工作对于某些游戏程序员而言可能是令人兴奋的,但是对于大多数游戏来说,这根本就不有趣,并且还需要钱包所有者的批准和支持。 作为游戏程序员,我从来没有机会练习编写高级测试。

5.由于[公司]认为不需要自动化测试


我们的主要目标是发布游戏。 我们生活在一个行业时代,随着点击量的营销成本最大化,点击率在销售的第一个月就可以充分利用他们的收入。 控制台的生命周期告诉我们,无论如何该代码都不会存在那么长时间。 如果我们正在开发在线游戏,那么很可能我们将获得更多时间来测试配对或服务器负载。 由于要发布游戏,我们需要使其性能井井有条,因此我们至少应执行性能测试,但不应使此过程自动化。 对于游戏行业的管理而言,自动化测试无非是浪费时间和金钱。 为了实现它,有必要聘请有经验的工程师来执行工作,其结果几乎是难以察觉的。 可以将相同的时间花在开发新功能上。 在短期内,使用质量检查人员来测试游戏更有利可图,这使我们进入了下一个要点。

6.因为一般来说,游戏中的测试被归为次等活动


我喜欢优秀的质量检查专家。 对我来说,它们值得在黄金中发挥作用。 他们知道如何以前所未有的方式破坏游戏,从而使游戏变得更好。 他们是您玩游戏方面的专业专家,在这些方面您不了解,甚至从不了解。 它们比一组功能强大的编译器更好,可以帮助您正确完成所有工作。 我很高兴在过去的几年中有机会与几位出色的质量检查专家一起工作。

我几乎总是不得不战斗以确保他们留在我的团队中。

在大型AAA公司中,质量检查组织通常是与任何开发团队完全独立的部门,具有自己的管理和组织结构。据称这样做是为了使他们在测试期间可以是客观的。实际上,一切都不是那么美丽。

它们就像一个巨大的机制中的齿轮一样,经常在没有警告的情况下扔到项目之间,并且通常被当作任何人都可以处理的工作。当项目从截止日期“撤离”时,工程师们会感觉到自己的皮肤紧缩,但是质量保证变得更加强大,因为他们必须在夜班和周末工作,而且他们还因为他们带来了当前形势的阴暗消息项目质量状态。

他们的薪水严重不足。在主题领域拥有多年专业知识的最有经验的测试人员所获得的薪水不到中级开发人员薪水的一半。我必须与最聪明的QA工程师合作,他们创建了具有跟踪和警报的性能测试管道,创建了用于测试API和压力测试的框架,并执行了许多其他所谓的“真正的工程师”所不值得的任务。我相信,如果他们在任何其他大型技术公司中工作,这些最聪明的人都会得到更多。

他们不受信任。测试人员与其他开发人员保持隔离的情况并不少见,他们的证章仅允许他们访问自己工作所在的建筑物的楼层,甚至使用单独的入口。

他们被迫服从。测试人员经常被告知不要打扰其他工程师。当他们需要直接报告错误时,要求他们与工程师联系,例如“ H夫人”。或“ Y先生”。有时,我会接到质保部门烦恼的老板打来的电话-在这种情况下,我与发现该错误的人联系以进行联合调查。

所有这些听起来像是一个可怕的故事,让每个人都不必处理这些事情,不幸的是,这种情况经常发生。工程师常常开始思考-也许是在不断施加压力的情况下,但这并不能为他们辩解-质量检查的工作是寻找自己的错误,或更糟糕的是,开始将错误归咎于质量检查。

在我必须与之合作的最好的团队中,我们坚持要求我们的团队拥有自己的质量检查工程师来与我们合作。但是,他们并没有失去其客观性或渴望获得更好结果的愿望。他们很高兴在编写自动化测试方面得到程序员的帮助。我当然不会怀疑,这对于游戏行业更频繁地参与自动化会很有用。

调试性能


有了以上所有这些-调试的习惯,仍在增长的API和工具的平台以及自动化测试的复杂性(加上缺乏文化),就清楚了为什么游戏开发人员坚持使用调试功能。

但与此同时,调试本身仍然存在问题,游戏开发人员如何应对当前的C ++开发向量也存在问题。

调试的主要问题是它无法扩展。在阅读本文的开发人员的游戏开发人员中,有些人认为我描述的现象与他们在实践中观察到的现象不一致。很有可能是由于这样的事实,他们自己迟早必须处理调试的可伸缩性问题,并且他们找到了解决之道。

换句话说,我们希望进行高效的调试,因为为了捕获错误,我们经常需要能够运行具有足够大且具有代表性的数据集的应用程序。但是实际上,当我们达到这一点时,调试器通常变得过于粗鲁,无法使用工具-无论它是否有效。当然,在数据上设置断点(数据断点)对于捕获中等大小的问题可能很有用,但是如果遇到真正的错误-我们似乎已经修复了所有问题之后仍然存在,那该怎么办?那些是在网络负载下出现的,或者是在内存不足的情况下,或者是在多线程功能的限制下工作的,或者仅在一百万个其他玩家的背景下发生在一个很小的,不明身份的子集上,或者仅在游戏的磁盘版本上出现,或者在德语的装配中,还是花了三个小时进行稳定性测试(浸泡测试)?

特点二,我们可以单独依靠调试器。在这种情况下,我们会做我们一直做的事情。我们正试图找出问题所在,使其更频繁地发生。我们添加日志并在其中筛选程序;我们调整计时器和流量设置;我们使用二进制构建搜索;我们研究核心转储和崩溃日志;我们尝试通过将内容最小化来重现该问题;我们思考可能导致问题的原因并进行讨论。

通常,在找到导致崩溃的真正原因之前,我们将有时间修复一些其他问题。换句话说,我们解决了问题,最后,使用调试器只是此过程的一小部分。因此,是的,调试速度是不错的选择,但它的不足并不妨碍我们继续成为工程师。我们仍然需要其他技能,例如解析核心转储和读取优化的汇编程序的能力。

当使用“现代C ++”时,我以与往常相同的方式使用调试器。我仔细阅读了刚编写的代码;我在我感兴趣的数据上设置了断点;我使用调试器探索不熟悉的代码。随着“现代C ++”的到来,这些都没有改变-是的,即使STL使用_ Ugly _标识符,这也不会使STL变得神奇。有时,查看STL在“幕后”所做的工作或超越它是很有用的。或者,您现在可以使用调试器对我隐藏库代码

当我遇到调试性能问题时,通常不是因为“现代C ++”会使我慢下来-事实是,到目前为止,我已经在尝试做很多事情了。与类型,工具和测试不同,使用调试器无法扩展

我自己担心C ++代码需要越来越多的优化这一问题,并且我对编译器开发人员对此的看法很感兴趣。事实是,没有单一的答案。我们已经处于连续状态,并且我们有机会朝着这个方向进一步发展而又不损害调试代码的能力。今天,我们的编译器对临时对象执行复制省略,即使我们不要求他们执行此优化。这不会影响我们调试应用程序的能力。我怀疑我们会抱怨调试版本开始包括NRVO或另外六种优化,这些优化可以以在调试过程中不会注意到它们的方式进行。我怀疑C ++正朝着这个方向发展。

结语:现代C ++的方式


如果您是游戏开发领域的程序员,并且不喜欢C ++的发展方向,那么您基本上有两个选择可以采取进一步的行动。

1.什么都不做


假设您仍要编写C ++代码,则可以像以前一样继续使用该语言。如果您不想这样做,则无需开始使用任何新功能。您现在使用的几乎所有内容都将继续得到支持-这样,在未来几年中,您将继续从改进编译器中受益。

对于那些为自己工作或与志同道合的人一起工作的人而言,这是完全适当的行为策略。 C ++ 98以及一些较新的功能仍然非常适合在其上编写游戏。

但是,如果您在大型公司工作,迟早您将不得不面对语言上的变化,因为您将不得不增加团队并雇用新员工。反过来,当您雇用C ++开发人员时,这将意味着雇用具有“现代” C ++的开发人员。世代的变化将发生-汇编程序,C和C ++ 98已经发生了变化。如果对代码库中允许的内容设置限制,则可以管理该过程,但是从长远来看,此解决方案将无法为您省钱。在这种情况下您应该怎么做?

2.参加


开始访问CppCon,而不是每年仅访问一个GDC ,您将从公司在机票上的花销中获得更多收益。参加标准讨论;加入团体并订阅新闻通讯;阅读标准草案,并向作者提供反馈。如果您也可以参加委员会会议,那很好,但是即使不能,您仍然可以做很多事情来向他人传达您的观点。

C ++委员会的成员资格向所有人开放。想要参加SG14或SG7或SG15或与您感兴趣的领域有关的任何其他工作组的人员的所有必要信息-可以在isocpp.org上找到该委员会没有秘密计划-实际上,您是否真的认为200多个程序员可以就一个共同的议程达成共识?在这里,即使是委员会的“老板”也常常不能“推”他们的想法。

如果您想发表自己的意见,则应该在可以听到您的意见的地方开始交谈,而不是在Twitter或Reddit上。请采纳此建议-我期待我们的讨论。

Source: https://habr.com/ru/post/zh-CN435036/


All Articles