Saturday, November 5, 2011

Components in Python (or, an ode to Simplicity)

It seems as though blogger Mick West's Evolve your Hierarchy article has become something of a reference in game development circles.

The principle advantage was immediately clear to me -- less code duplication.  Whether this is the best way of going about reducing code duplication is much less clear.  Design patterns are nice because they format your thinking and so help you keep ideas clear and concise.  Design patterns are also evil because they're a tempting default solution to a problem that may have subtle, but significant peculiarities.

I've been thinking of a number of approaches that implement strict component design in varying capacities.

One approach is to implement a system of shared component objects.    For example, a tank object would only contain member variables pertaining to its internal state (health, armor, speed, etc.).  The functionality would come from previously-initialized component objects that would have no access to internal state attributes.  These objects would serve only as offsite number crunchers.

To illustrate, consider this example:
class Tank:
    def __init__(self, groundMove, takeInput, groundAttack):
        # Internal State Attributes = 30
        self.damage = 10
        self.speed = 10
        self.dest = None = None
        # Component attributes
        self.move = groundmove    # These objects have been previously initialized
        self.inpt = takeInput
        self.attk = groundAttack
    def moveTo(self):
        self.move.goto(self.dest, self.speed)
    def attack(self):
        self.attk(, self.current_location, self.damage)

Note that in this example, member variables are passed into the component objects individually.  This is simply illustrative.  In reality, it is much simpler to pass a pointer to the entire self object.

This approach has the advantage of being memory-efficient.  There is one object for each component, and units are implemented as objects that issue calls to component-object member functions.  This is great but it has two drawbacks:

  • Complexity
  • Bad for concurrency
We expect to have to resort to concurrent programming at some point.  I'm honestly thrilled at the idea of learning more about it, but it's still tricky business.  Code like this doesn't make it any easier.

The obvious solution is to have a pool of component objects and to select one that isn't being used, but this sounds like an unholy mess of deadlocks.  And besides, we don't need the concurrent processing yet, so why bother implementing it before it's needed?  

In fact, do we even need the memory we're saving?  Probably not, at least for the time being.  The concurrency problem can thus be solved with a simple change:
# Component attributes
        self.move = groundMover()
        self.inpt = Playable()
        self.attk = groundAttacker()

All we do here is call the constructor of the relevant components.  Our tank now has its own component object, so no more concurrency problems.

This bothers me because we've added a huge layer of complexity when multiple inheritance can also fix this problem.  I'm not an expert in python, so I'm not entirely sure of the potential drawbacks of using multiple inheritance in terms of performance, but here's what I do know...

"Impulses to optimize are usually premature"

Jonathan Blow, lead developer of Braid, gave an excellent talk on the art of getting things done.  One of the things that struck me was his stern warning against premature optimization.  "Impulses to optimize," he says, "are usually premature."

It makes sense.  Most optimizations make assumptions that could be invalidated later in development.  More importantly, the code being optimized usually has a negligible effect on the overall experience -- people optimize the wrong things.

This approach makes a lot more sense to me:
class Tank(groundMover, pcUnit, groundAttacker):
    def __init__(self, groundMove, takeInput, groundAttack):
        # Internal State Attributes = 30
        self.damage = 10
        self.speed = 10

Boom.  Simple.  For one, there's a lot less code to write, which translates into fewer bugs.  Second, concurrency is fairly straightforward as all units are (mostly) independent.  Third it isn't optimized, so it's more likely to work with a novel case.

For the price of a bit of free ram, we have something that's much more elegant and simple to understand.  That said, I'm not very familiar with the workings of python inheritance, so perhaps this is doing something insanely suboptimal behind the scenes.  But then again... who cares?

If it doesn't work, I'll change it, but until that day comes, I'll go with the simple option.

No comments:

Post a Comment