3. Performance Considerations
Now that we can monitor the performance of our game,
let's take a look at some of the things that can cause it to slow down
and then determine some approaches to resolving these problems.
3.1. Texture Loading
What happens if we increase the number of planets significantly, perhaps to 1000? This is easily tested by modifying the PlanetCount constant at the top of the BenchmarkGame.cs source file. Give it a try and see what happens.
The result is a dramatic drop in the frames per second, as shown in Figure 2. It now displays a mere 5.9 frames drawn per second.
NOTE
This benchmark shows up an important feature of
the XNA engine: although the frame rate has dropped all the way down to
5.9 frames per second, the updates per second still stay exactly as
they were with just 10 planets. This is because XNA is detecting that
the game is running slowly due to the amount of time the sprites are
taking to draw, and as a result is calling the game's Draw method less frequently. It prioritizes calls to Update
so that the game logic runs at full speed even though the rendering
does not. This ensures that the game runs at the same speed across all
devices and regardless of processor power being drained by other
applications on the phone.
So this large frame rate drop is disappointing, but
perhaps that is all the device has the power to draw? There are a large
number of sprites after all. In fact, the sprite count is not the
actual cause of the frame rate dropping. The causeis the order in which
the sprites are being drawn.
Each time XNA draws a sprite, it must pass the
texture for the sprite to the graphics hardware ready for it to draw.
Most of the information needed to draw a sprite will fit into just a
few bytes of data (its position, rotation, scaling, and so on don't
need very much memory to describe), but compared with the sprite
location, the texture is comparatively huge. It is therefore in our
interest to minimize the amount of texture information that is passed
to the graphics hardware.
Once a texture has been loaded into the hardware it
can be used over and over again without needing to be reloaded, but
when another texture is loaded, the original texture is discarded. If
you recall, the way we set up the moons and planets in the game was by
first adding a planet object and then adding the corresponding moon
object. This means that planets and moons are alternating in the GameObjects
collection. Every single sprite we draw needs a new texture to be
loaded: first the planet texture, then the moon, then the planet again,
right through to the end of the sprite batch. In total we end up
loading 2000 textures in this configuration. No wonder the game slowed
down.
There are various ways to address this problem. The first is to sort the objects in the GameObjects list so that they are ordered by texture. To try out this approach, modify the Benchmark project's LoadContent method so that it calls into AddPlanets_InBlocks instead of AddPlanets_Interleaved.
This adds exactly the same number of objects (and the benchmark display
will confirm this), but as 1000 planets followed by 1000 moons. If you
run this again, you will see that the frame rate has now nearly
doubled. In this configuration, XNA has to load only two textures
rather than 2000, requiring just one load each of the planet texture
and then the moon texture.
If you try this out on both the emulator and on a
real device, you will find that the device performance is not nearly as
good as that of the emulator. The emulator doesn't cap its frame rate
or anything similar, and as a result can display far more updates per
second than an actual phone. It is important to performance test your
game on real hardware as you are writing it to avoid unpleasant
surprises later on.
|
|
In a real game, however, it is impractical to expect
the objects to always be ordered by their textures. As objects are
added and removed through the course of the game, the object collection
will naturally become scrambled. There are two additional approaches
that we can take to deal with this performance problem: one provided by
XNA and one by the game framework.
The XNA approach involves modifying the parameters that we send to the SpriteBatch object when calling its Begin method. Most of our examples have not passed any parameters at all, although we have passed the SpriteSortMode.BackToFront value so that the sprites' LayerDepths are observed.
One of the other sort modes is SpriteSortMode.Texture. In this mode, the LayerDepth
is ignored, but instead the sprites are, as you might expect, sorted by
their textures. Modify the example project so that it once again adds
the sprites interleaved rather than in blocks, and then take a look at
its Draw method. You will find three different versions of
the sprite batch code: comment out the current version 1 and uncomment
version 2.
Despite having added the sprites in alternating
order, the frame rate is still high just as it was when we added the
sprites in blocks. This has also resulted in just two texture loads for
all the sprites.
There are some drawbacks to this approach, however.
Most obviously, we have lost control of exactly which sprites are drawn
first; as evidence you will see that the moons have disappeared behind
the planets and the benchmark text has disappeared behind everything.
In addition, using the Texture sort mode means that we cannot use the LayerDepth feature at all.
In many cases, these drawbacks will be of no
significance and this approach will work fine. If you need to regain
some of the control of the rendering process, use the second approach.
The GameHost.DrawSprites method that we
have been calling actually has another overload that allows a texture
to be passed in as a parameter. When this version is used, only sprites
that use the supplied texture will be drawn; all others will be
completely ignored. This allows us to draw the sprites grouped by
texture, but to also be in control of which textures are processed
first.
If you comment out version 2 of the Draw
code and instead uncomment version 3, you will see this approach in
action. The moons are now rendered in front of the planets, and the
benchmark text is in front of everything.
However, none of these approaches gives the exact
same rendering of the planets and the moons that we started with, in
which each moon went in front of the planets defined earlier in the GameObjects
list and behind any planets defined after. Some effects cannot be
achieved when sprite ordering is in place and for which there is no
alternative but to repeatedly load the same textures into the graphics
hardware. The important thing is to be aware of this performance
bottleneck and to reduce or eliminate it wherever possible.
In most cases, the performance degradation will not be as significant as in this example. Use the BenchmarkObject
to help you to identify drops in frame rate as soon as they occur so
that you can focus your efforts to track them down as quickly as
possible.
3.2. Creating and Destroying Objects
You are probably aware of the .NET garbage
collection feature. Once in a while, .NET will reach a trigger point
that instructs it to search all its memory space, looking for objects
that are no longer in use. They are marked as unneeded and then the
remaining active objects are reorganized so that they use the system
memory efficiently. This process stops memory from becoming fragmented
and ensures that the application doesn't run out of space to create new
objects.
This has many benefits, primarily by simplifying the
creation of objects and removing any need to free up memory once those
objects are no longer needed. The disadvantage is that, when .NET does
decide to perform a garbage collection operation, it can have a
noticeable effect on your game. We therefore need to try to reduce the
amount of garbage that we create as far as possible.
The number one rule is to try to avoid
creating temporary object instances while the game is running. There
will, of course, be instances where it is essential (all our game
objects are object instances after all!), but we should keep our eyes
open for instances that can be avoided and remove them wherever
possible.
NOTE
Due to the way that .NET manages its memory, we need to worry about garbage collection only when using objects. A structure (struct)
has its memory allocated in a different way that does not result in
garbage being generated. It is therefore perfectly okay to create new Vector2, Rectangle, and Color structures, as well as any other struct-based data structure.
You will see examples of this efficient use of
object allocation within the default content created when a new XNA
project is created. The SpriteBatch object is declared at class level even though it is only used within the Draw
method. Declaring it at class level means that it can be instantiated
just once for the entire game run, whereas declaring it in Draw would result in a new object being created every time the sprites were drawn.
3.3. Using for and foreach Loops
The foreach syntax is extremely useful and in many cases faster to operate than a regular for
loop when using collections because moving from one item to the next
steps just one element forward through a linked list of objects. On the
other hand, accessing collection object by index requires the whole of
the collection to be traversed up to the point where the requested
object is reached.
But in Windows Phone 7 XNA games, there is a potential drawback with foreach
loops, which means that they need a little extra consideration. These
loops create an iterator object and use it to step through the
collection items. For certain types of collection, when the loop
finishes, the iterator object is discarded and ends up on the pile of
objects ready for garbage collection. As discussed in the previous
section, this process will increase the frequency of the garbage
collection operation and cause the game performance to suffer.
The collection types that suffer this problem are all nongeneric collections and the generic Collection<T> object. For code inside your game loop (anywhere inside the Update or Draw methods), you should either avoid using these collection types or iterate through them with a for loop instead of a foreach loop. It is fine to use foreach for arrays and all other generic collection types, however.
Also be aware that when you do use for
loops, if you include a method or property call for the end condition
of the loop, your compiled code will call into it for every iteration
of the loop. It is, therefore, a good idea to read the collection size
into a local variable prior to entering the loop and use it in your for loop instead.