diff --git a/01-Introduction.md b/01-Introduction.md index 2cd2d2e..c1c44fc 100644 --- a/01-Introduction.md +++ b/01-Introduction.md @@ -33,7 +33,7 @@ 有人可能会问,市面上已经存在各种各样的游戏编程类的书。为什么还要写一本? 我看过的大多数的游戏编程书籍,无非就是下面两种类型: - + - 介绍特定领域的书籍:这类书的写作范围一般比较狭小,通常专注于给你在游戏开发某些特定方面的深层次的指导。他们会教你比如:3D图形,实时渲染,物理世界模拟,人工智能或者音频处理方面的知识。这些都是从事游戏开发的工程师在职业发展阶段会碰到的专业技能。 - 全引擎类的书籍:不同的是,这类书籍试着跨度整个游戏引擎的不同组成部分。他们通常是面向构建一个完整的游戏引擎,来适合于特定类型的游戏,经常举的例子是三维下的第一人称射击游戏。 @@ -65,6 +65,44 @@ 最后一个章节是本书的重中之重。提出了13个我认为很有用的设计模式。他们被安排到4个类别:序列模式,行为模式,解耦模式和优化模式。 +- **含义**部分给出了这种设计模式的一个简要描述并告诉你这种模式可以解决哪种问题。把这部分放在前面可以帮助你迅速找到可以拓展你的知识的地方。 + +- **示例部分**提供了一个将要应用这种设计模式的一个例子。与具体的算法不同,通常,如果不把设计模式用在某个特定的问题上,模式的说明将会很抽象。教授一种设计模式而不用例子来说明类似于教授烘培而不提生面团。这一部分提供了稍后用来烘培的面团。 + +- **模式**部分脱离前面所给的例子提取了这个设计模式的精华部分。如果你想要一个枯燥的教科书般的描述的话,就是这一部分了。如果你已经很熟悉这种设计模式了,那么这一部分可以帮助你复习以确保你不会忘记。 + +- 目前为止,我们仅就一个例子对这个设计模式进行阐述,但是,你怎么知道这种设计模式对解决你的问题会很有效呢?而**用武之地**部分会告诉你什么时候有效,什么时候最好避免使用它。**铭记于心**部分则指出了使用这种设计模式的后果和风险。 + +- 像我,如果你需要一个具体的例子进行学习**实例代码**部分就是这么一个部分了,通过一步一步地实现这种设计模式,你可以确切地看到模式是如何工作的。 + +- 设计模式与纯粹的算法不同,它是开放的。每一次你使用某种设计模式,你都可以用不同的方式实现它。下一个部分,**设计与决策**会就这一方面进行讨论并向你展示使用这种模式时可以做某些的选择。 + +- 为了圆满结束这一部分,有一个简单的部分——**更多请参阅**。它向你展示了与这个设计模式相关的东西并指出了一些现实世界中使用这种设计模式的开源代码。 + + +# 关于示例代码 + +在本书中示例代码是用C++写的,但是这并不意味着设计模式只能对这种程序设计语言(C++)有效,也不是说用C++比其他语言好。几乎任何语言都可以很好地工作,但是一些设计模式会假设你的语言中有“对象”和“类”。 + +我选择C++有几个理由。首先在商业游戏中最流行用C++(译者:中国大概是Java?),它是行业语言。更重要的是C/C++式语法在诸如Java,C#,Javascript和其他语言中被广泛应用。在这种状况下,尽管你不了解C++,只要你稍微下点功夫你就可以理解示例代码了。 + +这本书的目标并不是教你C++,所以示例代码就写的尽可能的简单,这些代码并不代表优秀的C++编程风格。去读出代码所要表现出的想法,而不是去阅读代码是怎样表现这种思想的(译者:大概也就是代码无所谓,思想最重要)。 + +特别地,示例代码并不是用“现代”风格——C++11或更高——编写的,也没有用到标准库,极少用到模板。希望通过这样做可以让代码尽可能的简单,这将会对那些使用C,Object-C,Java或其他程序设计语言的人更亲近。 + +为避免在代码上浪费空间(译者:这里指的是纸质书籍的空间浪费,网页上怎么可能有这种问题?!),你已经看到过或是与这种设计模式无关的代码都会被省略,被省略的部分有省略号,还会告诉你省去的代码到哪里去找。 + +试想这里有一个做某个工作并返回一个值的函数。如果解释设计模式时只用考虑到返回值的时候而不是做了什么的时候,示例代码就会像下面一样: + +```cpp +bool update() +{ + // Do work... + return isDone(); +} +``` + + # 何去何从# 模式是一个在软件开发过程中不断变化,不断扩大的部分。这本书的是从Gang of Four经典理论的分享与讨论中开始的,并且这个讨论、分享的过程还会在本书之外继续进行。 diff --git a/02.1-Command.md b/02.1-Command.md index 8b793b8..2b19cd0 100644 --- a/02.1-Command.md +++ b/02.1-Command.md @@ -28,13 +28,13 @@ Tags: 游戏编程 设计模式 游戏开发 我认为,用这句话来诠释Command模式,远比上面那些他们选取的句子更恰当。 当然了,不管怎么样的诠释,听起来总是很抽象,很朦胧。所以,我加入一些具体的例子,来补充解释Command模式。 -#实例一:输入配置 +#输入配置 每个游戏都有一部分代码是专门用来读取用户输入的——比如:按下button,键盘事件,点击鼠标等等。这些输入都需要被记录下来,然后转变成游戏中有意义的行为。 ![此处输入图片的描述][1] 下面是一个简单的实现: -``` +```cpp void InputHandler::handleInput() { if (isPressed(BUTTON_X)) jump(); @@ -48,7 +48,7 @@ void InputHandler::handleInput() 为了能过实现这样的功能,我们需要将 *jump()* 和 *fireGun()* 之间的直接调用,改成一种能够置换的方式。所以,我们需要使用一个对象来表示游戏中的一个行为,这样,Command模式就来了。 我们定义一个基类来代表一个可以触发的游戏命令: -``` +```cpp class Command { public: @@ -57,7 +57,7 @@ public: }; ``` 然后,我们为不同的游戏行为定义相应的子类: -``` +```cpp class JumpCommand : public Command { public: @@ -69,9 +69,10 @@ class FireCommand : public Command public: virtual void execute() { fireGun(); } }; +// You get the idea... ``` 在我们的InputHandler类中,我们为每一个按键都保存了一个指向Command对象的指针: -``` +```cpp class InputHandler { public: @@ -87,7 +88,7 @@ private: }; ``` 现在的输入处理只是指向了相应的代理: -``` +```cpp void InputHandler::handleInput() { if (isPressed(BUTTON_X)) buttonX_->execute(); @@ -101,10 +102,10 @@ void InputHandler::handleInput() 以上就是Command模式的一个简单直观的例子了,如果你觉得自己已经掌握Command的真谛,那么下面的部分你就可以随便看看,当做巩固了。 -#实例二:控制演员 +#控制演员 前面给出的例子中,我们的代码勉强可以达到目的,不过作用还是相当有限的。原因是,上面的例子有一个假设作为前提,就是他们假定了有一些全局函数,例如*jump()*,*fireGun()*等等,这些函数能够直接得到一个控制器,然后很轻松的像控制木偶一样去控制主角。 这样的假定大大地限制了Command的使用范围。因为,在这种情况下,只有主角能够使用JumpCommand进行跳跃。让我们取消这样的假设来打破这层限制。我们不让调用的函数自己去找控制对象,而是将控制对象作为参数传给它。 -``` +```cpp class Command { public: @@ -113,7 +114,7 @@ public: }; ``` 上面的代码中,GameActor是我们的游戏对象类,代表了游戏世界中的一个演员。我们把它传递给execute函数,这样那些Command子类就可以条用相应的方法了。就像这样: -``` +```cpp class JumpCommand : public Command { public: @@ -122,7 +123,7 @@ public: actor.jump(); } }; -``` +``` 如果这个actor是玩家角色的一个引用,那么就可以根据玩家的输入准确的控制主角,这样就能实现跟第一个例子同样的效果了。与此同时,在Command跟Actor之间插入了一个层,这让我们有了一个更加灵活的能力:我们可以通过改变这个actor参数,让玩家能够控制游戏世界中的所有演员。 实际上,虽然这不是一个通常意义上的特性,不过倒是会时不时的被使用到。当目前为止,我们只留意了玩家控制主角,但是,那么游戏世界中的其他演员呢?他们是被游戏AI所驱动的。我们可以使用同样的Command模式,作为AI引擎和演员之间的借口。如此一来,AI代码只需要简单地抛出命令对象就可以了。 @@ -137,7 +138,7 @@ public: 最后的这个例子是Command模式最为著名的一个用法了。如果一个Command对象能够完成一件事,那么离他撤销这件事就不远了。撤销可以用在一些策略游戏中,你可以撤销一些你不喜欢或者后悔做的一些操作。在制作游戏的工具中,撤销也是一个非常必要的功能。让你游戏策划恨你的最佳方式,就是提供一个让他不能进行撤销操作的关卡编辑器。 如果没有Command模式,那么撤销将是一件非常困难的事情。一旦有了它,那就是小菜一碟了。如比,当我们要做一个单人回合制游戏,我们会希望能够加入一个撤销操作,这样玩家就可以把注意力放到策略上,而不用做过多没有必要的猜测了。 前面,我们已经使用了Command,非常方便地将用户的输入抽象化,因此玩家的每一步操作都已经封装到了Command之中。例如,玩家移动一个单位的代码如下: -``` +```cpp class MoveUnitCommand : public Command { public: @@ -162,7 +163,7 @@ private: 这里强调另一种Command模式的实现方法,在某些情况下,就像我们前两个例子,一个Command就是一个可以被复用的的对象,它代表了一个操作。前面我们的输入处理是在一个单独的Command对象中进行的,任何时候,一旦玩家按了正确的按键,它相应的execute()函数就会被调用。 在这里,这些Command就更加具体了。他们代表了在某一个特定的时间做了某件事。也就是说,每一次玩家选择一次移动,输入处理代码就会产生一个Command实例。就像这样: -``` +```cpp Command* handleInput() { Unit* unit = getSelectedUnit(); @@ -185,7 +186,7 @@ Command* handleInput() } ``` 实际上,这些Command只使用一次的优点马上就会体现出来。为了使Command能够被撤销,我们需要定义另外一个需要各个Command都实现的方法: -``` +```cpp class Command { public: @@ -195,7 +196,7 @@ public: }; ``` undo()方法会恢复被execute()方法改变过的游戏状态。下面代码是我们前面定义的Move Command加入了对撤销的支持: -``` +```cpp class MoveUnitCommand : public Command { public: @@ -238,7 +239,7 @@ private: 当玩家撤销,我们就撤销当前的Command,并将当前的引用向后移动一位。当他们重做,我们就向前移动指针,并且执行Command。当他们在撤销后选择了一步新的操作,那当前Command之后的所有Command就会被销毁。 当我第一次在一个关卡编辑器里面实现后,我感觉自己简直是个天才。我对它如此的简单明了,如此的运行顺畅感到吃惊。Command规定了所有的数据修改都通过一个个Command进行。但是一旦你确定了这个规则,剩下的就很容易了。 -# 优雅与功能弱化 +# 优雅亦或有缺陷? 之前,我说Command与一级函数或者闭包非常像,但是前面每一个例子,我都使用了类来定义。如果你对函数式编程比较熟悉,你可能会疑惑,说好的函数呢? 我用这种方式写例子,是因为C++对一级函数支持很弱。函数指针没有状态,仿函数很奇葩,并且同样需要定义类。C++11的lambdas表达式用起来很不顺手,原因是内存管理需要手动进行。 @@ -246,7 +247,7 @@ private: 这些不是说你在其他语言中不能在Command模式中使用函数。如果你使用的语言有真正的闭包,一定用起来!某种意义上说,Command模式就是一个没有闭包的语言模仿闭包的方式。 例如,如果我们用Javascript开发游戏,我们可以像这样创建一个单位的Command: -``` +```Javascript function makeMoveUnitCommand(unit, x, y) { // This function here is the command object: return function() { @@ -255,7 +256,7 @@ function makeMoveUnitCommand(unit, x, y) { } ``` 我们可以使用一对闭包来添加对撤销的支持: -``` +```Javascript function makeMoveUnitCommand(unit, x, y) { var xBefore, yBefore; return { diff --git a/02.2-Flyweight.md b/02.2-Flyweight.md index e5d2e60..93db560 100644 --- a/02.2-Flyweight.md +++ b/02.2-Flyweight.md @@ -14,18 +14,19 @@ Tags: 游戏编程 设计模式 游戏开发 #再探Flyweight模式 ============================ 迷雾消散,一个古老而壮丽的原始森林呈现在了我们的面前。不计其数的古老的铁杉,像一座塔尖林立的绿色大教堂。在巨大的树干面前,以至于,你只有往后拉开一段距离,才能从树干之间的缝隙中辨认出这是一个巨大的森林。 + 这是游戏开发者梦想中的世外桃源般的设计,而正是一个设计模式使得这一梦想中的场景得以在现实中得到实现。而这个模式的名字却再低调不过了:Flayweight(享元模式)。 #化木为林 -我可以很轻描淡写地就描绘了一个无边的森林场景,但是在一个实时的游戏中实现起来就是另一回事了。你在电脑前看到的是满屏的树木,而在图形程序员的严重,他们却是数以百万计的多边形,这些多边形必须以1/60秒的速度载入到GPU中。 +我可以很轻描淡写地就描绘了一个无边的森林场景,但是在一个实时的游戏中实现起来就是另一回事了。你在电脑前看到的是满屏的树木,而在图形程序员的眼中,他们却是数以百万计的多边形,这些多边形必须以1/60秒的速度载入到GPU中。 每棵树都有如下的一些数据结构: - 用于定义树干、枝杈、绿色植被等网格多边形; - 树皮和树叶的纹理; - 树木在森林中的位置和朝向; - 还有一些用来使得树木跟其他看起来不同的一些微调的参数:大小,色彩等; - + 写成代码,就是如下结构了: -``` +```cpp class Tree { private: @@ -45,7 +46,7 @@ private: ![此处输入图片的描述][1] 我们可以试着把对象分成两份(公有和私有)。首先,我们先把所有树木公用的数据搬到一个单独的类中: -``` +```cpp class TreeModel { private: @@ -55,7 +56,7 @@ private: }; ``` 游戏中只需要一个这样的对象就够了,因为没有必要把相同的模型和纹理贴图在内存中保存上千份。然后,在游戏中,每一个树木的实例都引用这个共享的*TreeModel*。这样之后,*Tree*这个类中就只剩下一些个性化的数据了: -``` +```cpp class Tree { private: @@ -99,12 +100,12 @@ Flayweight模式通过将一个对象的数据分成两类来解决问题。第 每一种地表类型都会有其特有的属性: - - 决定玩家移动速度的移动消耗。 - - 一个标明是否是水域的标识,它决定了这块地形可不可以过船。 + - 决定玩家移动速度的移动消耗。 + - 一个标明是否是水域的标识,它决定了这块地形可不可以过船。 - 一张渲染时候需要的纹理贴图。 由于游戏开发人员对性能一般都比较偏执,所以我们不可能把所有的这些属性保存在世界的每一个面片中。一个常用的方法是用枚举的方式定义一个地形类型: -``` +```cpp enum Terrain { TERRAIN_GRASS, @@ -114,7 +115,7 @@ enum Terrain }; ``` 然后,由World类来维护这些大量的面片: -``` +```cpp class World { private: @@ -122,7 +123,7 @@ private: }; ``` 为了能够从一个面片中得到有用的数据,我们通常是这么做的: -``` +```cpp int World::getMovementCost(int x, int y) { switch (tiles_[x][y]) @@ -148,7 +149,7 @@ bool World::isWater(int x, int y) 你应该看得懂这个思想,而且它确实是可行的。不过,我认为这样的做法一点也不优雅。我觉的移动带来的消耗和该面片是不是水域应该是与地形相关的数据,但是这里的代码并没有体现出来。更糟糕的是,一个地形相关的数据被分散到一堆其他函数里面。所以,我认为最理想的状况是把他们整合到一起,这个才是我们设计这些对象的目的。 如果我们有如下这样一个地形的定义就太棒了: -``` +```cpp class Terrain { public: @@ -174,7 +175,7 @@ private: 但是如果所有地形面片都有一个实例,那会带来我们不可承受的负担。如果你仔细观察这个类,你会发现其实里面并没有哪个属性是某个面片特有的。在Flyweight模式中,所有的这些属性都应该被划为“固有属性”或者“上下文无关”。 考虑到这些,就没有理由创建多个Terrain对象了。一块青草地的面片都跟其他面片并没有什么区别。我们不再使用地形枚举或者地形对象构成的网格了,我们用指向Terrain对象的指针构成网格来代替: -``` +```cpp class World { private: @@ -187,7 +188,7 @@ private: ![此处输入图片的描述][3] 由于这些Terrain实例在多个场合中被用到,如果它使用的内存是动态申请的,那他们的生命周期管理就会比较复杂。在这里,我们就直接在World中使用静态存储了。 -``` +```cpp class World { public: @@ -206,7 +207,7 @@ private: }; ``` 然后我们可以使用如下方式把地面绘制出来: -``` +```cpp void World::generateTerrain() { // Fill the ground with grass. @@ -220,7 +221,7 @@ void World::generateTerrain() tiles_[x][y] = &grassTerrain_; } } - } + } // Lay a river. int x = random(WIDTH); @@ -230,14 +231,14 @@ void World::generateTerrain() } ``` 现在我们可以直接得到Terrain对象,而不用再通过World的方法来访问Terrain的数据了: -``` +```cpp const Terrain& World::getTile(int x, int y) const { return *tiles_[x][y]; } ``` 这样,*World* 类就不再跟*Terrain* 的实现细节耦合在一起了。如果你想得到一个面片的属性,你可以直接从*Terrain* 对象中得到: -``` +```cpp int cost = world.getTile(2, 3).getMovementCost(); ``` 我们终于通过优秀的API找回了使用真正对象的快感,并且这没有带来额外的消耗——一个指针***通常*** 不会比一个枚举带来更大的消耗。 @@ -248,6 +249,17 @@ int cost = world.getTile(2, 3).getMovementCost(); 我唯一能够确信的是使用*Flyweight*,不会造成你程序的失控,它通常会带来更易维护的特点。他在不带来额外开销的前提下,给了你一个使用面向对象的优点。如果你发现,你的代码里有大量的枚举或者switch语句,你就可以考虑用这种模式来代替。而如果你担心效率问题,那么你至少要在你将代码改的更难维护之前,做一下性能测试,看看使用*Flyweight* 造成的性能消耗是否真的如你所想的那么大。 +#更多请参阅 +- 在这个面片的例子中,我们只是为每一个地形类型创建了一个实例然后把它存储在**World**类中。这使得我们可以很容易找到并重用这些共享对象。话是这么说,不过一般来说你也不会在事先就把所有*flyweights*都创建出来。 + +如果你事先知道你要用到哪些东西,那就最好先创建它们。为了体现这样做的优势,在你实例化一个对象之前,先看看你是否已经实例化过一个这种类型的对象了。如果是这样的话,只要返回那个已经实例的对象就行。 + +这通常意味着你必须封装一些接口,可以先查找现有对象的结构。像这样隐藏构造函数是工厂方法模式的一个例子。 + +- 为了返回之前创建好的flyweight对象,你必须跟踪好池中的已经实例化好的对象。正如字面含义,“对象池”是存储它们的一个好地方。 + +- 当你使用**状态**模式的时候,通常的“状态”对象不是针对那一个状态机的。同一个状态对象足够使用了。这时,你就可以用亨元模式在不同状态机中使用同一个对象,一点问题都没有。 + [1]: http://7pn4yt.com1.z0.glb.clouddn.com/patternsarchitectureflyweight-trees.png [2]: http://7pn4yt.com1.z0.glb.clouddn.com/patternsarchitectureflyweight-tree-model.png