游戏设计模式学习笔记(一) 命令模式
学习参考自 Robert Nystrom编写的 《游戏编程模式》
电子书链接: https://gpp.tkchu.me/command.html
该书中使用C++作为编程语言,我将使用C#和Unity。
命令模式:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化; 对请求排队或记录请求日志,以及支持可撤销的操作。
简单地说,命令是具现化的方法调用。
以角色动作控制为例
举个简单的例子,假设我们现在要控制一个Player的动作,他有跳跃,开火,向前冲刺,切换武器四个动作,那么最朴素的方法莫过于如下写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public MotionController : MonoBehaviour { private void Update() { if (Input.GetKeyDown(KeyCode.Space)) else if (Input.GetKeyDown(KeyCode.J)) else if (Input.GetKeyDown(KeyCode.LeftShift)) else if (Input.GetKeyDown(KeyCode.K)) } }
|
这个方法显而易见的一个问题就是,我们将用户的输入和程序行为硬编码在了一起,想要修改按键对应的功能是需要对源码进行大量修改的。但是通常我们都希望我们的游戏可以支持用户自己配置按键的功能,为了支持这一点,我们应该修改各个功能的实现部分。
首先我们可以引入一个ICommand接口:
1 2 3 4
| public interface ICommand { public abstract void Execute(); }
|
我们可以为不同的功能创建不同的对象,并且均要实现ICommand接口,这样对于不同功能,只需要实例化出对应的对象,然后调用Execute()方法。
例如跳跃:
1 2 3 4 5 6 7
| public JumpCommand : ICommand { public void Execute() { } }
|
在将开火、冲刺、切换武器等功能均实现后,我们的MotionController可改为如下形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public MotionController : MonoBehaviour { private ICommand keySpace; private ICommand keyJ; private ICommand keyLeftShift; private ICommand keyK;
private void Update() { if (Input.GetKeyDown(KeyCode.Space)) keySpace.Execute(); else if (Input.GetKeyDown(KeyCode.J)) keyJ.Execute(); else if (Input.GetKeyDown(KeyCode.LeftShift)) keyLeftShift.Execute(); else if (Input.GetKeyDown(KeyCode.K)) keyK.Execute(); } }
|
此时如果我们想要更换某个按键的功能,只需要将其ICommand用对应功能的类实例化即可
不过我有点好奇,书中是面向手柄的,按键分别为XYBA,如果是键盘游戏,为了让任意键均可被绑定,岂不是需要预先将所有按键的ICommand定义出来,未免有些麻烦和多余。故而我在下面会根据自己的理解再写一种方式,即预先将Player每个动作的KeyCode和ICommand均定义出来,这样更改按键对应的功能只需将其KeyCode更改即可,感觉更方便?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public MotionController : MonoBehaviour { public KeyCode JumpKey; public KeyCode FireKey; public KeyCode DashKey; public KeyCode SwitchWeaponKey;
private ICommand jumpCommand; private ICommand fireCommand; private ICommand dashCommand; private ICommand switchWeaponCommand;
private void Update() { if (Input.GetKeyDown(JumpKey)) jumpCommand.Execute(); else if (Input.GetKeyDown(FireKey)) fireCommand.Execute(); else if (Input.GetKeyDown(DashKey)) dashCommand.Execute(); else if (Input.GetKeyDown(SwitchWeaponKey)) switchWeaponCommand.Execute(); } }
|
在书中,作者推荐了类似以下用法:
1 2 3 4
| public interface ICommand { public abstract void Execute(GameObject actor); }
|
1 2 3 4 5 6 7
| public JumpCommand : ICommand { public void Execute(GameObject actor) { actor.GetComponent<...>().Jump(); } }
|
将Command独立出来,每个Player,或者游戏中的其他AI,都需要自己事先写好各项运动的逻辑,然后通过将自己传入Command中,触发功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class InputHandler { private ICommand keySpace; private ICommand keyJ; private ICommand keyLeftShift; private ICommand keyK;
public ICommand HandleInput() { if (Input.GetKeyDown(KeyCode.Space)) return keySpace; else if (Input.GetKeyDown(KeyCode.J)) return keyJ; else if (Input.GetKeyDown(KeyCode.LeftShift)) return keyLeftShift; else if (Input.GetKeyDown(KeyCode.K)) return keyK; retrun null; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class MotionController { private InputHandler inputHandler;
private void Update() { ICommand command = inputHandler.HandleInput(); if (command != null) { command.Execute(this.gameObject); } } }
|
这样写也有一个好处就是方便实现我们的Player去控制游戏中的其他单位移动,因为只需将相应的gameObject传进去。
命令的撤销和重做
这一部分看上去应该是备忘录模式去做的事情,不过在书中作者提到: 由于命令趋向于修改对象状态的一小部分,对数据其他部分的快照就是浪费内存。手动内存管理的消耗更小。
这部分以战棋类型游戏的移动为例,如果仅支持一步的撤销和重做,可以在每个Command下记录以下上一步所在的坐标即可。当然,复杂一点的话,我们可以创建一个链表或者栈,用来记录执行过的命令,撤销或者重做只需要执行链表中的前一个Command或者后一个。