游戏编程模式- 架构,性能与游戏

Tags: 设计模式 游戏开发

本系列博客是:Game Programming Patterns 的中文翻译版本。

翻译的github地址: cyh24. 如有兴趣,可联系博主共同翻译,一起造(wu)福(dao)他人。

博客虽然水分很足,但是也算是博主的苦劳了,

如需转载,请附上本文链接,不甚感激!

本系列博客 目录,可点击进入。


架构,性能与游戏

============================ 在我们埋头研究一堆的设计模式之前,我想先告诉你,对于软件架构,我个人是如何理解的。这样可以帮你对后续的文章有更好的理解。或者,当你跟别人争论一个软件架构或者设计模式是优秀还是糟糕的时候,这些都可以给你提供一些论据。

什么是软件架构

对于这本书而言,即使你反复地翻来覆去,你也无法从里面找到关于三维图形背后的线性代数知识,或者是游戏物理引擎背后的微积分;它不会向你展示如何在你的人工智能算法中使用搜索树,或者如何模拟一个房间里的各种混合起来的杂音。

取而代之的是,这本书向你展示的是如何组织这些代码,而不是简单的编写各个功能的代码。当然了,每个程序都有一定的组织形式,即使你把所有的代码都堆积到main()函数里,这也算是一种组织形式了。所以,有意思的事情是,你该如何设计一个好的组织结构?或者,你如何判断一个架构是好还是坏?

我个人已经为这个问题,仔细思考了至少5年了。当然,我也跟你一样,对于好的设计有一种直觉;但同时,我们也都深陷在糟糕的代码库里苦苦挣扎。所以,我希望你能够从中解脱出来。当然,极少数人有着相反的经历,他们有机会能够跟有着优秀设计的代码一起工作。那么,好的设计和糟糕的设计到底有什么不同呢?

什么是好的软件架构

对于我而言,一个好的设计就是,当我做了一些修改之后,并不需要去更改期待地方的代码,就好像整个程序一直待期待我这次的修改一样。当我需要实现一个任务的时候,我只需要调用几个函数,就可以完美地实现,而不用大刀阔斧的去写很多缝缝补补的代码。

这听起来非常完美,但是实际上这样并没有很大的操作性。“只要写你自己的代码,引起的改变也不会因影响到其他地方”

让我们说明一点。架构的关键点就在于变化。总有人需要去更改代码库的。如果没有去碰这些代码(可能是因为这些代码很完美很完整或者糟糕到没有人愿意去碰它),那么这样的设计是落后的。评价一个设计的好坏的标准就是它是如何应对变化的。如果没有变化,那么这就像一个跑步运动员从未离开起跑线。

你是如何修改你的代码?

当你要更改代码,比如:添加新特性,修复一个bug,或者其他原因,那么在此之前,你需要了解现存代码具体有什么左右。这不是说你一定要对整个程序都吃透了,但是你需要把那些跟你修改相关的代码都记在脑子里。

我们都希望能够在这个步骤上草草了事,搪塞一下就过去,但是实际上,这个步骤事软件开发中最耗时间的过程。但是 ,一旦你做好了这一步,将代码了然于胸,那么解决方案其实也就呼之欲出了。虽然不能绝对这么说,但是,总体上,你了解到你的问题,并且对跟这个代码相关的其他部分了解的更多,你写起代码来也就更简单了。

当你不停地敲击键盘,编写代码,知道屏幕出现了运行成功的标识,你的任务就结束了吗?当然不是!在你编写测试程序,准备将写好的代码提交去测试之前,其实你还有一些清理工作需要做好。

等等,我刚刚是提到了测试吗?好吧,是的。虽然,对于一些游戏代码来说,编写单元测试是一件很难的事情,但是,代码库里还是有很多代码是完全可以进行测试的。 虽然不是必须,但是,如果可以的话,我还是建议你能够考虑使用自动测试的方法进行测试。毕竟,比起一遍又一遍手工的检测,你还有很多有意义的事情可以在这段时间里完全。

可能,你添加了很多代码到你的游戏中,但是你肯定不希望别人会因为你的更改而无法正常运行吧。除非你的更改量很小,否则在你写完代码之后,通常需要做的一件事就是进行重新组织。如果这个步骤你做的很好,那么下一个人要来修改使用这些代码的时候,他甚至都感受不到你曾经改过了代码。

简单的说,这个程序设计的步骤如下图所示: 此处输入图片的描述

解耦 能帮你什么?

你可以找到解耦的很多定义,但是我觉得可以这么理解:如果两段代码是耦合的,也就意味着你理解其中一段的时候必然离不开另一段代码。但是,如果你进行解耦合的操作,那么你就可以将这两段代码变得更加独立。这样做有个很大的好处就是跟你程序相关的代码现在只有一段了。你只需要理解调用这段代码而不用去管另一段。

对于我而言,软件架构的关键目标就是:使你在编写代码实现功能的时候,你所需要了解的东西是最少的。

另一个对解耦的理解就是:当你要改变一段代码的时候,你不需要对另一段代码进行修改。毋庸置疑的是,我们总是会修改一些东西的,所以,我们代码耦合越小,开发起来更改代码的情况久越少。

解耦会带来什么cost?

把所有东西都进行解耦操作,每一次改变都只关联到一个或两个地方,那么你开发起来就像风一样的男子。这听起来很正确,对吧?

这就是为什么人们会对抽象,模块化,设计模式和软件架构,有着极大的热情。一个拥有好的组织架构的代码库,确实能够让人工作起来更愉快,更有效率。

但是,就像生活中的其他所有事情,这并不是随随便便就能得到的。一个好的架构的生产,需要坚持不懈的努力和坚守原则。每次你更改代码或者实现一个特性,你都一定要努力地将这些代码优雅的整合到你的程序中。在整个开发过程中,你都需要非常小心的组织这些(可能有几千个)由于小改动引起变化的代码。

你需要考虑程序中那些部分需要进行解耦,并且在这些地方引入抽象方法。同样的,你还要想到未来可能会有的变化,引入可扩展性的设计。

很多人对这点非常感兴趣。他们会设想将来的开发人员(或者就是他们自己)在使用这个代码库的时候,会觉得这个代码库非常的开放,强大,很有扩展性。他们想象着有一个游戏引擎管理所有事物。

但是,这里其实是一个利益权衡的地方。因为,如果每次你增加了一个抽象层或者支持一个可扩展功能,你都考虑将来的灵活性的话,那么你就大大增加了程序开发,调试,维护的时间。

如果你对于未来的需求改变猜的很准,那么你会得到很大的收获。但是通常,预测未来是一件很难的事情,而一旦模块化没有起到帮助的时候,它反而会带来一些害处,因为不管怎么说,你需要处理的代码量确实变大了。

当你团队中的人开始对解耦热衷到过的程度的时候,你会发现代码库的架构已经出现失控的地步了。你会在各个地方都看到各种各样的接口跟抽象。到处都是插件系统,各种抽象基类,虚方法,还有各式各样的扩展点。

当你需要一些真正能帮你做事情的代码时,你需要费劲千幸万苦才能找到。当你需要做一些更改时,的确代码库是提供了很多接口给你使用,但是要找到他们,只能祝你好运了。理论上,解耦的存在就是为了在你需要更改的时候,需要了解的代码更少。但是,这么多抽象层本身就已经给你带来了负担。

在一些情况下,正是这样的代码库导致了很多人反对使用软件架构和设计模式。你很容易实现在代码中实现这些解耦,但是不要忘了你是在开发一个游戏。很多开发者由于对可扩展性的追求,花了很多时间开发一个游戏引擎,却忘了这个引擎是用来干嘛的。

性能与速度

对于软件架构和抽象的一个常见的批判(尤其是游戏开发)就是:它会使得你的游戏性能降低。很多设计模式通过虚拟调度,接口,指针,消息等方式让你的代码变得更加灵活,但是同时,这些机制还是消耗一些运行时间。

大量的软件架构都是为了让你的程序更加灵活,让你在修改的时候需要的努力最小化。你使用接口,这样你就可以在任何类中实现这个接口对应的方法,而不是只为了今天这个特定的类。你使用观察者模式和消息传递机制使得游戏中的两个模块能够相互交流,同样你也可以在未来十分方便都实现三个或四个模块进行通信。

但是,性能都是基于假设的。你可以在具体的局限性下进行优化实践。我们可以百分百假设我们不会又超过256个敌人吗?很好,如果可以,我们就可以将ID定义成一个字节类型。我们能百分百确定在这里只会调用特定的一个具体类型的方法吗?很好,如果可以,那我们就可以使用静态的调度或者使用内联。我们能确定所有的实体都会是同一类吗?很好,如果可以,那么我们使用一个连续数组定义他们就行了。

所以,这并不是说灵活性不好。它能让我们修改我们的代码更加快速,而开发速度也是一件非常重要的事情。没有一个人,甚至是Will Wright(虚拟人生之父),能够在纸上就设计出一款非常理想的游戏。游戏开发一定是需要不断的迭代跟体验的。

你能越快的开发出来,那么你就有更多的实际去体验这个游戏,去优化它。即便你已经想出了一套非常好的机制,你同样需要时间去不断的调优的。因为,游戏中一个小小的不协调就会破坏整个游戏。

所以,这里并没有一个简单的是或否的回答。你让你的程序更加灵活,那么你就能更快的创造出原型,但是却会有性能上的损失;而如果你极力去提高你游戏的性能,那么你势必会失去代码的灵活性。

而我的经验是,让一款有趣的游戏变快比让一款性能好的游戏变的有趣来的更简单。一种妥协的方法就是,让你的程序保持灵活性直到你的设计完成,然后再抛弃一些抽象来提高你的性能。

坏代码也有好处

这本书很多地方都是讲关于如何让你的代码更具可维护性,如何清理你的代码,所以我的观点十分明确,就是让你写的代码尽量朝着好的方向。即便如此,即使是设计糟糕的代码,还是有价值的。

写一个好的架构的代码库需要你费心的去设计,而且,你要耗费更多的时间才能维护好一个设计优秀的代码库。就像一个露营者找到一个营地之后,你总是希望让你的营地看起来比你找到它之前更好。

在你投入需要大量时间精力才能完成的任务之前,你有这样的想法是很好的。但是,就像我前面提到的,游戏设计者是需要大量的实验跟探索的,所以,你可能写了一堆你自己都知道早晚都要丢弃的代码,尤其是在早期的时候。

如果你只是想快点找出一些好的游戏想法、点子,那么这意味着在你受到反馈之前,你会花费非常多的时间去构建优秀代码库。而当你发现这些点子并不是很好需要删除的时候,你就白白浪费了之前的构建优雅代码库的辛勤劳动了。

原型—拼凑在一起勉强能够实现功能的解决方案—它是完全合理的编程实践方式。但是,显然会存在一些非常大的问题。你需要确保在之后你能够把这些写过的原型代码都删除掉。我经常能够见到下面的场景一遍又一遍的发生:

老板“ 嘿,伙计,我们有一个非常好的idea,你能不能开发一个原型出来,不用管太多技术细节,只要原型就可以,你们多快能够整合出这些功能呢?” 开发者“这样啊,如果不用测试,不用考虑性能,不用写文档,不用管bug的话,几天就可以出来原型了”。 老板“太棒了!开工吧!”

几天之后,,,,

老板:“嘿,伙计,这个原型做的太棒了!你能不能花几天时间把它包装包装上线啊?”

所以啊,你需要让使用这些一次性代码的人,理解,虽然这些代码看起来是可以正常运行的,但是,其实它是不能被维护的,必须要删除重写。如果情况是你最终不得不保持这些代码,那么你一定要很小心的去写。

求平衡

我们需要找到下面这些要求的之间的平衡:

  1. 我们想要追求一个好的代码库,使得它们在项目的生存周期内都能被很好的理解;
  2. 我们想要运行效率更高;
  3. 我们想要目前的需求能够快速实现;

这些目标在很多程度上是处在相互对立的情况。比如,好的架构在长期项目开发中能够提升生产力,但是在维护它的时候,你需要在每一次的迭代之后都要按照一些原则去保持好的架构。

而最快的编码方式不一定意味着最快的运行效率。相反,优化程序需要大量的工程时间,而一旦优化程序之后,代码库的灵活性就会下降,因为:高度优化的代码往往不是很灵活,很难在接下来的开发中进行更改。

我们往往处在既要把今天的事情做好,又要担心明天的事情这样的压力之中。但是,如果我们只关注了今天的任务,过分追求速度,那么我们的代码库又会变得到处都是漏洞,bug,不一致性,这些又会削弱我们未来的生产力。

这里没有明确答案,只有折中的方法。从一些反馈给我的邮件中看到,这些问题同样是困扰了很多很多人。听起来很让人失望,尤其是对那些刚想做游戏的新手来说,不过实际就是:“这个问题永远没有正确答案,只有不同形式的错误的解决方案”

不过,对于我而言,这太令人兴奋了!你看看其他任何领域那些难题,都是存在着很多交互在一起的约束和折中。毕竟,如果一个问题有一个简单的解决方式,那么每个人都会这么去做。而那些你能够在一个星期内掌握的知识,通常最后都会变得很无聊,没有价值。比如,你不会听到有人因为挖沟而一鸣惊人。

对于我而言,这里存在很多跟游戏本身一样的共同点。比如,像国际象棋这样的游戏,不可能存在一套绝对致胜的策略,因为它的每一次决策都受限于其他步。这意味着,即使你穷尽一生,也不可能知道一个决胜策略。而一个设计糟糕的游戏会被一个更优的策略一遍又一遍的碾压直到你厌倦和放弃。

简洁

最近,我在想,如果有任何方法能够消除这些制约,我想应该就是简洁吧。我现在编程,总是会努力去使用那些最整洁,最直接的方式去解决问题,这样的代码在你读到它之后,你能够完全明白它的含义,理解它要做的事情,不会造成其他的歧义。

我的目标时把数据结构和算法写对。而我发现,如果我保持事情的简单性,那么代码量就会下降,而减少了的代码能够更好的被我自己理解,从而在更改的时候更容易。

通常来说,如果没有过多的头文件和代码的话,运行的效率也会相对较快(当然不是总是能够保证这样;比如,很多循环和递归的代码,虽然代码量很小,但是运行不见得快)。

请注意,我不是说简单的代码需要更少的时间书写。你会这么想是因为你可能会让位简单的代码最终的代码量会很少,所以开发时间更少。但是,记住,一个解决方案的优良不是从它的代码量来评价的。

我们很少有一个很简单的优雅的任务告诉你它的需求是什么,相反的,我们得到的往往都是一堆用例。比如,你在Z的情况下,你想要X去执行Y;但是在A的情况下,你却想让X去执行W,等等。总之就是一个长长的列表,上面有不同的用例行为。

而最不用费脑子的做法就是,一个用例一个用例的去是实现。我们常常能够看到新手程序员是这样工作的:他们通常为每一个情况都做一个逻辑判断,写了一大推堆砌在一起的if else。

这样做真的是一点美感都没有啊,往往当输入稍微有一点改变的时候,程序就无法正常运行了。而我们所谓的优雅的解决方案就是,使用一个小的逻辑,却能覆盖到大量的用例中。

寻找这种解决方案就有点像模式匹配或者解决一个迷宫。你需要花很多时间找到这些用例中隐藏的规律。而当你找到这些规律的时候,通常你都会有极大的满足感的。

做好准备,去行动吧!

几乎每个人都会跳过书籍的简介章节,所以我很感激,也祝贺你能一直看到这里。对于你的耐心(虽然没有什么礼物,哈哈),不过,我还是希望下面总结的这些建议能够对你有所帮助:

  1. 抽象和解耦能够让你的程序开发变得快速和简单,但是还是不希望你花过多的时间在这上面,除非你对于这段代码在任务要求和灵活性上有很大的信心;
  2. 在整个开发周期中,你都要思考性能上的设计,不过底层细节的优化,我建议还是能够尽量放到最后再做;
  3. 你需要快速的探索游戏的设计空间,但是也不要因为走的太快,就留下一大堆的坑,毕竟,你早晚还是要还的;
  4. 如果你知道这些代码早晚要被抛弃的,就不要花太多时间把它做的很好。一些摇滚明星的酒店房间都超乱,那是因为他们知道明天就要退房了;