Aggregation vs. Object-Oriented design issues
by Michael Vargas · in Torque X 2D · 03/19/2007 (10:22 am) · 10 replies
Hi,
I've been experimenting with TorqueX for several weeks now and I'm finding it difficult to discern what the best practices are for creating a game using a component-based aggregation model. There are many resources that outline best practices for object-oriented design, but aggregation (at least related to game development) is a topic that's much less explored, it seems.
I can't quite figure out when it's appropriate to define a class versus just adding components to a T2DSceneObject, for example. Should I make a class to encapsulate a player's space ship? Or instead should it be a T2DSceneObject with all sorts of random components attached to it, e.g. "PlayerControllable", "Explodable", etc.? I'm so accustomed to defining an object in a class of the source code, but should I instead be focusing on designing the object *outside* of the source code (i.e. in TGBX?).
It seems as though the solution offered by component-based design (i.e. the avoidance of "Frankenstein objects") just introduces an analogous problem, namely "Frankenstein components". Some components may need to be altered slightly and renamed in order to apply to a different type of game object, etc.; analogous to attempting to build both an airplane and a bus with the same shared set of parts. Am I off base? Any guidance would be greatly appreciated.
Thanks,
Mike Vargas
I've been experimenting with TorqueX for several weeks now and I'm finding it difficult to discern what the best practices are for creating a game using a component-based aggregation model. There are many resources that outline best practices for object-oriented design, but aggregation (at least related to game development) is a topic that's much less explored, it seems.
I can't quite figure out when it's appropriate to define a class versus just adding components to a T2DSceneObject, for example. Should I make a class to encapsulate a player's space ship? Or instead should it be a T2DSceneObject with all sorts of random components attached to it, e.g. "PlayerControllable", "Explodable", etc.? I'm so accustomed to defining an object in a class of the source code, but should I instead be focusing on designing the object *outside* of the source code (i.e. in TGBX?).
It seems as though the solution offered by component-based design (i.e. the avoidance of "Frankenstein objects") just introduces an analogous problem, namely "Frankenstein components". Some components may need to be altered slightly and renamed in order to apply to a different type of game object, etc.; analogous to attempting to build both an airplane and a bus with the same shared set of parts. Am I off base? Any guidance would be greatly appreciated.
Thanks,
Mike Vargas
#2
03/19/2007 (11:12 am)
Quote:Should I make a class to encapsulate a player's space ship?No.
Quote:Or instead should it be a T2DSceneObject with all sorts of random components attached to it, e.g. "PlayerControllable", "Explodable", etc.?Yes. :)
Quote:Some components may need to be altered slightly and renamed in order to apply to a different type of game object, etc.;I suspect that this is more just a question of properly architecting the components. Components should be designed to be generally reusable. Naturally it's not an exact science, but neither is designing the proper hierarchy in a traditional hierarchical object oriented design.
Quote:analogous to attempting to build both an airplane and a bus with the same shared set of parts.But airplanes and buses do share a lot of the same parts, or at least the same kinds of parts. Do they both need wheels? Yes. Do they both need fuel tanks? Yes. Do they both need passenger seating? Yes. I could keep going. So there will be a lot of commonality. But there will indeed be some divergence. The "trick" is to design your components so that they're controllable and flexible enough to meet the needs to both.
#3
One of the tutorials that the next release will have is called "Microbes", and it describes two very basic components, called repellers and attractors. These components are very basic--they simply keep a list of things that are currently in the force range (settable via public property/TGB-X), and affect the velocity of the owner T2DSceneObject based on the list of forces. It's a very basic, but very flexible and interesting mechanic.
As part of my learning process, I enhanced the tutorial to create a few other components:
ResourceComponent--the Owner acts as a "resource site" for a consumable resource.
SeekerComponent--the ability to search within a certain range, and locate a T2DSceneObject that has a ResourceComponent of the desired type.
ConsumerComponent--the ability to be "hungry", and shrink in size over time, unless within consuming range of an appropriate T2DSceneObject with an appropriate ResourceComponent.
During my prototyping, my architecting of the various components was a bit sloppy--for example, I hard coded the ResourceComponent behavior as part of my main AI, as well as the ConsumerComponent. However, once I had the prototype together, I was able to refactor the behaviors out into separate components, and make them configurable as to type of resource, consumption range, etc.
Once I properly refactored the behaviors, I was then able to create new "types" (I don't want to call them classes, since that would be slightly confusing) of objects in my game, such as:
Food Source--provides food for microbes, and will eventually run out. Has a ResourceComponent set to type food.
"Hungry Microbe"--I added the ConsumerComponent and SeekerComponent to my main microbes, and they began to shrink in size until hungry enough to seek out food, then would use their seeker component to find a Food Source.
"Bad Microbe"--I created an enemy "class" for my demo that currently seeks out the same food type as hungry microbes, and "camps" that Food Source. I gave the Bad Microbe a RepellerComponent that affects Microbes, and therefore when the bad microbe becomes hungry, it goes and eats from a Food Source, but when Hungry Microbes try to eat from that resource, they are repelled.
I plan in the future to turn the Bad Microbe into a consumer of Microbes, and of course give my Hungry Microbes a ResourceComponent that provides "Food for Enemy Microbes", and that will fundamentally change my game play--instead of Bad Microbes seeking out Food Sources and emergently "camping" them, they will instead seek out Hungry Microbes and consume them.
Since I refactored out my behaviors into components, this is now simply a matter of adding a few components that are already implemented to a couple of my game objects, and fine tuning their configurations (via the editor) to get the game play I'm looking for.
I agree, at first it seems confusing, especially to experienced developers that are very used to inheritance, but once you grasp best practices with aggregation, it really becomes pretty amazing how easy it is to refactor behavior into separate components, and then configure those components for game play.
03/19/2007 (11:40 am)
I agree with Dan fully--it's more of a matter of architecting your components to be both more generic, and more configurable.One of the tutorials that the next release will have is called "Microbes", and it describes two very basic components, called repellers and attractors. These components are very basic--they simply keep a list of things that are currently in the force range (settable via public property/TGB-X), and affect the velocity of the owner T2DSceneObject based on the list of forces. It's a very basic, but very flexible and interesting mechanic.
As part of my learning process, I enhanced the tutorial to create a few other components:
ResourceComponent--the Owner acts as a "resource site" for a consumable resource.
SeekerComponent--the ability to search within a certain range, and locate a T2DSceneObject that has a ResourceComponent of the desired type.
ConsumerComponent--the ability to be "hungry", and shrink in size over time, unless within consuming range of an appropriate T2DSceneObject with an appropriate ResourceComponent.
During my prototyping, my architecting of the various components was a bit sloppy--for example, I hard coded the ResourceComponent behavior as part of my main AI, as well as the ConsumerComponent. However, once I had the prototype together, I was able to refactor the behaviors out into separate components, and make them configurable as to type of resource, consumption range, etc.
Once I properly refactored the behaviors, I was then able to create new "types" (I don't want to call them classes, since that would be slightly confusing) of objects in my game, such as:
Food Source--provides food for microbes, and will eventually run out. Has a ResourceComponent set to type food.
"Hungry Microbe"--I added the ConsumerComponent and SeekerComponent to my main microbes, and they began to shrink in size until hungry enough to seek out food, then would use their seeker component to find a Food Source.
"Bad Microbe"--I created an enemy "class" for my demo that currently seeks out the same food type as hungry microbes, and "camps" that Food Source. I gave the Bad Microbe a RepellerComponent that affects Microbes, and therefore when the bad microbe becomes hungry, it goes and eats from a Food Source, but when Hungry Microbes try to eat from that resource, they are repelled.
I plan in the future to turn the Bad Microbe into a consumer of Microbes, and of course give my Hungry Microbes a ResourceComponent that provides "Food for Enemy Microbes", and that will fundamentally change my game play--instead of Bad Microbes seeking out Food Sources and emergently "camping" them, they will instead seek out Hungry Microbes and consume them.
Since I refactored out my behaviors into components, this is now simply a matter of adding a few components that are already implemented to a couple of my game objects, and fine tuning their configurations (via the editor) to get the game play I'm looking for.
I agree, at first it seems confusing, especially to experienced developers that are very used to inheritance, but once you grasp best practices with aggregation, it really becomes pretty amazing how easy it is to refactor behavior into separate components, and then configure those components for game play.
#4
Dan, regarding the airplane/bus example, I definitely see the similarities, but what do you do in the case of the definite differences? For lack of a better example, let's say the airplane needs some sort of propellor component that doesn't apply to any other component. I can think of two main ways to deal with this situation:
1) Create a propellor component which will only be used for the airplane object. Perhaps try to make it generic in case some other object down the road wants to use a propellor also.
2) Create some hybrid component (e.g. ForwardMotionComponent) whose general function applies to both buses and airplanes, but includes object-specific code which tests whether or not the component in question is a bus or an airplane, for example:
if (owner is a bus)
{
move forward in some automobile engine-specific way;
}
else if (owner is an airplane)
{
move forward in some propellor-specific way;
}
Please tell me that the 2nd option is as undesirable as it seems. :) It would seem to defeat the purpose of creating generic, reusable components, though I've seen similar stuff in some sample code.
03/19/2007 (4:08 pm)
Thank you everyone for your responses; they were incredibly helpful. I think I'm just going through the initial pains of trying to shift my worldview. I suppose what I'm looking for is a sort of guideline for how to design appropriate reusable components. It may just take practice.Dan, regarding the airplane/bus example, I definitely see the similarities, but what do you do in the case of the definite differences? For lack of a better example, let's say the airplane needs some sort of propellor component that doesn't apply to any other component. I can think of two main ways to deal with this situation:
1) Create a propellor component which will only be used for the airplane object. Perhaps try to make it generic in case some other object down the road wants to use a propellor also.
2) Create some hybrid component (e.g. ForwardMotionComponent) whose general function applies to both buses and airplanes, but includes object-specific code which tests whether or not the component in question is a bus or an airplane, for example:
if (owner is a bus)
{
move forward in some automobile engine-specific way;
}
else if (owner is an airplane)
{
move forward in some propellor-specific way;
}
Please tell me that the 2nd option is as undesirable as it seems. :) It would seem to defeat the purpose of creating generic, reusable components, though I've seen similar stuff in some sample code.
#5
Stephen's example shows some of the motivation for building objects via aggregation rather than through hierarchy. One of the things about designing and developing games is that you frequently have to diverge quite a bit from your starting plan, since "fun" is so hard to nail down in the abstract without lots of prototyping and experimenting. So having a development methodology that allows you to more easily refactor your objects, like in Stephen's example, is great. And then the added benefit is that making your components "better" for any given project generally means making them more configurable and workable between objects within your game, which has a greater likelihood that they'll be useful in other games. It's often harder to "strip out" individual chunks of functionality from the traditional hierarchical approach since you often get tendrils up and down the hierarchy or stuff like your "if I'm an airplane" example stuck somewhere counterintuitive.
So I think the two keys are 1: accepting the idea that it's a good way to develop (not an easy thing to do, since so many of us are used to the traditional hierachical approach) and training your brain to think in terms of aggregating functionality instead of hierarchies, and 2: practice. I don't think there are any hard and fast rules for the best way to build components and figuring out how to "chunk" the functionality you need into the particular components you need for a given game. But I don't think there are any rules for the hierarchical approach, either -- are your airplane and bus both subclasses of "passenger vehicle" or is one a subclass of "fixed wing aircraft" and the other one a subclass of "wheeled vehicle"? The question of what makes a good component is basically "is this chunk of functionality discrete enough that I can treat it as a black box that I can use as a building block for my game objects, and also nontrivial enough that it's worth the overhead of actually creating a new component for it?".
03/19/2007 (4:52 pm)
@Michael - Yes, the 2nd option is definitely undesirable. There's nothing wrong with having components that are unique to one object type if that's what makes sense for the game (for example, the object that represents the player might be the only object that has a component that does something with user input).Stephen's example shows some of the motivation for building objects via aggregation rather than through hierarchy. One of the things about designing and developing games is that you frequently have to diverge quite a bit from your starting plan, since "fun" is so hard to nail down in the abstract without lots of prototyping and experimenting. So having a development methodology that allows you to more easily refactor your objects, like in Stephen's example, is great. And then the added benefit is that making your components "better" for any given project generally means making them more configurable and workable between objects within your game, which has a greater likelihood that they'll be useful in other games. It's often harder to "strip out" individual chunks of functionality from the traditional hierarchical approach since you often get tendrils up and down the hierarchy or stuff like your "if I'm an airplane" example stuck somewhere counterintuitive.
So I think the two keys are 1: accepting the idea that it's a good way to develop (not an easy thing to do, since so many of us are used to the traditional hierachical approach) and training your brain to think in terms of aggregating functionality instead of hierarchies, and 2: practice. I don't think there are any hard and fast rules for the best way to build components and figuring out how to "chunk" the functionality you need into the particular components you need for a given game. But I don't think there are any rules for the hierarchical approach, either -- are your airplane and bus both subclasses of "passenger vehicle" or is one a subclass of "fixed wing aircraft" and the other one a subclass of "wheeled vehicle"? The question of what makes a good component is basically "is this chunk of functionality discrete enough that I can treat it as a black box that I can use as a building block for my game objects, and also nontrivial enough that it's worth the overhead of actually creating a new component for it?".
#6
I say this because what happens when later you have yet another object, a boat, that wants to use the ForwardMotionComponent. If you added object specific code, you would now have to go back into the component to add the boat specific functionality potentially breaking, in some way, your other objects (the plane and the bus).
Thinking in terms of QA, now not only do they have to check the boat, but now they have to go back and recheck the plane and the bus.
03/19/2007 (4:54 pm)
I would suggest avoiding placing object specific code in your components and lean heavily towards being generic as possible.I say this because what happens when later you have yet another object, a boat, that wants to use the ForwardMotionComponent. If you added object specific code, you would now have to go back into the component to add the boat specific functionality potentially breaking, in some way, your other objects (the plane and the bus).
Thinking in terms of QA, now not only do they have to check the boat, but now they have to go back and recheck the plane and the bus.
#7
03/19/2007 (5:16 pm)
Actually you would probobly just give it rattle value, and use math to figure out how you want it to rattle. simply leave the fields blank for things that don't rattle, (default = 0)
#8
03/19/2007 (9:02 pm)
I for one am definately looking forward to the Microbes tutorial. I'm doing the same stumbling along trying to keep myself from designing with a hierarchy.
#9
I would probably make a PropellerComponent that allows for user configuration of size, total thrust, possibly number of blades, fixed pitch vs variable pitch, etc., and have it affect it's owner with a resultant force, as well as have an "exhaust" of some sort.
Note that I would only be using my propeller component for the airplane in the above example, however:
I could re-use my propeller component in:
--A boat racing game
--a C-130 aircraft model
--a Fan in a game like Marble Blast
03/19/2007 (9:44 pm)
Just to re-hash the general idea behind components and aggregation, given the above example regarding propellers:I would probably make a PropellerComponent that allows for user configuration of size, total thrust, possibly number of blades, fixed pitch vs variable pitch, etc., and have it affect it's owner with a resultant force, as well as have an "exhaust" of some sort.
Note that I would only be using my propeller component for the airplane in the above example, however:
I could re-use my propeller component in:
--A boat racing game
--a C-130 aircraft model
--a Fan in a game like Marble Blast
#10
I'm going throw the 'should have used a component' pains right now with Simian Escape. The XBLA people want multiplayer yet nearly all our player related code is in game.cs. Stamina, movements, even the death animation related stuff are all in the main game file. Had I put those in components, I'd simply drop them on a 2nd player I'd add through TGBX and I'd be done. Now I have to recode all of those into components. I almost made the mistake of putting them all in one component, but am forcing myself to segregate them into several for better reuse.

03/20/2007 (7:55 am)
I think a key thing to remember here too is just because this ONE game may only use the component one time for one object, that doesn't mean you can't reuse that same object in OTHER games with little to no extra coding. The typical movement of a player is locked on the FPS style of movement with the WASD key style. If you have a player movement component which simply binds those keys and moves the player accordingly, then you just build a movement component that you can reuse in any FPS game and most other genres as well.I'm going throw the 'should have used a component' pains right now with Simian Escape. The XBLA people want multiplayer yet nearly all our player related code is in game.cs. Stamina, movements, even the death animation related stuff are all in the main game file. Had I put those in components, I'd simply drop them on a 2nd player I'd add through TGBX and I'd be done. Now I have to recode all of those into components. I almost made the mistake of putting them all in one component, but am forcing myself to segregate them into several for better reuse.

Torque 3D Owner Will O-Reagan
Modern Intrigues
Obviously your player is going to be sharing very few components with other objects in the game. But that doesnt mean that the other objects arn't going to want to share components. Still, I found myself sharing components alot, when I made my game "AstroFighters" for Dream Build Play.
One example was for Arrival Sounds and Departure Sounds, I found It was nice to be able to just slap on the sounds in the editor, and have them work when an object arrived or departed from the scene. Look at the SpaceShooter example, it has a destrucatble component shared by player and enemys