一、命令模式 + 核心定义

关键字:命令模式 + 核心定义

先拆解命令模式的核心: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帧/秒) } }

总结:命令模式的核心价值

✨ 命令模式的本质是“封装请求”,它带来的核心优势:

  1. 解耦:请求发送者(输入、AI)和接收者(角色、单位)分离,互不依赖
  2. 灵活:支持自定义按键、多角色控制、网络同步、撤销/重做
  3. 可扩展:新增命令无需修改原有代码,符合“开闭原则”
  4. 可管理:命令可存储、记录、回放,便于调试和功能扩展

无论是简单的输入绑定,还是复杂的撤销/回放,命令模式都能让你的代码更优雅、更灵活。下次遇到“需要灵活控制行为”的场景,记得试试这个“万能遥控器”哦!🚀