Showing posts with label computer science. Show all posts
Showing posts with label computer science. Show all posts

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.