Having been working with Game Object Composition for the past several months, my perspective of development in general has made a wide shift from the mental tyranny that classic inheritance hierarchies create, to the far more liberal mindset that data is a powerful force! What I mean by this is the problem a good majority of developers seem to have which is thinking about all objects in a “is a” way, instead of how they are composed - “has a”. Think about this. If I were to ask you to tell me what a car is, how will you go about answering me? Will you say “it’s an object that transports one or more passengers from point a to point b”? Well luckily for me, I am well aware of what a car is. but if I did not, I would probably look at you with a much more confused face than I did with the original question.
A better answer would be “a car is a type of vehicle in which has a internal engine or motor, four or more wheels, a chassis which houses it all, a device for steering the wheels in the direction you wish to go, and typically two or more spaces for passengers to sit”. Now if you look at that definition carefully, I described what a car is by listing the components it has vs the original vague answer which only provides us with a “high level” conceptual model of what a car is.
Now with that having been said, I by no means reject inheritance just because I found composition. Here’s why - a car is still a type of object. It has many of the same attributes that even a human has (i.e. it has a position in the world, it can move in time and with a velocity, it can’t occupy the same space as another object does at the same time, it’s visible meaning it has attributes we need to draw it, it can be killed or rather, destroyed, it has multiple pieces like we have limbs, etc). Technically put, the vehicle has spatial data, it can be drawn, so we need a way to initial the data we’ll be drawing, it can move, so we need a way to update it, it can be destroyed, so we need to keep track of it’s damage, it has many different components, so we need a way to manage and run each of them, etc.
As you can clearly see, there is a lot of common functionality that can be wrapped up into a nice little package that we can reuse over and over again for all of our game objects. However, HOW that is done, can happen in really many ways, but let’s focus on the main two: inheritance and composition. Using inheritance, I’d create a GameObject class which has all of the data/functionality I need..all I have to do is extend it, or even simply create instances and assign my components to it..but if we use pure composition, the solution would be to create multiple data classes such as “spatial”,”renderable”,”collision”,”simulation”,”health”, etc. These could also be interfaces, but then, we’d need to create the members/getters and setters for every single object we create, over and over again!
Using method two, we simply need to create member instance of each data class, create their getters and setters, add the components to the object, throw the object into some list where it’ll get managed, etc. Now, the object itself would never get directly interacted with by any other object. Instead, it’s components that deal with said interaction, would handle what happens themselves while applying the changes to the objects data, which in turn, would have an impact on the object as a whole.
Solution number two, is a very clean, viable solution..but it’s not the easiest either and will almost always lead to some duplication of code. The getters and setters alone are proof. You could always simply make all members public..but most of us know the issues that can present when others are working with your code. In addition, using this solution, not only will you need to create often duplicate code in your objects, but within the components as well (granted you have refused all forms of inheritance). For example, if you do not extend from various base classes, you must at least implement one or more interfaces or else, you have to manually manage every single object on a separate basis! Let’s say you have a basic rendering component that draws a hightmap to the screen, and another that draws a static mesh. While you could make them utilize the same rendering code, chances are, it won’t produce very great results because the two data sets are so much different. Instead, you decide that you will create an interface that both rendering components will implement.
Now, the first thing you’ll probably do is figure out the common public functionality that both components will have in common. Once that’s done, you start to create your first class for the terrain by implementing the interfaces methods. Next, you start on the mesh rendering component by doing the same as before. Oh look, duplicated code! The solution?? Inheritance, son! The ideal solution here would be to create a basic rendering component THAT STILL would implement an interface so that we can’t break the rest of the system if we make changes later down the road to the base rendering component. The base component would implement the methods and contain the shared data common to all rendering components.
Here’s my problem…
A weapon is game object that is used by other game objects. How do we “use” a weapon without passing events to it?? I personally, want to be able to call an method such as “fire” instead of sending the weapon I’m holding, the same event every time I want to pull the trigger..and what if I hold the trigger down? Maybe that is how user input is handled in the first place, but it doesn’t make sense to me, to reproduce this in order to tell my weapon that I want to shoot it. As it is, we’re already having to send the event to our player object that the trigger button was pressed, which then has to be distributed to it’s components. Also, ranged weapons also need to manage multiple game objects of their own: projectiles. Yes, a component could wrap that up, and another could tell it when to fire a projectile, where as another could tell the other two about ammo pickups…but we’re having to send events from each component, to the object, back to the other components…each time, we have to listen for the event, and based upon the data received, we may have to send a new event back out.. it can get to be a real night mare..where as if we have a shared data member we can use to communicate between all components directly, we can avoid much of the hassle of having to keep sending and waiting for events!
So what’s the problem exactly?? The problem is simply, do I actually need the shared weapon class? After experimenting with a few variations, I can say yes in that instance. It makes it much easier to work with. However, I recently began creating a few more base classes for other generalized types of objects such as actors, dynamic and static objects, etc…but my question is, do I really need them? An actor for example, is just a game object that is defined by the components I assign to it. An actor is not used by other objects directly, though it can be interacted with, but an actor is almost always a direct child of the world..there are cases such as telling our AI what “type” of actor it is, such as a player, npc, monster, etc…but that could easily be wrapped up in an actor component. The trick here is thinking in terms of data vs inheritance; if I can achieve the desired results by simply altering data for each case, vs extending the object to the actor class for example, then what’s the point?
Still I can not help but to desire having a common class for actors, for props, for static level geometry, etc. Here’s a little reason why: scripting. However, in script we can just as easily create our base actor object and just reuse it for each new type. Check out this example to get a better picture of what I’m speaking about.
Using this method is both good and base. Good because it allows for a great level of flexibility and gives us the option of creating most of our game in script, where as the down side is, lack of compile time validation…we have to wait until our app is launched before we’ll see any scripting errors..and not all may even be caught. We’ll just end up with an uncaught exception error.
More on this later!