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!

No comments:

Post a Comment