《游戏编程模式》学习笔记(十)更新方法 Sequencing Patterns

定义

游戏世界管理对象集合。 每个对象实现一个更新方法模拟对象在一帧内的行为。每一帧,游戏循环更新集合中的每一个对象。

这个模式的定义十分直观,简单地说,就是我们的游戏世界有一个大循环Update(),而我们在游戏对象中封装一个update()小方法来存储个体的更新逻辑,同时因为封装在了小方法里,我们可以自由地添加一些类变量来帮助我们存储状态。

代码

假设我们要实现骷髅和雕像这两种敌人,骷髅会不停地巡逻,而雕像则会释放闪电。按照更新方法模式的定义,我们定义Entity

class Entity
{
public:
  Entity()
  : x_(0), y_(0)
  {}

  virtual ~Entity() {}
  virtual void update() = 0;

  double x() const { return x_; }
  double y() const { return y_; }

  void setX(double x) { x_ = x; }
  void setY(double y) { y_ = y; }

private:
  double x_;
  double y_;
};

在游戏世界中,我们定义一个集合来存储这些Entity对象,游戏世界的定义如下

class World
{
public:
  World()
  : numEntities_(0)
  {}

  void gameLoop();

private:
  Entity* entities_[MAX_ENTITIES];//我们定义的游戏对象集合
  int numEntities_;
};

现在,我们在gameLoop()中实现对集合内每个对象的更新

void World::gameLoop()
{
  while (true)
  {
    // 处理用户输入……

    // 更新每个实体
    for (int i = 0; i < numEntities_; i++)
    {
      entities_[i]->update();
    }

    // 物理和渲染……
  }
}

更新方法模式的框架就写完了,实际上很多游戏引擎都使用了这个模式,如Unity框架在多个类中使用了这个模式,包括 MonoBehaviour。
现在我们完善一下细节,来实现骷髅和雕像这两个实体,在他们的update()方法中实现他们的具体行为逻辑

class Skeleton : public Entity
{
public:
  Skeleton()
  : patrollingLeft_(false)
  {}

  virtual void update()
  {
    if (patrollingLeft_)
    {
      setX(x() - 1);
      if (x() == 0) patrollingLeft_ = false;
    }
    else
    {
      setX(x() + 1);
      if (x() == 100) patrollingLeft_ = true;
    }
  }

private:
  bool patrollingLeft_;
};

这里patrollingLeft是我们的本地变量,用于存储状态,在这里指的就是骷髅的方向信息。
对于雕像

class Statue : public Entity
{
public:
  Statue(int delay)
  : frames_(0),
    delay_(delay)
  {}

  virtual void update()
  {
    if (++frames_ == delay_)
    {
      shootLightning();

      // 重置计时器
      frames_ = 0;
    }
  }


private:
  int frames_;
  int delay_;

  void shootLightning()
  {
    // 火光效果……
  }
};

这个模式让我们分离了游戏世界的构建和实现。 这同样能让我们灵活地使用分散的数据文件或关卡编辑器来构建游戏世界。

注意

使用更新方法模式我们需要注意以下的一些东西

  • 将代码分到一帧一帧实现显然会更复杂

有必要记住,将你的行为切片会增加很高的复杂性。

  • 当离开每帧时,你需要存储状态,以备将来继续。

就像刚写的patrollingLeft那样,你需要一些变量来存储状态,以便下一帧时实体知道自己运行到哪一步了

  • 对象逐帧模拟,但并非真的同步

这个很正常,因为你是在按顺序遍历Eneities集合更新各个对象的,A更新了后B才会更新。这也导致了B在更新时可能会看到A的新状态。如果,由于某些原因,你决定不让游戏按这样的顺序更新,你需要双缓冲模式。 那么AB更新的顺序就没有关系了,因为双方都会看对方之前那帧的状态。双缓冲模式和会保证所有的状态在下一帧才更新,在这一帧内,所有实体仍然使用的是之前的老状态。

  • 在更新时修改对象列表需小心

在对Eneitie集合进行更新时候,难免会遇到添加或者删除对象从而修改了这个集合的情况,在这个情况下我们需要注意。

  1. 添加对象

举个例子,假设骷髅守卫被杀死时掉落物品。 使用新对象,你通常可以将其增加到列表尾部,而不引起任何问题。 你会继续遍历这张链表,最终找到新的那个,然后也更新了它。
但这确实表明新对象在它产生的那帧就有机会活动,甚至有可能在玩家看到它之前。 如果你不想发生那种情况,简单的修复方法就是在游戏循环中缓存列表对象的数目,然后只更新那么多数目的对象就停止:

int numObjectsThisTurn = numObjects_;
for (int i = 0; i < numObjectsThisTurn; i++)
{
  objects_[i]->update();
}
  1. 移除对象

你击败了邪恶的野兽,现在它需要被移出对象列表。 如果它正好位于你当前更新对象之前,你会意外地跳过一个对象,(删除对象后后边的对象就会往前移动一个坑位)

for (int i = 0; i < numObjects_; i++)
{
  objects_[i]->update();
}

一种解决方案是小心地移除对象,任何对象被移除时,更新索引。

另一种简单的解决方案是在更新时从后往前遍历列表。 这种方式只会移动已经被更新的对象。

还有一种是在遍历完列表后再移除对象。 将对象标为“死亡”,但是把它放在那里。 在更新时跳过任何死亡的对象。然后,在完成遍历后,遍历列表并删除尸体。如果在更新循环中有多个线程处理对象, 那么你可能更喜欢使用这种方案,推迟任何修改,避免更新时同步线程的开销。

更新方法模式适用的情况

**
• 你的游戏有很多对象或系统需要同时运行。
• 每个对象的行为都与其他的大部分独立。
• 对象需要跟着时间进行模拟。

你可能感兴趣的:(游戏实用技术专栏,读书笔记,游戏,学习,笔记)