Page 4. Microsoft XNA's Frame Dependency vs Independency

Prerequisites

Before jumping into this project, it's important to make sure you've got the general framework covered. If you're entirely new to Microsoft XNA and MonoGame, it's recommended to start with our introductory project. Please follow the walkthrough provided on Page 1: Microsoft XNA's Hello World w/ MonoGame.

List of Prerequisites:

  • A working installation of Visual Studio

  • MonoGame framework installed

  • .xnb of your desired font (Review Page 1 to make one)

What is Frame Dependency?

If you've been tinkering around with your game and realized that the speed of your game objects is changing as you adjust the frame rate, you may have encountered a common issue known as "Frame Dependency." Frame dependency occurs when the speed, position, or any attribute of a game object is directly tied to the number of frames rendered per second. This means that the more frames you have in a second, the faster your game object will move, and vice versa. Such a design can be problematic as it introduces inconsistency in gameplay across different hardware setups.

Why is it a Problem?

Imagine playing a racing game where you seem to go faster on a more powerful machine simply because the game runs at a higher frame rate. That wouldn't be fair, would it? Such frame-dependent behavior can break the gaming experience, make certain aspects of the game uncontrollable, and even introduce bugs that are hard to trace.

Mitigating Frame Dependency

To create a consistent gaming experience, we want to aim to make our game logic frame-independent. This means decoupling the speed of game objects from the frame rate. Hereโ€™s how you can do it:

Use Elapsed Time

Instead of updating your game objects' attributes per frame, update them according to the amount of time that has passed since the last frame. This can be done using the gameTime.ElapsedGameTime.TotalSeconds property, which gives you the time elapsed since the last frame in seconds.

protected override void Update(GameTime gameTime)
{
    float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

    // Move text position
    position += velocity * elapsed * 200; // Multiply by a constant to control speed

    // Your bounce and clamp logic here remains the same

    // Change color in a frame-independent way
    byte newG = (byte)((color.G + 100 * elapsed) % 256);
    
    // Update the color
    color.G = newG;
}

Why the Multiplication by a Constant?

The constant value (like 200 in the example) provides you with the flexibility to adjust the speed to a level that feels right for your game. This constant can be fine-tuned based on your specific requirements.

How was the color updated?

  • newG is a byte variable that will hold the updated green component of a color.

  • 100 * elapsed multiplies a constant (100) by the time in seconds (elapsed) since the last frame was rendered. This ensures the change is consistent over real-world time.

  • color.G + 100 * elapsed adds this time-adjusted increment to the current value of color.G.

  • % 256 ensures that the value wraps around to stay within the byte range (0-255).

  • (byte) casts the resultant value back to a byte.

  • color.G = newG finally updates the Color.G property with the new byte value.

Full Code

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace YourGameNamespace
{
	public class Game1 : Game
	{
		GraphicsDeviceManager graphicsManager;
		SpriteBatch spriteBatch;
		SpriteFont font;
		Random random;

		TimeSpan elapsedTime = TimeSpan.Zero;
		Vector2 position;
		Vector2 velocity;
		Color color;

		int ticks;
		int frameRate;
		int frameCounter;

		float aspectRatio;


		int[] possibleValues = new int[] { -1, 0, 1 };

		public Game1()
		{
			graphicsManager = new GraphicsDeviceManager(this);
			Content.RootDirectory = "Content";

			frameRate = 60;											// Adjust this value to manager frameRate
			frameCounter = 0;

			aspectRatio = 4f / 3f; // Default 4:3 AR
			ticks = (int)((1.0 / frameRate) * 10_000_000);
			this.IsFixedTimeStep = true;
			this.TargetElapsedTime = TimeSpan.FromTicks(ticks);
		}

		protected override void Initialize()
		{
			aspectRatio = graphicsManager.GraphicsDevice.Viewport.AspectRatio;

			position = new Vector2(400, 600);
			velocity = new Vector2(2, 1);
			color = Color.White;
			base.Initialize();
		}

		protected override void LoadContent()
		{
			spriteBatch = new SpriteBatch(GraphicsDevice);
			font = Content.Load<SpriteFont>("Ariel");
		}

		protected override void Update(GameTime gameTime)
		{
			float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

			// Move text position
			position += velocity * elapsed * 200; // Multiple by a constant to control speed. E.g 200

			// Bounce off right and left walls with a slight offset
			if (position.X <= 0 || position.X >= graphicsManager.PreferredBackBufferWidth - 250)
			{
				velocity.X *= -1f;  // Reverse and slightly increase speed
			}

			// Bounce off top and bottom walls with a slight offset
			if (position.Y <= 0 || position.Y >= graphicsManager.PreferredBackBufferHeight - 50)
			{
				velocity.Y *= -1f;  // Reverse and slightly increase speed
			}

			// Clamp position to make sure it stays within screen bounds
			position.X = MathHelper.Clamp(position.X, 0, graphicsManager.PreferredBackBufferWidth - 100);
			position.Y = MathHelper.Clamp(position.Y, 0, graphicsManager.PreferredBackBufferHeight - 50);

			// Change Color
			byte newG = (byte)((color.G + 200 * elapsed) % 256);

			// Update the color
			color.G = newG;
			if (color.G > 255) color.G = 0;

			frameCounter++;
			elapsedTime += gameTime.ElapsedGameTime;

			if(elapsedTime > TimeSpan.FromSeconds(1))
			{
				elapsedTime -= TimeSpan.FromSeconds(1);
				frameRate = frameCounter;
				frameCounter = 0;
			}

			base.Update(gameTime);
		}


		protected override void Draw(GameTime gameTime)
		{
			GraphicsDevice.Clear(Color.CornflowerBlue);
			spriteBatch.Begin();
			spriteBatch.DrawString(font, "Hello, World!", position, color);
			spriteBatch.End();
			base.Draw(gameTime);
		}
	}
}

Last updated