Advertisement

Unit design question

Started by September 23, 2002 10:53 AM
44 comments, last by Dauntless 22 years, 3 months ago
This question is sorta half programming, but half design related too. Since I''m still trying to figure out programming, when a programmer designs units for a strategy game, do they code the attributes of the unit internal (intrinsically) to the unit itself, or is there a "database" that is external to the unit that keeps track of all this stuff? For example, let''s say I create an array of 10 elements: Attribute[0] = 5; //let''s say the first element is the move attribute Attribute[1] = 3; //make the 2nd element the defense rating Attribute[2] = 7; //the 3rd element will be it''s attack rating And so on and so forth. So my question is, do you code it like this? class Tank { int Attribute[10]; int * pAttributeSelector(); //function that returns a pointer to the array element you are looking for } Or do you make the attributes external to the unit like so: class Tank { //create a pointer to an array on the memory heap int * pAttributeDatabase = new Attribute[10]; } The reason I ask is because I''m wondering if there are any limitations with each of the above methods. It seems more natural to use the above method, where the data is kept internal to the object itself, but I can see some limitations. Suppose for example that an object is "killed". What happens to its equipment? I would imagine that if the unit is destroyed then all of the data for it is destroyed as well. If I use the external method, there can just be an element that keeps track of whether or not the object is alive or dead. Also, since I am designing this around a table-top wargame, it feels easier to design, since this is how table top games are played (you have sheets that keep track of the data).
The world has achieved brilliance without wisdom, power without conscience. Ours is a world of nuclear giants and ethical infants. We know more about war than we know about peace, more about killing than we know about living. We have grasped the mystery of the atom and rejected the Sermon on the Mount." - General Omar Bradley
It''s more common to name your attributes, for one thing.

  class Tank{public:  // apart from constructors and simple accessors, you''d also have  // methods for simulation within your game framework  void ApplyIncidentForce( const vector3 & force ); // eg a rocket, or collision with another tank  void FirePrimaryWeapon( void ); private:  // some designs would rather inherit these properties from a  // generic "physical object" or "vehicle" class.  vector3 velocity;  vector3 acceleration;  vector3 position;  vector3 turning_arm;   int primary_ammo_count;  int secondary_ammo_count;  static int max_payload weight;  static int max_occupant_weight;  // etc};  

In general, there will be processes that you will want to apply uniformly to a wide variety of units. Some people streamline the code for this by inheriting all their unit classes from a single base class - "Unit", for example. If you have C++ at your disposal, you can also use templates for the processes (algorithms), and simply ensure that all unit classes have common function names.

Now, while each unit maintains its own data, the values are loaded from external sources - files and the like. A database system is probably less useful because it means runtime access, which can be a major performance hit.
Advertisement
Those approaches seem very inflexible - maybe it was just a throwaway example, but I would rather avoid hard coding stuff like that.

I''d split data into two separate categories - constant and variable stats. Variable data includes things like current hit points, current ammo etc, and anything else that can change during the game, whereas constant data includes all the constant stuff - starting hit points, starting ammo, weapon strength, armour etc.

Each instance of a unit maintains its own copy of the variable data, and a pointer to the constant data. The constant data is loaded in from a file at the start of day.

I wouldn''t create a ''tank'' class. I''d make a ''unit'' class which I''d use for everything, and make it entirely data driven.
How would you do it when two types of units have drastically diffent behaviours? Perhaps a grunt vs a special ''casting'' unit. Or a vehicle like a tank that can move and shoot in different directions at the same time vs a soldier who can''t? Where do you specify these types of behaviours that not only have affect on behaviour but rendering and such. (If it''s frames from a texture which frame do you chose? Or a model, you need to specify the rotation of the turret perhaps. How do you build a generic case of this. ) This seems like a job for virtual functions. Is there another way? Function pointers are messy when dealing with classes.
Oluseyi-
I''m still trying to get to grips with inheritance...especially virtual functions . Polymorphism and multiple inheritance are a little beyond me too, although I can see where it might be useful. And templates are WAY beyond me right now. I can see what you are saying though, and you answered one of the things I was wondering, namely if having an external database would drastically slow things down.

However, since my game will be half turn based half RT, I wonder if the performance hit would be too bothersome?
The world has achieved brilliance without wisdom, power without conscience. Ours is a world of nuclear giants and ethical infants. We know more about war than we know about peace, more about killing than we know about living. We have grasped the mystery of the atom and rejected the Sermon on the Mount." - General Omar Bradley
Special ''casting'' abilities could concievably be treated just as a weapon. If your casting abilities are more fancy than straight ''damage/heal'' type effects, then a very simple scripting engine would probably suffice.

There are ways of easily coping with the animations too. Perhaps all models consist of two parts - a ''chassis'' and a ''turret''. You then render the turret pointing in the unit''s ''facing'' direction, and the chassis pointing in the ''movement'' direction. In the case of the soldier, you might want to constrain these two values to being the same, whereas in the case of the tank, you might allow say, 45 degrees of variation. In any case, if you decide that maybe you want to give your soldier more flexibility, just tweak a number in a file, and suddenly he can turn at the waist. Much easier than suddenly realising you have to redo all your models, and rebuild half your engine in order to cope with the new features.

Likewise with walking/driving animations. Say you have three ''walk'' keyframes for all models. In the case of the tank, these keyframes would all look rather similar, probably the only difference being texture coordinates on the wheels or something like that. In the case of the soldier, they would look very different, and would give you his walking motion. There are ways to improve on this obviously.

Of course, there will always be limitations, but it is up to you to decide how much complexity you need and design your code around that.
Advertisement
dang it, stupid forums are acting up again and I lost my last post

Sandman-
I can see what you mean by splitting up data between constants and variables. I was originally thinking of having a two dimensional array with the first "row" being the initial values of the attributes, and the second "row" being the current values. I guess that would be a more unwieldy not to mention not as well documented way of doing it. And it helps having constants to help out any debugging I guess, since those values couldn''t change. I was thinking a constant type could be MobilityType = "tracked"; (or "wheeled" for example....although I don''t know if it would be good to use them as literal constants or not).

As for object oriented programming, some concepts I understand, some I don''t. What really gets me is virtual functions. For example, let''s say I subclass Tank from ''Unit''. Let''s say I give the ''Unit'' class a method of fire(), but I make it a virtual method. If I make a call to the fire() from a Tank obect called M1Abrams, I don''t understand if I''m calling the ''Unit'' fire(), or if I''m calling the ''Tank'' subclasses fire() method. Oh well...I''ll get it eventually

Oluseyi-
I think I see what you are saying about loading values from external files, but what about object data persistence? Suppose for example that a Platoon of infantry has an attached anti-tank weapon team (my unit design will be modular), and that the Platoon takes damage and the AT weapon team is killed. Along comes Platoon B who still has its AT team, but they have no ammo. How can you make it so that the data intrinsic to the unit is not lost?

Since my game will have big element dealing with logistics, this is very important to me. I need to be able to keep track of variable, errr...variables, but it needs to be persistent. There is another reason. To me, there will be a difference between a "destroyed" unit, and a "out of commission" unit. For example, soldiers too wounded to fight, tanks that are out of ammo AND have their mobility destroyed would be "out of commission", but not "destroyed". It would seem easy to do with an external database, but I''m not sure how to do this by keeping track of the data internally (keep some of the data on the freestore with a pointer to it?).
The world has achieved brilliance without wisdom, power without conscience. Ours is a world of nuclear giants and ethical infants. We know more about war than we know about peace, more about killing than we know about living. We have grasped the mystery of the atom and rejected the Sermon on the Mount." - General Omar Bradley
Sandman:
While I see your point re: flexibility, I think you partly misunderstood me. Taking the simple (and very incomplete) Tank throwaway example, all of the data in the object is variable and instance-specific. The max_payload_weight and max_occupant_weight are global to all objects of that particular type (Tank in this case), thanks to the static keyword. Those two values (and any others like it) are loaded from file and do not vary over the course of the application. Since the data is specific to tanks, it''s also more memory efficient to maintain it within the tank class than to have some global Constants data structure to which each tank must now maintain a pointer (throw in dereferencing and member access and you''re two orders of magnitude slower as well).

On having one super Unit class versus multiple classes by unit type, specific design factors will influence this. If I were implementing an RTS design, I''d probably have abstract base classes like physical_object (can exert and receive force; contains overridable base implementations of common methods, etc), unit (can receive/respond to orders, etc) and so on, with specializations along the way tailoring or extending behavior. This allows me to easily create collections of seemingly heterogenous characters and apply common process to them.

Naturally, we want as much as possible of the simulation to be data-driven and scriptable, meaning that the above features/factors will be moderated by their process-intensiveness. If they are relatively inexpensive operations, it''s better to have them driven by the scripting language than hardcoded into a static, compiled executable.

Dauntless:
You have to weigh the pros and cons of centralized versus individualized data storage. With central data storage, you only have one location to alter data (and perhaps even process), but you have to be certain you are altering data for the appropriate type, which may create hard to track down bugs. With individualized data storage, you know that the data you are working on is specific to that type already; the only problem is locating the type definition (which isn''t too big a deal with a little logical organization of files). The colocation of type-specific data and process was considered so important that it gave birth to an entire paradigm of programming (object-orientation), so I wouldn''t lightly discard it.

OO isn''t a solution alone, however, as it sometimes complicates data sharing and type cooperation. That''s part of the reason for the advent of patterns - well-documented solutions to common programming problems, many OO. There are a couple of patterns repositories on the net, and it may be a good idea to peruse them for design pointers. At the same time, keep in mind that if you''re still a beginning-to-intermediate programmer, completing the game is more important than brilliant, academically-sanctioned design.

Side note: Another recent paradigm is aspect-oriented programming, which is the idea of defining how certain behaviors and functionality operate in abstract, independently of the data type. You now add aspects to your type designs and inherit all the existing functionality (which is extensible). The only language I can think of with something similar is Ruby''s mixins.
Oluseyi-
I definitely class myself as a beginning programmer, so I think you''re right in that my first priority should just be getting the game finished I''ll worry about making it more design led once I get more experience. I''m getting sidetracked with Python right now, which is bad since I''m not even that good with C++, but it seems like it''d be easier to make a rapid prototype with it just so I can iron out some design patterns like you were suggesting.

Is Ruby on the order of complexity of Python? Pickles in Python seem cool, but I know nothing of Ruby. It also doesn''t help that I''m totally self-taught so I don''t have anyone (other than the forum here) to get any guidance with. I will be going back to school in the spring though to get a CIS degree...which I''m sure will put a huge damper on my game project, but at least I''ll have a more solid foundation on which to design my game (I hope).

Speaking of programming, I''m going to practice some now....if I spent as much time learning programming as I spent posting, I''d probably be half-way done with my game by now, hehe.
The world has achieved brilliance without wisdom, power without conscience. Ours is a world of nuclear giants and ethical infants. We know more about war than we know about peace, more about killing than we know about living. We have grasped the mystery of the atom and rejected the Sermon on the Mount." - General Omar Bradley
quote: Original post by Dauntless
As for object oriented programming, some concepts I understand, some I don''t. What really gets me is virtual functions. For example, let''s say I subclass Tank from ''Unit''. Let''s say I give the ''Unit'' class a method of fire(), but I make it a virtual method. If I make a call to the fire() from a Tank obect called M1Abrams, I don''t understand if I''m calling the ''Unit'' fire(), or if I''m calling the ''Tank'' subclasses fire() method. Oh well...I''ll get it eventually

If the Tank class overrides the fire() method, then you are invoking Tank::fire(). If not, then you are invoking Unit::fire(). Where this becomes powerful is with collections of derived objects. Consider the following:
// abstract class relationshipsclass Unit {} = 0;  // abstract class; can never have an object of type ''Unit''class Tank : public Unit;class Soldier : public Unit; std::vector< Unit * > units;...units.push_back( new Tank );...units.push_back( new Soldier );...for_each( units.begin(), units.end(), bind1st( fire() ) ); // not exact usage 

The preceding code iterates over the collection of pointers to Unit objects, which may in fact be Tanks or Soldiers, and invokes fire() on them. If they provide an override, that version is called. If not, the base Unit::fire() is called (but must be defined).

quote:
I think I see what you are saying about loading values from external files, but what about object data persistence? Suppose for example that a Platoon of infantry has an attached anti-tank weapon team (my unit design will be modular), and that the Platoon takes damage and the AT weapon team is killed. Along comes Platoon B who still has its AT team, but they have no ammo. How can you make it so that the data intrinsic to the unit is not lost?

In this case the data is not intrinsic to the unit, per se. It is, instead, shared between the unit and the platoon, with ownership (up to capacity) passing from one to the other in the case of termination. In other words, when the Tank is terminated it passes its ammunition to the Platoon. If the Platoon is disbanded in the field because the leader and the majority of members die, it grabs as much ammo as it can and heads off to either complete the objective or return to base.

One way to implement this is for each Unit to be able to query a UnitAggregation (such as a Platoon) for available ammo. The other way is for the UnitAggregation to query the Unit for its ammo needs and pass ownership when the Unit is added. It is also necessary for the reverse to occur when one of the two is effectively killed:

  // forward declaration (a flaw in C++''s compilation model, imo)class UnitAggregation; class Unit{public:  void SignalAddedToAggregation( UnitAggregation * ua );  ammo_desc QueryAmmo( void ) { return ammo; }   void Croak(); // called when dyingprivate:  ammo_desc ammo;  UnitAggregation * ua;  // Unit can only belong to one UnitAggregation}; class UnitAggregation{public:  void AddUnit( const Unit * u ); private:  std::list< Unit * > units; // list because of fast insert/remove  ammo_desc ammo;}; void Unit::SignalAddedToAggregation( UnitAggregation * ua ){  this->ua = ua;} void Unit::Croak( void ){  if( ua )    // pass ammo to ua   // any other dying business} void UnitAggregation::AddUnit( Unit * u ){  u->SignalAddedToAggregation( this );  ammo_desc unit_ammo = u->QueryAmmo();  // determine if any empty fields in unit_ammo correspond to filled/surplus  // fields in UnitAggregation''s ammo member. if so, transfer ownership.   units.push_back( u );}  


I have to run now, but I''ll be back. This is one of the most interesting threads I''ve come across in ages!

This topic is closed to new replies.

Advertisement