Working With Components

As part of good software engineering, we don't want clutter our main class with a bunch of instance variables and low-level details -- that information should be encapsulated in other classes. You can certainly do all of the decomposition manually (and sometimes you need to!), but the XNA framework does give you some tools to help out. Today, we'll look at an example of how to use XNA components. Along the way, we will show how to draw text on the screen.

FPS Counter

We are going to add a component to our game that keeps track of the number of frames per second that our code is executing. First of all, we will need to create a new GameComponent, that gets an update every frame. First we create a new game project, which will show an exciting blue screen (clearly someone at Microsoft has a sense of humor, to have the default XNA application show a blue screen. It would be even better if the default was a darker "blue screen of death" hue). Now we are ready to add our first game component.

Drawable Components

Game components are classes that get an update call every frame. We'll be lazy and let Visual Studio create the template for us. Right-click on the projec the project explorer, and create a new class

New Class

We will create an XNA 3.1 Game Component:

Screenshot 2

We get a skeleton class for a component that gets updated every frame. We'll be a bit lazy, and make this a Microsoft.Xna.Framework.DrawableGameComponent instead of just a Microsoft.Xna.Framework.GameComponent:

public class FrameCounter : Microsoft.Xna.Framework.DrawableGameComponent
{
   // Body of FrameCounter
}

We will add override functions for drawing and loading content (Visual Stuio's autocomplete is your friend, here)

        protected override void LoadContent()
        {
            base.LoadContent();
        }

        public override void Draw(GameTime gameTime)
        {
            base.Draw(gameTime);
        }

Before we can draw strings on the screen, we will need to create a spritefont -- bitmaps for the characters in the font used for display on the 360 (and windows) screen. XNA makes this pretty easy. Right-click on the content section of the solution explorer, and add a new item

Screenshot3

We'll want to create a new SpriteFont, with a reasonable name -- like the name of the font and the fontsize. Any font that is installed on the machine should work

Screenshot4

Of course, we can name the spritefont whatever we want -- we will need to change the spritefont file itself to get the font we want. If we open the .spritefont file by clicking on it in the solution explorer, we can see that it is an XML file. We can change any of the properties that we like in this file (like, for instance, changing the font to Courier New and font size to 12:


    <!--
    Modify this string to change the font that will be imported.
    -->
    <FontName>Courier New</FontName>

    <!--
    Size is a float value, measured in points. Modify this value to change
    the size of the font.
    -->
    <Size>12</Size>

Now we have a font definition, we can load it in the LoadContent section our GameComponent:

        protected override void LoadContent()
        {
            mSpriteBatch = new SpriteBatch(GraphicsDevice);
            mFPSfont = Game.Content.Load("Courier12");

            base.LoadContent();
        }

Note that we also need to add the following instance variables to our class:

        SpriteBatch mSpriteBatch;
        SpriteFont mFPSfont;

Now that we have a SpriteBatch that we can use to draw elements on the screen, and a font to draw, we can update our draw() method:

        public override void Draw(GameTime gameTime)
        {
            mSpriteBatch.Begin();
            mSpriteBatch.DrawString(mFPSfont, "Hello World!", new Vector2(100, 100), Color.White);

            mSpriteBatch.End();
            base.Draw(gameTime);
        }

We run our code and get ... an empty blue screen :(. What happened? We created a new GameComponent class, but we never created an instance of this class and added it to our game. So, in the constructor of our game, we can create and add a frame counter component:

    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        FrameCounter mFrameCounterComponent;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            mFrameCounterComponent = new FrameCounter(this);
            Components.Add(mFrameCounterComponent);
        }
        // The reset of the Game1 class ...

Note that there is nothing magic going on here -- the superclass Microsoft.Xna.Framework.Game contains a collection Components of GameComponents, and on every update cycle it calls the Update method of each component (and on every cycle it calls the Draw method of all DrawableComponents). We can test this by modifying our Draw method to comment out the call to the superclass draw method:

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here

	        // Commented out the call to the base class draw method
            //base.Draw(gameTime);
        }

Now if we run the code, we no longer see the hello world message (since that method is now never called). OK, so now we can print some text on the screen -- it's time to actually print the Frames Per Second counter. We will print out two values: the instantaneous FPS, and the running 1-second average (the instantaneous could be tricky to read, since it will likely be changing 60 times a second ...)

We will start with the easier, instantaneous FPS. Each update phase, we recaluate the number of seconds taken in the last frame, and take the multiplicative inverse (1/x) to get the number of frames per second. Our new update would then be:

        public override void Update(GameTime gameTime)
        {
            mInstantFPS = (float)(1.0 / gameTime.ElapsedRealTime.TotalSeconds);
            base.Update(gameTime);
        }
While our new Draw method would be:
        public override void Draw(GameTime gameTime)
        {
            mSpriteBatch.Begin();
            mSpriteBatch.DrawString(mFPSfont, "Instant FPS = " + mInstantFPS, new Vector2(100, 100), Color.White);
            mSpriteBatch.End();
            base.Draw(gameTime);
        }

As you can see by running this, the numbers change too quickly to be completly useful. (Something graphical would be better for an instant FSP meter, though that is a bit of overkill.) We can update once a second with the previous second's average FPS by just counting frames until a second has elapsed:

        public override void Update(GameTime gameTime)
        {
            mInstantFPS = (float)(1.0 / gameTime.ElapsedRealTime.TotalSeconds);
            mTimeSinceLastUpdate += (float) gameTime.ElapsedGameTime.TotalSeconds;
            mFramesSinceLastUpdate++;
            if (mTimeSinceLastUpdate >= TIME_TO_AVERAGE)
            {
                mAverageFPS = mFramesSinceLastUpdate / mTimeSinceLastUpdate;
                mTimeSinceLastUpdate = 0.0f;
                mFramesSinceLastUpdate = 0;
            }
            base.Update(gameTime);
        }
        
        public override void Draw(GameTime gameTime)
        {
            mSpriteBatch.Begin();
            mSpriteBatch.DrawString(mFPSfont, "Instant FPS = " + mInstantFPS, new Vector2(100, 100), Color.White);
            mSpriteBatch.DrawString(mFPSfont, "Average FPS = " + mAverageFPS, new Vector2(100, 115), Color.White);
            mSpriteBatch.End();
            base.Draw(gameTime);
        }                

Note that the number of frames per second is right around 60 -- Now, I've (sort of) lied to you here -- this is not the actual number of framse per second, but the number of Update calls per second, which may not be the same thing! Earlier, when discussing the main XNA loop, I said that it repeatedly called Update/Draw/Update/Draw -- but that's not exactly accurate -- it depends on the IsFixedTimeStep property.

If we are dropping frames (That is, if there is not time to call Draw between updates), then the IsRunningSlowly property is set to true. We can count both the "update" FPS and the "drawing" fps by modifying our update and draw codes a bit ...
        public override void Update(GameTime gameTime)
        {
            mInstantFPS = (float)(1.0 / gameTime.ElapsedRealTime.TotalSeconds);
            mTimeSinceLastUpdate += (float) gameTime.ElapsedGameTime.TotalSeconds;
            mUpdateFramesSinceLastUpdate++;
            if (mTimeSinceLastUpdate >= TIME_TO_AVERAGE)
            {
                mAverageUpdateFPS = mUpdateFramesSinceLastUpdate / mTimeSinceLastUpdate;
                mAverageDrawFPS = mDrawFramesSinceLastUpdate / mTimeSinceLastUpdate;
                mTimeSinceLastUpdate = 0.0f;
                mDrawFramesSinceLastUpdate = 0;
                mUpdateFramesSinceLastUpdate = 0;
            }
            base.Update(gameTime);
        }
        
        public override void Draw(GameTime gameTime)
        {
            mDrawFramesSinceLastUpdate++;
            mSpriteBatch.Begin();
            mSpriteBatch.DrawString(mFPSfont, "Update  FPS = " + mAverageUpdateFPS, new Vector2(100, 100), Color.White);
            mSpriteBatch.DrawString(mFPSfont, "Draw FPS = " + mAverageDrawFPS, new Vector2(100, 115), Color.White);
            mSpriteBatch.End();
            base.Draw(gameTime);
        }   
(of course, we will need to create some different instance variables -- mAverageUpdateFPS, mAverageDrawFPS, etc -- in this class for this to compile nicely)

The default desired frame rate is 60 FPS. Let's see what happens when we try to change it:

In the main class:

        protected override void Initialize()
        {
            // TODO: Add your initialization logic here
            IsFixedTimeStep = true;
            TargetElapsedTime = new TimeSpan(0, 0, 0, 0, 10);
            base.Initialize();
        }
We get an update FPS of 100 *just what we wanted -- 10 miliseconds / frame) -- but our draw FPS is only 60 -- what's happening? LCDs can only refresh at fixed rates (in this example, 60 times a second), so that holds up our drawing FPS. The system drops calls to Draw, and gets the update FPS to be what we asked for. Note that if we turn off Fixed Time steps:
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here
            IsFixedTimeStep = false;
            base.Initialize();
        }
then we only get 60 FPS for both update and draw -- since without a fixed time step, we do update/draw/update/draw, and the draws cannot happen any more quickly than 60FPS