【问题标题】:How does the command pattern solve the problem of hard-wired commands/requests?命令模式如何解决硬连线命令/请求的问题?
【发布时间】:2022-01-12 01:46:35
【问题描述】:

我正在阅读 Robert Nystrom 的 Game Programming Patterns,并且对命令模式有疑问。

Configuring Input 部分的第一个示例中,if/else 语句将游戏操作硬编码到控制台按钮:

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();
}

由于键映射在编译时是硬编码的,因此用户无法在运行时根据自己的喜好更改/配置它们。然后引入命令模式作为解决此问题的方法:

// ***Command interface***
class Command
{
public:
  virtual ~Command() {}
  virtual void execute() = 0;
};

// ***Concrete commands***
class JumpCommand : public Command
{
public:
  virtual void execute() { jump(); }
};

class FireCommand : public Command
{
public:
  virtual void execute() { fireGun(); }
};

[...]


// ***Input handler***
class InputHandler
{
public:
  void handleInput();

  // Methods to bind commands...

private:
  Command* buttonX_;
  Command* buttonY_;
  Command* buttonA_;
  Command* buttonB_;
};

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();
}

问题

我不清楚命令模式如何帮助使输入映射在运行时可配置。 GUI 中必须有一些表允许用户为每个操作类型指定一个键:,但是我们如何在运行时创建这些键绑定?

我认为我们需要使用 new 关键字来初始化指针,例如buttonX_ = new JumpCommand;,但我不确定如何创建绑定,我不明白为什么不能在运行时使用 if/else 来完成。

我对 JS 有一些经验,而对 C++ 没有经验,所以如果熟悉这两种语言的人能帮助我充实/理解这个示例中发生的事情,我将不胜感激。

【问题讨论】:

  • 正如您所说,您需要通过初始化指针来设置绑定,因此您可能有一组默认绑定,您可以将按钮 x 分配给跳转等等。然后你可能有一种方法让用户配置绑定,例如他们可以选择左手设置,所以你只需将指针重新分配给新命令。如果您有不同的界面,使用命令也会有所帮助,例如使用键盘时您可能会跳到空格键等。
  • @Hovercraft Full Of Eels,我经常遇到这两种语言的命令模式示例,由于设计模式不是特定于语言的,我想也许有人在 Java 中使用过命令模式来解决这个问题将能够阐明一些问题。不过,我并没有违反社区准则的意思,我已经删除了这个标签。
  • 很公平。感谢您的回复。
  • @Ian4264,是的,我想看看重新分配是如何完成的。目前,我想象我们在 GUI 中有某种键设置表,其中每行的第一列是命令名称,例如“跳转”,第二列是某种输入,允许用户指定他们想要用来调用该命令的键,例如空格键。但是为什么我们需要命令模式在运行时进行这些绑定呢?将这个表转换成 格式的地图/字典然后使用简单的 if/else 来执行 if(isPressed(keymap["jump"])) jump() 是否就足够了? ——

标签: c++ oop design-patterns game-development


【解决方案1】:

实施

我对代码进行了一些更改,使其更简单。所以,这里是命令:

using Command = void (*)();
void jump(); // TODO
void fire(); // TODO

和按钮:

enum class Button { x, y, a, b };
bool pressed(Button); // TODO

以下是如何存储 [customizable key -> Command] 映射并用于处理输入。它与您介绍的有点不同:我 static_cast Button std::size_t 使用 Buttons 作为“键”; Commands 是“价值观”:

class Buttons {
    std::array<Command, 4> commands{jump, jump, fire, fire};
public:
    void rebind(Button button, Command command) {
        commands[static_cast<std::size_t>(button)] = command;
    }
    void handle() {
        for (std::size_t i{}; i < commands.size(); ++i)
            if (pressed(static_cast<Button>(i)))
                commands[i]();
    }
};

所以,如果结果是用户想要,例如使用x 跳转,应该调用rebind(Button::x, jump)。原方案没有提供统一的重新绑定,因为每个Command都是一个单独的字段,所以我改用了数组。

为什么?

但是为什么我们需要命令模式在运行时进行这些绑定呢?

我们没有,这只是一种选择,一种常见的方法。

将这个表转换成 格式的地图/字典然后使用简单的 if/else 来执行 if(isPressed(keymap["jump"])) jump() 是否就足够了?

如果将字符串名称更改为命名的整数常量、字典 - 到数组,我更喜欢您的解决方案,因为它避免了速度较慢且 template/overload-incompatible 的间接/虚拟函数调用。

【讨论】: