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.
      

5 comments:

  1. Hi from Reddit.
    Here's a trick I thought up, although perhaps it's been thought of before :)

    1. Assign each type a *prime* ID number
    2. In your 'catchall' collision function, square the first object's ID number and multiply again by the second object's ID number.
    3. Now dispatch the product through a switch statement, e.g.:
    bool collide(Obj* a, Obj* b) {
    __switch(a->id * a->id * b->id) {
    __case SHIP_ID*SHIP_ID*BULLET_ID:
    ____return colShipBullet(a,b);
    __case BULLET_ID*BULLET_ID*SHIP_ID:
    ____return colShipBullet(b,a);
    __...
    __}
    }
    The two multiplications perform the work of two RTTI lookups. The squared first term lets you distinguish the type of the left vs. right. If you only care about the pairing but not which is which you could use just one multiplication. Hopefully you see why selecting prime numbers for the ID values is important. :)

    ReplyDelete
  2. First order functions and types are your friend. You say you want a table of functionality? Build a table of functionality. If you don't have to worry about inheritance, you can have a hashmap of (obj1.__class__,obj2.__class__) => function. Sorting the tuple will take care of having only one entry per collision. If you do have to worry about inheritance, you'll have to iterate over a data structure and do the instanceOf, which will hit performance a little bit if you have a lot of types, but our input factor is likely to be small here. It still gets the nasty choosing logic completely away from the rest of your code though, and you can do it in a loop so it's much less error-prone.

    ReplyDelete
  3. Nice. Looks like ejtttje's solution is an example of MasterPi's hash function.

    ejtttje- that's nice in that it is easy to make order not matter. I'm working in Python, so I didn't think to phrase things in a switch statement (and there's little difference between looking up a ID class variable or a type). This looks like a great optimization for other languages (especially c++).

    MasterPi- that's a very elegant solution, especially if you wanted to change things during runtime. It does look to me like inheritance will make the hash function messy (essentially like the structure I used). What do you mean by iterate over the structure?

    I think that there's a lot of room to play around with the branching syntax; any arguments for a more OOP approach?

    ReplyDelete
  4. Multimethods are perfect for all those situations where, in a message-passing language, you struggle to decide to which class a certain behavior ought to belong. Is the sound a drum makes when it's hit with a drumstick a function of what kind of drum it is or what kind of stick you use to hit it? Both, of course. To model this situation in Common Lisp, you simply define a generic function beat that takes two arguments.
    (defgeneric beat (drum stick)
    (:documentation
    "Produce a sound by hitting the given drum with the given stick."))

    Then you can define various multimethods to implement beat for the combinations you care about. For example:
    (defmethod beat ((drum snare-drum) (stick wooden-drumstick)) ...)
    (defmethod beat ((drum snare-drum) (stick brush)) ...)
    (defmethod beat ((drum snare-drum) (stick soft-mallet)) ...)
    (defmethod beat ((drum tom-tom) (stick wooden-drumstick)) ...)
    (defmethod beat ((drum tom-tom) (stick brush)) ...)
    (defmethod beat ((drum tom-tom) (stick soft-mallet)) ...)


    -------

    http://www.gigamonkeys.com/book/object-reorientation-generic-functions.html


    Try a search on python multimethods.

    ReplyDelete
  5. This is indeed multiple dispatch as macoovacany said, it's one of those things they teach in the Programming Language Concepts course at my school; I dunno why I didn't mention it by name.

    Seems there's an implementation of it for Python, probably doing about what I was describing:
    http://en.wikipedia.org/wiki/Multiple_dispatch#Python

    I don't know if it handles inheritance or not. What I mean by iterating is that you will still have a data structure mapping from type pairs to functions, you'll just have to iterate over it checking each one instead of doing a dictionary lookup operation.

    Standard OOP is gonna suck at this, since the behavior doesn't properly belong in one class or the other. Functional programming on the other hand really shines here because you're dealing in behavior. Many functional languages have multiple dispatch built in (Lisp, Haskell) and ducktyped languages with first-order functions (Python) allow you to do it easily with RTTI as we've seen.

    ReplyDelete