游戏设计模式 #1 序言:架构,性能与游戏

作者:浅墨
2016-10-13
63 57 4

编者按

原文首发于作者浅墨的知乎专栏。本文系统整理并且以重新演绎的方式总结了阅读 Game Programming Patterns 一书的心得,针对这个系列,他在专栏开头写道:“第一次在知乎写专栏,一个全新的开始,希望各位多多关照。”。

系列前言

这个系列的诞生,是因为最近闲暇时一直在阅读一些之前已经列入待看书单的经典著作,并有将阅读过程中一些思考和总结写成文字进行记录。为了不枉费这些阅读、思考与总结的过程,决定将这些零散的内容整理成文,并集合成系列,将他们系统地记录下来,也希望能对热爱编程的各位有所帮助。

需要说明的是,虽说这些文章的原型是读书笔记,但会采用和传统读书笔记不一样的方式。他们不会仅仅停留在传达作者含义的阶段,而会是对一本书核心内容的全新演绎,内容解刨,精炼与解读。与其说是读书笔记,不妨说是原著经过的解读后的通俗版本。

希望自己的这些文章,能对各位有所帮助。也正如文章开头所说的,第一次在知乎写专栏,新的开始,希望各位多多指教。

Game Programming Patterns 其书

之前,已经在我的技术博客中解读了 Clean Code(中译《代码整洁之道》)这本书,承接 Clean Code 的解读,Game Programming Patterns 是我们下一个目标。

原书是英文原版,Game Programming Patterns,译为《游戏编程模式》,我们不妨在后文中将其简称为 GPP。根据全书内容,更贴切的诠释应为游戏设计模式。所以你会发现,我们这个系列文章的名称,就叫“游戏设计模式”。

一起来看看 Game Programming Patterns 这本书的内容。正如其名,它是一本专注于游戏编程领域的设计模式指南,它涵盖了游戏逻辑,游戏编辑器,和游戏引擎的编程中的常用技法。作者 Robert Nystrom 有二十年的从业经验,在 EA 工作 8 年有余。

“这本书将游戏开发中经常涉及到的编程模式拎出来,结合具体开发中遇到的实例一步步的引出对应的模式,这比起四人帮的《设计模式》更加具体。”

1

不同于传统的出版方式,这本书是网络出版,然后 Web 版完全免费,其更是在 Amazon 上具有罕见的5星评价,可见读者对其的好评程度之高。加之书中内容生动有趣,将各种经验之谈娓娓道来,实在是业界良心。

本文涉及知识点思维导图

先放出这篇文章所涉及内容知识点的一张思维导图,就开始正文。大家若是疲于阅读文章正文,直接看这张图,也是可以Get到本文的主要知识点的大概。(推荐放大后查看)。

2

何为好的软件架构

Game Programming Patterns 一书中说到,好的设计意味着当我改了点什么, 整个程序就好像正在等着这种改动。我们可以加入几个函数调用完成任务,同时丝毫不改变代码平静表面下的脉动。

这听起来很酷,只是实行起来很难。“把代码写到改变不会影响其平静表面。”若真能做到,确实不错。

这样太理想化了,还是让我们通俗些吧。架构是有关于变化的,让我们拥抱变化,从变化开始入手。总有人改动代码。如果没人碰代码,无论是因为代码至善至美,还是糟糕透顶,那么它的架构设计就毫无意义。评价架构设计就是评价它应对变化有多么轻松。没有了变化,它就是永远不会离开起跑线的运动员。

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

一个新特性的实现过程

在你改变代码去添加新特性,去修复漏洞,或者随便什么需要使用编辑器的时候,你需要理解现在的代码在做些什么。当然,你不需要理解整个程序,但你需要将所有相关的东西装进你的灵长类大脑。

我们通常无视了这步,但这往往是编程中最耗时的部分。 如果你认为将数据从磁盘上分页到RAM上很慢, 那么试着通过一对神经纤维将数据分页到大脑中。

一旦把所有正确的上下文都记到了你的大脑里, 想一会,然后找到解决方案。 这可能会有来回打转的时刻,但通常比较简单。一旦你理解了问题和需要改动的代码,实际的编码工作就很容易了。

你将一些代码加入了游戏,但不想下一个人被你留下来的小问题绊倒。 除非改动很小,否则就还需要一些工作去微调新代码,使之无缝对接到程序的其他部分。如果做对了,那么下个见到代码的人甚至无法说出哪些代码是新加入的。

简而言之,编程的流程图看起来是这样的:

3

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

解耦与学习阶段

其实,很多软件架构都和学习阶段(learning phase)息息相关。 将代码载入到神经元太过缓慢,找些策略减少载入的总量是很值得做的事。GPP一书中有整整一章是关于解耦模式(decoupling patterns), 还有很多常规的设计模式也牵扯到了解耦。

可以用多种方式定义“解耦”,这边是其中之一的理解方式:

如果有两块代码是耦合的, 那就意味着无法仅仅只理解了其中一个,而对另一个丝毫不了解。如果解耦了他俩,就可以独自的理解其中之一,根本无需牵扯到另一个。

GPP 一书中说道,我所理解的软件架构的关键目标,就是最小化在处理前需要进入大脑的知识。这也是一种很好的理解方式。

当然,也可以从后期阶段来看。 那么,另一种解耦的定义则是:当一块代码有变化时,没必要修改另外的代码。肯定需要修改一些东西,但耦合程度越小,变化会波及的范围就越小。

过度设计的代价

首先,我们需要这样一个设想:在一个系统中,解耦掉任何内容,然后,风烟俱净,天山共色,从流飘荡,任意东西,就可以像风一样写代码。每个变化都只修改一两个特定方法,万花丛中过,片叶不沾身,这是多么的惬意,是吧?

4

这大概就是人们对抽象,模块化,设计模式和软件架构兴奋的原因。在有好架构的程序上工作是很好的体验,每个人都希望能更有效率地工作。好架构能造成生产力上巨大的不同。很难再夸大它那强力的影响。

但是,就像生活中的任何事物一样,没有免费的午餐。好的设计需要汗水和纪律。 每次做出改动或是实现特性,你都需要将它优雅的集成到程序的其他部分。需要花费大量的努力去管理代码, 在开发过程中面对数千次变化仍然保持它的管理结构。

我们会看到无数程序有个优雅的开始,然后死于程序员一遍又一遍添加的“微小黑魔法”。就像园艺,仅仅增加新植物是不够的,还需要除草和修剪。你得考虑程序的哪部分需要解耦,然后再引入抽象。同样,你需要决定哪部分要设计得支持插件来方便未来的变化。(所谓的面向未来编程)。

人们对这点变得狂热。他们设想以后的开发者(或者只是未来的他们自己)进入代码库,并发现它极为开放,功能强大,极具扩展性,他们会惊叹道“有此游戏引擎,夫复何求”。当过分关注这点时,你会得到失控的代码库。 接口和抽象无处不在。插件系统,抽象基类,虚方法,还有各种各样的扩展点。当需求变更时,有可能某个接口能帮上忙,但能不能找到就只能祝你好运了。 理论上,解耦意味着在修改代码之前需要了解的代码更少,但其实你需要对抽象层有很多的了解。

还是那句话,理想很丰满,现实很骨感。 每当你添加了一层抽象或者支持扩展的部分,其实就是在赌这部分功能以后是否用得上。 添加代码和复杂性到游戏中,这都需要时间来开发,调试和维护。如果你猜对了,后来使用了这些代码,那么功夫不负有心人。但预测未来很难,如果模块化最终无益,那就有害。毕竟,你得花时间去实现这些代码。

有些人喜欢简写为术语“YAGNI”——You aren't gonna need it(你不需要那个)——来对抗预测将来需求的强烈欲望。

过度去关注设计模式和软件架构,会让一批人很容易地沉浸在代码中,而忽略要自己的最终目的是要发布游戏。无数的开发者听着加强可扩展性的“警世名言”,花费多年时间制作“引擎”,却没有搞清楚做引擎是为了什么。

性能与速度

软件架构和抽象有时会被批评,尤其是在游戏开发中: 它伤害了游戏的性能。许多让代码更灵活的模式依靠虚拟调度、接口、指针、消息,和其他机制,而这些都会消耗运行时成本。

一个有趣的反面例子是C++中的模板。模板编程有时可以给你抽象接口而无需运行时开销。

这是灵活性的两极。当写代码调用类中的具体方法时,你已经硬编码了调用的是哪个类:但通过虚方法或接口,直到运行时才知道调用的类。虽然这样更加灵活,但增加了运行时开销。

而模板编程是在两者之间——在编译时初始化模板,决定调用哪些类。

5

还有一个原因。很多软件架构的目的是使程序更加灵活。这让改变它需要较少的努力。编码时对程序有更少的假设。你可以使用接口,让代码可与任何实现它的类交互,而不仅仅是现在写的类。灵活性可以让我们快速改进游戏。

让你的程序更加灵活,在损失一点点性能的前提下更快地做出原型。但需要注意,优化现有的代码可能会让代码丧失原有的灵活性。

而一种折中的办法是保持代码灵活直到设计定下来,再抽出抽象层来提高性能。

烂代码在原型阶段的优势

天下武功,唯快不攻。

野百合也有春天,之前在 Clean Code 中被我们吐槽的烂代码,其实也有它们的优势——快。

我们知道,编写良好架构的代码需要仔细地思考,这会转为时间上的代价。 在项目的整个周期中保持良好的架构需要花费大量的努力。 你需要像露营者处理营地一样小心处理代码库:总是保持其优于你刚刚接触它的时候。就像我们之前 Clean Code 系列文章第一篇中说到的:让代码比你来时更干净。

当你要在项目上花费很久时间的话,保持编写良好架构的代码的习惯,是非常值得推崇的。但你知道,游戏开发需要很多实验、探索与试错。 特别是在早期,写一些你知道要扔掉的代码是很普遍的事情。

而如果只想试试游戏的某些主意是不是正确的,良好的设计意味着在屏幕上看到和获取反馈之前要消耗很长时间。如果最后证明这点子不对,那么删除代码时,你花费的那些为了让代码更加优雅的额外时间,就白费了。

但你得让人们清楚,可抛弃的代码即使看上去能工作,也不能被维护,必须重写。如果有可能要维护这段代码,就得防御性地好好编写它。

一个保证原型代码不会变成真正使用的代码的技巧是使用和正式游戏不同的编程语言。这样,在实际应用于正式游戏中之前必须重写。

在原型开发阶段,能尽快让你做出原型产品,最终让产品成功上线的最初的功臣,或许就是设计糟糕的烂代码。因为他们实现想法够快,不需要缜密的设计与架构。只是这些烂代码在经历了原型设计阶段之后,一定要被重写或者重构。

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

在整个开发周期中,如下三大要素一直在相互角力:

  • 为了在项目的整个生命周期保持其可读性,我们需要好的架构。
  • 需要更好的运行时性能。
  • 需要让现在的特性更快的实现。

有趣的是,这三点都是速度:长期开发的速度,游戏运行的速度,和短期开发的速度。

6

这些目标至少是部分对立的。好架构长期来看提高了生产力,也意味着维护每个变化都需要更多努力让代码保持整洁。

实现起来最快的代码很少是运行时最快的。相反,提升性能需要很多的编程时间。而且一旦完成,它就会污染代码库:高度优化的代码不灵活,很难改动。

总有今日事今日毕的压力。但是如果尽可能快地实现特性,代码库就会充满黑魔法,漏洞和混乱,阻碍未来的产出。

对于这个三者的权衡,没有简单明了的解决方案,只有具体问题具体分析,按实际的项目状况去去权衡,让三者保持友好的动态平衡,让整个项目保持良好的状态。

本文涉及知识点提炼整理

本文涉及知识点提炼整理,一些关于游戏架构与性能的心得总结:

  • 抽象和解耦会让代码的扩展性和灵活性更加强,但会花费额外的实现时间。除非你觉得这样的灵活性有必要,否则没必要过度的去追求。
  • 性能优化很重要,但是要注意时机。在整个开发周期中,最好先专注于实现基本需求,把那些可能限制到项目进度的性能优化尽量延后。
  • 在整个开发周期中,灵活性和高性能往往不能兼得。我们可以保持代码的灵活性直到设计定下来,再抽出抽象层来提高性能。
  • 在原型开发阶段,能尽快让你做出原型产品,最终让产品成功上线的最初的功臣,或许就是设计糟糕的烂代码。因为他们实现想法够快,不需要缜密的设计与架构。只是这些烂代码在经历了原型设计阶段之后,一定要被重写或者重构。
  • 如果打算抛弃这段代码,就不要尝试将其写完美。“摇滚明星将旅店房间弄得一团糟,因为他们知道明天会有人来打扫干净。”
  • 提倡去写出最简单,最直接的整洁代码。你读过这种代码后,完全理解了它在做什么,想不出其他完成的方法。“完美是可达到的,不是没有东西可以添加的时候,而是没有东西可以删除的时候。”
  • 但最重要的是,如果你想要做出让人享受的东西,那就享受完成它的过程。

参考文献

本文就此结束,系列文章未完待续。

With Best Wishes.

近期点赞的会员

 分享这篇文章

您可能还会对这些文章感兴趣

参与此文章的讨论

  1. 苯萘蒽菲 2016-10-13

    请问作者是不是就是csdn上的浅墨(http://my.csdn.net/zhmxy555)?
    --------------------------------------------------------------------------------------
    看了一下知乎专栏,可以确定就是了=。=

    最近由 苯萘蒽菲 修改于:2016-10-13 14:54:51
  2. TupleCat 2016-10-13

    诶诶诶Σ( ° △ °|||)︴居然是浅墨大大

  3. Pea 2016-10-19

    配图太小了 可否高清

  4. Nova-CrBXyF 2022-10-08

    发评论支持,入门程序员真的应该好好看看这篇文章,我就经常为了写“好看”的代码而浪费时间

您需要登录或者注册后才能发表评论

登录/注册