博主才学尚浅,难免会有错误,尤其是设计模式这种极富禅意且需要大量经验的东西,如果哪里书写错误或有遗漏,还请各位前辈指正。
打 算写设计模式的目的就是,首先自己可以理清思路,还有就是国内的设计模式资料很丰富,但是并没有专门用在游戏开发上的讲解,看过之后有些不知道怎么用在游 戏方面上,怎么用,博主在学习过程中会结合一些国外的游戏设计模式资料加上自己的理解与实践,写出文章,在自己理清思路的同时也希望能对像我这样的小白们 提供一点微薄帮助。
在我没学这个模式之前写的控制部分的代码,就是把按键控制写成if else简单的,一次性的写在update()函数中,对这样散乱的块来做轮询,这样的代码不整体,是一次性的,难以维护,如果想要在其中新添加功能会非 常难找,改动也容易出现错误。之前的代码例子如下(错误示例,仅给出部分):
void Update() { if (Input.GetKeyDown(KeyCode.J)) { …攻击操作… } if (Input.GetKeyDown(KeyCode.Space)) { …跳跃操作… } if (Input.GetKey(KeyCode.D)) { …向左移动操作… } }
相信不少人和我一样是这么写的,接下来要介绍命令模式,这种模式不仅能用在unity的脚本编程上,所有面向对象语言都适用。
命令模式是游戏中很有用的设计模式,四人帮有一句话是这样说的:
Encapsulate a request as an object, thereby letting users parameterize clients with different requests, queue or log requests, and support undoable operations.
大概意思是,将请求封装为一个对象,让用户参数化的提出不同的请求,并提供撤销操作。
我们可以理解为是把命令具体化来调用。
比如把控制命令变成实实在在的实体来调用,就不用像上面错误的例子那样轮询一堆散乱的东西。
如下图,我们先把具体控制封装成函数,J键定义为act操作,把space键定义为jump操作。
命令模式好处之一:
方便替换按键,大家都知道大部分游戏设置中会有替换按键的设置,把按键设置为自己的常用键,玩着顺手,使用命令模式我们把每个操作控制封装成块,把用户按键操作与实现控制通过命令解耦,更改按键十分容易。如下图:
我们通过赋值b1,b2可以动态改变按键操作。但是这样方便的一切的前提都是使用命令模式把命令具体化来调用。
再把所有button整合成一个数组button
然后代码就变成了这样
void Update() { if (Input.GetKeyDown(button[0])) { act(); } if (Input.GetKeyDown(button[1])) { jump(); } if (Input.GetKey(button[2])) { move(); } }
稍微规整了一些。而且这样我们就可以让玩家自定义按键了。
接下来我们就可以使用命令模式了,就是替换jump(),act(),move()这些操作为具体的command命令。
我们可以用一个借口command来抽象概括所有的这些命令,再分别实现它们。把这些操作看成客户下的命令,所以每一个command命令都应该有一个执行命令的函数execute()。代码如下:
using UnityEngine; using System.Collections; public interface ICommand{ void execute(); }
这个接口就是我们的抽象,然后我们在一一实现我们的命令类,拿jump举例:
using UnityEngine; using System.Collections; public class JumpCommand : ICommand { public void execute() { ….实现jump…. } }
但是这里出现了问题,jump命令是要用在我们的“hero”上的也就是玩家控制的人物,说白了就是一个 gameobject,所以只要把hero的gameobject传入我们的命令类即可。这样做带来了命令模式的另一个好处,举个博主最爱玩的游戏-传说 系列,里面一般有4-6个英雄可以替换控制(对,博主就是要安利你们玩。。),随意更换英雄,就是随意更换操作的人物,只要我们传参传入不同的hero 的gameobject即可。
using UnityEngine; using System.Collections; public interface ICommand{ void execute(GameObject Hero); }
然后这里我们可以选择是在一个新的脚本中实现操作,还是在这个command中实现,这两种都可以,前者重用性好,后者重用性差一些,但更具体。实例在一个新脚本HeroCtrl中实现具体操作:
using UnityEngine; using System.Collections; public class JumpCommand : ICommand { public void execute(GameObject Hero) { Hero.GetComponent<HeroCtrl>().jump(); } }
如果想在这个command中实现就可以这样:
using UnityEngine; using System.Collections; public class JumpCommand : ICommand { float pow = 0.1f; CharacterController controller; public void execute(GameObject Hero) { controller = Hero.GetComponent<CharacterController>(); if (controller.isGrounded) { controller.Move(Vector3.up * pow); Hero.GetComponent<Animation>().CrossFade(…); } } }
此时的结构就是这样了,我们成功的实现了解耦:
我们在写AI的时候也可以使用命令模式,此时的AI逻辑只负责“发号施令”就可以了,更加灵活的编写AI。
然 后再次揭开sims的一个秘密,在玩sims可以对一个小人下许多命令,但是小人需要花时间才能干完一件事,通常我们看到我们想让他做的事的图标就会堆在 上面,小人会一个一个的处理,这就是任务列表,我们可以把代做的任务存到一个list中,给每个任务一个index,完成一个就做下一个,产生新任务就堆 在后面,这也是命令模式的一大功能。
然后就是撤销undo和重做redo部分,这个一般用在策略游戏中,我们错误操作了可以及时撤销。
undo实现方法就是把该任务反过来的操作作为undo函数,举一个最简单的例子:
public class addCommand : ICommand { public int execute(int num) { return ++num; } public int undo (int num) { return --num; } }
加法的undo就是减法,前移的undo就是后移。
如果想要redo的话就要把之前的操作储存起来,最好的办法把undo,当前操作,redo都存在一个堆中:
当产生一个新操作时,后面的redo全都不要了
游戏回放功能原理与之相同,回放时执行已经记录好的命令,有些游戏记录每一帧整个游戏的状态来回放,这样会消耗很多内存。
实现结果:
博主写了一个“玩具”用来专门练习设计模式,gameplay 模仿传说系列前几代。
命令模式完美运行:
命令模式完了。总之就是希望前辈们多多指正或者建议,能够带来帮助就更好。
博主近期渲染:最近用unity5弄的一些渲染
—- by wolf96