unity AvatarTarget unity3d怎么导入模型没有head

【游戏设计模式】之三 状态模式、有限状态机 & Unity版本实现 - 知乎专栏
{"debug":false,"apiRoot":"","paySDK":"/api/js","wechatConfigAPI":"/api/wechat/jssdkconfig","name":"production","instance":"column","tokens":{"X-XSRF-TOKEN":null,"X-UDID":null,"Authorization":"oauth c3cef7c66aa9e6a1e3160e20"}}
{"database":{"Post":{"":{"title":"【游戏设计模式】之三 状态模式、有限状态机 & Unity版本实现","author":"mao-xing-yun","content":"游戏开发过程中,各种游戏状态的切换无处不在。但很多时候,简单粗暴的if else加标志位的方式并不能很地道地解决状态复杂变换的问题,这时,就可以运用到状态模式以及状态机来高效地完成任务。状态模式与状态机,因为他们关联紧密,常常放在一起讨论和运用。而本文将对他们在游戏开发中的使用,进行一些探讨。PS:这篇文章起源于《Game Programming Patterns》第二章第六节。这是一篇略长的文章,约5200余字,将分析游戏开发过程中状态模式与有限状态机的运用,已经非常了解相关内容的高端选手请略读。文中使用C++承载讲解内容,文章末尾也提供了Unity&C#版本的代码实现。一、文章的短版本与思维导图还是国际惯例,先放出这篇文章的短版本——所涉及知识点的一张思维导图,再开始正文。大家若是疲于阅读文章正文,直接看这张图,也是可以Get到本文的主要知识点的大概。二、引例假如我们现在正在开发一款横版游戏。当前的任务是实现玩家用按键操纵女英雄。当按下向上方向键的时候,女英雄应该跳跃。那么我们可以这样实现:void Heroine::handleInput(Input input)\n{\n\tif (input == PRESS_UP)\n\t{\n\t\tyVelocity_ = JUMP_VELOCITY;\n\t\tsetGraphics(IMAGE_JUMP);\n\t}\n}\nOK,实现是实现了,但是一堆BUG。比如,我们没有防止主角“在空中跳跃“,当主角跳起来后持续按向上键,会导致她一直飘在空中。简单地修复方法可以是:添加一个 isJumping布尔值变量。当主角跳起来后,就把该变量设置为True.只有当该变量为False时,才让主角跳跃,代码如下:void Heroine::handleInput(Input input)\n{\n
if (input == PRESS_UP)\n
if (!isJumping_)\n
isJumping_ = true;\n
// Jump...\n
}\n}\n接下来,我们想实现主角的闪避动作。当主角站在地面上的时候,如果玩家按下向下方向键,则下蹲躲避,如果松开此键,则起立。代码如下:void Heroine::handleInput(Input input)\n{\n\tif (input == PRESS_UP)\n\t{\n\t\t// Jump if not jumping...\n\t}\n\telse if (input == PRESS_DOWN)\n\t{\n\t\tif (!isJumping_)\n\t\t{\n\t\t\tsetGraphics(IMAGE_DUCK);\n\t\t}\n\t}\n\telse if (input == RELEASE_DOWN)\n\t{\n\t\tsetGraphics(IMAGE_STAND);\n\t}\n}\n找找看, 这次bug又在哪里?使用这段代码,玩家可以:按向下键下蹲,按向上键则从下蹲状态跳起,英雄会在跳跃的半路上变成站立图片…….是时候增加另一个标识了……void Heroine::handleInput(Input input)\n{\n\tif (input == PRESS_UP)\n\t{\n\t\tif (!isJumping_ && !isDucking_)\n\t\t{\n\t\t\t// Jump...\n\t\t}\n\t}\n\telse if (input == PRESS_DOWN)\n\t{\n\t\tif (!isJumping_)\n\t\t{\n\t\t\tisDucking_ = true;\n\t\t\tsetGraphics(IMAGE_DUCK);\n\t\t}\n\t}\n\telse if (input == RELEASE_DOWN)\n\t{\n\t\tif (isDucking_)\n\t\t{\n\t\t\tisDucking_ = false;\n\t\t\tsetGraphics(IMAGE_STAND);\n\t\t}\n\t}\n}\n下面再加一点功能,如果玩家在跳跃途中按了下方向键,英雄能够做下斩攻击就太炫酷了。其代码实现如下:void Heroine::handleInput(Input input)\n{\n\tif (input == PRESS_UP)\n\t{\n\t\tif (!isJumping_ && !isDucking_)\n\t\t{\n\t\t\t// Jump...\n\t\t}\n\t}\n\telse if (input == PRESS_DOWN)\n\t{\n\t\tif (!isJumping_)\n\t\t{\n\t\t\tisDucking_ = true;\n\t\t\tsetGraphics(IMAGE_DUCK);\n\t\t}\n\t\telse\n\t\t{\n\t\t\tisJumping_ = false;\n\t\t\tsetGraphics(IMAGE_DIVE);\n\t\t}\n\t}\n\telse if (input == RELEASE_DOWN)\n\t{\n\t\tif (isDucking_)\n\t\t{\n\t\t\t// Stand...\n\t\t}\n\t}\n}\nBUG又出现了,这次发现了没?目前在下斩的时候,按跳跃键居然可以继续向上跳, OK,要解决它又是另一个字段……很明显,我们采用的这种if else加标志位的做法并不好用。每次我们添加一些功能的时候,都会不经意地破坏已有代码的功能。而且,我们还没有添加“行走”的状态,加了之后问题恐怕更多。这一幕是不是有些似曾相识?我想各位同学在踏入游戏开发领域的早期,多少会碰到过一些类似的情况,反正我是碰到过。其实,在这种情况下,状态机是可以帮上我们忙的。三、使用有限状态机让我们画一个流程图。目前的状态有,站立,跳跃,下蹲,下斩。得到的状态图示大致如下:OK,我们成功创建了一个有限状态机(Finite-state machine, FSM)。它来自计算机科学的分支自动理论,那里有很多著名的数据结构,包括著名的图灵机。状态机是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。有限状态机是其中最简单的成员。(本文限于篇幅,更多状态机暂不讨论,在文章末尾进阶阅读中,列举了分层状态机Hierarchical State Machines与下推自动机Push down Automata的参考资料,有需要的朋友们可以阅读)有限状态机FSM的要点是:拥有一组状态,并且可以在这组状态之间进行切换。在我们的例子中,是站立,跳跃,蹲下和跳斩。状态机同时只能在一个状态。英雄不可能同时处于跳跃和站立。事实上,防止这点是使用FSM的理由之一。一连串的输入或事件被发送给机器。在我们的例子中,就是按键按下和松开。每个状态都有一系列的转换,转换与输入和另一状态相关。当输入进来,如果它与当前状态的某个转换匹配,机器转为转换所指的状态。举个例子,在站立状态时,按下下键转换为俯卧状态。在跳跃时按下下键转换为跳斩。如果输入在当前状态没有定义转换,输入就被忽视。目前而言,游戏编程中状态机的实现方式,有两种可以选择:用枚举配合switch case语句。用多态与虚函数(也就是状态模式)。下面让我们用代码来实现。不妨先从简单的方式开始,用枚举与switch case语句实现。四、用枚举配合switch case实现状态机我们知道,上文中实现的女英雄类Heroine有一些布尔类型的成员变量:isJumping_和isDucking,但是这两个变量永远不可能同时为True。OK,这边可以提供一个小经验:当你有一系列的标记成员变量,而它们只能有且仅有一个为True时,定义成枚举(enum)其实更加适合。在这个例子当中,我们的FSM的每一个状态可以用一个枚举来表示,所以,让我们定义以下枚举:enum State\n{\n\tSTATE_STANDING,\n\tSTATE_JUMPING,\n\tSTATE_DUCKING,\n\tSTATE_DIVING\n};\n好了,无需一堆flags了, Heroine类只需一个state成员就可以胜任。在前面的代码中,我们先判断输入事件,然后才是状态。那种风格的代码可以让我们集中处理与按键相关的逻辑,但是,它也让每一种状态的处理代码变得很乱。我们想把它们放在一起来处理,因此,我们先对状态做分支switch处理。代码如下:void Heroine::handleInput(Input input)\n{\n\tswitch (state_)\n\t{\n\tcase STATE_STANDING:\n\t\tif (input == PRESS_B)\n\t\t{\n\t\t\tstate_ = STATE_JUMPING;\n\t\t\tyVelocity_ = JUMP_VELOCITY;\n\t\t\tsetGraphics(IMAGE_JUMP);\n\t\t}\n\t\telse if (input == PRESS_DOWN)\n\t\t{\n\t\t\tstate_ = STATE_DUCKING;\n\t\t\tsetGraphics(IMAGE_DUCK);\n\t\t}\n\t\\n\n\tcase STATE_JUMPING:\n\t\tif (input == PRESS_DOWN)\n\t\t{\n\t\t\tstate_ = STATE_DIVING;\n\t\t\tsetGraphics(IMAGE_DIVE);\n\t\t}\n\t\\n\n\tcase STATE_DUCKING:\n\t\tif (input == RELEASE_DOWN)\n\t\t{\n\t\t\tstate_ = STATE_STANDING;\n\t\t\tsetGraphics(IMAGE_STAND);\n\t\t}\n\t\\n\t}\n}\n现在的代码看起来比之前的代码更加地道。我们简化了状态的处理,将所有处理单个状态的代码都集中在了一起。这样做是实现状态机的最简单方式,而且在特定情况下,这就是最佳的解决方案。我们的问题可能也会超过此方案能解决的范围。比如,我们想在主角下蹲躲避的时候“蓄能”,然后等蓄满能量之后可以释放出一个特殊的技能。那么,当主角处理躲避状态的时候,我们需要添加一个变量来记录蓄能时间。我们可以添加一个chargeTime成员来记录主角蓄能的时间长短。假设,我们已经有一个update方法了,并且这个方法会在每一帧被调用。那么,我们可以使用其来记录蓄能的时间,就像这样:void Heroine::update( )\n{\n\tif (state_ == STATE_DUCKING)\n\t{\n\t\tchargeTime_++;\n\t\tif (chargeTime_ & MAX_CHARGE)\n\t\t{\n\t\t\tsuperBomb( );\n\t\t}\n\t}\n}\n我们需要在主角躲避的时候重置这个蓄能时间,所以,我们还需要修改handleInput方法:void Heroine::handleInput(Input input)\n{\n\tswitch (state_)\n\t{\n\tcase STATE_STANDING:\n\t\tif (input == PRESS_DOWN)\n\t\t{\n\t\t\tstate_ = STATE_DUCKING;\n\t\t\tchargeTime_ = 0;\n\t\t\tsetGraphics(IMAGE_DUCK);\n\t\t}\n\t\t// Handle other inputs...\n\t\\n\n\t\t// Other states...\n\t}\n}\n总之,为了添加蓄能攻击,我们不得不修改两个方法,并且添加一个 chargeTime成员给主角,尽管这个成员变量只有在主角处于躲避状态的时候才有效。我们其实真正想要的是把所有这些与状态相关的数据和代码封装起来。接下来,我们正式介绍四人帮设计模式中的状态模式来解决这个问题。五、用状态模式实现状态机5.1 状态模式概述对于沉浸于面向对象思维方式的同学来说,每一个条件分支都可以用动态调度来解决(也就是虚函数和多态来解决)。但是,如果你不分青红皂白每次都这样做,可能就会简单的问题复杂化。其实有时候,一个简单的if语句就足够了。四人帮对于状态模式是这么描述的:“Allow an object to alter its behavior whenits internal state changes. The object will appear to change its class.允许对象在当内部状态改变时改变其行为,就好像此对象改变了自己的类一样。”其实,状态模式主要解决的就是当控制一个对象状态转换的条件表达式过于复杂的情况,它把状态的判断逻辑转移到表示不同的一系列类当中,可以把复杂的逻辑判断简单化。状态模式的实现要点,主要有三点:为状态定义一个接口。为每个状态定义一个类。恰当地进行状态委托。下面将分别进行概述。5.2 步骤一、为状态定义一个接口首先,我们为状态定义一个接口。每一个与状态相关的行为都定义成虚函数。对于上文的例子而言,就是handleInput和update函数。class HeroineState\n{\npublic:\n\tvirtual ~HeroineState( ) {}\n\tvirtual void handleInput(Heroine& heroine, Input input) {}\n\tvirtual void update(Heroine& heroine) {}\n};\n5.3 步骤二、为每个状态定义一个类对于每一个状态,我们定义了一个类并继承至此状态接口。它覆盖的方法定义主角对应此状态的行为。换句话说,把之前的switch语句里面的每个case语句中的内容放置到它们对应的状态类里面去。比如:class DuckingState : public HeroineState\n{\npublic:\n\tDuckingState( )\n\t\t:chargeTime_(0)\n\t{ }\n\n\tvirtual void handleInput(Heroine& heroine, Input input) {\n\t\tif (input == RELEASE_DOWN)\n\t\t{\n\t\t\t// Change to standing state...\n\t\t\theroine.setGraphics(IMAGE_STAND);\n\t\t}\n\t}\n\n\tvirtual void update(Heroine& heroine) {\n\t\tchargeTime_++;\n\t\tif (chargeTime_ & MAX_CHARGE)\n\t\t{\n\t\t\theroine.superBomb( );\n\t\t}\n\t}\n\nprivate:\n\tint chargeTime_;\n};\n注意我们也将chargeTime_移出了Heroine,放到了DuckingState(下蹲状态)类中。 因为这部分数据只在这个状态有用,这样符合设计的原则。5.4 步骤三、恰当地进行状态委托接下来,我们在主角类Heroine中定义一个指针变量,让它指向当前的状态。放弃之前巨大的switch,然后让它去调用状态接口的虚函数,最终这些虚方法就会动态地调用具体子状态的相应函数了:class Heroine\n{\npublic:\n\tvirtual void handleInput(Input input)\n\t{\n\t\tstate_-&handleInput(*this, input);\n\t}\n\n\tvirtual void update( )\n\t{\n\t\tstate_-&update(*this);\n\t}\n\n\t// Other methods...\nprivate:\n\tHeroineState* state_;\n};\n而为了“改变状态”,我们只需要将state_声明指向不同的HeroineState对象。至此,经过为状态定义一个接口,为每个状态定义一个类以及进行状态委托,经历这三步,就是的状态模式的实现思路了。六、状态对象的存放位置探讨这里忽略了一些细节。为了修改一个状态,我们需要给state指针赋值为一个新的状态,但是这个新的状态对象要从哪里来呢?我们的之前的枚举方法是一些数字定义。但是,现在我们的状态是类,我们需要获取这些类的实例。通常来说,有两种实现存放的思路:静态状态。初始化时把所有可能的状态都new好,状态切换时通过赋值改变当前的状态。实例化状态。每次切换状态时动态new出新的状态。下面分别进行介绍。6.1 方法一:静态状态如果一个状态对象没有任何数据成员,那么它的惟一数据成员便是虚表指针了。那样的话,我们就没有必要创建此状态的多个实例了,因为它们的每一个实例都是相等的。如果你的状态类没有任何数据成员,并且它只有一个函数方法在里面。那么我们还可以进一步简化此模式。我们可以通过一个状态函数来替换状态类。这样的话,我们的state变量只需要变成一个状态函数指针就可以了。在此情况下,我们可以定义一个静态实例。即使你有一系列的FSM在同时运转,所有的状态机都同时指向这一个惟一的实例。在哪里放置静态实例取决于你的喜好。如果没有任何特殊原因的话,我们可以把它放置到基类状态类中:class HeroineState\n{\npublic:\n\tstatic StandingState standing;//站立状态\n\tstatic DuckingState ducking;//下蹲状态\n\tstatic JumpingState jumping;//跳跃状态\n\tstatic DivingState diving;//下斩状态\n\n\t// Other code...\n};\n每一个静态成员变量都是对应状态类的一个实例。如果我们想让主角跳跃,那么站立状态的可以这样实现:if (input == PRESS_UP)\n{\n\theroine.state_ = &HeroineState::jumping;\n\theroine.setGraphics(IMAGE_JUMP);\n}\n6.2 方式二:实例化状态刚刚讲到的基于静态状态的方式有一定的局限性。比如说,一个静态状态就不能胜任上面例子中我们提到的躲避状态的实现。因为躲避状态有一个 chargeTime成员变量,而chargeTime成员变量是专属于每个主角类的躲避状态的。也就是说,如果我们的游戏里面只有一个主角,那么定义一个静态类没啥问题。但是,如果我们想加入多个玩家,那么此方法就行不通了。所以,当有多个玩家时,我们不得不在状态切换的时候动态地创建一个躲避状态实例。这样,我们的FSM就拥有了它自己的实例。当然,如果我们又动态分配了一个新的状态实例,我们需要负责清理老的状态实例。我们这里必须要相当小心,因为当前状态修改的函数是处在当前状态里面,我们需要小心地处理删除的顺序。另外,我们也会在handleInput方法里面可选地返回一个新的状态。当这个状态返回的时候,主角将会删除老的状态并切换到这个新的状态,如下所示:void Heroine::handleInput(Input input)\n{\n\tHeroineState* state = state_-&handleInput(*this, input);\n\tif (state != NULL)\n\t{\n\t\tdelete state_;\n\t\tstate_ =\n\t}\n}\n这样,我们直到从之前的状态返回,才需要删除它。 现在,站立状态可以通过创建一个新实例转换为俯卧状态:HeroineState* StandingState::handleInput(Heroine& heroine,\n\tInput input)\n{\n\tif (input == PRESS_DOWN)\n\t{\n\t\t// Other code...\n\t\treturn new DuckingState( );\n\t}\n\n\t//Stay in this state.\n\treturn NULL;\n}\n如果可以,推荐使用静态状态,因为它们不会在状态转换时消耗太多的内存和CPU。但是,对于更多的状态,实例化状态的方式将是不错的选择。七、实践:Unity中C#版本状态模式实现除了文章中讲解用到的C++版本的代码,我也使用C#在Unity中实现了几个版本的状态模式,如下图,包含一个Stucture(状态模式的框架)和四个Example(四种不同使用状态模式的实例)。上文引例中的示例,也就是女英雄在站立,跳跃,俯卧,下斩几个状态之间切换的问题,在example4中进行了Unity版本的实现。链接:运行场景,按键盘上的上下方向键,可以在console窗口中看到对应的状态,有点游戏发展史早期文字冒险游戏的感觉。运行截图:由于篇幅原因,代码就不贴在这边了。具体可以到我的Github的repo \"Unity-Design-Pattern”中看到。这个repo目前已经在Unity中实现了《设计模式:可复用面向对象软件的基础》一书中提出的23种设计模式。且每种模式都包含对应的结构实现、应用示例以及图示介绍。有感兴趣的朋友可以关注一下。链接:八、本文知识点总结本文涉及知识点总结如下:在游戏开发过程中,涉及到复杂的状态切换时,可以运用状态模式以及状态机来高效地完成任务。有限状态机的实现方式,有两种可以选择:用枚举配合switch case语句。用多态与虚函数(即状态模式)。状态模式的经典定义:允许对象在当内部状态改变时改变其行为,就好像此对象改变了自己的类一样。对状态模式的理解:状态模式用来解决当控制一个对象状态转换的条件表达式过于复杂的情况,它把状态的判断逻辑转移到表示不同的一系列类当中,可以把复杂的逻辑判断简单化。状态模式的实现分为三个要点:为状态定义一个接口。为每个状态定义一个类。恰当地进行状态委托。通常来说,状态模式中状态对象的存放有两种实现存放的思路:静态状态。初始化时把所有可能的状态都new好,状态切换时通过赋值改变当前的状态。实例化状态。每次切换状态时动态new出新的状态。九、参考文献与进阶阅读[1] [2] Gamma E. Design patterns: elements of reusableobject-oriented software[M]. Pearson Education India, 1995.[3] [4] UML state machine :[5] Hierarchical State Machines分层状态机:[6] Pushdown Automata下推自动机:","updated":"T12:14:32.000Z","canComment":false,"commentPermission":"anyone","commentCount":25,"collapsedCount":0,"likeCount":148,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","titleImage":"/v2-bab925e4df_r.jpg","links":{"comments":"/api/posts//comments"},"reviewers":[],"topics":[{"url":"/topic/","id":"","name":"设计模式"},{"url":"/topic/","id":"","name":"Unity(游戏引擎)"},{"url":"/topic/","id":"","name":"游戏开发"}],"adminClosedComment":false,"titleImageSize":{"width":1723,"height":1080},"href":"/api/posts/","excerptTitle":"","column":{"slug":"game-programming","name":"浅墨的游戏编程"},"tipjarState":"inactivated","annotationAction":[],"sourceUrl":"","pageCommentsCount":25,"hasPublishingDraft":false,"snapshotUrl":"","publishedTime":"T20:14:32+08:00","url":"/p/","lastestLikers":[{"bio":null,"isFollowing":false,"hash":"538cd452ccf9a227dc911da","uid":749200,"isOrg":false,"slug":"jsallen","isFollowed":false,"description":"/yaokailun","name":"Allen","profileUrl":"/people/jsallen","avatar":{"id":"v2-a19be33dbdd79cb689bb3a","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":null,"isFollowing":false,"hash":"fc68a9fab3049d4fee777f2","uid":24,"isOrg":false,"slug":"li-shi-xun","isFollowed":false,"description":"","name":"李世勋","profileUrl":"/people/li-shi-xun","avatar":{"id":"d2bfcae35","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":"DWG","isFollowing":false,"hash":"fdf0ab8cda6fee55da8d11","uid":56,"isOrg":false,"slug":"cheng-hui-73","isFollowed":false,"description":"","name":"程辉","profileUrl":"/people/cheng-hui-73","avatar":{"id":"da8e974dc","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":"u3d打杂工","isFollowing":false,"hash":"e8d51c32f0b90cd3c42cdf35e312dc61","uid":963000,"isOrg":false,"slug":"wen-cheng-cheng-15","isFollowed":false,"description":"没有主角光环","name":"柠檬可乐","profileUrl":"/people/wen-cheng-cheng-15","avatar":{"id":"v2-24a8b9af6bd76dbd87d5eee","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":null,"isFollowing":false,"hash":"9d30f4f182d8dda3e44dd0","uid":28,"isOrg":false,"slug":"li-lu-48-12","isFollowed":false,"description":"","name":"嚟璐","profileUrl":"/people/li-lu-48-12","avatar":{"id":"ab2d1fa4c6b6c79d6682ddaa8e3f0610","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false}],"summary":"游戏开发过程中,各种游戏状态的切换无处不在。但很多时候,简单粗暴的if else加标志位的方式并不能很地道地解决状态复杂变换的问题,这时,就可以运用到状态模式以及状态机来高效地完成任务。状态模式与状态机,因为他们关联紧密,常常放在一起讨论和运用…","reviewingCommentsCount":0,"meta":{"previous":{"isTitleImageFullScreen":true,"rating":"none","titleImage":"/v2-5fd1e247ad9_r.jpg","links":{"comments":"/api/posts//comments"},"topics":[{"url":"/topic/","id":"","name":"设计模式"},{"url":"/topic/","id":"","name":"游戏开发"},{"url":"/topic/","id":"","name":"编程"}],"adminClosedComment":false,"href":"/api/posts/","excerptTitle":"","author":{"bio":"网名「浅墨」,微软MVP,《Windows游戏编程之从零开始》作者","isFollowing":false,"hash":"b34bb40fe18653f1baeb09e","uid":76,"isOrg":false,"slug":"mao-xing-yun","isFollowed":false,"description":"网名\"浅墨\",90后,微软VC++领域最具价值专家,2013年度中国十大杰出IT博客作者。写博客:【C++游戏编程】(http://blog.csdn.net/poem_qianmo),写过两本书:《Windows游戏编程之从零开始》、《OpenCV3编程入门》","name":"毛星云","profileUrl":"/people/mao-xing-yun","avatar":{"id":"cac34dbc8","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},"column":{"slug":"game-programming","name":"浅墨的游戏编程"},"content":"这篇文章起源于《Game Programming Patterns》第二章第一节,将与大家一起探索游戏开发中命令模式的用法。命令模式的成名应用是实现诸如撤消,重做,回放,时间倒流之类的功能。如果你想知道《Dota2》中的观战系统、《魔兽争霸3》中的录像系统、《守望先锋》的全场最佳回放系统可能的一些实现思路,这篇文章或许也能给你一些启示。一、本文涉及知识点思维导图 还是国际惯例,先放出这篇文章所涉及内容知识点的一张思维导图,就开始正文。大家若是疲于阅读文章正文,直接看这张图,也是可以Get到本文的主要知识点的大概(推荐放大后查看,其实,单看总结的这些概念还是太抽象,关键还是在于文中三、四、五节中的代码与图示)。二、命令模式的定义在许多大型游戏中,都可以见到命令模式(Command Pattern)的身影。设计模式界的扛鼎之作《Design Patterns: Elements ofReusable Object-Oriented Softwar》(中译版《设计模式:可复用面向对象软件的基础》) 一书的作者四人帮Gang of Four对命令模式这样概括:“命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象,同时支持可撤消的操作。Encapsulate a request as an object, therebyletting you parameterizeclients with different requests, queue or log requests,and \tsupport undoable operations.” 这句话解读版本应该是这样:将一组行为抽象为对象,这个对象和其他对象一样可以被存储和传递,从而实现行为请求者与行为实现者之间的松耦合,这就是命令模式。接着看看Gang of Four随后提出的另一个阐述: “命令模式是回调机制的面向对象版本。Commands are an object-oriented replacementfor callbacks.”这句话从另一个方面道出了命令模式的思想,它是回调的面向对象版本。OK,定义都给出了,不妨我们举一些栗子,在实际例子中看看命令模式到底能带给我们哪些惊喜。三、引例每个游戏都有一些代码块用来读取用户的输入操作,按钮点击,键盘事件,鼠标点击,或者其他输入。这些代码记录每次的输入,并将之转换为游戏中一个有意义的动作(action),如下图:一种最简单粗暴的实现大概是这样:void InputHandler::handleInput()\n{\n
\tif(isPressed(BUTTON_X)) jump();\n \telse if (isPressed(BUTTON_Y)) fireGun();\n \telse if (isPressed(BUTTON_A)) swapWeapon();\n \telse if (isPressed(BUTTON_B)) lurchIneffectively();\n}\n我们知道,这个函数通常会通过游戏循环被每帧调用。这段代码在我们想将用户的输入和程序行为硬编码在一起时,是完全可以胜任自身的工作的。但如果想实现用户自定义配置他们的按钮与动作的映射,就需要进行修改了。为了支持自定义配置,我们需要把那些对 jump() 和 fireGun() 的直接调用转换为我们可以换出(swap out)的东西。”换出“(swapping out)听起来很像分配变量,所以我们需要个对象来代表一个游戏动作。这就用到了命令模式。于是,我们定义了一个基类用来代表一个可激活的游戏命令:class Command\n{\npublic:\n\tvirtual ~Command() {}\n\tvirtual void execute() = 0;\n};\n然后我们为每个不同的游戏动作创建一个子类,public继承自我们的Command类:class JumpCommand : public Command\n{\npublic:\n\tvirtual void execute() { jump(); }\n};\n\nclass FireCommand : public Command\n{\npublic:\n \tvirtual void execute() { fireGun(); }\n};\n在负责输入处理的InputHandler中,我们为为每个键存储一个指向Command的指针。class InputHandler\n{\npublic:\n \tvoid handleInput();\n \n
//Methods to bind commands...\n \nprivate:\n \tCommand* buttonX_;\n \tCommand* buttonY_;\n \tCommand* buttonA_;\n \tCommand* buttonB_;\n}; \n那么现在,InputHandler就可以该写成这样:void InputHandler::handleInput()\n{\n
\tif(isPressed(BUTTON_X)) buttonX_-&execute();\n \telse if (isPressed(BUTTON_Y)) buttonY_-&execute();\n \telse if (isPressed(BUTTON_A)) buttonA_-&execute();\n \telse if (isPressed(BUTTON_B)) buttonB_-&execute();\n}\n不难理解,以前每个输入都会直接调用一个函数,现在则会有一个间接调用层。那么图示看起来就是这样:这就是命令模式的最基础的实现,按照其思路画了一个大概的型出来。简而言之,命令模式的关键在于引入了抽象命令接口(execute( )方法),且发送者针对抽象命令接口编程,只有实现了抽象命令接口的具体命令才能与接收者相关联。而且命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。四、进一步地使用命令模式我们刚才定义的命令类在上个例子中可以跑得起来,但很受限。问题在于,他们假设存在jump() , fireGun() 等这样的函数能与玩家关联并控制玩家。这种假设耦合限制了这些命令的的效用。JumpCommand类唯一能做的事情就是控制玩家的跳跃。让我们放宽限制,传进去一个我们想要控制的对象进去,而不是用命令对象自身来调用函数:class Command\n{\npublic:\n \tvirtual ~Command() {}\n \tvirtual void execute(GameActor& actor) = 0;\n};\n这里GameActor是代表游戏世界中角色的“游戏对象”类。 我们将其传给execute(),这样可以在它的子类中添加函数,来与我们选择的角色关联,就像这样:class JumpCommand : public Command\n{\npublic:\n \tvirtual void execute(GameActor& actor)\n
\t\tactor.jump();\n
\t}\n};\n现在,我们可以使用这个类控制游戏中的任何角色。 还少了一块在输入控制和在正确的对象上起作用之间的代码。 首先,我们修改handleInput()这样它可以返回命令:Command* InputHandler::handleInput()\n{\n
\tif(isPressed(BUTTON_X)) return buttonX_;\n
\tif(isPressed(BUTTON_Y)) return buttonY_;\n
\tif(isPressed(BUTTON_A)) return buttonA_;\n
\tif(isPressed(BUTTON_B)) return buttonB_;\n \n
\t//Nothing pressed, so do nothing.\n \treturn NULL;\n}\n这段代码不能直接执行命令,因为它并不知道该传入那个角色对象。命令是一个对象化的调用,是回调的面向对象版本,这里正是我们可以利用的地方——我们可以延迟调用。我们需要一些代码来保存命令并且执行对玩家角色的调用。像下面这样:Command* command =inputHandler.handleInput();\nif (command)\n{\n\tcommand-&execute(actor);\n}\n假设 actor 是玩家角色的一个引用,这将会基于用户的输入来驱动角色,所以我们可以赋予角色与前例一致的行为。在命令和角色之间加入的间接层使得我们可以让玩家控制游戏中的任何角色,只需通过改变命令执行时传入的角色对象即可。目前我们只考虑了玩家驱动角色(player-driven character),但是对于游戏世界中的其他角色呢?他们由游戏的AI来驱动。我们可以使用相同的命令模式来作为AI引擎和角色的接口;AI代码部分提供命令(Command)对象用来执行,代码也就是:command-&execute(AI对象);
\nAI选择命令,角色执行命令,它们之间的解耦给了我们很大的灵活性。我们可以为不同的角色使用不同的AI模块,或者可以为不同种类的行为混合AI。你想要一个更加具有侵略性的敌人?只需要插入一段更具侵略性的AI代码来为它生成命令即可。事实上,我们甚至可以将AI使用到玩家的角色身上,这对于像游戏需要自动运行的demo模式是很有用的。通过将控制角色的命令作为对象,我们便去掉了直接调用指定函数这样的紧耦合。我们不妨将这样的方式理解成一个队列或者一个命令流(queue or stream of commands):如图,一些代码(输入控制器或者AI)产生一系列指令然后将其放入流中。 另一些指令(调度器或者角色自身)消耗指令并调用他们。这样,通过在中间加入了一个队列,我们解耦了行为请求者和行为实现者。而且,如果我们把这些命令序列化,便可以通过互联网来发送数据流。可以把玩家的输入通过网络发送到另外一台机器上,然后进行回放,这就是多人网络游戏里面非常重要的一块。五、实现撤消与重做功能撤消和重做是命令模的成名应用了。如果一个命令对象可以做(do) 一些事情,那么应该可以很轻松的撤消(undo)它们。撤销这个行为经常在一些策略游戏中见到,在游戏中如果你不喜欢的话可以回滚一些步骤。在创建游戏时这是必不可少的的工具之一。如果你想让游戏策划同事们喷你,最可靠的办法就是在关卡编辑器中不提供撤消功能,让他们不能撤消不小心犯的错误,我保证他们会打你。利用命令模式,撤消和重做功能实现起来非常容易。假设我们在制作单人回合制游戏,想让玩家能撤消移动,这样他们就可以集中注意力在策略上而不是猜测上,而之前我们已经使用了命令来抽象输入控制,所以每个玩家的举动都已经被封装其中。举个例子,移动一个单位的代码可能如下:class MoveUnitCommand : public Command\n{\npublic:\n
MoveUnitCommand(Unit* unit, int x, int y)\n
: unit_(unit),\n
virtual void execute()\n
unit_-&moveTo(x_, y_);\n
}\n\nprivate:\n
Unit* unit_;\n
int x_, y_;\n};\n注意这和前面的命令模式有些许不同。 在前面的例子中,我们需要从修改的角色那里抽象命令。而在这个例子中,我们将命令绑定到要移动的单位上。这条命令的实例不是通用的“移动某物”指令,而是游戏回合中特殊的一次移动。这边就可以展示出命令模式的几种形态。 在某些情况下,指令是可重用的对象,代表了可执行的事件。我们在文章开头展示的输入控制将其实现为一个命令对象,然后在按键按下时调用其execute()方法。而这里的命令代表了特定时间点能做的特定事件。这意味着输入控制代码可以在玩家下决定时创造一个实例。就像这样:Command* handleInput()\n{\n
Unit* unit = getSelectedUnit();\n\n
if (isPressed(BUTTON_UP)) {\n
// Move the unit up one.\n
int destY = unit-&y() - 1;\n
return new MoveUnitCommand(unit, unit-&x(), destY);\n
if (isPressed(BUTTON_DOWN)) {\n
// Move the unit down one.\n
int destY = unit-&y() + 1;\n
return new MoveUnitCommand(unit, unit-&x(), destY);\n
// Other moves...\n\n
return NULL;\n}\n而为了撤消命令,我们定义了一个undo的操作,每个命令类都需要来实现它:class Command\n{\npublic:\n
virtual ~Command() {}\n
virtual void execute() = 0;\n
virtual void undo() = 0;\n};\n当然,在像C++这样没有垃圾回收的语言,这意味着执行命令的代码也要负责释放内存。undo()方法用于回滚execute()方法造成的游戏状态改变。下面我们针对上一个移动命令加入撤消支持:class MoveUnitCommand : public Command\n{\npublic:\n
MoveUnitCommand(Unit* unit, int x, int y)\n
: unit_(unit),\n
xBefore_(0),\n
yBefore_(0),\n
virtual void execute()\n
// Remember the unit's position before the move\n
// so we can restore it.\n
xBefore_ = unit_-&x();\n
yBefore_ = unit_-&y();\n\n
unit_-&moveTo(x_, y_);\n
virtual void undo()\n
unit_-&moveTo(xBefore_, yBefore_);\n
}\n\nprivate:\n
Unit* unit_;\n
int xBefore_, yBefore_;\n
int x_, y_;\n};\n需要注意的是,我们为类添加了更多状态。 当单位移动时,它忘记了它之前是什么样的。 如果我们想要撤销这个移动,我们需要记得单位之前的状态,也就是xBefore_和yBefore_做的事。其实,这样的实现看起来挺像备忘录模式()的,但是你会发现备忘录模式用在这里并不能愉快地工作。因为命令试图去修改一个对象状态的一小部分,而为对象的其他数据创建快照是浪费内存。只手动存储被修改的部分相对来说就节省很多内存了。持久化数据结构是另一个选择。通过它们,每次对一个对象进行修改都会返回一个新的对象,保留原对象不变。通过这样的实现,新对象会与原对象共享数据,所以比拷贝整个对象的代价要小得多。使用持久化数据结构,每个命令存储着命令执行前对象的一个引用,所以撤销意味着切换到原来老的对象。为了让玩家能够撤销一次移动,我们保留了他们执行的上一个命令。当他们敲击Control+Z 时,我们便会调用 undo() 方法。(如果已经撤消了,那么会变为”重做“,我们会再次执行那个命令。)支持多次撤消也很容易实现。也就是我们不再保存最后一个命令,取而代之保存了一个命令列表和”current“(当前)命令的一个引用。当玩家执行了某个命令时,我们将此命令添加到列表中,并将”current“指向它即可。思路如下图:当玩家选择”撤消“时,我们撤消掉当前的命令并且将当前的指针移回去。当他们选择”重做“,我们将指针前移然后执行命令。如果他们在撤消之后选择了一个新的命令,就把列表中位于当前命令之后的所有命令进行舍弃。若你是第一次在游戏关卡编辑器中用命令模式实现撤消重做的功能,或许你会惊叹它是如此的简单、高效而且优雅。六、录像与回放系统的实现思路上文刚刚讲到了如何用命令模式实现撤消与重做。重做在游戏中并不常见,但回放(replay)、录像、观战系统却很常见。一个简单粗暴的实现方法就是记录每一帧的游戏状态以便能够回放,但是这样会占用大量的内存。所以,许多游戏会记录每一帧每个实体所执行的一系列命令,就可以轻松的实现回放功能。而为了回放游戏,引擎只需要运行正常游戏的模拟,执行预先录制的命令即可。那我们便可以这样理解,录像与回放等功能,可以基于命令模式实现,也就是执行并解析一系列经过预录制的序列化后的各玩家操作的有序命令集合。以下只是提供一些分析的思路,并不代表这三款游戏当时就是这样实现的:《魔兽争霸3》中的replay录像,大概就是通过将所有玩家的操作命令,序列化到一个.rep后缀的文件中,然后在游戏中进行解析后回放来实现。《Dota2》中的录像功能也大致如此,而观战功能也就是通过在线不断获取该局比赛中各个玩家经过序列化后的有序命令流,然后在自己的客户端中解析并重放。《守望先锋》的回放系统,大概也就是将各个玩家的一系列操作命令通过网络发送到其他玩家的机器上(其实对战过程中就已经在实时发送),然后进行解析后进行模拟回放。这大致就是各种游戏中录像、回放、观战系统所用的一些设计思路。七、命令模式的要点总结OK,例子讲完了,下面对命令模式进行一些要点的总结。7.1 命令模式的要点总结首先,给出命令模式的UML图:然后,让我们再次看看文章开头给出的GOF对于命令模式的定义:\"命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象,同时支持可撤消的操作。\"接着是对命令模式的一些解读与思考:将一组行为抽象为对象,这个对象和其他对象一样可以被存储和传递,从而实现行为请求者与行为实现者之间的松耦合,这就是命令模式。命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。命令模式是回调机制的面向对象版本。每一个命令都是一个操作:请求的一方发出请求,要求执行一个操作;接收的一方收到请求,并执行操作。命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行,以及是怎么被执行的。命令模式的关键在于引入了抽象命令接口,且发送者针对抽象命令接口编程,只有实现了抽象命令接口的具体命令才能与接收者相关联。命令模式很适合实现诸如撤消,重做,回放,回退一步,时间倒流之类的功能。命令模式有不少的细分种类,实际使用时应根据当前所需来找到合适的设计方式。7.2 命令模式的优点1.对类间解耦。调用者角色与接受者角色之间没有任何依赖关系,调用者实现功能时只需调用Command抽象类的execute方法即可,不需要了解到底是哪个接收者在执行。2.可扩展性强。Command的子类可以非常容易地扩展,而调用者Invoker和高层次的模块Client之间不会产生严重的代码耦合。3.易于命令的组合维护。可以比较容易地设计一个组合命令,维护所有命令的集合,并允许调用同一方法实现不同的功能。4.易于与其他模式结合。命令模式可以结合责任链模式,实现命令族的解析;而命令模式结合模板方法模式,则可以有效减少Command子类的膨胀问题。7.3 命令模式的缺点会导致类的膨胀。使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个命令都需要设计一个具体命令类,这将导致类的膨胀。上文讲解优点时已经提到了应对之策,我们可以将命令模式结合模板方法模式,来有效减少Command子类的膨胀问题。也可以定义一个具体基类,包括一些能定义自己行为的高层方法,将命令的主体execute()转到子类沙箱中,往往会有一些帮助。八、本文涉及知识点提炼整理本文涉及知识点提炼整理如下:GOF对命令模式的定义是,命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象,同时支持可撤消的操作。命令模式是回调机制的面向对象版本。将一组行为抽象为对象,这个对象和其他对象一样可以被存储和传递,从而实现行为请求者与行为实现者之间的松耦合,这就是命令模式。命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。命令模式很适合实现诸如撤消,重做,回放,时间倒流之类的功能。而基于命令模式实现录像与回放等功能,也就是执行并解析一系列经过预录制的序列化后的各玩家操作的有序命令集合。而录像与回放等功能,就是在执行并解析一系列经过预录制的序列化后的各玩家操作的有序命令集合。命令模式的优点有:对类间解耦、可扩展性强、易于命令的组合维护、易于与其他模式结合,而缺点是会导致类的膨胀。命令模式有不少的细分种类,实际使用时应根据当前所需来找到合适的设计方式。当然,单看以上的这些概念也许太过于抽象,关键还是在于理解文中三、四、五节的代码与图示。九、参考文献[1] [2] Gamma E. Design patterns: elements of reusable object-oriented software[M]. Pearson Education India, 1995.[3] Freeman E, Robson E, Bates B, et al. Head first design patterns[M]. \" O'Reilly Media, Inc.\", 2004.[4] [5] 本文就此结束,系列文章未完待续。With Best Wishes.","state":"published","sourceUrl":"","pageCommentsCount":0,"canComment":false,"snapshotUrl":"","slug":,"publishedTime":"T20:49:51+08:00","url":"/p/","title":"游戏设计模式(二) 论撤消重做、回放系统的优雅实现:命令模式","summary":"这篇文章起源于《Game Programming Patterns》第二章第一节,将与大家一起探索游戏开发中命令模式的用法。 命令模式的成名应用是实现诸如撤消,重做,回放,时间倒流之类的功能。如果你想知道《Dota2》中的观战系统、《魔兽争霸3》中的录像系统、《守望先锋…","reviewingCommentsCount":0,"meta":{"previous":null,"next":null},"commentPermission":"anyone","commentsCount":14,"likesCount":122},"next":{"isTitleImageFullScreen":false,"rating":"none","titleImage":"/v2-af263c03f4_r.png","links":{"comments":"/api/posts//comments"},"topics":[{"url":"/topic/","id":"","name":"设计模式"},{"url":"/topic/","id":"","name":"游戏开发"},{"url":"/topic/","id":"","name":"Unity(游戏引擎)"}],"adminClosedComment":false,"href":"/api/posts/","excerptTitle":"","author":{"bio":"网名「浅墨」,微软MVP,《Windows游戏编程之从零开始》作者","isFollowing":false,"hash":"b34bb40fe18653f1baeb09e","uid":76,"isOrg":false,"slug":"mao-xing-yun","isFollowed":false,"description":"网名\"浅墨\",90后,微软VC++领域最具价值专家,2013年度中国十大杰出IT博客作者。写博客:【C++游戏编程】(http://blog.csdn.net/poem_qianmo),写过两本书:《Windows游戏编程之从零开始》、《OpenCV3编程入门》","name":"毛星云","profileUrl":"/people/mao-xing-yun","avatar":{"id":"cac34dbc8","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},"column":{"slug":"game-programming","name":"浅墨的游戏编程"},"content":"这是一篇超过万字的读书笔记,总结提炼了《Game Programming Patterns》(中译版《游戏编程模式》)一书中所有章节与内容的知识梗概。我们知道,游戏行业其实一直很缺一本系统介绍游戏编程进阶技巧的书籍,而《游戏编程模式》的出现,正好弥补了这一点。之前已经有提到过,不同于传统的出版方式,这本书是网络出版,然后Web版完全免费,其更是在Amazon上具有罕见的五星评价,可见读者对其的好评程度之高。加之书中内容生动有趣,将各种经验之谈娓娓道来,实在是业界良心。书本主页:Web版全文阅读:Web版全文中文翻译:在这篇文章之前,我已经写了三篇相关的文章,但感觉一次一种模式的介绍,节奏太慢,就用这篇总结式的文章来把19种设计模式一次介绍完。文章的短版本:全书内容思维导图以下是《游戏编程模式》一书的内容梗概,全书内容19种模式的思维导图:目录与阅读指南本文按照《游戏编程模式》书中顺序,一、常用GOF设计模式1.命令模式2.享元模式3.观察者模式4.原型模式5.单例模式6.状态模式二、序列型模式7.双缓冲模式8.游戏循环9.更新方法三、行为型模式10.字节码11.子类沙箱12.类型对象四、解耦型模式13.组件模式14.事件队列15.服务定位器五、优化型模式16.数据局部性17.脏标识模式18.对象池模式19.空间分区对全书的19种模式分以下三个方面进行了介绍:要点使用场合引申与参考依次介绍完19种模式之后,最终给出了一些更多的参考与学习资源。需要注意,设计模式本身在理解上就比较抽象。而因为本文是设计模式内容的总结式介绍,所以理解坡度自然会比较陡。若总结的部分不太理解的地方,建议大家去阅读原文(在每种模式的“引申与参考”一部分都已经给出了链接),将这篇文章对原文的总结与原文结合起来理解,这样掌握起来会比较顺畅。一、常用GOF设计模式这一部分介绍了游戏开发中较为常用的六种GOF设计模式:命令模式享元模式观察者模式原型模式单例模式状态模式1.
命令模式 Command Pattern命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象,同时支持可撤消的操作。要点将一组行为抽象为对象,这个对象和其他对象一样可以被存储和传递,从而实现行为请求者与行为实现者之间的松耦合,这就是命令模式。命令模式是回调机制的面向对象版本。命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。命令模式的优点有:对类间解耦、可扩展性强、易于命令的组合维护、易于与其他模式结合,而缺点是会导致类的膨胀。命令模式有不少的细分种类,实际使用时应根据当前所需来找到合适的设计方式。使用场合命令模式很适合实现诸如撤消,重做,回放,时间倒流之类的功能。基于命令模式实现录像与回放等功能,也就是执行并解析一系列经过预录制的序列化后的各玩家操作的有序命令集合。引申与参考最终我们可能会得到很多不同的命令类。为了更容易实现这些类,定义一个具体的基类,包含一些能定义行为的高层方法,往往会有帮助。可以将命令的主体execute()转到子类沙箱中。对象可以响应命令,或者将命令交给它的从属对象。如果我们这样实现了,就完成了一个职责链模式。对于等价的实例,可以用享元模式提高内存利用率。命令模式的Unity版本实现:本节内容相关的英文原文:本节内容相关的中文翻译:2. 享元模式 Flyweight Pattern享元模式,以共享的方式高效地支持大量的细粒度的对象。通过复用内存中已存在的对象,降低系统创建对象实例的性能消耗。要点享元模式中有两种状态。内蕴状态(Internal State)和外蕴状态(External State)。内蕴状态,是不会随环境改变而改变的,是存储在享元对象内部的状态信息,因此内蕴状态是可以共享的。对任何一个享元对象而言,内蕴状态的值是完全相同的。外蕴状态,是会随着环境的改变而改变的。因此是不可共享的状态,对于不同的享元对象而言,它的值可能是不同的。享元模式通过共享内蕴状态,区分外蕴状态,有效隔离系统中的变化部分和不变部分。使用场合在以下情况都成立时,适合使用享元模式:当系统中某个对象类型的实例较多的时候。由于使用了大量的对象,造成了很大的存储开销。对象的大多数状态都可变为外蕴状态。 在系统设计中,对象实例进行分类后,发现真正有区别的分类很少的时候。引申与参考为了返回一个已经创建的享元,需要和那些已经实例化的对象建立联系,我们可以配合对象池来进行操作。当使用状态模式时,很多时候可以配合使用享元模式,在不同的状态机上使用相同的对象实例。享元模式的Unity版本实现:本节内容相关的英文原文:本节内容相关的中文翻译:3. 观察者模式 Observer Pattern观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。要点观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。我们知道,将一个系统分割成一个一些类相互协作的类有一个不好的副作用,那就是需要维护相关对象间的一致性。我们不希望为了维持一致性而使各类紧密耦合,这样会给维护、扩展和重用都带来不便。观察者就是解决这类的耦合关系的。目前广泛使用的MVC模式,究其根本,是基于观察者模式的。观察者模式应用广泛,Java甚至将其放到了核心库之中(java.util.Observer),而C#直接将其嵌入了语法(event关键字)中。使用场合当一个抽象模式有两个方面,其中一个方面依赖于另一个方面,需要将这两个方面分别封装到独立的对象中,彼此独立地改变和复用的时候。当一个系统中一个对象的改变需要同时改变其他对象内容,但是又不知道待改变的对象到底有多少个的时候。当一个对象的改变必须通知其他对象作出相应的变化,但是不能确定通知的对象是谁的时候。引申与参考观察者模式的Unity版本实现:本节内容相关的英文原文:本节内容相关的中文翻译:4.
原型模式 Prototype Pattern用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。要点原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是一种比较简单的模式,也非常容易理解,实现一个接口,重写一个方法即完成了原型模式。在实际应用中,原型模式很少单独出现。经常与其他模式混用,他的原型类Prototype也常用抽象类来替代。使用原型模式拷贝对象时,需注意浅拷贝与深拷贝的区别。原型模式可以结合JSON等数据交换格式,为数据模型构建原型。使用场合产生对象过程比较复杂,初始化需要许多资源时。希望框架原型和产生对象分开时。同一个对象可能会供其他调用者同时调用访问时。参考与引申原型模式的Unity版本实现:本节内容相关的英文原文:本节内容相关的中文翻译:5.
单例模式 Singleton Pattern保证一个类只有一个实例,并且提供了访问该实例的全局访问点。要点单例模式因其方便的特性,在开发过程中的运用很多。单例模式有两个要点,保证一个类只有一个实例,并提供访问该实例的全局访问点。尽量少用单例模式。单例模式作为一个全局的变量,有很多全局的变量的弊病。它会使代码更难理解,更加耦合,并且对并行不太友好。使用场合当在系统中某个特定的类对象实例只需要有唯一一个的时候。单例模式要尽量少用,无节制的使用会带来各种弊病。为了保证实例是单一的,可以简单的使用静态类。 还可以使用静态标识位,在运行时检测是不是只有一个实例被创建了。参考与引申下文中介绍的子类沙箱模式通过对状态的分享,给实例以类的访问权限而无需让其全局可用。下文中介绍的服务定位器模式不但让一个对象全局可用,还可以带来设置对象的一些灵活性。单例模式的Unity版本实现:本节内容相关的英文原文:本节内容相关的中文翻译:6.
状态模式 State Pattern允许对象在当内部状态改变时改变其行为,就好像此对象改变了自己的类一样。要点状态模式用来解决当控制一个对象状态转换的条件表达式过于复杂的情况,它把状态的判断逻辑转移到表示不同的一系列类当中,可以把复杂的逻辑判断简单化。状态模式的实现分为三个要点:为状态定义一个接口。为每个状态定义一个类。恰当地进行状态委托。通常来说,状态模式中状态对象的存放有两种实现存放的思路:静态状态。初始化时把所有可能的状态都new好,状态切换时通过赋值改变当前的状态。实例化状态。每次切换状态时动态new出新的状态。使用场合在游戏开发过程中,涉及到复杂的状态切换时,可以运用状态模式以及状态机来高效地完成任务。有限状态机的实现方式,有两种可以选择:用枚举配合switch case语句。用多态与虚函数(即状态模式)。有限状态机在以下情况成立时可以使用:有一个行为基于一些内在状态的实体。状态可以被严格的分割为相对较少的不相干项目。实体可以响应一系列输入或事件。参考与引申Hierarchical State Machines分层状态机:Pushdown Automata下推自动机:状态模式的Unity版本实现:本节内容相关的英文原文:本节内容相关的中文翻译:二、序列型模式 Sequencing Patterns本章的三种模式都是游戏开发中的常客:游戏循环是游戏运行的主心骨。游戏对象通过更新方法来进行每帧的更新。我们可以用双缓冲模式存储快照,来隐藏计算机的顺序执行,从而使得游戏世界能够同步更新。7.
双缓冲模式 Double Buffer双缓冲模式,使用序列操作来模拟瞬间或者同时发生的事情。要点一个双缓冲类封装了一个缓冲:一段可改变的状态。这个缓冲被增量的修改,但我们想要外部的代码将其视为单一的元素修改。 为了实现这点,双缓冲类需保存两个缓冲的实例:下一缓存和当前缓存。当信息从缓冲区中读取,我们总是去读取当前的缓冲区。当信息需要写到缓存,我们总是在下一缓冲区上操作。 当改变完成后,一个交换操作会立刻将当前缓冲区和下一缓冲区交换, 这样新缓冲区就是公共可见的了。旧的缓冲区则成为了下一个重用的缓冲区。双缓冲模式常用来做帧缓冲区交换。使用场合双缓冲模式是那种你需要它时自然会想起来的模式。以下情况都满足时,使用这个模式很合适:我们需要维护一些被增量修改的状态在修改过程中,状态可能会被外部请求。我们想要防止请求状态的外部代码知道内部是如何工作的。我们想要读取状态,而且不想在修改的时候等待。引申与参考我们几乎可以在任何一个图形API中找到双缓冲模式的应用。如OpenGl中的 swapBuffers() 函数, Direct3D中的“swap chains”,微软XNA框架的 endDraw() 方法。本节内容相关的英文原文:本节内容相关的中文翻译:8.
游戏循环模式 Game Loop游戏循环模式,实现游戏运行过程中对用户输入处理和时间处理的解耦。要点游戏循环模式:游戏循环在游戏过程中持续运转。每循环一次,它非阻塞地处理用户的输入,更新游戏状态,并渲染游戏。它跟踪流逝的时间并控制游戏的速率。游戏循环将游戏的处理过程和玩家输入解耦,和处理器速度解耦,实现用户输入和处理器速度在游戏行进时间上的分离。游戏循环也许需要与平台的事件循环相协调。如果在操作系统的高层或有图形UI和内建事件循环的平台上构建游戏, 那就有了两个应用循环在同时运作,需要对他们进行相应的协调。使用场合任何游戏或游戏引擎都拥有自己的游戏循环,因为游戏循环是游戏运行的主心骨。引申与参考讲述游戏循环模式的一篇经典文章是来自Glenn Fiedler的“Fix Your Timestep“。Witters的文章 game loops 也值得一看。Unity的框架具有一个复杂的游戏循环,这里有一个对其很详尽的阐述。本节内容相关的英文原文:本节内容相关的中文翻译:9.
更新方法 Update Method更新方法,通过每次处理一帧的行为来模拟一系列独立对象。要点更新方法模式:在游戏中保持游戏对象的集合。每个对象实现一个更新方法,以处理对象在一帧内的行为。每一帧中,游戏循环对集合中的每一个对象进行更新。当离开每帧时,我们也许需要存储下状态,以备不时之需。使用场合更新方法和游戏循环模式一般一起使用。更新方法适应以下情况:游戏中有很多对象或系统需要同时运行。每个对象的行为都与其他的大部分独立。游戏中的对象需要随时间模拟。引申与参考更新方法模式,以及游戏循环模式和组件模式,是构建游戏引擎核心的铁三角。Unity引擎在多个类中使用了这个模式,包括MonoBehaviour。微软的XNA框架在 Game 和GameComponent 类中使用了这个模式。当你关注在每帧中更新实体或组件的缓存性能时,数据局部性模式可以帮上忙。本节内容相关的英文原文:本节内容相关的中文翻译:三、行为型模式 Behavioral Patterns本章的模式可以帮助我们快速定义和完善多种多样的行为:类型对象定义行为的类别而无需完成真正的类。子类沙盒定义各种行为的安全原语。字节码,将行为从代码中拖出,放入数据。10. 字节码模式 Bytecode字节码模式,将行为编码为虚拟机器上的指令,来赋予其数据的灵活性。从而让数据易于修改,易于加载,并与其他可执行部分相隔离。要点字节码模式:指令集定义了可执行的底层操作。一系列的指令被编码为字节序列。 虚拟机使用中间值堆栈 依次执行这些指令。 通过组合指令,可以定义复杂的高层行为。可以理解为项目中的转表工具,将excel中的数据转为二进制数据,并读取到工程中,如在项目中使用googleprotobuf或json。字节码类似GOF的解释器模式,这两种方式都能让我们将数据与行为相组合。其实很多时候都是两者一起使用。用来构造字节码的工具会有内部的对象树,而为了编译到字节码,我们需要递归回溯整棵树,就像用解释器模式去解释它一样。唯一的不同在于,并不是立即执行一段行为,而是生成整个字节码再执行。使用场合这是GPP一书中最复杂的模式,不能轻易的加入到游戏中。 当我们需要定义很多行为,而游戏实现语言因为以下原因不能很好地完成任务时,就可以使用字节码模式:这些行为过于底层,繁琐易错。这些行为遍历起来很缓慢,导致编译时间长。这些行为太受依赖。如果想保证行为不会破坏游戏,你需要将其与代码的其他部分隔开。如果是上述的这些情况,就比较适合使用字节码模式。但需要注意,字节码比本地代码慢,所以最好不要用于引擎对性能敏感的部分。引申与参考Lua的内部实现就是一个非常紧凑的,基于寄存器的字节码虚拟机。Kismet是个可视化脚本编辑工具,应用于Unreal引擎的编辑器UnrealEd。()本节内容相关的英文原文:本节内容相关的中文翻译:11. 子类沙箱模式 Subclass Sandbox用一系列由基类提供的操作定义子类中的行为。要点子类沙箱模式:基类定义抽象的沙箱方法和几个提供操作的实现方法,将他们设为protected,表明它们只为子类所使用。每个推导出的沙箱子类用提供的操作实现了沙箱方法。使用场合子类沙箱模式是潜伏在编程日常中简单常用的模式,哪怕是在游戏之外的地方。 如果有一个非虚的protected方法,你可能早已在用类似的技术了。沙箱方法在以下情况适用:你有一个能推导很多子类的基类。基类可以提供子类需要的所有操作。在子类中有行为重复,你想要更容易的在它们间分享代码。你想要最小化子类和程序的其他部分的耦合。引申与参考当你使用上文中介绍到的更新模式时,你的更新函数通常也是沙箱方法。这个模式与GOF模板方法正好相反。两种模式中,都使用了一系列受限操作实现方法。使用子类沙箱时,方法在推导类中,受限操作在基类中。使用模板方法时,基类有方法,而受限操作在推导类中。你也可以认为这个模式是GOF外观模式的变形。 外观模式将一系列不同系统藏在简化的API后。使用子类沙箱,基类起到了在子类前隐藏整个游戏引擎的作用。子类沙箱模式的Unity版本实现:本节内容相关的英文原文:本节内容相关的中文翻译:12. 类型对象模式 Type Object创造一个类A来允许灵活的创造新的类,而类A的每个实例都代表了不同类型的对象。要点类型对象模式:定义类型对象类与有类型的对象类。每个类型对象实例代表一种不同的逻辑类型。每种有类型的对象保存描述它类型的对类型对象的引用。类型对象的基本思想就是给基类一个品种类(breed类),而不是用一些子类继承自这个基类。所以我们在做种类区分的时候就可以只有两个类,怪物类monster和品种类breed,而不是monster,dragon,troll等一堆类。所以在此种情况下,游戏中的每个怪物都是怪物类的一个实例,而实例中的breed类包含了所有同种类型怪物共享的信息。使用场合这个模式在任何你需要定义不同“种”事物,使用不当会让你的系统过于僵硬。而下面两者之一成立时,就非常适合使用:不知道后续还需什么新类型。(举个例子,如果你的游戏需要支持增量更新,让用户下载后续新包含进来的怪物品种)想要不改变代码或不重新编译就能修改或添加新类型。引申与参考这个模式引出的进阶问题是如何在不同对象之间共享数据。以不同的方式解决同一个问题的是GOF设计模式中的原型模式(prototype pattern)。类型对象是GOF设计模式中享元模式的亲兄弟。两者都让你在实例间分享代码。使用享元,意图是节约内存,而分享的数据也许不代表任何概念上对象的“类型”。而使用类型对象模式,焦点在组织性和灵活性。这个模式和GOF设计模式中状态模式有很多相似之处,两者都是委托了对象的部分定义给另外一个对象。本节内容相关的英文原文:本节内容相关的中文翻译:四、解耦型模式
Decoupling Patterns这一部分的三种模式,专注于解耦:组件模式将一个实体拆成多个,解耦不同的领域。事件队列解耦了两个互相通信的事物,稳定而且实时。服务定位器让代码使用服务而无需绑定到提供服务的代码上。13. 组件模式 Component允许单一的实体跨越多个领域,无需这些领域彼此耦合。要点组件模式:在单一实体跨越了多个领域时,为了保持领域之间相互解耦,可以将每部分代码放入各自的组件类中,将实体简化为组件的容器。Unity引擎在设计中频繁使用了这种设计方法,从而让其易于使用。使用场合组件通常在定义游戏实体的核心部分中使用,当然,它们在其他地方也适用。这个模式在如下情况下可以很好的适用:有一个涉及了多个领域的类,而你想保持这些领域互相隔离。一个类正在变大而且越来越难以使用。想要能定义一系列分享不同能力的类,但是使用接口不足以得到足够的重用部分。引申与参考Unity核心架构中GameObject类完全根据此模式来进行设计。此模式与GOF设计模式中的策略模式类似。两种模式都是将对象的行为取出,委派到一个单独的从属对象中。两者的不同点在于:策略模式中分离出的策略对象通常是无状态的——它封装的是算法,而不是数据。策略模式定义了对象的行为,而不是该对象是什么。而组件模式就更加复杂。组件经常保存了对象的状态,这有助于确定其真正的身份。但是,其界限往往很模糊。有些情况下组件也许根本没有任何状态。在这种情况下,你可以在不同的容器对象中使用相同的组件实例。这样看来,它的行为确实更像一种策略。本节内容相关的英文原文:本节内容相关的中文翻译:14. 事件队列模式 Event Queue事件队列模式,对消息或事件的发送与处理进行时间上的解耦。要点事件队列:在先入先出的队列中存储一系列通知或请求。发送通知时,将请求放入队列并返回。处理请求的系统在稍晚些的时候从队列中获取请求并进行处理。 这样就解耦了发送者和接收者,既静态又及时。事件队列很复杂,会对游戏架构引起广泛影响。中心事件队列是一个全局变量。这个模式的通常方法是一个大的交换站,游戏中的每个部分都能将消息送过这里。事件队列是基础架构中很强大的存在,但有些时候强大并不代表好。事件队列模式将状态包裹在协议中,但是它还是全局的,仍然存在全局变量引发的一系列危险。使用场合如果你只是想解耦接收者和发送者,像观察者模式和命令模式都可以用较小的复杂度来进行处理。在需要解耦某些实时的内容时才建议使用事件队列。不妨用推和拉来的情形来考虑。有一块代码A需要另一块代码B去做些事情。对A自然的处理方式是将请求推给B。同时,对B自然的处理方式是在B方便时将请求拉入。当一端有推模型另一端有拉模型时,你就需要在它们间放一个缓冲的区域。 这就是队列比简单的解耦模式多出来的那一部分。队列给了代码对拉取的控制权——接收者可以延迟处理,合并或者忽视请求。发送者能做的就是向队列发送请求然后就完事了,并不能决定什么时候发送的请求会受到处理。而当发送者需要一些回复反馈时,队列模式就不是一个好的选择。引申与参考很大程度上,事件队列模式就是广为人知的GOF设计模式中观察者模式的异步实现。就像其他很多模式一样,事件队列有很多别名。其中一个是“消息队列”。 消息队列通常指代一个更高层次的实现。可以这样理解,事件队列在应用中进行交流,而消息队列通常在应用间进行交流。另一个别名是“发布/提交”,有时被缩写为“pubsub”,这个别名通常指代更大的分布式系统中的应用。在有限状态机与状态模式中,往往需要一个输入流。如果想要异步响应,可以考虑用队列模式来存储它们。Go语言内建的“Channel”机制,其本质上就是事件队列。本节内容相关的英文原文:本节内容相关的中文翻译:15. 服务定位模式 Service Locator提供服务的全局接入点,而不必让用户和实现它的具体类耦合。要点服务定位模式:服务类定义了一堆操作的抽象接口。具体的服务提供者实现这个接口。 分离的服务定位器提供了通过查询合适的提供者, 获取服务的方法,同时隐藏了提供者的具体细节和需要定位它的进程。一般通过使用单例或者静态类来实现服务定位模式,提供服务的全局接入点。服务定位模式可以看做是更加灵活,更加可配置的单例模式。如果用得好,它能以很小的运行时开销,换取很大的灵活性。相反,如果用得不好,它会带来单例模式的所有缺点以及更多的运行时开销。使用服务定位器的核心难点是它将依赖,也就是两块代码之间的一点耦合,推迟到运行时再连接。这有了更大的灵活度,但是代价是更难在阅读代码时理解其依赖的是什么。使用场合服务定位模式在很多方面是单例模式的亲兄弟,在应用前应该考虑看看哪个更适合你的需求。让大量内容在程序的各处都能被访问时,就是在制造混乱。对何时使用服务定位模式的最简单的建议就是:尽量少用。与其使用全局机制让某些代码直接接触到它,不妨先考虑将对象传过来。因为这样可以明显地保持解耦,而且可以满足我们大部分的需求。当然,有时候不方便手动传入对象,也可以使用单例的方式。引申与参考Unity引擎在它的GetComponent()方法中使用了这个模式,协助组件模式的使用,方便随时获取到指定的组件。微软的XNA框架将这个模式内嵌到它的核心类Game中。每个实例有一个 GameServices 对象,能够用来注册和定位任何类型的服务。本节内容相关的英文原文:本节内容相关的中文翻译:五、优化型模式 Optimization Patterns这一部分,描述了几个优化和加速游戏的中间层模式:数据局部性介绍了计算机的存储层次以及如何使用其以获得优势。脏标识帮我们避开不必要的计算。对象池帮我们避开不必要的分配。空间分区加速了虚拟世界和其中内容的空间布局。16. 数据局部性模式 Data Locality合理组织数据,充分使用CPU的缓存来加速内存读取。要点现代的CPU有缓存来加速内存读取,其可以更快地读取最近访问过的内存毗邻的内存。基于这一点,我们通过保证处理的数据排列在连续内存上,以提高内存局部性,从而提高性能。为了保证数据局部性,就要避免的缓存不命中。也许你需要牺牲一些宝贵的抽象。你越围绕数据局部性设计程序,就越放弃继承、接口和它们带来的好处。没有银弹,只有权衡。使用场合使用数据局部性的第一准则是在遇到性能问题时使用。不要将其应用在代码库不经常使用的角落上。 优化代码后其结果往往更加复杂,更加缺乏灵活性。就本模式而言,还得确认你的性能问题确实由缓存不命中而引发的。如果代码是因为其他原因而缓慢,这个模式自然就不会有帮助。简单的性能评估方法是手动添加指令,用计时器检查代码中两点间消耗的时间。而为了找到糟糕的缓存使用情况,知道缓存不命中有多少发生,又是在哪里发生的,则需要使用更加复杂的工具—— profilers。组件模式是为缓存优化的最常见例子。而任何需要接触很多数据的关键代码,考虑数据局部性都是很重要的。引申与参考Tony Albrecht的《Pitfalls of Object-OrientedProgramming》是传播广泛的内存友好设计游戏指南。PDF下载:Noel Llopis一篇博客,也分析了内存友好的游戏设计。本节内容相关的英文原文:本节内容相关的中文翻译:17. 脏标识模式 Dirty Flag将工作延期至需要其结果时才去执行,以避免不必要的工作。要点脏标记,就是用来表示被标记的内容是否有被修改过的一个标志位。脏标识模式:考虑情况,当前有一组原始数据随着时间变化而改变。由这些原始数据计算出目标数据需要耗费一定的计算量。这个时候,可以用一个脏标识,来追踪目前的原始数据是否与之前的原始数据保持一致,而此脏标识会在被标记的原始数据改变时改变。那么,若这个标记没被改变,就可以使用之前缓存的目标数据,不用再重复计算。反之,若此标记已经改变,则需用新的原始数据计算目标数据。使用场合就像其他优化模式一样,此模式会增加代码复杂度。只在有足够大的性能问题时,再考虑使用这一模式。脏标记在这两种情况下适用:当前任务有昂贵的计算开销当前任务有昂贵的同步开销。\t若满足这两者之一,也就是两者从原始数据转换到目标数据会消耗很多时间,都可以考虑使用脏标记模式来节省开销。若原始数据的变化速度远高于目标数据的使用速度,此时数据会因为随后的修改而失效,此时就不适合使用脏标记模式。引申与参考脏标记模式在游戏外的领域也是常见的,比如像Angular这种browser-side web框架,其利用赃标记来跟踪浏览器中变动的数据以及需要提交到服务端的数据。本节内容相关的英文原文:本节内容相关的中文翻译:18. 对象池模式 Object Pool放弃单独地分配和释放对象,从固定的池中重用对象,以提高性能和内存使用率。要点对象池模式:定义一个包含了一组可重用对象的对象池。其中每个可重用对象都支持查询“使用中”状态,说明它是不是“正在使用”。 对象池被初始化时,就创建了整个对象集合(通常使用一次连续的分配),然后初始化所有对象到“不在使用中”状态。当我们需要新对象时,就从对象池中获取。从对象池取到一个可用对象,初始化为“使用中”然后返回给我们。当不再需要某对象时,将其设置回“不在使用中”状态。 通过这种方式,便可以轻易地创建和销毁对象,而不必每次都分配内存或其他资源。使用场合这个模式广泛使用在可见事物上,比如游戏物体和特效。但是它也可在不那么视觉化的数据结构上使用,比如正在播放的声音。满足以下情况可以使用对象池:需要频繁创建和销毁对象。对象大小相仿。在堆上分配对象缓慢或者会导致内存碎片。每个对象都封装了像数据库或者网络连接这样很昂贵又可以重用的资源。引申与参考对象池模式与GOF设计模式中享元模式类似。 两者都控制了一系列可重用的对象。不同在于重用的含义。享元对象分享实例间同时拥有的相同部分。享元模式在不同上下文中使用相同对象避免了重复内存使用。对象池中的对象也被重用了,但是是在不同的时间点上被重用的。重用在对象池中意味着对象在原先的对象用完之后再分配内存。对象池的对象不会在它的生命周期中与其他对象共享数据。将内存中同样类型的对象进行整合,能确保在遍历对象时CPU缓存是满载的。这便是数据局部性模式中介绍的内容。本节内容相关的英文原文:本节内容相关的中文翻译:19. 空间分区模式 Spatial Partition将对象存储在基于位置组织的数据结构中,来有效的定位对象。要点对于一系列对象,每个对象都有空间上的位置。将它们存储在根据位置组织对象的空间数据结构中,让我们有效查询在某处或者附近的对象。 当对象的位置改变时,更新空间数据结构,这样它可以继续找到对象。最简单的空间分区:固定网格。想象某即时战略类游戏,一改在单独的数组中存储我们的游戏对象的常规思维,我们将它们存到网格的格子中。每个格子存储一组单位,它们的位置在格子的边界内部。当我们处理战斗时,一般只需考虑在同一格子或相邻格子中的单位,而不是将每个游戏中的单位与其他所有单位比较,这样就大大节约了计算量。使用场合空间分区模式在需要大量存储活跃、移动的游戏物体,和静态的美术模型的游戏中比较常用。因为复杂的游戏中不同的内容有不同的空间划分。这个模式的基本适用场景是你有一系列有位置的对象,当做了大量通过位置寻找对象的查询而导致性能下降的时候。空间分区的存在是为了将O(n)或者O(n?) 的操作降到更加可控的数量级。 你拥有的对象越多,此模式就越好用。相反的,如果n足够小,也许就不需要使用此模式。引申与参考了解了空间分区模式,下一步应该是学习一下常见的结构,比如:每种空间划分数据结构基本上都是将一维数据结构扩展成更高维度的数据结构。而了解它的直系子孙,有助于分辨其对当前问题的解答是不是有帮助:网格其实是持续的。BSP,k-d tree,和层次包围盒是。四叉树和八叉树是。本节内容相关的英文原文:本节内容相关的中文翻译:六、更多参考与学习资源[1] 本书的英文Web原版目录:[2] 本书的中文翻译web版目录:[3] [4] [5] [6] [7] 《设计模式:可复用面向对象软件的基础》[8] ","state":"published","sourceUrl":"","pageCommentsCount":0,"canComment":false,"snapshotUrl":"","slug":,"publishedTime":"T09:50:48+08:00","url":"/p/","title":"【游戏设计模式】之四 《游戏编程模式》全书内容提炼总结","summary":"这是一篇超过万字的读书笔记,总结提炼了《Game Programming Patterns》(中译版《游戏编程模式》)一书中所有章节与内容的知识梗概。我们知道,游戏行业其实一直很缺一本系统介绍游戏编程进阶技巧的书籍,而《游戏编程模式》的出现,正好弥补了这一点。之…","reviewingCommentsCount":0,"meta":{"previous":null,"next":null},"commentPermission":"anyone","commentsCount":20,"likesCount":200}},"annotationDetail":null,"commentsCount":25,"likesCount":148,"FULLINFO":true}},"User":{"mao-xing-yun":{"isFollowed":false,"name":"毛星云","headline":"网名\"浅墨\",90后,微软VC++领域最具价值专家,2013年度中国十大杰出IT博客作者。写博客:【C++游戏编程】(http://blog.csdn.net/poem_qianmo),写过两本书:《Windows游戏编程之从零开始》、《OpenCV3编程入门》","avatarUrl":"/cac34dbc8_s.jpg","isFollowing":false,"type":"people","slug":"mao-xing-yun","bio":"网名「浅墨」,微软MVP,《Windows游戏编程之从零开始》作者","hash":"b34bb40fe18653f1baeb09e","uid":76,"isOrg":false,"description":"网名\"浅墨\",90后,微软VC++领域最具价值专家,2013年度中国十大杰出IT博客作者。写博客:【C++游戏编程】(http://blog.csdn.net/poem_qianmo),写过两本书:《Windows游戏编程之从零开始》、《OpenCV3编程入门》","profileUrl":"/people/mao-xing-yun","avatar":{"id":"cac34dbc8","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false,"badge":{"identity":null,"bestAnswerer":null}}},"Comment":{},"favlists":{}},"me":{},"global":{},"columns":{"game-programming":{"following":false,"canManage":false,"href":"/api/columns/game-programming","name":"浅墨的游戏编程","creator":{"slug":"mao-xing-yun"},"url":"/game-programming","slug":"game-programming","avatar":{"id":"v2-70efafd2e60","template":"/{id}_{size}.jpg"}}},"columnPosts":{},"columnSettings":{"colomnAuthor":[],"uploadAvatarDetails":"","contributeRequests":[],"contributeRequestsTotalCount":0,"inviteAuthor":""},"postComments":{},"postReviewComments":{"comments":[],"newComments":[],"hasMore":true},"favlistsByUser":{},"favlistRelations":{},"promotions":{},"switches":{"couldAddVideo":false},"draft":{"titleImage":"","titleImageSize":{},"isTitleImageFullScreen":false,"canTitleImageFullScreen":false,"title":"","titleImageUploading":false,"error":"","content":"","draftLoading":false,"globalLoading":false,"pendingVideo":{"resource":null,"error":null}},"drafts":{"draftsList":[],"next":{}},"config":{"userNotBindPhoneTipString":{}},"recommendPosts":{"articleRecommendations":[],"columnRecommendations":[]},"env":{"isAppView":false,"appViewConfig":{"content_padding_top":128,"content_padding_bottom":56,"content_padding_left":16,"content_padding_right":16,"title_font_size":22,"body_font_size":16,"is_dark_theme":false,"can_auto_load_image":true,"app_info":"OS=iOS"},"isApp":false},"sys":{}}}

我要回帖

更多关于 unity怎么读 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信