游戏编程模式学习笔记(一) 命令模式

游戏设计模式学习笔记(一) 命令模式

学习参考自 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或者后一个。

文章作者: FcAYH
文章链接: http://www.fcayh.cn/2022/04/12/game-promramming-pattern-study-notes-1/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Forever丶CIL