4. The GameHost Class
The next framework class is the GameHost class. This class holds collections of various objects that we will want to use in our games, specifically Dictionary objects containing textures and fonts, and a List of the actual game objects. The game objects are stored in a list containing objects of type GameObjectBase, which allows us to store within it the derived SpriteObject and TextObject game objects that we have so far discussed, as well as any game-specific classes that derive from any one of these.
The class also contains some simple methods that we
can call to save having to write boilerplate functions in the main
project's Game class.
The GameHost position within the framework is shown in Figure 3. It derives from the Microsoft.XNA.Framework.Game
class (from which all actual game projects must derive a single class).
This means that in our game projects we can derive the main game class
from GameFramework.GameHost instead of from Microsoft.XNA.Framework.Game. As a result, we get all the functionality of the XNA Game class and all the functionality added into GameHost.
The object collections are accessed by the following properties:
Textures stores a dictionary of Texture2D objects. Any stored texture can be easily retrieved via its named dictionary key.
Fonts performs the same function for SpriteFont objects.
GameObjects is the list into which all the game's active objects will be placed. This is defined as a generic collection of GameObjectBase objects.
In many cases, we can add game objects to the class
and then simply allow them to carry out their own tasks until one
encounters a circumstance that requires us to interact with it. For
example, in an Asteroids game we can
simply set each asteroid object to move around the screen and then
pretty much forget about them until one happens to collide with a bullet
or the player's ship. When we detect that this has happened, we will
process the asteroid as required. There is no reason to track the
asteroids other than by having them present in the GameObjects list.
For other objects, however, we might need to be more
proactive. The player's spaceship, for example, will need direct
modification from the game so that it can respond to player input. To
keep track of this, a separate reference to the game object will need to
be stored in the game itself. The same applies for text objects whose
values need to be updated (for the player score, for example).
Once the game is initialized and running, two additional methods are available to simplify the code in the main game class:
UpdateAll loops through all the items in the GameObjects list and calls the Update method on each one. This function can be called from the main game class's Update method to keep everything moving forward.
DrawSprites identifies all SpriteObject (and derived) objects in the GameObjects list and calls the Draw method of each. It requires an initialized SpriteBatch object to be passed in, and it is the calling code's responsibility to call its Begin and End methods. Keeping the call to Begin
in the game class means that it can be given whatever parameters are
appropriate for the sprite batch operation. A second overload draws only
the sprites with a specified texture.
DrawText identifies all TextObject (and derived) objects in the GameObjects list and calls the Draw method for them, just as DrawSprites does for SpriteObject game objects.
DrawSprites and DrawText are
essentially provided simply for convenience and need not be used if
additional functionality is required. If several sprite batches are
needed with different parameters, for example, and the DrawSprites
overload that separates the sprites by texture is not sufficiently
flexible, the game class can simply implement its own custom version of
this function.
The code within UpdateAll is more complex than a simple for each
loop and it is worth some further exploration, so let's take a look at
what it is doing. The reason for its complexity is that, when .NET is
iterating through a collection, GameObjects in this case, the
collection is not allowed to be modified in any way. Attempting to add
an object to, or remove an object from, the collection will result in an
immediate exception because, if the order of the object is changed in
any way, .NET cannot ensure that it hasn't skipped over or
double-processed any of the objects.
A simple way in which we could have worked around this would be to call the collection's ToArray method and iterate over this instead of the collection, as shown in Listing 9.
Example 9. A simple but flawed approach to iterating over the objects while still allowing collection modifications
foreach (GameObjectBase obj in GameObjects.ToArray())
{
obj.Update(gameTime);
}
|
This appears to work very well and meets our
objectives: all the objects are updated and each can perform any
modification to the collection that it wants. It has a flaw, however:
every time the loop is prepared, it creates another array of object
pointers and as a result consumes a little extra memory. This might not
sound important, but the method is being called 30 times per second and
might be processing hundreds of objects. This quickly runs into
noticeable amounts of memory being used: 30 updates per second with 100
objects requiring 4 bytes per object pointer results in 12,000 bytes
allocated per second.
When memory is being allocated this quickly on a
device running .NET Compact Framework (CF), the garbage collection ends
up triggering on a frequent basis. Each time this happens, the
application briefly pauses while the process executes. This is not
likely to be noticed in an e-mail or diary application, but in a
fast-running game it will really attract the user's attention in an
unpleasant way.
We therefore want to try to minimize the amount of memory that is allocated within the game loop. To solve this within the GameHost, a private array of GameObjects is declared at class level, as shown in Listing 10. We will use this array over and over again, minimizing memory allocation as far as possible.
Example 10. The class-level _objectArray variable
public class GameHost : Microsoft.Xna.Framework.Game
{
//--------------------------------------------------------------------------------
// Class variables
private GameObjectBase[] _objectArray;
[...]
|
This array initially starts off uninitialized. The UpdateAll
function first checks for this state; when found, it creates the array
with enough space to hold all the current known objects in the GameObjects
collection, plus space for 20 percent more. Allowing a little extra
like this means that we don't need to reallocate the array every time a
new object is added. If there are fewer than 20 objects in the game, it
allocates a minimum of 20 to prevent reallocation each time a new object
is added with low object numbers (adding 20 percent to 4 objects
results in still only 4 objects after rounding).
If UpdateAll instead finds that the array
has been previously created, it checks its capacity against the current
game object count. If the object count fits within the array, it leaves
the array alone. On the other hand, if the object count is now too large
for the array, it reallocates the array by creating a new object
collection, once again providing space for 20 percent more objects than
are currently present.
This ensures that the array starts at a reasonable
size and increases in stages rather than one object at a time. The old
arrays that are discarded will be garbage-collected in time, but will be
small in number and so won't waste large amounts of memory.
The opening section of UpdateAll that performs the steps discussed so far can be seen in Listing 11.
Example 11. The beginning of the UpdateAll function, allocating space in the _objectArray variable
public virtual void UpdateAll(GameTime gameTime)
{
int i;
int objectCount;
// First build our array of objects.
// We will iterate across this rather than across the actual GameObjects
// collection so that the collection can be modified by the game objects'
// Update code.
// First of all, do we have an array?
if (_objectArray == null)
{
// No, so allocate it.
// Allocate 20% more objects than we currently have, or 20 objects,
// whichever is more
_objectArray = new GameObjectBase[
(int)MathHelper.Max(20, GameObjects.Count * 1.2f) ];
}
else if (GameObjects.Count > _objectArray.Length)
{
// The number of game objects has exceeded the array size.
// Reallocate the array, adding 20% free space for further expansion.
_objectArray = new GameObjectBase[(int)(GameObjects.Count * 1.2f)];
}
|
With the array created at an appropriate size, UpdateAll
now populates it. This takes place for every update because we cannot
tell at a high level whether the objects have been manipulated (checking
the object count would not be sufficient as objects might have been
removed and inserted in equal numbers). This might seem wasteful, but is
really just assigning object pointers and so is extremely quick to
execute.
There is another important thing that we need to do
with the array, and that is to release any references we have to objects
that have been removed from the GameObjects collection.
Without this, our array could potentially keep them alive for extended
periods within the elements past the end of our main object loop, even
though nothing is actually using them any more. Releasing them ensures
that their memory can be freed the next time garbage collection runs.
Removing these references is simply a matter of setting the array elements to null for all objects other than those in the GameObjects
collection. As we are looping through the array to tell each object to
update, it is easy to just continue looping to the end of the array
setting the elements that we are not using to null. This will release all the expired object references.
The array population section of UpdateAll is shown in Listing 12.
Example 12. Copying the current game object references into _objectArray and removing expired object references
// Store the current object count for performance
objectCount = GameObjects.Count;
// Transfer the object references into the array
for (i = 0; i < _objectArray.Length; i++)
{
// Is there an active object at this position in the GameObjects collection?
if (i < objectCount)
{
// Yes, so copy it to the array
_objectArray[i] = GameObjects[i];
}
else
{
// No, so clear any reference stored at this index position
_objectArray[i] = null;
}
}
|
With the array populated, we can now iterate through the array and tell each object to update. The GameObjects
collection is not being iterated, and so the objects are free to
manipulate it in any way they want without exceptions occurring.
We know how many objects we actually have to process because we already queried the GameObjects.Count property and stored it in the objectCount
variable. Remember that we can't rely on the array length for this
because we are allocating space for additional objects. The stored
object count value has an additional use in the update loop: because the
GameObjects collection might be modified during the loop (which would affect the result returned by the Count property), we cannot rely on the value being stable while the updates are processing.
Once we know how many objects to process, it is then just a matter of calling the Update method for each. The remainder of the UpdateAll function is shown in Listing 13.
Example 13. Updating the game objects
// Loop for each element within the array
for (i = 0; i < objectCount; i++)
{
// Update the object at this array position
_objectArray[i].Update(gameTime);
}
}
|
When the number of game objects is static, the array
will persist without any reallocations from one update to the next. Each
time the number of objects is allocated past the current maximum, a
reallocation will occur, but the frequency of these reallocations will
be limited by the addition of the 20 percent buffer for additional
objects. Once the game's actual maximum is reached, no further
allocations will take place at all. The only overhead for all this is
two simple loops through the array, one to copy references from the GameObjects
collection, and the other to draw the objects and remove expired object
references. In return, we gain complete flexibility to manipulate the
set of objects from within each object's Update code.
5. The GameHelper Class
Finally there is the GameHelper class, a place into which generally useful functions can be added (similar to XNA's own MathHelper class). The class is declared as static, so it cannot be instantiated.
For the moment, the class just contains a number of functions relating to random numbers. It hosts an instance of a Random object and exposes several overloads of a function called RandomNext, each of which returns a random number. There are two reasons for having these functions:
They provide our game code with immediate
access to random numbers (a common requirement for game development)
without needing to instantiate its own Random instance.
They add some useful overloads that return random float
values, either between zero and a specified upper limit, or between an
arbitrary lower and upper limit. They can be extremely useful when
randomizing game objects because they use float variables for nearly all their properties.