Unity 游戏开发中的命令模式:从入门到实战(附极简代码 + 命令栈实现)
在游戏开发中,我们经常需要处理操作撤销、技能系统、输入与逻辑解耦合等场景,而命令模式作为一种经典的行为型设计模式,正是解决这类问题的 “利器”。很多新手会觉得命令模式复杂、难以理解,其实它的核心思想非常简单 ——把 “操作” 打包成独立的对象。
本文将从新手视角出发,用最通俗的语言和极简的代码,讲解命令模式的核心逻辑、与原始写法的差异、实战实现(含命令栈撤销 / 重做),以及游戏开发中的实际应用场景,内容兼顾专业性和易懂性,适合刚接触设计模式的开发者学习。
一、命令模式的核心思想:把操作 “打包” 成对象
命令模式的本质可以用一句话概括:将 “请求 / 操作” 封装成一个独立的命令对象,使调用者与执行者完全解耦。
举个生活中的例子:
-
你(调用者)去餐厅点餐,只需要把写好的菜单(命令对象)交给服务员,不用直接指挥厨师;
-
厨师(接收者)按照菜单执行操作,不用知道是谁点的餐;
-
菜单上写清楚了 “要做什么”(比如宫保鸡丁),这就是命令对象的核心作用。
对应到游戏开发中:
- 玩家按空格键(调用者)→ 触发 “跳跃命令”(命令对象)→ 角色执行跳跃(接收者)。
二、先看:不用命令模式的原始写法(耦合严重)
我们以 Unity 中 “玩家按空格键让角色跳跃” 为例,先看最直接的写法,感受其中的问题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using UnityEngine;
// 接收者:角色(实际执行跳跃的对象)
public class Player : MonoBehaviour
{
public void Jump()
{
Debug.Log("角色跳跃!");
transform.Translate(Vector3.up * 2f); // 简单模拟跳跃
}
}
// 调用者:输入管理器(触发操作的对象)
public class InputManager : MonoBehaviour
{
private Player _player;
private void Awake()
{
_player = FindObjectOfType<Player>();
}
private void Update()
{
// 按下空格,直接调用角色的Jump方法
if (Input.GetKeyDown(KeyCode.Space))
{
_player.Jump();
}
}
}
问题分析:
-
调用者(
InputManager)直接依赖接收者(Player),还需要知道接收者的方法名(Jump()),耦合性极高; -
无法实现撤销操作(比如角色跳错了,想回到原来的位置);
-
新增操作(比如攻击)时,需要修改
InputManager的代码,违反 “开闭原则”。
三、命令模式的核心:新增的 3 个关键层(解耦的关键)
使用命令模式后,会在调用者和接收者之间新增3 个核心层(其中CommandManager是实战必备的扩展层,非模式强制要求),正是这几层实现了完全解耦:
| 层级 | 作用(通俗解释) | 对应示例 |
|---|---|---|
| 命令接口层(ICommand) | 定义所有命令的统一标准(执行 + 撤销) | ICommand(含Execute()/Undo()) |
| 具体命令类层 | 封装具体操作(绑定接收者和操作逻辑) | JumpCommand(封装跳跃操作) |
| 命令管理器层(CommandManager) | 管理命令的执行、撤销、重做(用栈记录命令) | CommandManager(含命令栈) |
结构变化:
1
调用者(InputManager)→ CommandManager → 命令接口(ICommand)→ 具体命令(JumpCommand)→ 接收者(Player)
四、Unity 实战:极简版命令模式(带命令栈,支持撤销 / 重做)
下面给出实战中最常用的简化版代码,保留核心功能(执行、撤销、重做),去掉冗余逻辑,注释清晰,可直接复制到 Unity 中测试:
1. 命令接口(核心标准)
1
2
3
4
5
6
7
8
9
10
11
using UnityEngine;
using System.Collections.Generic;
/// <summary>
/// 命令接口:所有命令的统一标准(执行+撤销)
/// </summary>
public interface ICommand
{
void Execute(); // 执行命令
void Undo(); // 撤销命令
}
2. 接收者:角色(实际执行操作)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/// <summary>
/// 角色(接收者):包含具体操作和撤销的回滚逻辑
/// </summary>
public class Player : MonoBehaviour
{
private Vector3 _jumpOriginPos; // 记录跳跃前位置(用于撤销)
// 跳跃操作
public void Jump()
{
_jumpOriginPos = transform.position; // 记录原始位置
transform.Translate(Vector3.up * 2f);
Debug.Log($"角色跳跃:位置→{transform.position}");
}
// 撤销跳跃(回滚逻辑)
public void UndoJump()
{
transform.position = _jumpOriginPos;
Debug.Log($"撤销跳跃:位置→{transform.position}");
}
// 攻击操作
public void Attack()
{
Debug.Log("角色发起攻击!");
}
// 撤销攻击
public void UndoAttack()
{
Debug.Log("撤销攻击:取消攻击效果");
}
}
3. 具体命令类(封装操作)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/// <summary>
/// 跳跃命令:绑定角色和跳跃操作
/// </summary>
public class JumpCommand : ICommand
{
private readonly Player _player; // 接收者引用
public JumpCommand(Player player)
{
_player = player;
}
public void Execute() => _player.Jump();
public void Undo() => _player.UndoJump();
}
/// <summary>
/// 攻击命令:绑定角色和攻击操作
/// </summary>
public class AttackCommand : ICommand
{
private readonly Player _player;
public AttackCommand(Player player)
{
_player = player;
}
public void Execute() => _player.Attack();
public void Undo() => _player.UndoAttack();
}
4. 命令管理器(核心:命令栈,管理撤销 / 重做)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/// <summary>
/// 命令管理器(实战必备):用栈管理命令,支持执行、撤销、重做
/// </summary>
public class CommandManager : MonoBehaviour
{
// 单例:全局唯一(Unity游戏开发常用)
public static CommandManager Instance { get; private set; }
// 执行栈:存储已执行的命令(用于撤销)
private readonly Stack<ICommand> _executeStack = new Stack<ICommand>();
// 重做栈:存储被撤销的命令(用于重做)
private readonly Stack<ICommand> _redoStack = new Stack<ICommand>();
private void Awake()
{
// 单例初始化(避免多个管理器)
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject); // 场景切换不销毁
}
else
{
Destroy(gameObject);
}
}
/// <summary>
/// 执行命令(核心方法)
/// </summary>
public void ExecuteCommand(ICommand command)
{
command.Execute();
_executeStack.Push(command); // 压入执行栈
_redoStack.Clear(); // 执行新命令后,清空重做栈(符合用户直觉)
}
/// <summary>
/// 撤销上一个命令
/// </summary>
public void UndoLast()
{
if (_executeStack.Count == 0)
{
Debug.LogWarning("无命令可撤销!");
return;
}
var cmd = _executeStack.Pop();
cmd.Undo();
_redoStack.Push(cmd); // 压入重做栈
}
/// <summary>
/// 重做上一个被撤销的命令
/// </summary>
public void RedoLast()
{
if (_redoStack.Count == 0)
{
Debug.LogWarning("无命令可重做!");
return;
}
var cmd = _redoStack.Pop();
cmd.Execute();
_executeStack.Push(cmd);
}
}
5. 调用者:输入管理器(仅检测输入,解耦合)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/// <summary>
/// 输入管理器(调用者):仅检测输入,不关心具体操作
/// </summary>
public class InputManager : MonoBehaviour
{
private Player _player;
private void Awake()
{
_player = FindObjectOfType<Player>();
}
private void Update()
{
// 空格:执行跳跃命令
if (Input.GetKeyDown(KeyCode.Space))
{
CommandManager.Instance.ExecuteCommand(new JumpCommand(_player));
}
// 鼠标左键:执行攻击命令
if (Input.GetMouseButtonDown(0))
{
CommandManager.Instance.ExecuteCommand(new AttackCommand(_player));
}
// Z键:撤销
if (Input.GetKeyDown(KeyCode.Z))
{
CommandManager.Instance.UndoLast();
}
// R键:重做
if (Input.GetKeyDown(KeyCode.R))
{
CommandManager.Instance.RedoLast();
}
}
}
测试步骤(Unity 中直接运行)
-
给空物体挂载
CommandManager、InputManager; -
给角色挂载
Player脚本; -
运行游戏:按空格跳跃→按鼠标左键攻击→按 Z 键撤销→按 R 键重做,观察控制台输出。
五、命令模式的核心优势(新手必懂)
新增的几层看似多写了代码,但带来的好处是原始写法无法比拟的,也是工业级项目中必须考虑的点:
1. 完全解耦
调用者(InputManager)只需要创建命令对象,交给CommandManager即可,不需要知道接收者(Player)的存在,也不需要知道操作的具体逻辑。比如要把 “跳跃” 改成 “冲刺”,只需要新增DashCommand,无需修改InputManager。
2. 轻松实现撤销 / 重做
通过CommandManager的命令栈,能记录操作的顺序,实现多次撤销 / 重做 —— 这是游戏开发中(如战棋游戏悔棋、编辑器 Ctrl+Z)的核心需求,原始写法几乎无法实现。
3. 符合开闭原则
新增操作时,只需要添加新的具体命令类(如DashCommand、SkillCommand),无需修改原有代码,降低维护成本。
4. 支持批量操作(宏命令)
可以把多个命令组合成一个 “宏命令”(比如格斗游戏的连招),一次性执行,扩展能力极强。
六、游戏开发中的常见使用场景(新手重点关注)
命令模式在游戏开发中应用广泛,新手可以重点关注以下场景:
1. 操作撤销 / 悔棋
-
战棋游戏(如《三国志》):玩家移动棋子、释放技能后,点击 “悔棋” 按钮,通过命令栈撤销操作;
-
关卡编辑器:玩家摆放物体后,按 Ctrl+Z 撤销操作。
2. 输入与逻辑解耦合
-
自定义按键:玩家把 “跳跃键” 从空格改成 C 键,只需要绑定命令对象,无需修改输入逻辑;
-
多平台适配:PC 端用键盘、移动端用触屏,触发的是同一个命令对象,逻辑统一。
3. 技能系统 / 连招系统
-
技能释放:把每个技能封装成命令对象,支持技能撤销(如 BUG 导致的误放)、技能组合(连招);
-
自动战斗:把多个技能命令按顺序放入队列,让角色自动执行。
4. 异步操作的统一管理
- 异步加载资源:把 “加载完成后更新 UI” 封装成命令对象,交给主线程执行,避免线程安全问题。
七、总结(核心要点,新手牢记)
-
命令模式的核心:将操作封装成独立的命令对象,实现调用者与接收者的解耦;
-
核心新增层:命令接口、具体命令类、
CommandManager(命令栈),这是实战中必备的结构; -
核心优势:解耦、支持撤销 / 重做、符合开闭原则、可扩展宏命令;
-
适用场景:操作撤销、输入解耦合、技能系统、连招系统等。
命令模式并不是 “多余的复杂代码”,而是工业级项目中解决特定问题的标准方案。新手可以先从极简版代码入手,理解核心逻辑后,再根据项目需求扩展功能(如宏命令、命令序列化)。
八、命令模式的灵活实践:简化版实现与选择
前面介绍的三层结构(接口 + 具体类 + 管理层)是命令模式的标准实现,但在实际开发中,并非所有场景都需要这么 “重” 的结构。本章节将补充命令模式的简化实践(委托实现),并解答:什么场景下无需三层结构?简化版为何算命令模式?以及如何区分不同实现方案的选择。
1. 核心思想:命令模式的本质是 “思想”,而非 “三层结构”
命令模式的核心是 “封装操作 + 解耦调用者与接收者”,而 “三层结构” 只是实现这一思想的常见方式,而非唯一方式。
GoF 对命令模式的定义核心是:
将 “请求 / 操作” 封装为独立对象,实现 “发出请求的调用者” 与 “执行请求的接收者” 解耦,并支持操作的排队、存储、扩展(如撤销)。
简化版命令模式(委托实现),正是在 “无需扩展功能(如撤销)” 的场景下,用更轻量的方式实现了命令模式的核心思想。
2. 无需使用三层命令模式的场景
当场景满足以下条件时,简化版(委托实现)更合适:
(1)操作简单,无需 “扩展能力”
当操作仅需 “执行”,不需要 “撤销、重做、日志记录” 时,三层结构的 “扩展设计” 成了冗余。
典型案例:
- Unity 子线程更新 UI:子线程执行耗时任务后,仅需通知主线程更新文本、图片等简单 UI;
- 后台任务回调:接口请求成功后,仅需更新页面数据;
- 简单工具的单次操作:游戏中 “领取奖励” 后,仅需弹出提示文本。
(2)追求开发效率,避免 “抽象过度”
三层结构需要为每个操作写一个具体命令类(如UpdateTextCommand、ChangeColorCommand),若操作类型多但逻辑简单,会导致 “类爆炸”。
(3)操作需 “排队执行”,但无需复杂管理
当操作需要 “跨线程调度” 或 “延迟执行”(如子线程任务需主线程执行),但仅需 “排队” 而无需 “存储历史记录” 时,简化版的队列 + 委托足以满足需求。
典型案例:Unity 的MainThreadDispatcher—— 子线程将 UI 操作封装为委托,加入主线程队列,主线程逐帧执行。
3. 简化版(委托实现):为何算命令模式?
简化版完全满足命令模式的核心角色和意图,只是用 C# 的Action/Func委托,隐式替代了三层结构中的 “命令接口” 和 “具体命令类”。
| 命令模式核心角色 | 三层结构实现 | 简化版实现(委托) | 角色职责(两者完全一致) |
|---|---|---|---|
| 命令接口(ICommand) | 自定义ICommand接口(含Execute()) |
System.Action委托(含Invoke()) |
定义 “执行操作” 的统一契约 |
| 具体命令类 | JumpCommand类(封装跳跃操作) |
lambda 表达式(如() => player.Jump()) |
封装 “具体操作逻辑” 和 “接收者” |
| 调用者(Invoker) | CommandManager类(管理命令队列) |
MainThreadDispatcher类(管理委托队列) |
负责 “存储命令”和”调度执行” |
| 接收者(Receiver) | Player组件 |
Player组件 |
实际执行操作的对象 |
代码对比:
1
2
3
4
5
6
7
8
9
10
11
12
// 三层结构实现:跳跃命令
public class JumpCommand : ICommand {
private Player _player;
public JumpCommand(Player player) { _player = player; }
public void Execute() { _player.Jump(); }
}
// 调用者执行:
commandManager.ExecuteCommand(new JumpCommand(player));
// 简化版实现:委托封装命令
Action jumpCommand = () => player.Jump(); // 封装接收者+逻辑
commandQueue.Enqueue(jumpCommand); // 调用者执行
两者的逻辑完全一致:都是 “封装操作→交给调用者→调用者触发执行”,唯一区别是简化版用委托省去了 “自定义接口和类” 的步骤。
4. 关键区分:”单纯用委托” vs “用委托实现命令模式”
核心区别在于:是否满足 “封装操作 + 解耦 + 操作管理” 的命令模式核心意图。
(1)单纯用委托(不是命令模式)——Unity 按钮点击回调
1
2
3
4
button.onClick.AddListener(() => {
Debug.Log("点击成功");
text.text = "点击成功";
});
为什么不是?
- 没有 “解耦”:按钮直接绑定操作逻辑,没有 “调用者” 中间层;
- 没有 “操作管理”:逻辑触发后直接执行,没有 “排队、存储”;
- 核心目的是 “回调”,而非 “封装操作并管控”。
(2)用委托实现命令模式(是命令模式)——Unity 主线程更新 UI
1
2
3
4
5
6
7
8
9
10
11
12
// 子线程:封装操作成委托(命令对象)
Action uiCommand = () => text.text = "任务完成";
// 交给调用者(MainThreadDispatcher)管理
mainThreadDispatcher.Enqueue(uiCommand);
// 主线程:调用者逐帧执行命令
private void Update() {
while (queue.Count > 0) {
var command = queue.Dequeue();
command.Invoke();
}
}
为什么是?
- 解耦:子线程不知道主线程何时执行,主线程不知道命令具体逻辑;
- 操作管理:命令被存储在队列中,支持 “排队执行”;
- 封装操作:委托是独立对象,包含 “接收者” 和 “操作逻辑”。
5. 三层模式 vs 简化版:选择指南
| 维度 | 三层命令模式(接口 + 类) | 简化版命令模式(委托) |
|---|---|---|
| 核心优势 | 支持撤销、重做、日志记录;操作类型清晰 | 代码轻量、开发效率高;避免类爆炸 |
| 核心劣势 | 代码冗余;抽象过度(简单场景下) | 不支持扩展功能;复杂场景下逻辑分散 |
| 适用场景 | 1. 需要撤销/重做(如编辑器、设计工具);2. 操作类型多且需长期维护;3. 需要日志记录 | 1. 简单操作(如 UI 更新、后台任务回调);2. 无需扩展功能;3. 跨线程排队执行 |
| 典型案例 | Word 的 “撤销打字”、PS 的 “历史记录”、审批系统的 “步骤回退” | Unity 子线程更新 UI、APP 的 “接口请求回调”、工具的 “单次操作提示” |
九、总结(核心要点,新手牢记)
命令模式的价值,从来不是 “写出接口 + 类的三层结构”,而是 “用封装和解耦的思想,解决操作管控的问题”。
- 命令模式的核心:将操作封装成独立的命令对象,实现调用者与接收者的解耦;
- 实现方式的选择:
- 当需要 “精细化管控”(撤销、日志)时,选择三层结构;
- 当只需要 “简单执行 + 解耦” 时,选择委托简化版;
- 判断标准:是否封装了操作、是否解耦了调用者与接收者——而非 “是否用了接口” 或 “是否用了委托”。
希望本文能帮助你跳出 “设计模式 = 固定模板” 的误区,真正做到 “因场景择方案”,让设计模式成为提升效率的工具,而非束缚思维的框架。
希望本文能帮助你彻底理解命令模式,并能在 Unity 项目中灵活应用!