Fluid and smooth user experience is a key element of any good Windows Store app. If you are writing a game, you most likely will want to measure and display framerate related data (current framerate, min. framerate, etc.) . In this example I am going to show how to add a simple FPS counter to a MonoGame powered game.
XAML's DebugSettings.EnableFrameRateCounter
If you are familiar with XAML development, you might have used DebugSettings.EnableFrameRateCounter flag to display performance and framerate related statistics. This can be enabled in App.xaml.cs by adding one line to the constructor.
public App() { InitializeComponent(); Suspending += OnSuspending; DebugSettings.EnableFrameRateCounter = true; }
It is a very useful feature, this however wont work when using full screen SwapChainBackgroundPanel(which is the default technique for MonoGame + XAML Windows Store apps).
Simple MonoGame framerate counter
Frame rate calculation algorithm we use is simple and is based on following principles:
- every time a scene has been drawn using Draw() increase FrameCounter by 1,
- in Update() method measure ElapsedTime which is the time (in milliseconds) since last frame rate update,
- if ElapsedTime is more than 1 second (1000 ms) use FrameCounter as the new FPS value.
public class FrameRate { /// <summary> /// Current FPS /// </summary> public int Rate; /// <summary> /// Frame counter /// </summary> public int Counter; /// <summary> /// Time elapsed from the last update /// </summary> public float ElapsedTime; /// <summary> /// Min FPS /// </summary> public int MinRate = 999; /// <summary> /// Seconds elapsed /// </summary> public int SecondsElapsed; }
public interface IUpdate<T> where T : class { void Update(GameTime gameTime, T objectToUpdate); }
public interface IDraw<T> where T:class { void LoadContent(ContentManager contentManager); void Draw(DrawingContext context, T objectToDraw); }
DrawingContext is just a placeholder for SpriteBatch (and GraphicsDevice if you need it). Feel free to pass in SpriteBatch directly - this will simplify IDraw signature a little bit
public class DrawingContext { public SpriteBatch SpriteBatch { get; set; } public GraphicsDevice GraphicsDevice { get; set; } }
Here is the logic that will be executed in Game.Update() method.
public class FrameRateUpdater : IUpdate<FrameRate> { public void Update(GameTime gameTime, FrameRate frameRate) { frameRate.ElapsedTime += (float)gameTime.ElapsedGameTime.TotalMilliseconds; if (frameRate.ElapsedTime >= 1000f) { frameRate.ElapsedTime -= 1000f; frameRate.Rate = frameRate.Counter; if(frameRate.SecondsElapsed > 0 && frameRate.MinRate > frameRate.Rate) { frameRate.MinRate = frameRate.Rate; } frameRate.Counter = 0; frameRate.SecondsElapsed++; } } }
public class FrameRateDrawer : IDraw<FrameRate> { SpriteFont spriteFont; private const string FontName = "MapFont"; private readonly Vector2 _fpsPositionBlack = new Vector2(20,20); private readonly Vector2 _fpsPositionWhite = new Vector2(20,20); private readonly Vector2 _minPositionBlack = new Vector2(54, 20); private readonly Vector2 _minPositionWhite = new Vector2(55, 20); public void LoadContent(ContentManager contentManager) { spriteFont = contentManager.Load<SpriteFont>(FontName); } public void Draw(DrawingContext context, FrameRate objectToDraw) { context.SpriteBatch.DrawString(spriteFont, objectToDraw.Rate.ToString(), _fpsPositionBlack, Color.Black); context.SpriteBatch.DrawString(spriteFont, objectToDraw.Rate.ToString(), _fpsPositionWhite, Color.White); context.SpriteBatch.DrawString(spriteFont, objectToDraw.MinRate.ToString(), _minPositionBlack, Color.Black); context.SpriteBatch.DrawString(spriteFont, objectToDraw.MinRate.ToString(), _minPositionWhite, Color.Orange); objectToDraw.Counter++; } }
Please note that instead of creating new Vector2 objects used to position the strings every time we call SpriteBatch.DrawString(), we are reusing instances created earlier. This aims to reduce unnecessary garbage collection and even though this may seem obvious to many, such optimizations can be easily overlooked.
Draw method has twofold purpose - obviously it needs to draw frame-rate strings, but apart from that we need it to increase frame counter. To be more flexible you may want to measure the strings instead of using hard-coded position values.
Now, in your Game class you can add and instantiate the following fields:
private FrameRate _frameRate = new FrameRate(); private FrameRateDrawer _frameRateDrawer = new FrameRateDrawer(); private FrameRateUpdater _frameRateUpdater = new FrameRateUpdater();
protected override void Update(GameTime gameTime) { //... _frameRateUpdater.Update(gameTime, _frameRate); //... } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.Black); _spriteBatch.Begin(); //... _frameRateDrawer.Draw(_drawingContext, _frameRate); //... _spriteBatch.End(); //... }
Also don't forget to load the sprite font and initialize content:
protected override void LoadContent() { _spriteBatch = new SpriteBatch(GraphicsDevice); _background = Content.Load<Texture2D>("background"); _frameRateDrawer.LoadContent(Content); //.. _drawingContext.SpriteBatch = _spriteBatch; _drawingContext.GraphicsDevice = _graphics.GraphicsDevice; }
The end result will look somewhat like this, it is definitely a no frills solution, but its not something that is normally visible to the end-user anyway.
I imagine that frame-rate data (especially min/avg. FPS) could also be used to gather some performance data directly from the users running the game, especially if you cannot test it on all device types that the game is intended for (this shouldn't be that hard with Windows 8/Windows RT). Although this is a material for another post :)
The code is not Windows 8 specific in any way and should work on all MonoGame supported platforms, but I have only tested it in a Windows 8 XAML scenario.