如何写出优雅的碰撞处理机制?

时间:2010-09-04 21:27:58

标签: c++ collision

我有点发痒:说我正在制作一个简单的,2D,类似塞尔达的游戏。 当两个对象发生碰撞时,每个对象都应该有一个结果。然而,当主角与某物碰撞时,他的反应完全取决于他碰撞的物体的类型。如果它是一个怪物,他应该反弹,如果它是一堵墙,什么都不应该发生,如果它是一个带有缎带的魔法蓝盒子,他应该愈合,等等(这些只是例子)。

我还应该注意,两件事情都是碰撞的一部分,也就是说,角色和怪物都会发生碰撞事件,而不仅仅是碰撞事件。

你会怎么写这样的代码?我可以想到一些非常不优雅的方法,例如,在全局WorldObject类中使用虚函数来识别属性 - 例如,GetObjectType()函数(返回int,char * s,将对象标识为Monster的任何内容) ,Box或Wall),然后在具有更多属性的类中,比如Monster,可能会有更多的虚函数,比如GetSpecies()。

但是,维护变得很烦人,并导致冲突处理程序中的大型级联切换(或If)语句

MainCharacter::Handler(Object& obj)
{
   switch(obj.GetType())
   {
      case MONSTER:
         switch((*(Monster*)&obj)->GetSpecies())
         {
            case EVILSCARYDOG:
            ...
            ...
         }
      ...
   }

}

还有使用文件的选项,文件可能包含:

Object=Monster
Species=EvilScaryDog
Subspecies=Boss

然后代码可以检索属性,而不需要虚拟函数混乱一切。但是,这并不能解决级联If问题。

然后可以选择为每个案例设置一个函数,比如CollideWall(),CollideMonster(),CollideHealingThingy()。这是我个人最不喜欢的(尽管它们远非可爱),因为它似乎是最难维护的。

有人可以对这个问题提供更优雅的解决方案吗? 感谢您的帮助!

5 个答案:

答案 0 :(得分:11)

我会这样做反之亦然 - 因为如果角色与一个物体碰撞,物体也会与角色碰撞。因此,您可以拥有一个基类Object,如下所示:

class Object  {
  virtual void collideWithCharacter(MainCharacter&) = 0;
};

class Monster : public Object  {
  virtual void collideWithCharacter(MainCharacter&) { /* Monster collision handler */ }
};

// etc. for each object

通常在OOP设计中,虚拟函数是这种情况的唯一“正确”解决方案:

switch (obj.getType())  {
  case A: /* ... */ break;
  case B: /* ... */ break;
}

修改
澄清之后,您需要稍微调整一下。 MainCharacter应该为它可能碰撞的每个对象重载方法:

class MainCharacter  {
  void collideWith(Monster&) { /* ... */ }
  void collideWith(EvilScaryDog&)  { /* ... */ }
  void collideWith(Boss&)  { /* ... */ }
  /* etc. for each object */
};

class Object  {
  virtual void collideWithCharacter(MainCharacter&) = 0;
};

class Monster : public Object  {
  virtual void collideWithCharacter(MainCharacter& c)
  {
    c.collideWith(*this);  // Tell the main character it collided with us
    /* ... */
  }
};

/* So on for each object */

通过这种方式,您可以通知主角关于碰撞,并且可以采取适当的措施。此外,如果您需要一个不应该通知主角的碰撞对象,您只需删除该特定类中的通知调用。

这种方法称为double dispatch

我还会考虑将MainCharacter本身设为Object,将重载移至Object并使用collideWith代替collideWithCharacter

答案 1 :(得分:2)

如何从一个公共抽象类派生所有可碰撞对象(让我们称之为Collidable)。该类可以包含可以通过collission和一个HandleCollision函数更改的所有属性。当两个对象发生碰撞时,您只需在每个对象上调用HandleCollision,另一个对象作为参数。每个对象操纵另一个对象来处理碰撞。这两个对象都不需要知道它刚刚弹回的其他对象类型,并且没有大的switch语句。

答案 2 :(得分:1)

使所有可colidable实体使用collideWith(Collidable)方法实现接口(假设为“Collidable”)。 然后,在您的碰撞检测算法上,如果您检测到A与B发生碰撞,您将调用:

A->collideWith((Collidable)B);
B->collideWith((Collidable)A);

假设A是MainCharacter而B是怪物,并且都实现了Collidable接口。

A->collideWith(B);

将调用以下内容:

MainCharacter::collideWith(Collidable& obj)
{
   //switch(obj.GetType()){
   //  case MONSTER:
   //    ...
   //instead of this switch you were doing, dispatch it to another function
   obj->collideWith(this); //Note that "this", in this context is evaluated to the
   //something of type MainCharacter.
}

这将调用Monster :: collideWith(MainCharacter)方法,你可以在那里实现所有怪物角色行为:

Monster::CollideWith(MainCharacter mc){
  //take the life of character and make it bounce back
  mc->takeDamage(this.attackPower);
  mc->bounceBack(20/*e.g.*/);
}

更多信息:Single Dispatch

希望它有所帮助。

答案 3 :(得分:1)

你所说的“烦人的开关声明”我称之为“一场精彩的比赛”,所以你走在正确的轨道上。

每个交互/游戏规则都有一个功能正是我所建议的。它使查找,调试,更改和添加新功能变得容易:

void PlayerCollidesWithWall(player, wall) { 
  player.velocity = 0;
}

void PlayerCollidesWithHPPotion(player, hpPoition) { 
  player.hp = player.maxHp;
  Destroy(hpPoition);
}

...

所以问题是如何检测这些案例中的每一个。假设你有某种碰撞检测导致X和Y碰撞(就像N ^ 2重叠测试一样简单(嘿,它适用于植物大战和僵尸,而且还有很多事情发生!)或者像扫描和修剪一样复杂+ gjk)

void DoCollision(x, y) {
  if (x.IsPlayer() && y.IsWall()) {   // need reverse too, y.IsPlayer, x.IsWall
     PlayerCollidesWithWall(x, y);    // unless you have somehow sorted them...
     return;
  }

  if (x.IsPlayer() && y.IsPotion() { ... }

  ...

这种风格虽然详细,但

  • 易于调试
  • 易于添加案例
  • 告诉你什么时候 逻辑/设计不一致或 遗漏“哦,如果一个X是一个 球员和墙壁由于 “PosessWall”能力,那么!?!“ (然后让你简单地添加案例 处理那些)

Spore的细胞阶段正好使用这种风格并且大约有100次检查,导致大约70种不同的结果(不包括参数反转)。这只是一个十分钟的游戏,这是整个舞台每6秒1次新的互动 - 现在这是游戏玩法的价值!

答案 4 :(得分:0)

如果我正确地解决了你的问题,我会像

一样
Class EventManager {
// some members/methods
handleCollisionEvent(ObjectType1 o1, ObjectType2 o2);
// and do overloading for every type of unique behavior with different type of objects.
// can have default behavior as well for unhandled object types
}