The Ultimate World Object Node
Please be patient while I add content to this page, it takes a alot of analysis, much of which I'm sure I haven't fully covered yet! When it's done I should have fairly good suggestions of how to handle certain things, including some sample object definitions given the limits of any given game.
The objects in your game world will use a combination of their own data, static data for types that they belong to, and possibly refer to other dynamic structures created to help with behavior and animation. This section will give you some ideas of what should be stored at the world object level (in your linked list) and what should be stored in static entries that multiple objects could refer to.
- Determines default model, collision detection method, and behavior when object is created
- Current primary position and orientation - this is where you START the rendering
- Rotation speed and angle, movement velocity vector
- Target direction, position, velocity
- Current waypoint list, current target waypoint
- Set Collision Detection model, don't bind to object type since objects can change shape
- Which model?
- Joint array
-- Joint current position
-- Joint target position
-- Render this path? (See discussion below)
Animated Image reference array - there could be multiple images, possibly even more than on any single model. Imagine a figure with an animated face bitmap picks up a sword with an animated blade bitmap? This might mean there is a node independent of the list attached to the joint in some way, or a "hardpoint" on the model?
- For each image being used
-- Which one is current?
-- Which loop are we using?
- Last time rendered - since we will not always render the object, we will need to track its rendering time separately from its movement and behavior, and catch it up when rendering finally takes place.
- Last time moved - An object will generally always move every tick.
- Last time behaved - And object will generally always behave every tick, too, but just in case, keep mvoement time and behavior time separate.
-- Current type of behavior (this is a multi-level structure, possibly referencing a state machine, a la Game Programming Gems)
- Deleted Node? (Node will be in deleted list with deleted flag set, so anything referring to the object can determine it has been deleted without getting an illegal operation). If node is in primary list when this flag is seen, delete it (maybe cleanup after everything else is finished?)
- ObjectID (Cycles around at 65536, if ID changed since last reference, the node was deleted and re-allocated)
- HostID (Number ID for sending information across the network)
- Possible array, possibly only a single entry, of a sound that will play off this object repeatedly. We need a reference to the sound maintenance buffer to see if the sound has stopped, and if it has, try to start it up again. An example would be the jet sound. Single-shot sounds do not need this, they are one-time events.
Any other type of data important to your game (which team are they on? which target are they attacking, escorting, etc)
Generally where arrays are concerned, to help avoid memory defragmentation, is to figure out exactly the maximum number of any array you would need at the object level. When a world object is deleted, move the array off to a linked list that tracks it so you can re-use it when you create another node. Don't depend on getting a node back that had it allocated already, because any node could have a variable number of arrays attached to it, and there's no guarantee what you'll get. To save the time of retrieving the arrays from storage, you could just see if the object has the array, and if it doesn't, create another one - but you'll be using up a lot of memory doing that, it's you're call. The reason we need to be so dynamic is that we never know what a node will be used for. Hmm... what if we prepared the structures we needed for every object ahead of time in structures... and had a pointer reference to them at the node level... hmm.. I'll need to think about that one!
The fewer arrays you need on an object, the better. For some simpler object for which you might have many of them, consider using a single entry in addition to an array. The single entry would be for an object that is guaranteed to have only one animated face, like a particle. The rest could be stored in an array in a different place for more complex objects. The rendering models should be aware of where to get get these values when the rendering takes place.
Be careful how "neat" you get with your structures here. The parsing MUST be fast and have as little referencing as possible. For instance, it is faster to move along an array than it is to move along a linked list.
I'm not sure if it would be prudent to set up a base class that will automatically handle the timing of behaviors and movement, and rendering based on model type. Because every game, for its own reasons, might be different. But I'll look into that.
Don't waste cpu time performing transformations along a joint path that has no models to render. You will still need to calculate positions of hardpoints or other important information, though. Flags along the way can guide with paths to go down.
I'm evaluating the idea of creating a class for every object that might be in the game, with virtual finctions to override. For example, say I write a function called TargetObject(target, weapon hardpoint to fire, time to move), which returns a boolean as to whether or not the targeting was successful (meaning the shot is lined up and ready to fire), I could write a specific function for every obejct type - a turret that has to rotate its base and pitch its cannon, a ship that needs to reorient itself to face its target, or even an object that could fire in any direction at all immediately, like a glowing lightning ball. If it returns true, then I just call FireWeapon(weapon hardpoint, target), which may also be different for every object.
In some cases, TargetObject doesn't just line up the target. A jet could be close to lined up but a missile is locking on. Returning true means it is reasonable to fire the weapon with some degree of success.
I can see a whole new section getting developed. Man this is a lot of work.
Other types of functions could be SetPitchControlPosition(),
for models that would respond to the position of a control, like a mouse for
TurnTo() - turn the object in this direction. This might make a robot step around, a turret spin, etc.
Let's see... A list of possible functions...
PerformMove(time) - called every tick - move the object, may
call FollowPath() if the object has a path set up. Or may call MoveTo() specifically.
PerformBehave(time) - called every tick - make the object behave based on its current state, and make decisions based on what it encounters during the behavior this tick.
SetMovementDest(pos) - could call PlotPath() and choose the first point for FollowPath(), or set a rudimentary target for call MoveTo(). FollowPath() and MoveTo() would be called by PerformMove()
PlotPath(pos) - Set movement target. Object must determine its path. No actual movement takes place, just the object setting itself up. Called whenever a path needs to be set up.
FollowPath(time) - Follow the path set up, calls MoveTo() if necessary, returns true if reaches final destination and no more path. Called every tick.
MoveTo(pos, time) - Move to a specific position. Object's attributes are set to perform turns or whatever else is necessary to get there, returns true if made it to the target. Update last time moved. Calls TurnTo(angle, time) if necessary.
TurnTo(angle, time) - Spin to face a given angle, return true if reach target angle.
What is tricky about this is collisions can take place during the move, so how will we cause objects to react accurately? It depends on how accurate we need to be. In billiards, we wanted it to be perfect. In a RTS where there are many objects, we may have to settle for something less accurate timing-wise. Exactly how to handle this will follow.
I kind of like this... this could turn out well...
One of the problems I face as a programmer is deciding how to handle the timing of behaving and collision detection. I imagine let every object go through its behaviors and move to its destination. All objects should be allowed to go through with this, because we wouldn't want any objects having an unfair advantage based on their position in the list. Then, after all behavior is finished, we check for collision detection. Again, we didn't do it this way with billiards. With billiards we behaved, moved and took care of collision detection all at the same time. Is it possible to do this no matter what kind of game we are programming??
Also, there's the problem of two craft being in a dog fight. We don't want one having an advantage because the other has moved already, making his shot more accurate. Should lining up with the target be based on the target's movement and velocity at the exact same time frame for every object? I see storage for position and movement attributes at the beginning of a tick, and at the end of a tick. Either the beginning values or end values will be used, depending on whether or not the object has been caught up from last turn. A pointer points to the latest one and the earliest one. Both have the time at which those states occur as well. This may be overkill, but I'm still one for accuracy...
I think I finally came up with a good way to automatically implement object types without having to use a Type variable and a bunch of if-then-else's...
The world object will have all its data stored in its node.
In addition, the node will have a pointer to a behavior interface class.
This interface class is just a class with virtual functions ONLY. Functions will include Behave(), MoveTo(), WeaponsBearOnTarget(), etc. The base class has only empty functions. The derived versions, one for every possible object type, will contain these functions, specifically handling each object type.
(Previously, I thought I would just declare the class as the whole world object with all data and the specific functions. This turned out to be a bad idea, because since I reuse the nodes that were deleted previously, and each node would be created as an object type, there was no easy way to change the node from one object type to another, because when you set one class equal to another, is only copies the members, not the functions. that means a node of a specific type would have to stay that way forever. And that woulnd't do me any good for reuse.)
For example, if I name the base class CBaseBehavior, and I derive
two classes off from it named CTurretBehavior and CJetBehavior, I can code
the WeaponsBearOnTarget() function to make the corresponding object type brings
its weapons to bear on the target, and return true if the weapon is in position
In CTurretBehavior, the function would cause the turret to rotate its base and pitch its turret to point at the target.
In CJetBehavior, the function would cause the jet to bank and turn until its active weapon hardpoint is pointing at the target.
When the game initializes, I will create two classes with these types and name them turretBehavior and jetBehavior.
The world object node will have a CBaseBehavior * member to hold the interface to use, named lpBehavior.
If I create a turret object, I will just set the lpBehavior
member to &turretBehavior.
If I create a jet object, lpBehavior is set to &jetBehavior.
When it comes time to get any given object to bring its weapons to bear, I can just call lpBehavior->WeaponsBearOnTarget(). The nature of virtual functions causes the corresponding behavior for the specfic object type to kick off.
This is a great way to contain object-specific routines. It is also possible to assemble behavior classes with some common functions, and derived more specialized behavior classes off from them. For instance, CTreadBehavior might contain a MoveTo() function that suits all treaded vehicles, so CArtilleryBehavior and CTankBehavior might be derived off CTreadBehavior for the MoveTo() function, but still possess their own custom WeaponsBearOnTarget() function.
This also makes it easy to change the object to something else
just by changing the lpBehavior pointer to a different interface.
Because the classes are functions only, they contain no world object data. That data must be provided via parameters to the function calls. Data included would be a pointer to the node itself, current session time, etc.
I'll continue my analysis and see how all this can fit with with custom behavior state machines, and whatever else might be necessary to make the whole thing work!
The behavior state machine will also be a part of this class, since each state machine is also object-specific.
Collision detection will be a different story. Collision detection occurs between 3D volumes, not as a result of object-specific function calls. The two 3D volumes will need to be compared and the appropriate function called (sphere to sphere, line to sphere, cube to line, etc.)
Also, as a standard practice, I will flag nodes to be deleted, but actually delete them at the end of the game tick. That way I can avoid any special operations required during a linked list parse.