游戏编程模式:命令模式(Part III)

3、撤销和恢复(Undo and Redo)

        最后一个例子是该模式最著名的运用。如果一个命令可以事情,那么让其可以撤销它们就只是一小步了。撤销运用于一些策略游戏,在其中你可以回滚一些你不喜欢的移动。在人们用于创作(create)游戏的工具中,这是必不可少的(de rigueur:prescribed or required by fashion, etiquette, or custom)。让你的游戏设计者们憎恨你的事情莫过于给他们一个不能撤销他们因为手贱而导致的错误(fat-fingered mistakes)的关卡编辑器(level editor)了。

    我这里说的话可能是根据个人经验的。

        没有命令模式的话,实现撤销功能困难得令人惊讶。有了它,那就小菜一碟了。假设我们正在制作一个单人的回合制游戏,并且我们想要让用户可以撤销其移动,以便让他们更关注策略,而不是猜测。

        我们已经在使用命令来抽象输入处理了,所以玩家的每一个移动已经封装在其中了。举例来说,移动一个单位可能像这样:

class MoveUnitCommand : public Command
{
public:
  MoveUnitCommand(Unit* unit, int x, int y)
  : unit_(unit),
    x_(x),
    y_(y)
  {}

  virtual void execute()
  {
    unit_->moveTo(x_, y_);
  }

private:
  Unit* unit_;
  int x_, y_;
};


        注意这与我们前面的命令有一点不同。在上一个例子中,我们想要将命令所修改的actor抽象出来。在当前情况下,我们明确地想要将命令与被移动的单位绑定在一起。这个命令的一个实例并不是一个通用的你可以用于很多情境的“移动某物”的操作;相反,它是游戏的回合序列中一个给定的具体的(specific concrete)移动。

        这个演示了实现命令模式的方式的一个变体。在某些情况下,像我们的前两个例子,一个命令是一个代表着“一件可以被做的事情”的可以复用的对象。我们先前的输入处理器附着在单个的一个命令对象上,并且每当正确的按钮被按下的时候,就会调用其execute()方法。

        在这里,命令更加具体。它们代表着在一个具体的时间点可以被做的事情。这意味着每当玩家选择一个移动的时候,输入处理代码都会创建它的一个实例。就像这样:

Command* handleInput()
{
  Unit* unit = getSelectedUnit();

  if (isPressed(BUTTON_UP)) {
    // Move the unit up one.
    int destY = unit->y() - 1;
    return new MoveUnitCommand(unit, unit->x(), destY);
  }

  if (isPressed(BUTTON_DOWN)) {
    // Move the unit down one.
    int destY = unit->y() + 1;
    return new MoveUnitCommand(unit, unit->x(), destY);
  }

  // Other moves...
  
  return NULL;
}

    当然,在像C++这样的没有垃圾回收机制的语言中,这意味着这些能够命令的代码还需要为释放其内存而负责。

        “这些命令只有一个用途”的事实很快就会变成我们的优势。为了让命令变得可以撤销,我们为每个命令类定义另外一个需要实现的操作:

class Command
{
public:
  virtual ~Command() {}
  virtual void execute() = 0;
  virtual void undo() = 0;
};

        一个undo()方法反转由对应的execute()方法所带来的游戏状态的变化。下面是我们前面的移动命令,这次带上了对撤销的支持:

class MoveUnitCommand : public Command
{
public:
  MoveUnitCommand(Unit* unit, int x, int y)
  : unit_(unit),
    xBefore_(0),
    yBefore_(0),
    x_(x),
    y_(y)
  {}

  virtual void execute()
  {
    // Remember the unit's position before the move
    // so we can restore it.
    xBefore_ = unit_->x();
    yBefore_ = unit_->y();
    
    unit_->moveTo(x_, y_);
  }
  
  virtual void undo()
  {
    unit_->moveTo(xBefore_, yBefore_);
  }

private:
  Unit* unit_;
  int xBefore_, yBefore_;
  int x_, y_;
};

        注意我们给这个类增加了一些状态。当一个单位移动的时候,它会忘记它曾经所在的地方。如果我们想要撤销那个移动的话,我们得要自己记住该单位之前的位置,而这就是xBefore_和yBefore_所做的事情。

    这看上去应该是备忘录(Memento)模式所做的事情,但是我发现它的效果并不好。由于命令倾向于仅仅修改一个对象的状态的一小部分,记录(snapshotting)它其余的数据是对内存的浪费。手动地存储你改变的那一部分数据更加划算。

    持久性数据结构(persistent data structures)是另一个选择。用这个的话,对一个对象的每个修改都会返回一个新的对象,并保持原有的对象不变。通过聪明的实现,这些新的对象会与原有的对象共享数据,所以这比克隆整个对象要划算得多。

    使用持久性数据结构的话,每一个命令都会存储在命令被执行之前的对象的一个引用,而撤销仅仅意味着转换回旧的对象。

        为了让玩家撤销一个移动,我们保存他们执行的上一个命令。当他们敲打Ctrl+Z时,我们调用命令的undo()方法。(如果他们已经撤销过了,那么这个行为就变成了“恢复”,而我们则重新执行这个命令。)

        支持多层级的撤销操作并没有困难到哪儿去。我们保存命令的一个列表以及指向“当前”命令的一个引用,而不是记住上一个命令。当玩家执行一个命令的时候,我们将它附加到列表最后,并将“当前”命令指向它。

游戏编程模式:命令模式(Part III)_第1张图片

        当玩家选择“撤销”的时候,我们撤销当前的命令,并将当前的指针往回移动。当他们选择“恢复”的时候,我们将当前指针前进一个单位,并执行该命令。如果他们在撤销某个命令后选择了一个新命令,列表中在当前命令后面的所有命令都将被丢弃。

        当我第一次在一个关卡编辑器中实现这个的时候,我感觉像是一个天才一样。我震惊了:它是如此直截了当,而且如此管用。需要经过训练才能够保证每个数据的修改都是通过一个命令完成的,但是一旦你做到了这一点,剩下的就容易了。

    撤销也许在游戏中不常见,但是重(re-play)是常见的。一个天真的实现是,记录整个游戏每一帧的状态以使得其可以被重演,但是那会用掉太多的内存。

    相反,很多游戏记录每一个实体(entity)每一帧所执行的命令所组成的集合。要重演游戏的话,引擎只需要运行正常的游戏模拟并执行提前记录好的命令就行了。


你可能感兴趣的:(游戏开发,设计模式)