Wednesday, May 11, 2011

Game objects, Part 2

Hardy Drivey sat on a desk.
Hardy Drivey had a big fall.
All the king's restarts and all the king's shouts
Couldn't put Hardy back together again.

So yeah, Floor 1, Laptop Hard Drive 0. Fortunately, it seems that all the updates I made to the code since my last SVN commit are intact. Specifically (among bugfixes), I had added sound to the engine. I had a surprisingly simple time with it, I thought adding sound would be a lot more complicated. No 3D positioning or anything yet, but from what I read in the documentation, that's not too hard either.

I had also started to work on animation, but I've sort of hit a brick wall due to lack of assets to test with. I don't have 3DS Max, and maybe I just suck with Blender, but the last time I tried to use it, I couldn't get anything to work correctly. The mesh would have holes in it only when exported, making a skeleton gave me trouble, etc... you can tell I'm not very good at this whole "asset" thing. I'll write about my plans with the animation system once I can get some stuff going there.

Anyway, I wanted to continue my discussion of game object basics. That problem I mentioned at the end of my last post about objects needing to know their position in a relationship isn't a big deal at all. I decided to add the object's scene handle, its object ID, and its bucket ID to GameObject's member variables. That lets me quickly figure out what bucket the child object is in and whether a bucket change is necessary. If it is, I tell Scene to change the child object's bucket, and Scene informs the child object of this change so it can move its children down too, and so on. Getting object handles where needed is also a lot simpler this way.

Now to move on to the structure of the game objects. The old way of doing it is through a inheritance hierarchy that's "hueg liek xbox". You start from very basic base classes and work your way up. So a player class's position in the hierarchy may look like this:

GameObject->MovableObject->RenderableObject->AnimatedObject->ControllableObject->Player

As one can guess, as we move down the hierarchy we gain more and more functionality, to the point that when we get to Player, we just have to implement player-specific stuff. Here we are saying that Player is controllable, and thus animated, and thus renderable, and thus movable. An enemy would work similarly:

GameObject->MovableObject->RenderableObject->AnimatedObject->Enemy

Like Player, Enemy inherits all this functionality without having to do anything. Since an Enemy is not controllable, we don't inherit from ControllableObject.

Obviously, polymorphism works wonders here. You also have the assurance that if an object is of type AnimatedObject, it is guaranteed that it is also movable and renderable. So what's the problem with this?

Imagine that you had laid out all the types of game objects you were going to need and came up with a hierarchy like the one above. You work on the project and get, say, 70% done. Your team's game designer comes busting through the door in his usual overdramatic style, and entrusts upon you a simple task: make a ControlCrate class for a crate that can be moved with the arrow keys. We know that crates do not need animation, but obviously it needs to be controllable. Now to just put that in the hierarchy... uh oh. Where do we put it?

In our hierarchy, ControllableObject inherits from AnimatedObject. If we want our crate to be controllable, we should inherit from ControllableObject, but that would mean we'd get animation stuff too, which we don't want. We gotta change our hierarchy around somehow. We could move ControllableObject back and get this:

GameObject->MovableObject->RenderableObject->ControllableObject->AnimatedObject

This seems to solve our problem, until we realize that now all animated objects are considered to be also controllable. In all likelihood, this is not true. A second option is to branch off from RenderableObject, so we'd get this:

GameObject->MovableObject->RenderableObject->AnimatedObject->ControllableObject
                                                                  | ->NoAnimControllable->ControlCrate

So now we have two classes that inherit from RenderableObject, one that's animated and one that's not. Great, but now we have to copy a whole boatload of code from ControllableObject into NoAnimControllable. If you later change something in ControllableObject, I hope you remember to change it in NoAnimControllable too (spoiler: you probably won't). A third option is to just say "screw it" and let ControlCrate inherit from AnimatedObject too:

GameObject->MovableObject->RenderableObject->AnimatedObject->ControllableObject->ControlCrate

Just don't use the animation functions, and all is well, yes? Well, no. Cause now you've got animation-related variables taking up memory for a class that will never use them. This is what's referred to as the "blob effect". You get blob classes that can do everything, but never actually do everything. And what if your fellow coder Steve is working with this crate, sees that it inherits from AnimatedObject, and tries to animate it? Yeah, leave it to Steve to ruin everything.

Even if you're some kind of hierarchy ninja and get everything to work, keep in mind we may have hundreds of other game objects in that hierarchy. You're going to have to make sure every last one of them jives with your new way of doing things. And doing so will probably involve a LOT of code rewriting.

The alternative to hierarchies that I chose to use when defining game objects is that of a component-based system. I'll write more about this in my next post.

No comments:

Post a Comment