游戏编程之命令模式
1、什么是命令模式
最近看了《游戏编程模式》这本书,里面介绍了游戏开发时常用的设计模式,当然这些设计模式不只是在开发游戏时才管用,它们同样适用于其他软件开发,适用于各种语言。这里我记录一下自己的学习笔记以及结合unity的使用方法。命令模式是常用的设计模式之一,它的定义是这样:将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。这个定义听起来似乎晦涩难懂,下面用unity游戏开发的例子来说明:
2、对客户进行参数化
比如在游戏开发中,产品经理给你提了这样一个需求:按下按键A,控制角色攻击;按下按键B,控制角色奔跑;按下按键C,控制角色跳跃。面对这样一个简单的需求,我们或许会这样写:
void HandleInput() { if (Input.GetKeyDown(KeyCode.A)) { Attack(); } else if (Input.GetKeyDown(KeyCode.B)) { Run(); } else if (Input.GetKeyDown(KeyCode.C)) { Jump(); } }
然后,产品经理又提了需求,用户可以自定义按键功能,在很多游戏中都有做这样的功能,为了实现这样的功能,我们应该将这些对Attack()和Run()的调用转化成可以变换的东西,下面用命令模式来重写一下这个功能:
先定义一个抽象类Command作为基类,再定义具体的子类来重写Excute();
public abstract class Command{ public abstract void Excute(GameActor actor); } public class AttackCommand : Command { public override void Excute() { //攻击逻辑 } } public class RunCommand : Command { public override void Excute() { //奔跑逻辑 } } public class JumpCommand : Command { public override void Excute() { //跳跃逻辑 } }
在MonoBehaviour的Update函数中,每帧去监听用户输入,并返回对应的command
public class GameControl : MonoBehaviour { private Command buttonA; private Command buttonB; private Command buttonC; private void Start() { buttonA = new AttackCommand(); buttonB = new JumpCommand(); buttonC = new RunCommand(); } private void Update() { Command cmd = HandleInput(); if (cmd != null) { cmd.Excute(actor); } } //处理用户输入 private Command HandleInput() { if (Input.GetKeyDown(KeyCode.A)) { return buttonA; } else if (Input.GetKeyDown(KeyCode.B)) { return buttonB; } else if (Input.GetKeyDown(KeyCode.C)) { return buttonC; } else { return null; } } }
这样,在按键触发和函数调用中间就加了一层Command,如果要自定义按键功能,直接修改Button对应的Command就行了。现在我们也可以修改一下上面的代码,让我们可以用这套机制去控制任意角色对象,只需将要控制的角色对象传进来即可:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameActor { } public class Actor1 : GameActor { } public class Actor2 : GameActor { } public abstract class Command{ public abstract void Excute(GameActor actor); } public class AttackCommand : Command { public override void Excute(GameActor actor) { //攻击逻辑 } } public class RunCommand : Command { public override void Excute(GameActor actor) { //奔跑逻辑 } } public class JumpCommand : Command { public override void Excute(GameActor actor) { //跳跃逻辑 } } public class GameControl : MonoBehaviour { private Command buttonA; private Command buttonB; private Command buttonC; private GameActor actor; private void Start() { buttonA = new AttackCommand(); buttonB = new JumpCommand(); buttonC = new RunCommand(); actor = new Actor1(); } private void Update() { Command cmd = HandleInput(); if (cmd != null) { cmd.Excute(actor); } } //处理用户输入 private Command HandleInput() { if (Input.GetKeyDown(KeyCode.A)) { return buttonA; } else if (Input.GetKeyDown(KeyCode.B)) { return buttonB; } else if (Input.GetKeyDown(KeyCode.C)) { return buttonC; } else { return null; } } }
3、支持可撤销的操作
命令模式在需要支持可撤销操作的情况下也能轻松应对,假如我们需要给玩家提供撤销移动操作的功能时,我们可以先把玩家输入产生的command存入栈中(或者其他数据结构),在撤销时,从栈中取出栈顶的Command,再调用该Command的Undo(),就实现了撤销功能(Undo()为撤销方法,与Excute()相反),代码如下:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameActor { public Transform selfTra; public void Move(Vector3 offset) { selfTra.Translate(offset); } } public class Actor1 : GameActor { } public class Actor2 : GameActor { } public abstract class Command{ public abstract void Excute(GameActor actor);//执行 public abstract void Undo(GameActor actor);//撤销 } public class MoveCommand : Command { public Vector3 moveOffset; public MoveCommand(Vector3 offset) { moveOffset = offset; } public override void Excute(GameActor actor) { actor.Move(moveOffset); } public override void Undo(GameActor actor) { actor.Move(-moveOffset); } } public class CommandControl : MonoBehaviour { private Command moveCommand; private GameActor actor; private Stack<Command> commandStack; private void Start() { moveCommand = new MoveCommand(Vector3.one); actor = new Actor1(); commandStack = new Stack<Command>(); } private void Update() { Command cmd = HandleInput(); if (cmd != null) { commandStack.Push(cmd); cmd.Excute(actor); } } //需要撤销操作时调用这个函数 public void PlayReverse() { if (commandStack.Count > 0) { commandStack.Pop().Undo(actor); } } //处理用户输入 public Command HandleInput() { if (Input.GetKeyDown(KeyCode.A)) { return new MoveCommand(new Vector3(2, 4, 5)); } if (Input.GetKeyDown(KeyCode.B)) { return new MoveCommand(new Vector3(1, 2, 4)); } else { return null; } } }
上面代码中, 每次产生一个command时就将它存到Stack中,当需要撤销操作时,就取出Stack顶部的command,并执行它的Undo(),按照这种方法,可以实现多重撤销。
4、总结
通过上面的例子,我们再看命令模式的定义:将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。现在我们差不多明白了命令模式的用法,它优点很明显,缺点也是有的:第一个优点是类间解耦,调用者和接收者之间没有任何依赖关系,调用者在实现功能时只需调用Command抽象类的Excute方法即可,不需要关注是哪个接收者执行;第二个优点是可扩展性,Command的子类可以很容易地扩展;缺点是如果有大量命令,那么Command的子类将会非常庞大。我们在实际开发中,应该发挥出命令模式的优点,并结合其他模式,减少Command子类庞大的问题。