购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

第1章
架构、性能和游戏

在我们一头扎进一堆模式之前,我想为你介绍一些关于我如何看待软件架构以及它是如何应用到游戏的一些背景,这可能会帮助你更好地理解这本书的其余部分。至少,当你陷入关于设计模式和软件架构是多么糟糕(或者很棒)的一场争论中时,它会给你一些论据来使用。

请注意,我没有假设你站在争论中的哪一方。就像任何军火商一样,我为所有战斗方提供武器。

1.1 什么是软件架构

如果你从头到尾阅读了这本书,那么你并不会了解到3D图形背后的线性代数或者游戏物理背后的演算。这本书也不会告诉你如何一步步改进你的AI搜索树或者模拟音频播放中的房间混响。

哇,此段简直为这本书打了一个糟糕的广告。

相反,这本书是关于上面这一切要使用的代码的组织方式。这里少谈代码,多谈代码组织。每个程序都具有一定的组织性,即使它只是“把所有东西扔到main()函数里然后看看会发生什么”,所以我认为讨论如何形成好的组织性会更有趣些。我们如何分辨一个架构的好坏呢?

我大概有5年时间一直在思索这个问题。当然,像你一样,我对好的设计有着一种直觉。我们都遇见过非常糟糕的代码库,最希望做的就是剔除它们,结束自己的痛苦。

不得不承认,我们大多数人只接触到一部分这样的工作。

少数幸运儿有相反的经验,他们有机会与设计精美的代码共事。那种代码库,感觉就像在一个完美的豪华酒店里站了很多礼宾在翘首等待你的光临。两者之间有什么区别呢?

1.1.1 什么是好的软件架构

对于我来说,好的设计意味着当我做出一个改动时,就好像整个程序都在期待它一样。我可以调用少量可选的函数来完美地解决一个问题,而不会为软件带来副作用。

这听起来不错,但还不够切实。“只管写你的代码,架构会为你收拾一切。”没错!。

让我解释下。第一个关键部分是,架构意味着变化。人们不得不修改代码库。如果没人接触代码(不管是因为代码非常完美,又或者糟糕到人人都懒得打开文本编辑器来编辑它),那么它的设计就是无法体现其意义的。衡量一个设计好坏的方法就是看它应对变化的灵活性。如果没有变化,那么这就像一个跑步者从来没有离开过起跑线一样。

1.1.2 你如何做出改变

在你打开编辑器添加新功能,修复bug或者由于其他原因要修改代码之前,你必须要明白现有的代码在做什么。当然,你不必知道整个程序,但是你需要将所有相关的代码加载到你的大脑中。

这在字面上是一个OCR1过程,不过这个想法有些奇怪。

我们倾向于略过这一步,但它往往是编程中最耗时的部分。如果你认为从磁盘加载一些数据到RAM很慢的话,试着通过视觉神经将这些数据加载到你的大脑里。

一旦你的大脑有了一个全面正确的认识,则只需稍微思考一下就能提出解决方案。这观点值得反复斟酌,但通常这是比较明确的。一旦你理解了这个问题和它涉及的代码,则实际的编码有时是微不足道的。

我说“测试”了吗?哦,是的,我说了。

为一些游戏代码编写单元测试比较难,但是大部分代码是可以完全测试的。

我这里不是要慷慨陈词,不过,如果你之前没有考虑过多做自动化测试的话,我希望你多做一些。难道没有比一遍一遍手动验证东西更好的事情要做吗?

你的手指游走于键盘间,直到右侧的彩色灯光在屏幕上闪烁时,你就大功告成了,是吗?还没有!在你编写测试,并将它发送给代码审查之前,你通常有一些清理工作要做。

你在游戏中加入了一些代码,但是你不想后面处理代码的人花大量时间理解或修改你的代码。除非变动很小,通常都会做些重新组织工作来让你新加的代码无缝集成到程序中。如果你做得很好,那么下一个人在添加代码的时候就不会察觉到你的代码变动。

简而言之,编程的流程图如图1-1所示。

图1-1 编程的流程图

现在想想,流程图的环路中没有出口有点小惊悚。

1.1.3 我们如何从解耦中受益

虽然不是很明显,但我认为很多软件架构师还处于学习阶段。将代码加载到脑中如此痛苦缓慢,得自己寻找策略来减少装载代码的体积。这本书有一整章(第5章)是关于解耦模式的,许多的设计模式也有相同的思想。

你可以用一堆方式来定义“解耦”,但我认为如果两块代码耦合,意味着你必须同时了解这两块代码。如果你让它们解耦,那么你只需了解其一。这很棒,因为如果只有一块代码和你的问题相关,则你只需要将这块代码装载到你的脑袋中,而不用把另外一块也装载进去。

对我来说,这是软件架构的一个关键目标:在你前进前,最小化你脑海中的知识储存量。

当然,对解耦的另一个定义就是当改变了一块代码时不必更改另外一块代码。很明显,我们需要更改一些东西,但是耦合得越低,更改所波及的范围就会越小。

1.2 有什么代价

这听起来很不错,不是吗?对一切进行解耦,你就可以迅速编写代码。每一次变化意味着只会涉及某一个或两个方法,然后你就可以在代码库上行云流水地编写代码。

这种感觉正是为什么人们会为抽象、模块化、设计模式和软件架构感到兴奋的原因。一个架构良好的程序工作起来真的会令人愉悦,每个人都会更加高效。良好的架构在生产力上会产生巨大的差异。怎么夸大它带来的效果是如何深远都不为过。

但是,天下没有免费的午餐。良好的架构需要很大的努力及一系列准则。每当你做出一个改变或者实现一个功能时,你必须很优雅地将它们融入到程序的其余部分。你必须非常谨慎地组织代码并保证其在开发周期中经过数以千计的小变化之后仍然具有良好的组织性。

你必须要考虑程序的哪一部分应该要解耦然后在这些地方引入抽象。同样地,你要确定在哪里做一些扩展以便将来很容易应对变化。

人们对此非常兴奋。他们设想着,未来的开发者(或者是他们自己)进入代码库,发现代码库开放、强大,只等着被加些扩展。他们想象一个游戏引擎便可统治一切。

这小节的下半部分(维护你的设计)需要特别注意。我曾见过许多程序在开始时写得很漂亮,但死于一个又一个“一个小补丁而已”。就像园艺一样,只种植是不够的。你必须要除草、修剪。

但是,事情就在这里开始变得棘手。当你添加了一个抽象层或者支持可扩展的地方,你猜想到你以后会需要这种灵活性,于是你便为你的游戏增加了代码和复杂性,这需要时间来开发、调试和维护。

如果你猜对了,那么你之前的辛苦就没白费,而且也无须再对代码进行任何修改。但是猜测未来是很难的,并且当模块最终没起到作用时,很快它就变得有害。毕竟,你必须处理这些多出来的代码。

有人杜撰了“YAGNI”一词(You aren’t gonna need it 你不需要它)作为口头禅,用它来与猜测未来的自己会想要什么这种冲动进行斗争。

当你过度关注这点时,便会得到一个架构已经失控的代码库。你会看到接口和抽象无处不在。插件系统、抽象基类、虚方法众多,还有各种的扩展点。

你将花费大量时间去找到有实际功能的代码。当你需要做出改变时,当然有可能有接口能帮上忙,但你会很难找到它。从理论上讲,解耦意味着在你进行扩展时仅需理解少量代码,然而抽象却增加了理解代码的难度。

像这样的代码库正是让人们反对软件架构尤其是设计模式的原因。对代码进行包装很容易,以至于让你忽视了你要推出一款游戏的事实。一味地追求可扩展性让无数开发者在一个“引擎”上花费数年却没有搞清楚引擎究竟是用来做什么的。

1.3 性能和速度

一个有趣的范例是C++模板。模板元编程有时可以让你获得抽象接口而没有任何运行时开销。

对灵活的定义,不同人有不同的看法,当你在某些类中调用一个具体方法时,你相当于将这个类固定(很难做出改变)。当你使用一个虚方法或者接口时,被调用的类将直到真正运行起来才能被追踪到,这样的程序更具灵活性但是会增加额外的运行成本。

模板元编程介于两者之间。在模板元编程中,在编译期间你就能决定在模板实例化时调用哪个类。

你有时候会听到关于软件架构和相关概念的批评声,尤其在游戏开发中:它会影响到游戏的性能。许多模式让你的代码更加灵活,但是它依赖于虚函数派发、接口、指针、消息以及其他至少有一些运行成本的机制。

还有一个原因。很多软件架构的目标是使你的程序更加灵活,这样只需较少的代价便可对代码进行改变,这也意味着在程序中更少的编码。你使用接口,以便代码可以与任何实现这些接口的类进行工作,而不是使用具体类。你使用观察者模式(第4章)和通信模式(第15章)使得游戏的两部分互相沟通,而将来它们自身就会成为另外两个需要沟通的部分。

但是性能优化总是在某些假设下进行的。优化的方法在特定的条件下进行更好。我们能肯定地假设永远不会有超过256个敌人吗?好极了,我们可以将ID打包成一个单字节。在这里我们只会在一个具体类型上调用方法吗?好,我们就静态调度或者对它内联。所有的实体都是同一个类吗?太好了,我们可以将它们做成一个很棒的连续排列(第17章)。

这并不意味着它的灵活性很差!它可以让我们快速地进行游戏更新,开发速度是让游戏变得有趣的关键性因素。没有人,哪怕是Will Wright ,可以在纸上设计出一个平衡的游戏。这需要迭代和实验。

你越快地对想法付诸实践并观察效果,你就能越多地尝试并越有可能找到一些很棒的东西。即便在你已经找到合适的技术之后,你也要用充足的时间来进行调整。一个细小的不平衡就会破坏掉游戏的乐趣。

这里没有简单的答案。将你的程序做得更具有灵活性,以便能够更快速地进行原型编写,但这会带来一些性能损失。同样地,对你的代码进行优化会降低它的灵活性。

根据我的经验,将一款有趣的游戏做得高效要比将一款高性能的游戏做的有趣更简单些。一种折中的办法是保持代码的灵活性,直到设计稳定下来,然后去除一些抽象,以提高游戏的性能。

1.4 坏代码中的好代码

这使我想到的下一个点是,编码风格讲求天时地利。本书的很多部分是关于编写可维护的、干净的代码,所以我的意图很明确,就是用“正确”的方式做事情,但是也存在一些草率的代码。

编写架构良好的代码需要仔细的思考,这是需要时间的。更多的是,在项目的生命周期内维护一个良好的架构需要很大的努力。你必须把你的代码库看作一个好的露营者在寻找营地一样:总是试着寻找比眼下更好的扎营点。

当你准备要长期和那份代码打交道时,这样是好的。但是,就像我之前提到的,游戏设计需要大量的试验和探索,特别是在早期,编写一些你知道迟早要扔掉的代码是很稀松平常的。

如果你只是想验证一些游戏想法是否能够正确工作,那么对其精心设计架构就意味着在想法真正显示到屏幕并得到反馈之前需要花费更多时间。如果它最终没有工作,那么当你删除代码时,花费在编写优雅代码上的时间其实都浪费掉了。

原型(把那些仅仅在功能上满足一个设计问题的代码融合在一起)是一个完全正确的编程实践。然而,特别提醒下,如果你编写一次性的代码,那么你必须要确保能将之扔掉。我不止一次看到一些糟糕的经理重演以下场景。

老板:“嘿,我们已经有想法了,准备尝试下。只是一个原型,所以不必感觉必须要做得正确。大概多久能实现?”

开发:“嗯,如果我简化很多,不测试,不写文档,不管bug,我几天内就可以给你一些临时的代码。”

老板:“太好了!”

几天后……

有一个小技巧确保你的原型代码不会变成真正的代码,就是使用不同于你游戏使用的语言来编写。这样的话,你就必须用游戏使用的语言重写一遍了。

老板:“嘿,原型写得很不错。你能花几个小时清理下代码然后开始真枪实弹的干么?”

你需要确保这些使用一次性代码的人们明白这种一次性代码看起来能够运行,但是它却不可维护,必须被重写。如果可能,最终你也许会保留它们,但需要后续修改得特别好。

1.5 寻求平衡

开发中我们有几个因素需要考虑。

我认为一个有趣的地方是这些都是关于某种速度:我们的长期开发速度,游戏的执行速度,以及我们短期内的开发速度。

1.我们想获得一个良好的架构,这样在项目的生命周期中便会更容易理解代码。

2.我们希望获得快速的运行时性能。

3.我们希望快速完成今天的功能。

这些目标至少部分是相冲突的。好的架构从长远来看,改进了生产力,但维护一个良好的架构就意味着每一个变化都需要更多的努力来保持代码的干净。

最快编写的代码实现却很少是运行最快的。相反,优化需要消耗工程时间。一旦完成,也会使代码库僵化:高度优化过的代码缺乏灵活性,很难改变。

完成今日的工作并担心明天的一切总伴随着压力。但是,如果我们尽可能快的完成功能,我们的代码库就会充满了补丁、bug和不一致的混乱,会一点点地消磨掉我们未来的生产力。

这里没有简单的答案,只有权衡。从我收到的电子邮件中,看得出来,这让很多人头疼。特别是对于想做一个游戏的新手们来说,听到这样说挺恐吓人的,“没有正确答案,只是错误口味不同”。

你绝对没听到过某人在挖掘水沟上的卓越事迹。也许你有,我却没有研究过这个领域。据我所知,那里也许有热衷于水沟挖掘的爱好者,水沟挖掘准则,并且有一个自己的文化圈子。我们凭什么去评判呢?

但是,对于我而言,这令人兴奋!看看人们从事致力的领域,在这中心,你总能找到一组相互交织的约束。毕竟,如果有一个简单的答案,每个人都会这么做。在一周内便可掌握的领域最终是无聊的。你不会接触到在别人的杰出职业生涯中所挖掘出的东西。

对于我而言,这和游戏本身有很多共同点。就像国际象棋永远无法掌握,因为它是如此完美的平衡。这意味着你可以穷尽一生来探索可行的战略空间。设计不当的游戏如果用一个稳赢的战术一遍遍玩,会让你厌倦并退出。

1.6 简单性

最近,我觉得如果有任何方法来缓解这些限制,那便是简单性了。在今天我所写的代码中,我非常努力地尝试着编写最干净、最直接的函数来解决问题。这种代码在你阅读之后,就会明白它究竟做了什么,并且不敢想象还有其他可能的解决方案。

我致力于保持数据结构和算法的正确性(在这个顺序下),然后继续往下做。我觉得如果我能保持简单性,代码量就会变少。这意味着更改代码时,我的脑袋里只需装载更少的代码。

它通常运行速度快,因为根本就没有那么多的开销,也没有太多的代码要执行(这当然并非总是如此,你可以在小部分代码中进行很多的循环和递归)。

但是,请注意,我并不是说简单的代码会花费较少的时间来编写。你会觉得最终的总代码量更少了,但是一个好的解决方案并不是更少的实际代码量,而是对代码的升华。

我们很少会遇到一个非常复杂的问题,用例反而有一大堆,例如,你想让X在Z的情况下执行Y而在A的情况下执行W,以此类推。换句话说,是一个不同实例行为的长列表。

最省脑力的方法就是只编写一次测试用例。看一下新手程序员,这是他们经常做的:为每个需要记住的用例构建大量的条件逻辑。

在那里面毫无优雅性,当程序有输入或者编码者稍微考虑得跟用例有些不一样时,这种风格的代码就最终会沦陷。当我们考虑优雅的解决方案时,浮现脑海中的就有一个:一小块逻辑就能正确地处理一大片用例。

你会发现这有点像模式匹配或解谜。它需要努力识破测试用例的分散点,以找到它们背后隐藏的秩序。当你把它解决时,会感觉很棒。

Blaise Pascal用了一句名言作为了一封信的结尾:“我会写一封更简短的信,但我没有足够的时间。”

另一种引用来自Antoine de Saint- Exupery:“极臻完美,并非无以复加,而是简无可减。”言归正传,我注意到,每次我修改这本书的章节时,它都会变得更短。一些章节在完成时要比原来缩短20%。

1.7 准备出发

几乎每个人都会跳过介绍章节,所以在这里我祝贺你能够阅读到这里。我没有太多的东西来回报你的这份耐心,但是这里我能给你提供一些建议,希望对你有用。

· 抽象和解耦能够使得你的程序开发变得更快和更简单。但不要浪费时间来做这件事,除非你确信存在问题的代码需要这种灵活性。

· 在你的开发周期中要对性能进行思考和设计,但是要推迟那些降低灵活性的、底层的、详尽的优化,能晚则晚。

· 尽快地探索你的游戏的设计空间,但是不要走得太快留下一个烂摊子给自己。毕竟你将不得不面对它。

· 如果你将要删除代码,那么不要浪费时间将它整理得很整洁。摇滚明星把酒店房间弄得很乱是因为他们知道第二天就要结账走人。

· 但是,最重要的是,若要做一些有趣的玩意,那就乐在其中地做吧。

相信我,在游戏发布前的两个月并不是你开始担心“游戏的FPS只有1帧”问题的时候。 k/yQ+0s+8LW4M05r0DulTAH+DRlYzG1ISbS9DZfoI9gQTr5xqklrDt7yFvEVieOT

点击中间区域
呼出菜单
上一章
目录
下一章
×