意图

  模拟一批相互独立的物体,让他们每次只进行一帧的动作。

  动机

  玩家控制着强大的瓦尔基里(北欧神话中奥丁神的婢女之一),去偷巫王遗留的稀世珍宝。她试探着接近巫王宏伟的藏宝室,然后…没有遇到任何阻击。没有被诅咒的雕像向她射击。没有不死的骷髅兵在入口巡逻。她只是径直走过去用连锁勾起战利品。游戏结束,你赢了。

  呃,不会吧。

  藏宝室里需要有守卫的敌人,让我们的英雄去们。首先,我们需要一队带动作的骷髅兵,在门前来回巡逻。如果忽略掉你可能已经掌握的游戏编程内容,最简单的,让他们来回巡逻的代码就像这样:

 

  1. while (true)
  2. {
  3. // Patrol right.
  4. for (double x = 0; x < 100; x++)
  5. {
  6. skeleton.setX(x);
  7. }
  8.  
  9. // Patrol left.
  10. for (double x = 100; x > 0; x--)
  11. {
  12. skeleton.setX(x);
  13. }
  14. }

  开始更新heroine时,i是1。她击败了野兽所以野兽被从数组中清除。heroine被置换到了0,hapless peasant被置换到了1。更新完heroine后,i变成了2.就像有图所示hapless peasant没有被更新,直接越过去了。

  一种解决方法是在移除对象的时候小心点,把迭代变量也考虑进去。另一种是在遍历完整个列表后在处理移除。把这些要移除的对象标记成“dead”,但是仍放在那里。在更新的时候,确保略过那些死掉的对象,更新完后。再遍历一遍列表去清除掉这些尸体。

  实例代码

  这个模式非常直观,因此示例代码看起来有些多余。但这不代表这个模式没用。它是一种很简洁的解决方案。

  具体起见,我们从头做一个简单的实例。从一个Entity类开始,它表示那些骷髅和他们的状态。

 

  1. 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_;
                                • };

  传递时间

  目前为止,我们都假设每次调用update,都用的的固定时间间隔。

  我当然希望这样做,但是更多游戏使用的时可变的时间间隔。每次游戏循环可能在模拟更多或者更少的内容,因此这个时间取决于上一帧处理和渲染的时间。

  这意味着每次update都需要知道,虚拟时钟走了多长时间,所以你需要把消耗的时间传入。例如,我们可以这样处理巡逻骷髅的时间间隔:

 

  1. void Skeleton::update(double elapsed)
    • {
      • if (patrollingLeft_)
        • {
          • x -= elapsed;
            • if (x <= 0)
              • {
                • patrollingLeft_ = false;
                  • x = -x;
                    • }
                      • }
                        • else
                          • {
                            • x += elapsed;
                              • if (x >= 100)
                                • {
                                  • patrollingLeft_ = true;
                                    • x = 100 - (x - 100);
                                      • }
                                        • }
                                          • }
复制代码
  现在,随着耗费时间的增长,骷髅移动的距离也会变大。你也可以看到这个时间间隔的变量带来的额外的复杂度。如果时间间隔太长,骷髅有可能会超出巡逻范围,这个我们要小心处理。

  设计决策

  对这样一个简单的模式,可讨论的点比较少,但是仍然有几个需要注意的地方。

  update方法放在哪个类里

  最明显最重要的决策就是,你要把update放到哪个类里。

  1> Entity类

  这是最简单的选项,如果你只有一个entity类,或者entity的种类不多,可以这样用。但是实际上的游戏工业离这个条件相去甚远。

  每次用子类化Entity的方式去实现一个新行为,时非常脆弱和痛苦的,因为你有大量不同类型的Entity。你会发现有时候你不得不用一些不优雅的方式复用代码,去将就一个继承结构,然后你就蒙逼了。

  2> Component类

  如果你已经使用Compnent模式了,那就很容易了。它可以让每一个组件相互独立得更新。同时,Update Method模式让你实现了游戏中Entity得解偶。渲染,物理,和AI只需要关注自己就可以了。

  3> Delegate类

  有另外几种模式可以实现代理另外一个对象的行为。State模式可以让你通过改变代理对象去修改被代理对象的行为。Type Object模式可以让很多同类型的Entity之间共享行为。

  如果你用到这些模式,就可以很自然地把update放进代理类中。这样,你仍然可以在主类中放update,但是不需要虚拟函数,只需要转调到代理对象中即可:

 

  1. void Entity::update()
    • {
      • // Forward to state object.
        • state_->update();
          • }
复制代码
  未激活的对象如何处理

  游戏中经常有这样的一些对象,处于各种各样的原因,他们临时不需要更新。他们可能无效,可能在屏幕以外,可能未解锁。如果有大量类似的对象存在,每一帧都去遍历这样的对象可能浪费CPU时钟。

  一个可选的解决方法是维护一个激活对象的子集,这个子集中的元素才会被更新。当一个对象无效后,就从这个集合中移除。当再次被激活时,就添加回来。这样,你就可以遍历那些真正起作用的对象了。

  1> 如果用同一个集合保存无效对象

  这样做浪费时间,对于无效对象,你仍然需要访问它是否有效的标记,或者调用一个什么都不做的方法。

  2> 如果用另外一个集合保存激活对象

  你需要额外的内存去保存第二个集合。但仍然需要一个主集合去保存所有的Entity,以备不时之需。这样,这个集合严格来说就是多余的。如果速度比内存更敏感(通常都是这样),这就是个有用的方案。

  另外一个选择同样需要两个集合,不过另一个只保存无效对象,而不是所有的。

  你必须保证这两个集合同步。当对象被创建或者完全销毁(而不是临时失效)时,你需要记住,要修改主集合和激活对象集合。

  这里有一个参考的标准就是你有多少无效对象。无效对象越多,把他们分离到一个单独集合从而节约游戏循环的遍历时间,就显得越重要。

www.wantgame.net编译 【原译文

相关阅读:游戏编程设计模式——Game Loop
锐亚教育

锐亚教育,游戏开发论坛|游戏制作人|游戏策划|游戏开发|独立游戏|游戏产业|游戏研发|游戏运营| unity|unity3d|unity3d官网|unity3d 教程|金融帝国3|8k8k8k|mcafee8.5i|游戏蛮牛|蛮牛 unity|蛮牛