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

发布于 2024-09-17 01:27:07 字数 1083 浏览 10 评论 0原文

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

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

你会如何编写这样的代码?我可以想到许多令人难以置信的不优雅的方法,例如,在全局 WorldObject 类中使用虚拟函数来识别属性 - 例如,GetObjectType() 函数(返回 ints、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()。这是我个人最不喜欢的(尽管它们都远非讨人喜欢),因为它似乎维护起来最麻烦。

有人可以深入了解这个问题的更优雅的解决方案吗? 感谢您的任何帮助!

I'm in a bit of a pickle: say I'm making a simple, 2D, Zelda-like game.
When two Objects collide, each should have a resulting action. However, when the main character collides with something, his reaction depends solely on the type of the object with which he collided. If it's a monster, he should bounce back, if it's a wall, nothing should happen, if it's a magical blue box with ribbons, he should heal, etc. (these are just examples).

I should also note that BOTH things are part of the collision, that is, collision events should happen for both the character AND the monster, not just one or the other.

How would you write code like this? I can think of a number of incredibly inelegant ways, for instance, having virtual functions in the global WorldObject class, to identify attributes - for instance, a GetObjectType() function (returns ints, char*s, anything that identifies the object as Monster, Box, or Wall), then in classes with more attributes, say Monster, there could be more virtual functions, say GetSpecies().

However, this becomes annoying to maintain, and leads to a large cascading switch (or If) statement in the collision handler

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

}

There's also the option of using files, and the files would have things like:

Object=Monster
Species=EvilScaryDog
Subspecies=Boss

And then the code can retrieve the attributes without the need for virtual functions cluttering everything up. This doesn't solve the cascading If problem, however.

And THEN there's the option of having a function for each case, say CollideWall(), CollideMonster(), CollideHealingThingy(). This is personally my least favourite (although they're all far from likeable), because it seems the most cumbersome to maintain.

Could somebody please give some insight into more elegant solutions to this problem?
Thanks for any and all help!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

怼怹恏 2024-09-24 01:27:07

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

I would do it vice versa - because if the character collides with an object, an object collides with the character as well. Thus you can have a base class Object, like this:

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

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

// etc. for each object

Generally in OOP design virtual functions are the only "correct" solution for cases like this:

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

EDIT:
After your clarification, you will need to adjust the above a bit. The MainCharacter should have overloaded methods for each of the objects it can collide with:

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

This way you notify the main character about the collision and it can take appropriate actions. Also if you need an object that should not notify the main character about the collision, you can just remove the notification call in that particular class.

This approach is called a double dispatch.

I would also consider making the MainCharacter itself an Object, move the overloads to Object and use collideWith instead of collideWithCharacter.

黑白记忆 2024-09-24 01:27:07

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

How about deriving all collidable objects from one common abstract class (let's call it Collidable). That class could contain all properties that can be changed by a collission and one HandleCollision function. When two objects collide, you just call HandleCollision on each object with the other object as the argument. Each object manipulates the other to handle the collision. Neither object needs to know what other object type it just bounced into and you have no big switch statements.

书信已泛黄 2024-09-24 01:27:07

使所有可碰撞实体使用 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.*/);
}

更多信息:单次调度

希望有帮助。

Make all colidable entities implement an interface (lets say "Collidable") with a collideWith(Collidable) method.
Then, on you collision detection algorithm, if you detect that A collides with B, you would call:

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

Assume that A is the MainCharacter and B a monster and both implement the Collidable interface.

A->collideWith(B);

Would call the following:

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.
}

This would in turn call the Monster::collideWith(MainCharacter) method and you can implement all monster-character behaviour there:

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

More info: Single Dispatch

Hope it helps.

浊酒尽余欢 2024-09-24 01:27:07

你所说的“烦人的 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”能力,然后呢!?!”
    (然后让您只需添加案例
    来处理这些)

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

What you call "an annoying switch statement" i would call "a great game" so you are on the right track.

Having a function for every interaction/game rule is exactly what I would suggest. It makes it easy to find, debug, change and add new functionality:

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

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

...

So the question is really how to detect each of these cases. Assuming you have some sort of collision detection that results in X and Y collide (as simple as N^2 overlap tests (hey, it works for plants vs zombies, and that's got a lot going on!) or as complicated as sweep and prune + 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() { ... }

  ...

This style, while verbose is

  • easy to debug
  • easy to add cases
  • shows you when you have
    logical/design inconsistencies or
    omissions "oh what if a X is both a
    player and a wall due to the
    "PosessWall" ability, what then!?!"
    (and then lets you simply add cases
    to handle those)

Spore's cell stage uses exactly this style and has approximately 100 checks resulting in about 70 different outcomes (not counting the param reversals). It's only a ten minute game, that's 1 new interaction every 6 seconds for the whole stage - now that's gameplay value!

鼻尖触碰 2024-09-24 01:27:07

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

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
}

If I am getting your problem correctly, I would sth like

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
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文