一、命令模式 + 核心定义
关键字:命令模式 + 核心定义
先拆解命令模式的核心:GoF 的定义太绕,咱们直接看通俗版——
命令模式 = 具现化的方法调用
“具现化”(Reify)就是把抽象的“方法调用”变成具体的“对象”——这个对象能存储、传递、延迟执行,甚至撤销操作。简单说:把“做什么”和“谁去做”“什么时候做”分离开。
更直白的解释:
命令模式是“回调的面向对象实现”,和函数指针、闭包、第一公民函数本质上解决的是同一个问题——让“行为”像数据一样被操作。但命令模式的优势在于:
- 支持多操作(比如执行 + 撤销)
- 可存储命令历史(用于回放、日志)
- 解耦请求发送者和接收者(比如输入和游戏行为)
你用遥控器换台——遥控器(命令发送者)不用知道电视(命令接收者)是怎么换台的,只需要按下按钮(触发命令对象),命令对象会告诉电视该做什么。这就是命令模式的核心逻辑!
二、配置输入 + 解耦硬编码
关键字:配置输入 + 解耦硬编码
游戏开发中最常见的场景:输入处理。比如手柄按键对应游戏行为,硬编码写法虽然简单,但无法支持玩家自定义按键——这时候命令模式就能派上大用场!
1. 硬编码的痛点
先看一段典型的硬编码输入处理:
void InputHandler::handleInput()
{
if (isPressed(BUTTON_X)) jump();
else if (isPressed(BUTTON_Y)) fireGun();
else if (isPressed(BUTTON_A)) swapWeapon();
else if (isPressed(BUTTON_B)) lurchIneffectively();
}
这段代码在游戏循环中每帧调用,逻辑清晰,但问题很明显:
- 按键和行为强耦合:想让玩家把“跳跃”绑定到 B 键,必须修改代码
- 扩展性差:新增行为(比如“冲刺”)需要修改
handleInput(),违反“开闭原则”
2. 命令模式改造步骤
步骤 1:定义命令基类(接口)
所有命令都继承这个基类,只包含一个 execute() 方法(执行命令):
class Command
{
public:
virtual ~Command() {}
virtual void execute() = 0; // 纯虚函数,子类必须实现
};
小技巧:如果接口只有一个无返回值的方法,大概率能用命令模式!
步骤 2:实现具体命令类
为每个游戏行为创建命令子类,封装具体操作:
// 跳跃命令
class JumpCommand : public Command
{
public:
virtual void execute() { jump(); }
};
// 开火命令
class FireCommand : public Command
{
public:
virtual void execute() { fireGun(); }
};
// 切换武器命令
class SwapWeaponCommand : public Command
{
public:
virtual void execute() { swapWeapon(); }
};
步骤 3:输入处理器存储命令指针
输入处理器不再直接调用行为,而是存储每个按键对应的命令对象:
class InputHandler
{
public:
void handleInput();
// 提供绑定方法,允许动态修改按键对应的命令
void bindCommand(BUTTON button, Command* cmd) {
switch(button) {
case BUTTON_X: buttonX_ = cmd; break;
case BUTTON_Y: buttonY_ = cmd; break;
// 其他按键...
}
}
private:
Command* buttonX_;
Command* buttonY_;
Command* buttonA_;
Command* buttonB_;
};
步骤 4:修改输入处理逻辑
按键触发时,调用命令对象的 execute() 方法,而非直接调用行为:
void InputHandler::handleInput()
{
if (isPressed(BUTTON_X)) buttonX_->execute();
else if (isPressed(BUTTON_Y)) buttonY_->execute();
else if (isPressed(BUTTON_A)) buttonA_->execute();
else if (isPressed(BUTTON_B)) buttonB_->execute();
}
3. 改造后的优势
- 支持自定义按键:玩家想把“跳跃”绑定到 B 键,只需调用
bindCommand(BUTTON_B, new JumpCommand()) - 扩展性强:新增“冲刺”行为,只需创建
DashCommand类,无需修改输入处理器代码 - 解耦输入和行为:输入处理器只负责“触发命令”,不用知道命令具体做什么
空对象模式优化:如果想让某些按键“无操作”,可以创建 NullCommand 类(execute()空实现),避免 NULL 指针检测:
class NullCommand : public Command
{
public:
virtual void execute() {} // 什么也不做
};
三、角色解耦 + 多主体控制
关键字:角色解耦 + 多主体控制
上面的实现还有个局限:jump()、fireGun()等函数默认操作“玩家角色”,无法控制其他角色(比如 AI 角色、队友)。命令模式可以进一步解耦“命令”和“操作对象”!
1. 优化命令基类
让 execute() 方法接收一个“角色对象”参数,命令不再绑定固定角色:
class Command
{
public:
virtual ~Command() {}
// GameActor 是游戏角色的基类(包含 jump()、fireGun()等方法)
virtual void execute(GameActor& actor) = 0;
};
2. 修改具体命令类
命令通过参数获取操作对象,实现“通用命令”:
class JumpCommand : public Command
{
public:
virtual void execute(GameActor& actor) {
actor.jump(); // 操作传入的角色,而非固定角色
}
};
class FireCommand : public Command
{
public:
virtual void execute(GameActor& actor) {
actor.fireGun();
}
};
3. 调整输入处理和执行逻辑
输入处理器返回命令对象(而非直接执行),由调用者决定操作哪个角色:
// 输入处理器返回命令对象
Command* InputHandler::handleInput()
{
if (isPressed(BUTTON_X)) return buttonX_;
if (isPressed(BUTTON_Y)) return buttonY_;
// 其他按键...
return NULL;
}
// 执行命令时传入具体角色
Command* command = inputHandler.handleInput();
if (command) {
command->execute(playerActor); // 控制玩家
// command->execute(enemyActor); // 也可以控制敌人
}
4. 扩展:AI 控制也能用命令模式
不仅玩家输入可以用命令,AI 控制也能复用这套逻辑——AI 生成命令对象,角色执行命令:
// AI 生成命令(比如让敌人开火)
Command* EnemyAI::decideCommand()
{
if (enemy->isNearPlayer()) {
return new FireCommand(); // 敌人靠近玩家,生成开火命令
}
return new JumpCommand(); // 否则跳跃
}
// 执行 AI 命令
Command* aiCmd = enemyAI.decideCommand();
if (aiCmd) {
aiCmd->execute(enemyActor);
}
5. 核心价值
- 命令与角色解耦:同一个“跳跃命令”可以让玩家、敌人、NPC 都跳跃
- 统一控制接口:玩家输入和 AI 控制共用一套命令系统,代码更统一
- 支持网络同步:命令可以序列化后通过网络传输(比如多人游戏中同步玩家操作)
四、撤销 / 重做 + 命令历史
关键字:撤销 / 重做 + 命令历史
命令模式的终极用法:支持撤销(Undo)和重做(Redo)。这在策略游戏、关卡编辑器中至关重要——玩家误操作后可以回滚,设计师调整场景后可以反悔。
1. 扩展命令基类:添加 undo()方法
要实现撤销,命令需要记录“执行前的状态”,并提供回滚方法:
class Command
{
public:
virtual ~Command() {}
virtual void execute() = 0; // 执行命令
virtual void undo() = 0; // 撤销命令
};
2. 实现可撤销的具体命令
以“移动单位”命令为例,需要记录单位移动前的位置,撤销时恢复:
class MoveUnitCommand : public Command
{
public:
// 构造时传入要移动的单位和目标位置
MoveUnitCommand(Unit* unit, int targetX, int targetY)
: unit_(unit), targetX_(targetX), targetY_(targetY),
prevX_(0), prevY_(0) {}
// 执行命令:记录原位置,移动单位
virtual void execute() {
prevX_ = unit_->x(); // 保存移动前的 X 坐标
prevY_ = unit_->y(); // 保存移动前的 Y 坐标
unit_->moveTo(targetX_, targetY_); // 执行移动
}
// 撤销命令:恢复到移动前的位置
virtual void undo() {
unit_->moveTo(prevX_, prevY_);
}
private:
Unit* unit_; // 要移动的单位
int prevX_, prevY_;// 移动前的位置(用于撤销)
int targetX_, targetY_; // 目标位置
};
3. 管理命令历史:实现撤销 / 重做
用“命令列表”存储执行过的命令,用“当前指针”标记当前位置,实现多步撤销 / 重做:
class CommandHistory
{
public:
// 执行命令并加入历史
void executeCommand(Command* cmd) {
cmd->execute();
// 清除当前指针之后的命令(比如撤销后新增命令,之前的重做记录失效)
commands_.erase(commands_.begin() + currentIdx_ + 1, commands_.end());
commands_.push_back(cmd);
currentIdx_++; // 更新当前指针
}
// 撤销上一步
bool undo() {
if (currentIdx_ < 0) return false; // 没有可撤销的命令 commands_[currentIdx_]->undo();
currentIdx_–;
return true;
}
// 重做上一步
bool redo() {
if (currentIdx_ >= commands_.size() – 1) return false; // 没有可重做的命令
currentIdx_++;
commands_[currentIdx_]->execute();
return true;
}
private:
vector<Command*> commands_; // 命令历史列表
int currentIdx_ = -1; // 当前命令指针(初始为 -1,无命令执行)
};
4. 使用示例:回合制游戏的移动撤销
// 创建命令历史管理器
CommandHistory history;
// 玩家按下“上移”键,生成移动命令
Command* moveCmd = new MoveUnitCommand(selectedUnit, 5, 3);
history.executeCommand(moveCmd); // 执行命令并记录
// 玩家想撤销移动,按下 Ctrl+Z
history.undo(); // 单位恢复到原来的位置
// 玩家想重做移动,按下 Ctrl+Y
history.redo(); // 单位再次移动到目标位置
5. 关键要点
- 命令需记录状态:撤销的核心是“恢复执行前的状态”,命令对象必须存储足够的状态信息(比如移动前的位置)
- 避免重复内存泄漏:在 C ++ 等无 GC 语言中,需手动管理命令对象的内存(比如命令历史销毁时释放所有命令)
- 支持重做:新增命令时要清除当前指针之后的命令(比如撤销 3 步后新增命令,之前的 3 步重做记录失效)
五、命令模式 + 不同语言实现
关键字:命令模式 + 不同语言实现
前面的例子用 C ++ 实现(面向对象风格),但命令模式并非只能用类——在支持闭包、第一公民函数的语言(如 JavaScript、Python)中,用函数实现更简洁!
1. JavaScript 实现(闭包风格)
闭包可以自动捕获状态,无需定义类就能实现命令模式:
// 普通命令(无撤销)
function makeJumpCommand(actor) {
return function() {
actor.jump();
};
}
// 可撤销的移动命令(闭包捕获状态)
function makeMoveUnitCommand(unit, targetX, targetY) {
let prevX, prevY; // 闭包捕获,存储移动前的位置
return {
execute: function() {
prevX = unit.x;
prevY = unit.y;
unit.moveTo(targetX, targetY);
},
undo: function() {
unit.moveTo(prevX, prevY);
}
};
}
// 使用示例
const player = {
x: 1,
y: 1,
moveTo: function(x,y) {
this.x = x;
this.y = y;
console.log(` 移动到(${x},${y})`);
}
};
const moveCmd = makeMoveUnitCommand(player, 3, 3);
moveCmd.execute(); // 输出:移动到(3,3)
moveCmd.undo(); // 输出:移动到(1,1)
2. 类 vs 函数:怎么选?
用类实现:适合需要多操作(execute+undo+redo)、状态复杂的场景,代码结构清晰,适合大型项目
用函数 / 闭包实现:适合简单命令(仅执行)、快速开发的场景,代码简洁,无需定义大量类
核心原则:如果命令需要多个方法(比如 undo、日志记录),用类;如果只是简单的“执行一个行为”,用函数 / 闭包更高效。
六、命令模式 + 实际应用场景
关键字:命令模式 + 实际应用场景
除了前面的输入处理、撤销 / 重做,命令模式还有很多实用场景:
- 日志记录与重放:记录游戏中所有命令(比如玩家操作、AI 行为),重放时依次执行命令,实现“录像回放”功能
- 任务队列:将命令加入队列,按顺序执行(比如游戏加载时的初始化命令队列)
- 事务处理:多个命令组成一个事务,要么全部执行,要么全部撤销(比如策略游戏中“建造建筑”需要消耗资源 + 创建建筑,失败则回滚)
- 网络同步:将命令序列化(比如转为 JSON/protobuf),通过网络传输到其他客户端,实现多人游戏操作同步
示例:游戏回放功能
// 记录命令(游戏运行时)
vector<Command*> replayCommands;
void onCommandExecuted(Command* cmd) {
replayCommands.push_back(cmd->clone()); // 克隆命令,避免原命令被修改
}
// 重放命令(游戏回放时)
void replayGame() {
for (Command* cmd : replayCommands) {
cmd->execute();
sleep(16); // 模拟每帧间隔(60 帧 / 秒)
}
}
总结:命令模式的核心价值
✨ 命令模式的本质是“封装请求”,它带来的核心优势:
- 解耦:请求发送者(输入、AI)和接收者(角色、单位)分离,互不依赖
- 灵活:支持自定义按键、多角色控制、网络同步、撤销 / 重做
- 可扩展:新增命令无需修改原有代码,符合“开闭原则”
- 可管理:命令可存储、记录、回放,便于调试和功能扩展
无论是简单的输入绑定,还是复杂的撤销 / 回放,命令模式都能让你的代码更优雅、更灵活。下次遇到“需要灵活控制行为”的场景,记得试试这个“万能遥控器”哦!🚀
“`