11

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

我还应该注意,这两件事都是碰撞的一部分,也就是说,碰撞事件应该同时发生在角色和怪物身上,而不仅仅是其中之一。

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

但是,这变得很烦人,并导致碰撞处理程序中的大型级联 switch(或 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()。这是我个人最不喜欢的(尽管它们都远非讨人喜欢),因为它似乎维护起来最麻烦。

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

4

5 回答 5

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 */

这样你就可以通知主角发生碰撞,它可以采取适当的行动。此外,如果您需要一个不应该通知主角有关碰撞的对象,您可以删除该特定类中的通知调用。

这种方法称为双重分派

我还会考虑将 MainCharacter 本身设置为Object,将重载移动到Object并使用collideWith而不是collideWithCharacter.

于 2010-09-04T21:38:37.237 回答
2

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

于 2010-09-04T21:38:28.727 回答
1

使用 collideWith(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.*/);
}

更多信息:单次派遣

希望能帮助到你。

于 2010-09-04T22:37:51.547 回答
1

你所说的“令人讨厌的 switch 语句”我会称之为“一个伟大的游戏”,所以你在正确的轨道上。

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

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”能力而既是玩家又是墙怎么办,然后呢!?!” (然后让您简单地添加案例来处理这些案例)

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

于 2010-09-22T23:57:57.017 回答
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
}
于 2010-09-04T22:25:14.640 回答