Showing posts with label component design. Show all posts
Showing posts with label component design. Show all posts

Wednesday, November 23, 2011

Towards full Component Design

I was wrong.


Or shortsighted, or optimistic, or simply inexperienced.  In any case, we're working towards a more complete component-based system for our game objects.


If you recall the post from a few weeks ago, I was of the opinion that game objects could be implemented in terms of multiple inheritance.  This was good because it avoided the added complexity of managing various component objects.  What we didn't foresee was that this actually made things more complicated in certain cases.


Here's a really simple one, to illustrate.  Let's say we're creating a unit similar to the StarCraft II Viking.  The Viking is a unit whose distinguishing feature is that it is an aircraft capable of transforming into a ground-roaming mech.  As such, it must incorporate code to handle movement in the air and movement on the ground.


At first glance this seems not to cause any problems.  Being naive, we go ahead and have class Viking_clone inherit from class Walker and class Flyer, but now we have a problem!  Which move() function gets called?


The Viking from StarcCraft II
One solution is to name them separately -- perhaps fly() and walk() -- but that hasn't really solved anything.  All we've done is spread the problem out in the codebase.  Now, anytime we have code that tells a game object to move, it has to check which type of object it is and call the corresponding method.  To make matters worse, if we later decide to add another type of unit, say a boat, we have to edit code throughout the project to accommodate this new type of object.


On a more formal level, what we've done is violate the golden rule of game design:  keep things separate.  Module A should not care about how module B's internal structure.  It should instead interact with a constant and stable interface.


To get back to the Viking Problem, one solution is to implement a metaclass such as this:
class vikingClone:
    def __init__(self):
        self.vclone_air = vclone_air()
        self.vclone_grnd = vlcone_grnd()
        self.active = self.vclone_air
        
    def go(self):
        self.active.move()
        self.syncAG()
        
    def syncAG:
        ...

All we've done is create two separate classes, Viking_clone_air and Viking_clone_ground, each with its own methods.  To move the unit, the one would just call the appropriate method from the metaclass, which in turn calls the corresponding method from self.active.


From there, it's just a matter of calling a function that updates the non-active version of the Viking object.


If you're following my logic, you can see how this is only a short step away from full-blown component management.  To be sure, this would be a great refactoring solution if we had already coded up the vast majority of unit-related stuff, but it's still hackish and wasteful.  We don't need two copies of the Viking clone's health or attack function, for example.


What we're doing instead is managing all aspects of the object's behavior by assigning component objects.  The metaclass will only house the information which is completely unique to the unit:  health, for example.


Also, this potentially allows us to save memory by destroying the unused movement-type object:  if the Viking clone is walking, we destroy the instance of class Flyer.  Of course, this has to be weighed against the cost of initializing a new object every time the unit changes configuration or the complexity of using a pool of objects, but the point is we now have options.


So there it is, I stand corrected.  The good news is that the problem became apparent pretty early on, so we haven't wasted too much time.  The whole process of implementing components has been relatively pain free as well.



I'm currently working on an input management class along with the ground movement class.  The latter is nearly done!

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
        self.health = 30
        self.damage = 10
        self.speed = 10
        
        self.dest = None
        self.target = None
        self.current_location
        
        # 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.target, 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
        self.health = 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.