Page 6. Microsoft XNA: Refactoring our code

Cleaning, reviewing and breaking down

Prerequisites

Before we dive into implementing mouse interactivity in your MonoGame project, let's ensure that you have the necessary foundation in place. If you're new to Microsoft XNA and MonoGame or need a refresher, it's highly recommended to start with our introductory project. You can follow the step-by-step walkthrough provided on Page 1: Microsoft XNA's Hello World w/ MonoGame to get acquainted with the basics.

List of Prerequisites:

  1. Visual Studio: Make sure you have a working installation of Visual Studio, which is the development environment we'll use for this project.

  2. MonoGame Framework: Ensure that you have the MonoGame framework installed. This framework provides the essential tools and libraries for game development.

  3. .xnb Font File: To display text in your MonoGame project, you'll need a .xnb file of your desired font. If you haven't created one yet, you can refer back to Page 1 for guidance on how to generate it.

Class Structure and Refactoring for OOP Adherence

Before we dissect, it's essential to understand the structure that we are aiming for. Our goal is to adhere to the Object-Oriented Programming (OOP) principles, ensuring that each class has a single responsibility and interacts with other classes in a well-defined way.

What does it mean to refactor and why is it important to Object-Oriented Programming (OOP)

Refactoring is like tidying up your code, making it neat and understandable without changing what it actually does. This practice is crucial in Object-Oriented Programming (OOP) for several reasons:

  1. Maintainability: It makes the code more understandable and easier to maintain.

  2. Extensibility: A well-organized codebase is easier to extend with new features.

  3. Error Reduction: Refactoring can help identify and fix hidden bugs.

  4. Performance: Improved code structure can lead to better performance.

  5. Readability: It improves the readability of the code, making it easier for others (and future-you) to understand.

  6. Reuse: Promotes code reuse, reducing the amount of duplicated code.

  7. Adherence to Design Principles: Helps in adhering to OOP principles like SOLID, making the system well-architected.

The SOLID principles are a set of five design guidelines in Object-Oriented Programming (OOP) that encourage clean, maintainable, and robust code. Here they are broken down:

  1. Single Responsibility Principle (SRP): Each class should have one reason to change, meaning it should have only one responsibility. For example, a "Printer" class should only handle printing, not also scanning or faxing.

  2. Open/Closed Principle (OCP): Software entities should be open for extension but closed for modification. You should be able to add new features or functionality to a system without altering its existing code, like adding new accessories to a smartphone without changing the phone itself.

  3. Liskov Substitution Principle (LSP): Subtypes should be substitutable for their base types without causing errors. If Class B is a subclass of Class A, you should be able to use Class B wherever you use Class A without any hitches, like using a rechargeable battery in place of a regular battery.

  4. Interface Segregation Principle (ISP): It's better to have many specific interfaces rather than one general-purpose interface. Instead of having one big set of features in a single interface, break it down into smaller sets, so users aren't forced to work with features they don't need.

  5. Dependency Inversion Principle (DIP): Depend on abstractions, not on concrete implementations. For example, a car depends on an engine running, not the specifics of how an engine works.

Let's identify the potential classes we could extract from our Game1.cs:

  1. MouseInputManager: This class is responsible for managing mouse input. It keeps track of the current and previous mouse states and provides methods to check for specific mouse actions, like left or right button clicks.

  2. TextElement: This class encapsulates the text element, holding properties such as position, velocity, color, and the text string itself. It represents the text that is being manipulated in your game.

  3. PhysicsEngine: This class is focused on the physics of the text element, managing its movement based on its velocity, and handling any collisions with boundaries, ensuring the text bounces back when it hits the edges of the window.

  4. ColorManager: This class takes charge of managing color changes over time. It has a method to update the color of the text element, which in your code, alters the green color component in a cyclic manner based on elapsed time.

Structuring Our Classes

MouseInputManager.cs

The MouseInputManager class is dedicated to managing mouse interactions in your game. It resides under a namespace, YourNameSpaceNameHerewhich helps organize related classes. The class is publicly accessible, with two pivotal members: currentMouseState and previousMouseState, to track the mouse's state across frames. The update method is crucial as it refreshes these state trackers each frame, ensuring accurate, up-to-date information. Two methods, isLeftButtonClicked and isRightButtonClicked, leverage this state information to determine if the left or right mouse buttons have been clicked, providing a clear interface to mouse button interactions. Through encapsulation and clear method naming, this class greatly eases the handling of mouse inputs, a crucial aspect for interactive applications like games, making the codebase more readable and manageable.

using Microsoft.Xna.Framework.Input;

namespace YourNameSpaceNameHere
{
	public class MouseInputManager
	{
		public MouseState currentMouseState { get; private set; }
		private MouseState previousMouseState;

		public void update()
		{
			previousMouseState = currentMouseState;
			currentMouseState = Mouse.GetState();
		}

		public bool isLeftButtonClicked()
		{
			return currentMouseState.LeftButton == ButtonState.Pressed && previousMouseState.LeftButton == ButtonState.Released;
		}

		public bool isRightButtonClicked()
		{
			return currentMouseState.RightButton == ButtonState.Pressed && previousMouseState.RightButton == ButtonState.Released;
		}
	}
}

By now you may have noticed the use of { get; set; } line of code. This defines a property named and type. This is a common pattern in C# known as an auto-implemented property. The public keyword denotes that this property can be accessed and modified from outside the class. The get and set accessors imply that the property can be both read and written to. This simplifies code by automatically generating a private backing field behind the scenes, while still providing the encapsulation benefits that come with defining properties, aligning with the Object-Oriented Programming (OOP) paradigm of encapsulation.

TextElement.cs

The TextElement class, nested within the namespace YourNameSpaceNameHere, serves as a robust representation of a textual element within your game, embodying vital properties like Position, Velocity, Font, Color, Text, and Texture. It showcases well-structured code through its constructor, which not only accepts external configurations but also provides sensible defaults. The GenerateTextureFromText method is a meticulous procedure converting text into a texture for rendering, emphasizing the class's purpose in bridging textual content and graphical representation. By encapsulating related properties and methods, TextElement epitomizes object-oriented principles, promoting a clear, manageable approach to handling textual graphics within your game environment.

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

namespace YourNameSpaceNameHere
{
	public class TextElement
	{
		public Vector2 Position { get; set; }
		public Vector2 Velocity { get; set; }
		public SpriteFont Font { get; set; }
		public Color Color { get; set; }
		public string Text { get; set; }
		public Texture2D Texture { get; private set; }



		public TextElement(
		GraphicsDevice graphicsDevice,
		ContentManager content,
		string text,
		SpriteFont font = null,
		Vector2? velocity = null,
		Color? color = null)
		{
			Text = text;
			Font = font ?? content.Load<SpriteFont>("Fonts/MyFont");	// Default font if none provided
			Velocity = velocity ?? new Vector2(2, 1);					// Default velocity if none provided
			Color = color ?? Color.White;								// Default color if none provided
			Texture = new Texture2D(graphicsDevice, 100, 100);
		}

		public void GenerateTextureFromText(SpriteBatch spriteBatch)
		{
			var textSize = Font.MeasureString(Text);
			RenderTarget2D renderTarget = new RenderTarget2D(spriteBatch.GraphicsDevice, (int)textSize.X, (int)textSize.Y);
			spriteBatch.GraphicsDevice.SetRenderTarget(renderTarget);
			spriteBatch.GraphicsDevice.Clear(Color.Transparent);

			spriteBatch.Begin();
			spriteBatch.DrawString(Font, Text, Vector2.Zero, Color);
			spriteBatch.End();

			spriteBatch.GraphicsDevice.SetRenderTarget(null);
			Texture = renderTarget;
		}
	}
}

The initializer (constructor) in the TextElement class is crafted to accept six parameters, where the last three have default values set to null. This is designed for flexibility, allowing optional customization at the point of object creation. The ?? operator plays a crucial role here; it's a null-coalescing operator that checks if a value is null, and if so, provides a default value. For instance, Font = font ?? content.Load<SpriteFont>("Fonts/MyFont"); checks if font is null, and if it is, loads a default font. This pattern is useful in providing fallback values, ensuring that crucial properties like Font, Velocity, and Color have valid initial values, thus enhancing robustness and reducing potential runtime errors.

PhysicsEngine.cs

PhysicsEngine.cs a class encapsulating the physics logic within your game realm. It's tasked with managing how the TextElement interacts with the game environment, especially regarding its movement and response to mouse events. The methods ApplyWallBouncingToText and ApplyMouseEventHandlingToText are the crux of this class, handling the text elements bouncing off walls and reacting to mouse clicks, respectively. This segregation of physics logic into its own class promotes a cleaner, more organized codebase, making it easier to pinpoint and amend any physics-related logic independently from other game logic, adhering to the Single Responsibility Principle of SOLID.

using Microsoft.Xna.Framework;

namespace YourNameSpaceNameHere
{
	public class PhysicsEngine
	{
		private GraphicsDeviceManager graphicsManager;

		public PhysicsEngine(GraphicsDeviceManager graphicsManager)
		{
			this.graphicsManager = graphicsManager;
		}

		public void ApplyWallBouncingToText(TextElement textElement, float elapsed, float horizontalOffset=0, float verticalOffset=0)
		{
			Vector2 newPosition = textElement.Position + textElement.Velocity * elapsed * 200;  // Update position
			horizontalOffset = 200;
			verticalOffset = 50;

			// Bounce logic
			Vector2 newVelocity = textElement.Velocity;
			if (newPosition.X <= 0 || newPosition.X >= graphicsManager.PreferredBackBufferWidth - horizontalOffset)
			{
				newVelocity.X *= -1f;
			}

			if (newPosition.Y <= 0 || newPosition.Y >= graphicsManager.PreferredBackBufferHeight - verticalOffset)
			{
				newVelocity.Y *= -1f;
			}
			textElement.Velocity = newVelocity;

			// Clamping
			newPosition.X = MathHelper.Clamp(newPosition.X, 0, graphicsManager.PreferredBackBufferWidth - horizontalOffset);
			newPosition.Y = MathHelper.Clamp(newPosition.Y, 0, graphicsManager.PreferredBackBufferHeight - verticalOffset);
			textElement.Position = newPosition;
		}


		public void ApplyMouseEventHandlingToText(TextElement textElement, MouseInputManager mouseInputManager, bool isPrecisionClicks=false)
		{
			Rectangle bounds = new Rectangle((int)textElement.Position.X, (int)textElement.Position.Y, textElement.Texture.Width, textElement.Texture.Height);

			if (mouseInputManager.isLeftButtonClicked())
			{
				if ((isPrecisionClicks && IsMouseOnNonTransparentPixel(textElement, mouseInputManager.currentMouseState.Position)) ||
					(!isPrecisionClicks && bounds.Contains(mouseInputManager.currentMouseState.Position)))
				{
					textElement.Velocity *= 2.25f;
				}
			}
			else if (mouseInputManager.isRightButtonClicked())
			{
				if ((isPrecisionClicks && IsMouseOnNonTransparentPixel(textElement, mouseInputManager.currentMouseState.Position)) ||
					(!isPrecisionClicks && bounds.Contains(mouseInputManager.currentMouseState.Position)))
				{
					textElement.Velocity *= 0.5f;
					System.Diagnostics.Debug.WriteLine("Speed Down\n");
				}
			}

		}

		private bool IsMouseOnNonTransparentPixel(TextElement textElement, Point mousePosition)
		{
			Color[] pixelColorData = new Color[textElement.Texture.Width * textElement.Texture.Height];
			textElement.Texture.GetData(pixelColorData);

			int x = mousePosition.X - (int)textElement.Position.X;
			int y = mousePosition.Y - (int)textElement.Position.Y;

			int index = x + y * textElement.Texture.Width;

			if (index >= 0 && index < pixelColorData.Length)
			{
				return pixelColorData[index].A > 0;
			}

			return false;
		}

	}
}

ColorManager

In the updated ColorManager class, the UpdateTextColor method has been enhanced for flexibility. It now accepts additional parameters: colorChangeSpeed, changeRed, changeGreen, and changeBlue with default values. This setup allows the user to control the speed of color change and choose which color channels to modify. Each color channel (Red, Green, Blue) is independently updated based on the respective boolean flags. If a flag is set to true, that color channel's value is incremented by a rate of colorChangeSpeed * elapsed modulo 256, ensuring it stays within valid bounds. This setup offers a tailored color change experience while keeping the code user-friendly and adaptable.

using Microsoft.Xna.Framework;

namespace HelloWorldXNA
{
	public class ColorManager
	{
		public void UpdateTextColor(TextElement textElement, float elapsed, float colorChangeSpeed = 200, bool changeRed = false, bool changeGreen = true, bool changeBlue = false)
		{
			byte newR = changeRed ? (byte)((textElement.Color.R + colorChangeSpeed * elapsed) % 256) : textElement.Color.R;
			byte newG = changeGreen ? (byte)((textElement.Color.G + colorChangeSpeed * elapsed) % 256) : textElement.Color.G;
			byte newB = changeBlue ? (byte)((textElement.Color.B + colorChangeSpeed * elapsed) % 256) : textElement.Color.B;

			Color newColor = new Color(newR, newG, newB, textElement.Color.A);
			textElement.Color = newColor;
		}
	}
}

Game1.cs

Now, integrate these classes back into Game1 class, ensuring each class interacts harmoniously with others. Note that this is a simplified restructuring. Real-world scenarios may require more complex designs.

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;

namespace HelloWorldXNA
{
	public class Game1 : Game
	{
		GraphicsDeviceManager graphicsManager;
		SpriteBatch spriteBatch;
		TextElement textElement;
		PhysicsEngine physicsEngine;
		ColorManager colorManager;
		MouseInputManager mouseInputManager;
		Random random = new Random();

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

		protected override void Initialize()
		{
			IsMouseVisible = true;
			Window.AllowUserResizing = true;
			textElement = new TextElement(GraphicsDevice, Content, "Click Me!");
			physicsEngine = new PhysicsEngine(graphicsManager);
			colorManager = new ColorManager();
			mouseInputManager = new MouseInputManager();
			base.Initialize();
		}

		protected override void LoadContent()
		{
			spriteBatch = new SpriteBatch(GraphicsDevice);
			textElement.GenerateTextureFromText(spriteBatch);
		}

		protected override void Update(GameTime gameTime)
		{
			float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
			mouseInputManager.update();
			physicsEngine.ApplyWallBouncingToText(textElement, elapsed);
			physicsEngine.ApplyMouseEventHandlingToText(textElement, mouseInputManager, true);
			colorManager.UpdateTextColor(textElement, elapsed, changeRed: false, changeGreen: true, changeBlue: true);
			base.Update(gameTime);
		}

		protected override void Draw(GameTime gameTime)
		{
			GraphicsDevice.Clear(Color.CornflowerBlue);
			spriteBatch.Begin();
			spriteBatch.DrawString(textElement.Font, textElement.Text, textElement.Position, textElement.Color);
			spriteBatch.End();
			base.Draw(gameTime);
		}
	}
}

The Game1 class serves as the core orchestrator for various components and activities within the game. It's structured within the HelloWorldXNA namespace and inherits from the Game class provided by Microsoft.Xna.Framework. Within Game1, several critical objects are declared that represent various aspects of the game: graphicsManager for graphics device management, spriteBatch for rendering, textElement for text manipulation, physicsEngine for physics logic, colorManager for color adjustments, and mouseInputManager for mouse input handling.

The constructor Game1() initializes the graphicsManager and sets the content root directory. The Initialize method is overridden to set up essential game settings like mouse visibility, window resizing allowance, and the instantiation of various game-related objects like textElement, physicsEngine, colorManager, and mouseInputManager.

LoadContent is another overridden method where resources are loaded; here, it initializes spriteBatch and generates texture from the text for textElement.

The Update method, called once per frame, updates the state of these objects and handles the logic for mouse input, text movement, and color change, with a calculated elapsed time for smooth transitions.

Lastly, the Draw method is overridden to clear the graphics device to a solid color and then draw the text element to the screen. This setup ensures a structured approach to managing game states and rendering, making it easier to understand, extend, and maintain.

Knowledge Check

  1. What is the purpose of refactoring in Object-Oriented Programming?

  2. Explain the Liskov Substitution Principle (LSP) with an example provided in the article.

  3. What is the role of the MouseInputManager class in the Game1.cs file?

  4. Which of the following SOLID principles emphasizes that a class should have only one responsibility? a) Single Responsibility Principle (SRP) b) Open/Closed Principle (OCP) c) Liskov Substitution Principle (LSP) d) Dependency Inversion Principle (DIP)

  5. In the UpdateTextColor method of ColorManager class, find and fix the error:

public void UpdateTextColor(TextElement textElement, float elapsed, float colorChangeSpeed = 200, bool changeRed = false, bool changeGreen = true, bool changeBlue = false)
{
    byte newR = changeRed ? (byte)((textElement.Color.R + colorChangeSpeed * elapsed) 256) : textElement.Color.R;  // Error line
    byte newG = changeGreen ? (byte)((textElement.Color.G + colorChangeSpeed * elapsed) % 256) : textElement.Color.G;
    byte newB = changeBlue ? (byte)((textElement.Color.B + colorChangeSpeed * elapsed) % 256) : textElement.Color.B;

    Color newColor = new Color(newR, newG, newB, textElement.Color.A);
    textElement.Color = newColor;
}

Interactive Challenges

Challenge 1: Changing Text Behavior Update the PhysicsEngine class to reverse the text's velocity whenever a mouse button is clicked, regardless of the mouse position.

Challenge 2: Dynamic Color Changes Modify the ColorManager class to change the text color to a random color whenever the UpdateTextColor method is called. You can use the Random class to generate random numbers for the RGB values.

Last updated