Advertisement

The 5millionth post on OO design principles..

Started by December 06, 2000 08:30 PM
51 comments, last by Remnant 24 years ago
Well, when you come to the cloaked ship issue, there are a couple of ways to approach it. The first is to say that a cloaked ship is a special kind of ship. Another is to say a cloaked ship is a ship that has a cloaking device. So the choice becomes encapsulate or inherit.

If we go with the is a relation ship then we can create class Ship; and class CloakableShip : public Ship; This makes sense if in this particular universe cloaking depends on having more than just a cloaking device, it requires special hull materials, power systems, etc. In this case we don''t worry about non-cloakable ships become cloakable, and the is a relationship holds true.

On the downside: But then, what if StarBases can be cloakable too? The we have class StarBase; and class CloakableStarBase : public StarBase; Or we can go with multiple inheritence and CloakableShip inherits from class Cloakable and class Ship. Ugly ugly ugly. Let''s pretend we never had that thought.

Another thought is that cloaking devices still suck down power, don''t they? (At least I would imagine they would.) So we might end up creating cloaking device components anyway, for purposes of damage control, power consumption, etc.

For the real downer, let''s consider what if we created a ship that suddenly became cloakable? If we lived in the Star Trek universe, for example, there are numeruous examples of this happening. True we could try replacing the Ship object with a new CloakableShip object, but updating pointers can be a major headache. So instead we''ll declare that there''s only one class that can actually use pointers to Ship objects (outside of components of the Ship object). So we create the new ShipManager class. Ship manager handles the creation and deletion of Ship objects, and any other class needs to use handles in order to access Ship objects. So let''s create the ShipHandle class. Assuming for simplicity that there is only one ShipManager object (and we have a global pointer or something similarly easy to access it), we can implement ShipManager as a hash table or vector or something similarly quick to query, and that vector will hold the actual pointer to the class. Then the ShipHandle only needs to maintain an index into the hash table or vector. So now when we change a Ship into a CloakableShip, we create a new CloakableShip with all the same attributes as the old Ship, and replace the pointer in the vector with the new pointer. Because everything on the outside only uses handles in order to access Ship objects, the change is transparent to everyone on the outside.

And now for the real real downer. What if Cloakable wasn''t the only really special ship attribute? Then we get weird stuff like CloakbleInvulnerableShip and InsubstantialInertialessShip and CloakableInvulnerableInsubstantialInertiallessShip. Either you''ve got multiple inheritence ugliness on a truly unprecedented scale, or you''ve got a lot of cutting and pasting to do.

So now what if we consider a cloakable Ship as a Ship that has a cloaking device. Now making a non-cloaked ship a cloaked ship is as easy as swapping out a component. And also as a separate component, you can do things to the cloaking system just as you would other components. Swap them with other ships, damage them, etc.

We still have the downside of image rendering problems. One thought is that maybe cloaking is important enough or common enough that we just make cloaking part of every ship, and take that into effect when we do our drawing effects. i.e. every Ship when drawing will poll an isCloaked variable.

Another possibility is using function objects for your drawing functions. Let''s say that a Ship object has two special member variables. The first is DrawingFunction * m_pDefaultDrawingFunction. The second is DrawingFunction * m_pEffectiveDrawingFunction. So at the top of your Act() call for the ship, set m_pEffectiveDrawingFunction to m_pDefaultDrawingFunction (or a copy). This represents how the ship will draw itself without any weird effects. Then when different components get Act() called they might change the m_pEffectiveDrawingFunction variable. A cloaking device might use the old m_pEffectiveDrawingFunction object and set the alpha blending to near max. Or the ships engines if severly damaged might replace the drawing function with a function object that calls the old drawing funciton as well as drawing sparks near the engine. Of course this kind of thing introduces some downsides. After all, those function objects are going to create all sorts of vtables and extra pointers, which will eat up space and cycles as they get dereferenced.

Actually there''s a third choice: combine the two (a.k.a. chicken out). Make every ship cloakable, and whether or not it has a cloaking device determines it''s cloakability.

This is different from the first case because you''ve enabled any ship to become cloakable, and to some degree added cloaking overhead to the logic and drawing methods of every ship. Maybe before only CloakbleShips had the m_pCloakingDevice member variable, and now all ships will have this variable, so the non-cloaking ships take up a little more memory.

This is different from the second case, because in the second case, we actually put all the cloaking logic into the component,and for the most part is less flexible overall than the encapsulation method.

However, because something like cloaking is so involved, many times it''s implemented like this. This doesn''t prevent you from using the other methods for different components.
I think the second is the way to go, since it allows you great flexibility in adding new types of components.
Advertisement
quote: Original post by Houdini

Wow, I love the idea of having a design patterns section that we can all add to. It''ll help a lot of people out tremdously. I only wish that the patterns all had examples, as examples make it much easier to understand how to apply the pattern.


- Houdini



Well, one of the things I''m leaving open for the design patterns section is that people can make additions to existing patterns. Like if you have an example of the Spatial Index pattern and submitted it to me, I''d modify the pattern and give the modification a reference to you. This way the patterns aren''t set in stone.

Plus, I''m always looking for people to submit new patterns! (hint, hint)

Kevin

Admin for GameDev.net.

Yeah, some good ideas thrown about here. Of course, there is no ''right'' answer.

I have something similar implemented in my MUD: a character can have any number of spells cast upon them, and each spell might have any number of given effects. So, some basic pseudocode for checking a character''s Strength might look like this:
currentStrength = basic character StrengthFor all Spells cast upon that character    For all Effects within this spell        If Effect.type == Strength            currentStrength += Effect.Magnitudereturn currentStrength 


Now, that might seem pretty trivial, and may not even seem relevant, but if you substitute "Character" for "Ship", "Spell" for "Component", you may see how it could work. The example above works for boolean variables (eg. "Presence of cloaking device") as well as integers too. Just start with the default of 0 (no such ability) and the ''effects'' that give that ability will use 1 for their magnitude. The final result will be 0 (ability not present) or greater than 0 (ability present). This is all achieved simply by containing a list of Effects within the Spell/ShipComponent class, which are themselves in a list within the Character/Ship class. It allows any number of parts to co-exist, be added, modified or removed, at runtime.

However, this system represents ''properties''. It doesn''t actually do anything. For example, a single function call can query all components and tells you whether you have a cloaking device or not, but how to implement the graphic for that is external to the system. Personally, I think this is the way to go: the Ship class should be responsible for taking actions, it just collects information from its components beforehand. If you allowed the Components to directly modify the members of Ship, then you might have to start tracking the order in which it happened, or you might have to maintain reference counts, etc etc. (eg. Component 1 contains a Cloaking Device. Component 2 also contains a Cloaking Device. When you install Component 1, it changes Ship.Graphic to "CloakedShip" or whatever. Component 2, when installed, would do the same. But then, what happens when you remove one of those Components? Will removing Component 1 set Ship.Graphic to "UncloakedShip" since it was the Component that added the Cloak? etc) So I would prefer to just do:
if (GetFeature(CLOAKING_DEVICE) > 0)    graphic = "cloaked.pcx";else    graphic = "uncloaked.pcx"; 

Similarly, code such as:
velocity += GetFeature(ENGINE_POWER);
can work for much of the design.

The only feature you mentioned that I can''t see how to easily and cleanly represent this way is "an autonomous turret needs targeting information from the ship''s sensors". I would probably model it something like this:

numSensors = GetFeature(SENSOR);
if (numSensors > 0 && GetFeature(AUTO_TURRET > 0)
OperateTurrents(numSensors); // A member of Ship

That example is almost certainly too simplistic for what you are doing, but hopefully you can see what I mean.
wow, that''s a very exhaustive list of options there, Crane.

For my game, only the last option "Ship has-a cloaking device" really would work properly, for the reason you stated - ships can load/offload parts to give them different attributes and special abilities, with only a few restrictions (ie, perhaps the ship has to have Tech Lvl4 hull design to use a cloaking device, but any ship can use a reactive-armor type device, or afterburners -- you get the idea).

So the option that I want to go with is #2 (has-a cloaking device).. I think what I''m going to do is look into ways of doing as you said, and having a generic Ship have several different functions that allow the special-ability parts to change looks/behavior of the ship.

- Remnant
- (Steve Schmitt)
- Remnant- (Steve Schmitt)
quote: Original post by Kylotan

Yeah, some good ideas thrown about here. Of course, there is no 'right' answer.

currentStrength = basic character Strength
For all Spells cast upon that character
For all Effects within this spell
If Effect.type == Strength
currentStrength += Effect.Magnitude
return currentStrength


Might this be better if you abstract the different spell types from a common root rather than having a flag for each effect? You could decouple the spells from the character, which would allow the internal implementations to be hidden. Like:

Character:   basicStr, currentStr   Character()       { currentStr = basicStr }   increaseStr(amount)       { currentStr += amount }   list  spellsOnMe   bool addToSpellsOnMe (refSpell)       { if (spellAlreadyOnMe() and not refSpell->isCumulative)             return (FALSE)         else spellsOnMe += refSpell         return TRUE       }Spell:   virtual castMe(target)   bool isCumulative   virtual failureMessage()        {  "Spell failed" }StrSpell : Spell   magnitude   castMe(target)      {  if target.addToSpellsOnMe(this)            target.increaseStr(magnitude)        else failureMessage() } 


Now the character only needs to understand the basic spell interface, and the spell only needs to know the basic character interface. The details remain isolated, while the important functions are available for other objects to use.

You could modify and expand this, of course, to include a wearing off function, but you basically refactor the design into something more flexible.

As you said, there is no right way of doing things. This is just another option.

Edited by - JSwing on December 12, 2000 8:55:36 AM
Advertisement
I must admit slight ignorance in that I can''t really see what you''re trying to tell me. Perhaps because we''re thinking along very different lines.

I don''t want a class for each spell, as this system doesn''t just have to model spells. It can model any effect. For example, a Drunkenness effect might modify Awareness by -10% and Agility by -20%.

You might then just say "well, have a class for any effect then", but that still isn''t useful/necessary. There are times when I want to generate a new effect, at run time. To go into more technical, the system doesn''t have ''spells'' on a character. A spell is an object that, as part of it''s execution, may attach an Effect object to that character. An Effect object is essentially a list of Modifier objects. A Modifier object is an attribute/value pair, such as {"Awareness", "-10"} from the example above. My example in a previous post uses different terms as I was just trying to be abstract.

Here''s the actual code I use:
  int Character::GetTotalModifiersTo(String stat) const{	// Loop through all effects. If the stat matches this one, apply the	// difference to the running total. Return the final amount	int total_modifier = 0;	EffectList::const_iterator eli;	for (eli = effects.begin(); eli != effects.end(); ++eli)		total_modifier += (*eli)->GetModifiersTo(stat);	return total_modifier;}  

The Character loops through its Effects, and asks them in turn if they have any modifiers to the specified statistic. Obviously, the Effect object then loops through its own Modifier objects, totalling up anything for the given stat. In the end, the result is returned and total_modifier will get altered accordingly. The Character class doesn''t need any knowledge about spells or skills or drunkenness: all it needs to know is how to call the GetModifiersTo(stat) function. Similarly, skills and spells etc don''t need to know much about Characters: they just need to be able to generate an Effect object and attach it to the specified character.

Technically, I can even add new statistics at run-time within this system, as the statistic type to be queried can be specified with a string.

Does that address your points? Is there any other way I could improve this?
Fixed my post so it indents ok.

Actually we''re approaching it from different sides. I think we''ve each got different pieces of the same elephant.

You''ve got a composite pattern for the effects, which is (IMO) great. I threw in a generic list instead, but your construction is better.

I was approaching it from the effect side, where the effect changes the player attribute when invoked (more like a visitor pattern). You approach it from the character side, where the character calls for the effect when needed. (Replace ''spell'' in my example with ''effect'')

With your routine every effect has to know how to deal with every stat, which may not be a big deal. With my version, if the character interface changes (the addToStr function) then the effects break.

I think either would work. (In fact they could dovetail together). I guess it comes down to how often you will need to call your loop, which I think depends more on your game design than your code.

Hi

Take care with OOP in games...

OOP may be efficient for the programmers point of view ...but is
awensome slow so totaly ineficient for speed whatsoever...

A good game can be done with no OOP whatsoever...(besides menu systems) so use OOP wisely and only in no speed critical sections...

IMHO of course
Bogdan
obysoft
quote: Original post by bogdanontanu

Hi

Take care with OOP in games...

OOP may be efficient for the programmers point of view ...but is
awensome slow so totaly ineficient for speed whatsoever...

A good game can be done with no OOP whatsoever...(besides menu systems) so use OOP wisely and only in no speed critical sections...

IMHO of course
Bogdan


I disagree.

People love to point out the fact that OOP is slow and inefficient.
This is their ONLY argument against OOP. People who use OOP, use
it wisely. They don''t use it blindly. Smart designers use profilers
to judge the performance of their program.

I know there are fast, commercial games out there on the market
that use OOP. Perhaps someone should create a list of these games
to kill off arguments like this once and for all.


This topic is closed to new replies.

Advertisement