views:

162

answers:

5

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!

+2  A: 

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.

Adrian Grigore
This is the general setup I have now. However, not everything colliding with a Monster will react the same way, that's why I say I need to know what's colliding, not just whether or not collisions are occurring.
RobotGymnast
+8  A: 

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.

dark_charlie
I agree, the collisionbehavior depends more on the type of object than on the maincharacter imho, so the specific objects should implement the behavior. Then you can add objects later without changing the maincharacter. Your maincharacter should obviously expose an interface of maincharacterbehaviours (eg. bounceback(vector direction),..) that lets the objects interact with the maincharacter.
Emile Vrijdags
I've just updated the original post, as I sort of forgot to make clear: Both the object and the character may have events occur; it's not that only the character does something or only the object does, but both do.
RobotGymnast
@Robot: You're looking for double dispatch. In MainCharacter, write an overload for his events on each type of Object that you have. In Object, write a virtual function that calls the MainCharacter's OnCollision function - normally, people just use *this. This way, you essentially regenerate type information at runtime.
DeadMG
Updated my post to match your edit. Double-dispatch, as suggested by DeadMG, is the cleanest solution.
dark_charlie
A: 

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
}
Neeraj
+1  A: 

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.

jumpifzero
Interesting idea. As a side note, I should point out that there's some minor issues with that code, but the intent is clear and concise. This is similar to my current code, but instead of having a Monster::CollideWith function overloaded for each type, I just have one that's similar to the one in MainCharacter. I'll have to try it this way.
RobotGymnast
+1  A: 

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!

Jeff Gates
Jeff Gates