游戏设计模式之架构 性能与游戏

2016年09月13日 14:52 0 点赞 0 评论 更新于 2025-11-21 20:25

一、《Game Programming Patterns》其书

《Game Programming Patterns》,从书名可知,它是一本聚焦于游戏编程领域的设计模式指南。该书涵盖了游戏逻辑、游戏编辑器以及游戏引擎编程中的常用技法。作者 Robert Nystrom 拥有二十年的行业从业经验,其中在 EA 工作了 8 年。

书中将游戏开发中频繁涉及的编程模式提炼出来,结合具体开发实例逐步引出对应的模式,相较于四人帮的《设计模式》,它的内容更加具体。

与传统出版方式不同,这本书采用网络出版,其 Web 版完全免费,并且在 Amazon 上获得了罕见的 5 星评价,足见读者对它的高度认可。此外,书中内容生动有趣,将各种经验娓娓道来,堪称业界良心之作。

二、本文涉及知识点思维导图

在开始正文之前,先展示这篇文章所涉及内容知识点的思维导图。如果读者不想阅读文章正文,直接查看这张图,也能大致了解本文的主要知识点。

三、何为好的软件架构

《Game Programming Patterns》一书中提到,好的设计意味着当进行某些改动时,整个程序仿佛早已为此做好准备。我们可以通过添加几个函数调用完成任务,同时不会影响代码底层的稳定运行。

这听起来很棒,但实际操作起来颇具难度。若真能做到“让代码的改动不影响其整体稳定性”,那自然是极好的。然而,这种设想过于理想化,我们不妨说得更通俗些。架构与变化密切相关,我们应正视变化,并从变化入手。在实际开发中,代码总会被改动。如果代码无人问津,无论是因为它完美无缺还是糟糕透顶,那么其架构设计也就失去了意义。评价架构设计的优劣,关键在于看它应对变化的轻松程度。没有变化,架构就如同永远无法起跑的运动员。

能够轻松应对变化,是好的软件架构的主要优点之一。

四、一个新特性的实现过程

当我们需要修改代码来添加新特性、修复漏洞或进行其他编辑操作时,首先需要理解当前代码的功能。当然,我们无需了解整个程序的所有细节,但要将相关的代码信息存入大脑。

我们常常忽略这一步骤,但实际上这是编程过程中最耗时的部分。如果认为从磁盘将数据分页到 RAM 的速度很慢,那么尝试将数据通过神经纤维传输到大脑的过程会更慢。

一旦将所有正确的上下文信息记在脑中,经过思考找到解决方案。这个过程可能会有些曲折,但通常并不复杂。当我们理解了问题以及需要改动的代码后,实际的编码工作就会变得相对容易。

在向游戏中添加代码时,我们不希望下一个接手代码的人被遗留的小问题困扰。除非改动非常小,否则还需要对新代码进行微调,使其能够无缝融入程序的其他部分。如果处理得当,后续查看代码的人甚至难以分辨哪些是新添加的代码。

简而言之,编程的流程图大致如下:

PS: 看起来,这是一个令不少程序员闻之色变的死循环。

五、解耦与学习阶段

实际上,很多软件架构都与学习阶段(learning phase)密切相关。由于将代码信息载入神经元的过程十分缓慢,因此寻找策略减少载入的信息量是非常有必要的。在《Game Programming Patterns》(以下简称 GPP)一书中,有整整一章是关于解耦模式(decoupling patterns)的内容,许多常规的设计模式也涉及到解耦。

可以从多种角度定义“解耦”,下面是其中一种理解方式:如果两块代码存在耦合关系,那么意味着我们无法只理解其中一块代码而对另一块毫无了解;而如果实现了解耦,我们就可以独立理解其中一块代码,无需考虑另一块。

GPP 一书中指出,软件架构的关键目标之一是最小化在处理问题前需要存入大脑的知识量。

从后期阶段来看,解耦还有另一种定义:当一块代码发生变化时,不需要对另一块代码进行修改。虽然可能需要进行一些调整,但耦合程度越低,变化的影响范围就越小。

六、过度设计的代价

首先,我们做一个设想:如果能将所有内容解耦,那么编写代码将会变得像风一样自由。每次变化只需修改一两个特定方法,就像“万花丛中过,片叶不沾身”,这听起来十分惬意。

这或许就是人们对抽象、模块化、设计模式和软件架构充满期待的原因。在具有良好架构的程序上工作是一种不错的体验,每个人都希望提高工作效率。好的架构确实能在生产力方面带来巨大的提升,其影响力不容小觑。

然而,就像生活中的其他事物一样,没有免费的午餐。好的设计需要付出努力和遵循一定的规则。每次进行改动或实现新特性时,都需要确保新代码能够优雅地集成到程序的其他部分。在开发过程中,面对数千次的变化,还需要花费大量精力来管理代码结构。

我们会看到许多程序在开始时架构优雅,但最终却因为程序员不断添加的“小改动”而陷入混乱。这就像园艺工作,仅仅种植新植物是不够的,还需要进行除草和修剪。

有些人过于关注未来的扩展性,他们设想未来的开发者(或者未来的自己)进入代码库时,会发现它开放、强大且极具扩展性,发出“有此游戏引擎,夫复何求”的感慨。

但当过度关注这一点时,代码库可能会失控。接口和抽象层无处不在,插件系统、抽象基类、虚方法以及各种扩展点充斥其中。当需求发生变更时,虽然理论上某个接口可能会起到作用,但要找到合适的接口并非易事。而且,解耦虽然意味着在修改代码前需要了解的代码量减少,但实际上我们需要对抽象层有更深入的了解。

理想很美好,但现实往往很残酷。每次添加一层抽象或支持扩展的部分,其实都是在对未来的需求进行赌博。向游戏中添加代码和增加复杂性,都需要花费时间进行开发、调试和维护。如果我们的预测正确,后续能够使用到这些代码,那自然是皆大欢喜;但预测未来并非易事,如果模块化最终没有起到作用,反而会带来负面影响,毕竟我们为此付出了时间和精力。

有些人用术语“YAGNI”(You aren't gonna need it,即“你不需要那个”)来提醒自己,避免过度预测未来需求。

过度关注设计模式和软件架构,容易让一些人陷入代码的细节中,而忽略了发布游戏这一最终目标。许多开发者听从加强可扩展性的建议,花费多年时间开发“引擎”,却没有明确开发引擎的真正目的。

七、性能与速度

软件架构和抽象有时会受到批评,尤其是在游戏开发领域,因为它们可能会影响游戏的性能。许多提高代码灵活性的模式依赖于虚拟调度、接口、指针、消息等机制,而这些都会增加运行时的开销。

一个有趣的反例是 C++ 中的模板。模板编程有时可以在提供抽象接口的同时,避免运行时开销。

这体现了灵活性的两个极端。当直接调用类中的具体方法时,我们在编写代码时就明确指定了调用的类,这种方式缺乏灵活性;而通过虚方法或接口,直到运行时才确定调用的类,虽然增加了灵活性,但会增加运行时开销。模板编程则介于两者之间,它在编译时初始化模板,确定调用的类。

软件架构的一个重要目标是使程序更加灵活,这样在进行修改时所需的工作量会减少,编写代码时对程序的假设也会更少。我们可以使用接口让代码与任何实现该接口的类进行交互,而不仅仅局限于当前编写的类。灵活性可以帮助我们快速改进游戏。

为了在牺牲少量性能的前提下更快地做出原型,我们可以让程序更加灵活。但需要注意的是,对现有代码进行优化可能会降低代码的灵活性。一种折中的方法是在设计确定之前保持代码的灵活性,之后再提取抽象层来提高性能。

八、烂代码在原型阶段的优势

在《代码整洁之道》中被吐槽的烂代码,其实也有其优势——速度快。

编写具有良好架构的代码需要经过深思熟虑,这会消耗大量的时间。在项目的整个周期中,保持良好的架构需要付出巨大的努力。我们需要像露营者对待营地一样小心管理代码库,确保代码比我们接手时更加整洁,就像《代码整洁之道》系列文章第一篇中所说的那样。

如果项目需要长期投入,保持编写良好架构代码的习惯是值得推荐的。但在游戏开发中,需要进行大量的实验、探索和试错。特别是在早期阶段,编写一些可能会被丢弃的代码是很常见的做法。

如果只是想验证某个游戏想法是否可行,良好的设计可能会导致在看到屏幕反馈之前花费大量时间。如果最终证明这个想法不可行,那么为了让代码更加优雅而付出的额外时间就白费了。

但需要明确的是,可抛弃的代码即使能够正常运行,也不适合进行维护,必须进行重写。如果有可能需要维护这段代码,就应该认真编写。

一个确保原型代码不会直接用于正式游戏的技巧是使用与正式游戏不同的编程语言。这样,在将代码应用于正式游戏之前,必须进行重写。

在原型开发阶段,设计糟糕的烂代码可能是让我们尽快做出原型产品并成功上线的功臣。因为它们能够快速实现想法,无需复杂的设计和架构。但这些烂代码在原型设计阶段结束后,一定要进行重写或重构。

九、开发周期中因素的动态平衡

在整个开发周期中,以下三大要素相互影响:

  • 为了保证项目在整个生命周期内的可读性,我们需要采用好的架构。
  • 需要提高游戏的运行时性能。
  • 需要尽快实现当前的特性。

有趣的是,这三个目标都与速度有关:长期开发的速度、游戏运行的速度和短期开发的速度。

这些目标至少在一定程度上是相互对立的。好的架构从长期来看可以提高生产力,但也意味着在维护每个变化时需要付出更多努力来保持代码的整洁。

实现速度最快的代码往往不是运行时最快的代码。相反,提升性能需要投入大量的编程时间,而且高度优化的代码通常缺乏灵活性,难以进行改动。

在开发过程中,我们总是面临着尽快完成任务的压力。但如果为了追求速度而尽可能快地实现特性,代码库可能会充满混乱和漏洞,从而影响未来的开发效率。

对于这三个要素的权衡,没有简单的解决方案,需要根据具体的项目情况进行分析和决策,使三者保持动态平衡,确保项目的顺利进行。

十、本文涉及知识点提炼整理

本文涉及的关于游戏架构与性能的心得总结如下:

  1. 抽象和解耦可以增强代码的扩展性和灵活性,但会增加实现时间。除非确实需要这种灵活性,否则无需过度追求。
  2. 性能优化很重要,但要注意时机。在开发周期中,应先专注于实现基本需求,将可能影响项目进度的性能优化尽量推迟。
  3. 在整个开发周期中,灵活性和高性能往往难以兼得。可以在设计确定之前保持代码的灵活性,之后再提取抽象层提高性能。
  4. 在原型开发阶段,设计糟糕的烂代码可能是快速做出原型产品的功臣。但在原型阶段结束后,这些代码必须进行重写或重构。
  5. 如果打算抛弃一段代码,就不要试图将其写得完美。“摇滚明星将旅店房间弄得一团糟,因为他们知道明天会有人来打扫干净。”
  6. 提倡编写最简单、最直接的整洁代码。当你阅读这种代码时,能够完全理解其功能,并且想不到其他更好的实现方式。“完美是可达到的,不是没有东西可以添加的时候,而是没有东西可以删除的时候。”
  7. 最重要的是,如果你想做出让人享受的东西,那就享受做它的过程。

With Best Wishes.

作者信息

孟子菇凉

孟子菇凉

共发布了 3994 篇文章