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


我有点傻瓜:说我正在做一个简单的2D,塞尔达式游戏。 当两个对象发生碰撞时,每个对象都会产生一个结果。然而,当主角与某物碰撞时,他的反应仅取决于他碰撞的物体的类型。如果它是一个怪物,他应该反弹,如果它是一堵墙,没有任何事情发生,如果它是一个带有缎带的魔法蓝盒子,他应该愈合,等等(这些只是例子)。

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

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

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

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

}

还有使用文件的选项,文件可能包含以下内容:

Object=Monster
Species=EvilScaryDog
Subspecies=Boss

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

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

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


10504
2017-09-04 21:27


起源

牛顿会为你说几句话:) - Ed S.


答案:


反之亦然 - 因为如果角色与对象碰撞,对象也会与角色碰撞。因此,您可以拥有一个基类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 */

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

这种方法称为a 双重调度

我也会考虑让MainCharacter本身成为一个 Object,将重载移动到 Object 并使用 collideWith 代替 collideWithCharacter


11
2017-09-04 21:38



我同意,碰撞行为更多地取决于对象的类型而不是主角上的imho,因此特定对象应该实现行为。然后,您可以稍后添加对象而不更改主要字符。你的主角应该显然暴露主要特征的界面(例如,反弹(矢量方向),...),让对象与主角交互。 - Emile Vrijdags
我刚刚更新了原帖,因为我有点忘了说清楚:对象和角色都可能发生事件;并不是只有角色做某事或只有对象做了,但两者都做。 - bfops
@Robot:你正在寻找双重派遣。在MainCharacter中,为您拥有的每种类型的Object上的事件编写重载。在Object中,编写一个调用MainCharacter的OnCollision函数的虚函数 - 通常,人们只使用* this。这样,您基本上可以在运行时重新生成类型信息。 - Puppy
更新了我的帖子以匹配您的修改。如DeadMG所建议的,双重调度是最干净的解决方案。 - Karel Petranek


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


2
2017-09-04 21:38



这是我现在的一般设置。然而,并非所有与怪物碰撞的事情都会以同样的方式反应,这就是为什么我说我需要知道碰撞的是什么,而不仅仅是碰撞是否发生。 - bfops


使所有可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.*/);
}

更多信息: 单一调度

希望能帮助到你。


1
2017-09-04 22:37



有趣的想法。作为旁注,我应该指出该代码存在一些小问题,但意图清晰简洁。这与我当前的代码类似,但是我没有为每种类型重载Monster :: CollideWith函数,而是只有一个类似于MainCharacter中的函数。我必须这样试试。 - bfops


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

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

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次新的互动 - 现在这是游戏玩法的价值!


1
2017-09-22 23:57



对于那些专注于OO类型继承的东西,请记住这些测试可以是任何东西:IsPlayerFlying(),IsWallMadeOfFire()&& WallFireTemperature()> 1000 Kelvin),IsItTuesday() - Jeff Gates


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

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
}

0
2017-09-04 22:25