游戏设计中的观察者模式

游戏设计中的观察者模式

观察者模式是最初的四种模式中使用最广泛、最广为人知的一种,但是在游戏设计领域中,观察者模式确并不是十分常见。今天来给大家举例说明观察者模式在游戏设计中的应用。

成就解锁系统

假设我们在游戏中添加了一个成就系统。它将有几十个不同的成就,玩家可以通过完成特定的行为来获得这些成就。比如说“杀死100个怪物”、“从桥上跌落”等等。

这是很难简单明了地实现的,因为我们有一个庞大的可以通过各种不同的行为来实现的成就系统。如果我们不小心的话,成就系统的各个分支会充斥我们代码库的各个角落。当然,“从桥上跌落”多少和物理引擎有关,但是我们真的希望看到在冲突解决算法的线性代数中调用unlockFallOffBridge()方法吗?

我们想要的是,像往常一样,把所有与游戏的一个方面有关的代码很好地集中在一个地方。挑战在于成就是由游戏玩法的不同方面所触发的。如何在不将成就代码与所有代码耦合的情况下工作?

这就是观察者模式的作用。它允许一段代码宣布一些事件发生了,而不关心谁收到了通知。

例如,我们有一段物理代码用来处理重力并可以跟踪哪些物体在光滑的平面上滚动,哪些物体正直线下降,走向死亡。要实现“从桥上摔下来”的成就,我们可以直接那里添加成就代码,但那将会十分糟糕。相反,我们可以这样做:

void Physics::updateEntity(Entity& entity)
{
  bool wasOnSurface = entity.isOnSurface();
  entity.accelerate(GRAVITY);
  entity.update();
  if (wasOnSurface && !entity.isOnSurface())
  {
    notify(entity, EVENT_START_FALL);
  }
}

这么做只是说明有一个东西跌落了下来,而并不在意有没有人关心。

成就系统自己进行注册,这样每当物理代码发送通知时,成就系统就会收到通知。然后,成就系统就可以检查坠落的物体是否是玩家在游戏中的英雄,以及他坠落的位置是否是一座桥梁。如果是这样的话,它就可以展示菜单来告诉玩家已解锁对应的成就,这样它就在不涉及物理代码的情况下完成了所有这些工作。

工作原理

下面简单介绍一下观察者模式的设计原理

观察者

我们将从nosy类开始,它想知道另一个对象什么时候做了一些特定的事情。这些对象是由这个接口定义的:

class Observer
{
public:
  virtual ~Observer() {}
  virtual void onNotify(const Entity& entity, Event event) = 0;
};

任何实现它的具体类都成为一个观察者。在这个例子中,这是成就系统:

class Achievements : public Observer
{
public:
  virtual void onNotify(const Entity& entity, Event event)
  {
    switch (event)
    {
    case EVENT_ENTITY_FELL:
      if (entity.isHero() && heroIsOnBridge_)
      {
        unlock(ACHIEVEMENT_FELL_OFF_BRIDGE);
      }
      break;

      // Handle other events, and update heroIsOnBridge_...
    }
  }

private:
  void unlock(Achievement achievement)
  {
    // Unlock if not already unlocked...
  }

  bool heroIsOnBridge_;
};

主体

通知方法由被观察的对象调用。这个对象即为“主体”。它有两个工作。首先,它有一份观察者的名单,他们非常耐心地等待着它的来信:

class Subject
{
private:
  Observer* observers_[MAX_OBSERVERS];
  int numObservers_;
};

重要的一点是,主体公开了修改列表的公共API:

class Subject
{
public:
  void addObserver(Observer* observer)
  {
    // Add to array...
  }

  void removeObserver(Observer* observer)
  {
    // Remove from array...
  }

  // Other stuff...
};

这允许外部代码控制谁接收通知。主体与观察者交流,但与观察者不耦合。在我们的示例中,没有一行物理代码会提到成就。然而,它仍然可以与成就系统对话。这就是这个模式的巧妙之处。

同样重要的是,主体有一个观察者列表,而不是一个单一的观察者。它确保观察者不会隐式地彼此耦合。例如,假设音频引擎也观察秋季事件,以便它能播放适当的声音。如果主体只支持一个观察者,当音频引擎自己注册时,就会取消对成就系统的注册。

这意味着这两个系统会互相干扰——而且以一种特别恶劣的方式,因为第二个系统会使第一个系统失效。支持一个观察者列表,可以确保每个观察者都被独立对待。据他们所知,他们每个人都是世界上唯一关注这个话题的人。

主体的另一个工作就是发送通知:

class Subject
{
protected:
  void notify(const Entity& entity, Event event)
  {
    for (int i = 0; i < numObservers_; i++)
    {
      observers_[i]->onNotify(entity, event);
    }
  }

  // Other stuff...
};

可观察的物理引擎

现在,我们只需要将所有这些连接到物理引擎中,这样它就可以发送通知,而成就系统就可以自己连接起来接收通知。我们将保持接近原始设计模式和继承主体:

class Physics : public Subject
{
public:
  void updateEntity(Entity& entity);
};

这让我们可以在Subject protected中使用notify()。通过这种方式,派生的物理引擎类可以调用它来发送通知,但是它外部的代码不能。同时,addObserver()和removeObserver()是公共的,所以在物理系统中的任何东西都可以观察到它。

现在,当物理引擎做了一些值得注意的事情时,它调用notify(),就像前面的示例一样。它将遍历观察者列表并给他们所有的提示。

总结

观察者模式解决了项目中的耦合问题,使游戏中对象之间的通信更加容易,提高了代码的重用能力。

posted @ 2018-10-24 19:39  Henkel  阅读(695)  评论(0编辑  收藏  举报