一《Game Programming Patterns》其书

 

 

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

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

泰课在线

 

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

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

 

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

泰课在线

 

 

四、何为好的软件架构

 

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

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

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

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

 

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

 

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

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

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

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

 

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

 

      

 

 

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

 

 六、解耦与学习阶段

 

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

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

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

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

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

 

七、过度设计的代价

 

首先,来一个设想。

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

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

 

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

我们会看到无数程序有个优雅的开始,然后死于程序员一遍又一遍添加的“微小黑魔法”。就像园艺,仅仅增加新植物是不够的,还需要除草和修剪。

你得考虑程序的哪部分需要解耦,然后再引入抽象。同样,你需要决定哪部分要设计得支持插件来方便未来的变化。(所谓的面向未来编程)。

人们对这点变得狂热。他们设想以后的开发者(或者只是未来的他们自己)进入代码库,并发现它极为开放,功能强大,极具扩展性,他们发行“有此游戏引擎,夫复何求”。

 

当过分关注这点时,你会得到失控的代码库。 接口和抽象无处不在。插件系统,抽象基类,虚方法,还有各种各样的扩展点。

当需求变更时,有可能某个接口能帮上忙,但能不能找到就只能祝你好运了。 理论上,解耦意味着在修改代码之前需要了解的代码更少,但其实你需要对抽象层有很多的了解。

但是,还是那句话,理想很丰满,现实很骨感。 每当你添加了一层抽象或者支持扩展的部分,其实就是在赌这部分功能以后是否用得上。 添加代码和复杂性到游戏中,这都需要时间来开发,调试和维护。如果你猜对了,后来使用了这些代码,那么功夫不负有心人。 但预测未来很难,如果模块化最终无益,那就有害。 毕竟,你得花时间去实现这些代码。有些人喜欢简写为术语“YAGNI”——You aren't gonna need it(你不需要那个)——来对抗预测将来需求的强烈欲望。

 

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

 

八、性能与速度

 

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

 

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

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

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

 

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

 

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

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

 

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

 

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

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

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

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

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

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

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

 

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

 

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

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

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

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

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

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

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

 

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

 

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

1 抽象和解耦会让代码的扩展性和灵活性更加强,但会花费额外的实现时间。除非你觉得这样的灵活性有必要,否则没必要过度的去追求。

性能优化很重要,但是要注意时机。在整个开发周期中,最好先专注于实现基本需求,把那些可能限制到项目进度的性能优化尽量延后。

在整个开发周期中,灵活性和高性能往往不能兼得。我们可以保持代码的灵活性直到设计定下来,再抽出抽象层来提高性能。

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

5 如果打算抛弃这段代码,就不要尝试将其写完美。“摇滚明星将旅店房间弄得一团糟,因为他们知道明天会有人来打扫干净。”

提倡去写出最简单,最直接的整洁代码。你读过这种代码后,完全理解了它在做什么,想不出其他完成的方法。“完美是可达到的,不是没有东西可以添加的时候,而是没有东西可以删除的时候。”

但最重要的是,如果你想要做出让人享受的东西,那就享受做它的过程。

 

With Best Wishes.