Wednesday, January 5, 2011

2D RTTI in collision detection

RTTI is run time type information, e.g. "if isinstance(object, Ship):".  The action depends on what (sub)type an object is.  It's usually better to make each class handle it's own special cases by overriding an inherited method.

Here's an exception: the action depends on what each of two (or more, I suppose, but I haven't dealt with that) objects

are. This comes up doing collision detection and resolving the collisions. I'm writing a game, and I have different objects floating around in space- parts, bullets, ships, planets, etc. They can hit each other, and what happens depends on what the parts are. If a ship hits a part, it collects it. If a ship hits a bullet, the ship takes damage and the bullet is destroyed.

One option is to keep separate lists of each type of objects, and do:
for bullet in universe.bullets:
   for ship in universe.ships:
      //handle case

for part in universe.parts:
   for ship in universe.ships:
     //handle case
//etc. - O(n^2) cases

  • This means having to maintain many more lists.  If a new type of object is added, Universe needs to gain a new list. 
  • It will make collision optimizations (like quadtrees or sorting) much more complicated. 
  • It does avoid any RTTI, but Universe still needs to be aware of all the possibilities, so it isn't any more abstract. 

Another option is to force everything into instance methods:
class Ship:
    def collide(self, other):
       if isinstance(other, bullet):
             //handle case
       else if isinstance(other, part):
            //handle case
class Bullet:
     def collide(self, other):
          if isinstance(other, ship)
//etc.

  • This is nasty.  
  • It means that everytime you add a new type of object, every other object has to gain a new case!  This maintenance is the antitheses of encapsulation.  
  • You get the choice of doubling all the code (Bullet can handle Ships and Ship can handle Bullets) or creating a precedence list and ensuring that (Bullet, Ship) and (Ship, Bullet) both result in Ship.collide(Bullet)--never Bullet.collide(Ship), which will take more RTTI. 
  • It's also the way you end up doing things if you start by trying to follow the usual OOP solution to RTTI. 

I believe that the best solution is to write one big, ugly function. The function should work like a table, with types on the top and side, and how to handle each type of collision in the cells:

def collide(a, b):
    if isinstance(a, Ship):
        if is instance(b, Part):
             //handle case
        else if isinstance(b, Bullet):
            //handle case
    else if isinstance(a, Part):
          if isinstance(b, Ship):
               //handle case
//etc.

  • This is ugly, but all the ugliness is encapsulated in one function, so everything else can stay in blissful ignorance.
  • When adding a new object type, you have to dig back into this function and add the necessary branches.
  • This function makes extensive use of RTTI calls, but nothing outside it should have to. 
  • If you're careful and keep to the table analogy, the function will have an intuitive ordering and it will be clear what needs to added when.
  • An optimization can remove the redundant triangle of the table:

if isinstance(b, Ship):
      a, b = b, a
if isinstance(a, Ship):
        if is instance(b, Part):
             //handle case
        else if isinstance(b, Bullet):
            //handle case
//now nothing below will have to handle cases where b is a Ship.
if isinstance(b, Part):

      a, b = b, a
if isinstance(a, Part):
          if isinstance(b, Bullet):
               //handle case
//etc.