命令模式 – 游戏编程的“万能遥控器”:封装请求,解锁灵活控制🚀

239次阅读

一、命令模式 + 核心定义

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

先拆解命令模式的核心: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. 可管理:命令可存储、记录、回放,便于调试和功能扩展

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

“`

正文完
 0