Page 7. Microsoft XNA: Custom UI/UIX Panels

Code your own UI/UIX Library for Microsoft XNA's Monogame

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.

  4. Assembled Classes: Make sure you have all the necessary classes. If any are missing please refer to HERE (https://drive.google.com/drive/folders/1V4k4Pn97yQjtj02gJDG3gZdPJfwBvlfR?usp=sharing) or alternatively refer to the previous pages

Introduction to

The crucial aspects of designing a User Interface (UI) and User Experience (UX) for applications developed using Microsoft XNA and MonoGame. Our goal is to provide a solid understanding of UI design principles and how to implement them effectively.

Key Objectives

  1. Introduce UI Elements: We will introduce various UI elements that are essential for creating interactive and user-friendly applications, such as TextElements, PanelElements, ButtonElements, TextboxElements, DropdownElements, and MenubarElements.

  2. Customization: Emphasize the importance of customization in UI design. Teach readers how to adjust parameters and functions to tailor the UI to their specific needs.

  3. UI Hierarchy: Explain the concept of hierarchical UI structure using PanelElement and demonstrate how it can be used to create organized and visually pleasing layouts.

  4. Event Handling: Illustrate how to handle user interactions with UI elements through event handling, allowing readers to define actions for button clicks, text input, and more.

  5. Styling and Theming: Introduce UI styling and theming, empowering readers to maintain visual consistency and adapt their UI to different contexts.

  6. Documentation and Guidance: Provide comprehensive documentation and guidance for each UI element, ensuring that readers have clear instructions on how to use parameters, event handling, and styling options effectively.

Let's clarify the difference between UI (User Interface) and UIX (User Interface eXperience), without any room for ambiguity.

User Interface (UI):

UI, or User Interface, refers to the visual and interactive components of a software application that users interact with. It encompasses all the elements, such as buttons, text fields, menus, icons, and screens, that enable users to interact with the software. UI design focuses on the layout, appearance, and functionality of these elements to ensure they are user-friendly, aesthetically pleasing, and efficient.

Key aspects of UI include:

  1. Visual Design: Designing the appearance of UI elements, including colors, typography, icons, and overall layout.

  2. Interactivity: Implementing functionality for user interactions, such as clicking buttons, entering text, and navigating menus.

  3. Consistency: Maintaining a consistent visual style and interaction patterns throughout the application.

  4. Usability: Ensuring that the UI elements are intuitive and easy for users to understand and navigate.

In summary, UI is primarily concerned with the design and functionality of the visual components that users directly interact with within an application.

User Interface eXperience (UIX):

UIX, or User Interface eXperience, extends beyond the surface-level design and functionality of UI. It encompasses the entire user's journey and the emotions and perceptions they have while interacting with the software. UIX considers the holistic experience of using the application, including how it makes users feel, how efficiently they can achieve their goals, and whether it leaves a positive or negative impression.

Key aspects of UIX include:

  1. Emotional Impact: Evaluating how the application's design and interactions make users feel. A positive UIX aims to create a delightful and emotionally satisfying experience.

  2. Efficiency and Effectiveness: Focusing on how well users can accomplish their tasks within the application. A good UIX ensures that users can achieve their goals efficiently and without frustration.

  3. Consistency Across Touchpoints: Considering the user's experience across different devices and platforms to maintain a consistent and seamless interaction.

  4. Feedback and Improvement: Gathering user feedback and continuously improving the UI and UX based on user insights.

In essence, UIX encompasses UI but goes a step further by considering the holistic experience, emotions, and efficiency of the user throughout their interaction with the software. It aims to create a positive and memorable experience that goes beyond the visual design and functional aspects of the UI.

Introducing the UI Elements

PanelElement (New)

The PanelElement serves as a container for organizing other UI elements. You can customize its size, and color, and add borders to it. It provides structure to your UI, helping you group related elements together and create visually appealing layouts.

TextElement (Enhanced)

The TextElement is more than just text display; it's the foundation of our UI system. We're enhancing it to handle various sizes and alignments. You can now customize properties like font size, text color, and alignment, allowing you to create a wide range of text styles to suit your application's aesthetic.

ButtonElement (New) - coming soon (Page 8)

Buttons are essential for user interaction, and the ButtonElement is highly versatile. You can define its size, color, and text, and specify what action should occur when it's clicked. This element empowers you to create interactive features and user-friendly interfaces.

TextboxElement (New) - coming soon (Page 9)

The TextboxElement allows users to input text. You can control its size, color, and apply validation rules to ensure data integrity. It's a valuable addition for forms and user input scenarios, enabling smooth data entry.

Dropdown menus are handy for presenting selectable options, and the DropdownElement is here for that purpose. You can customize its appearance, populate it with choices, and determine how it behaves when users make selections. It simplifies user choices and enhances usability.

The MenubarElement acts as the main menu for your application, providing navigation and access to essential functions. You can customize its appearance, add buttons or dropdowns, and define the actions associated with each menu item. It serves as the central control hub for your app.

PanelElement.cs

At the core of this part of the code is the creation of a new class, PanelElement. This class will serve as the blueprint for creating various panel elements within your game interface. Each PanelElement instance will have properties like Position, Size, BackgroundColor, BorderColor, and a few others, which will dictate how each panel will look and where it will be placed on the screen.

The constructor of PanelElement is designed to accept various parameters with default values, making it easy for a developer to create a new panel with minimal information. If certain details are not provided, the constructor will use default values. For example, if no position is provided, it defaults to Vector2.Zero, which represents the coordinates (0,0). Similarly, if no size is provided, it defaults to the current window size.

In your project, create a new class named PanelElement.cs under your namespace. This class will serve as the foundation for creating panels in your UI.

using Microsoft.Xna.Framework;
using System.Collections.Generic;

namespace HelloWorldXNA
{
	public class PanelElement
	{
		public Vector2 Position { get; set; }
		public Vector2 Size { get; set; } 
		public Color BackgroundColor { get; set; }
		public bool HasBorder { get; set; }
		public Color BorderColor { get; set; }
		public int Layer { get; set; }
		public bool IsVisible { get; set; }
		public List<TextElement> TextElements { get; } = new List<TextElement>();


		public PanelElement(
			Color backgroundColor,
			Vector2 position = default,
			Vector2 size = default,
			bool hasBorder = false,
			Color borderColor = default,
			int layer = 0,
			bool isVisible = true)
		{
			Position = position == default ? Vector2.Zero : position; // Default to (0, 0) if no position provided
			Size = size == default ? new Vector2(GraphicsDeviceManager.DefaultBackBufferWidth, GraphicsDeviceManager.DefaultBackBufferHeight) : size; // Default to the current window size if no size provided
			BackgroundColor = backgroundColor;
			HasBorder = hasBorder;
			BorderColor = borderColor != default ? borderColor : Color.Black; // Default to Black if no border color provided
			Layer = layer;
			IsVisible = isVisible;
		}
		public void AddTextElement(TextElement textElement)
		{
			TextElements.Add(textElement);
		}
	}
}

This code defines the PanelElement class, which is used to create panels in your game. Here's a breakdown of each part:

  • Properties: These properties define various characteristics of the panel, such as its Vector2 position, Vector2 size, background color, whether it has a border, border color, rendering layer, and visibility.

As a reminder in the context of game development and computer graphics, a Vector2 is a common data structure used to represent a two-dimensional vector. It's part of the XNA and MonoGame frameworks, which are used for developing games in C#.

A Vector2 typically consists of two components:

  • X Component: Represents the value along the horizontal axis (usually the X-axis)

  • Y Component: Represents the value along the vertical axis (usually the Y-axis)

  • Constructor: The constructor allows you to create a PanelElement instance with specific properties. It takes several parameters:

    • backgroundColor: The background color of the panel

    • position: The position of the panel, defaulting to (0, 0) if not provided

    • size: The size of the panel, defaulting to the current window size if not provided

    • hasBorder: A boolean indicating whether the panel should have a border

    • borderColor: The color of the border, defaulting to black if not provided

    • layer: The rendering layer of the panel, where 0 is at the bottom (used for layering multiple panels)

    • isVisible: A boolean indicating whether the panel should be visible

This class provides the necessary structure to create and manage panels in your game, allowing you to customize their appearance, position, and behavior.

The first line in the constructor, Position = position == default ? Vector2.Zero : position;, is responsible for setting the position of the panel. Here's a breakdown:

  • Position =: This part starts the assignment of the Position property.

  • position == default: Here, you are checking whether the position parameter provided to the constructor is equal to the default value for Vector2. In C#, the default keyword represents the default value for a data type. For Vector2, the default value is (0, 0).

  • ?: This is called the conditional operator, also known as the ternary operator. It's used for inline conditional expressions. It has the form condition ? trueValue : falseValue. If the condition is true, it returns trueValue, otherwise, it returns falseValue.

  • Vector2.Zero: If the position parameter is not the default value (i.e., it's not (0, 0)), then this part of the line assigns the value of position to the Position property.

So, in plain English, this line of code is saying: "If the position parameter is not the default Vector2 value (0, 0), then set the Position property to the value of position. Otherwise, set it to Vector2.Zero (which is (0, 0))."

Setting the Size

The second line you mentioned is similar:

Size = size == default ? new Vector2(GraphicsDeviceManager.DefaultBackBufferWidth, GraphicsDeviceManager.DefaultBackBufferHeight) : size;

Here, you are setting the Size property of the PanelElement object:

  • You check whether the size parameter is the default value. If it's not, you assign its value to the Size property.

  • If size is the default value, you create a new Vector2 object with dimensions obtained from GraphicsDeviceManager.DefaultBackBufferWidth and GraphicsDeviceManager.DefaultBackBufferHeight. This is done to provide a default size based on the current window dimensions.

In essence, these two lines ensure that the Position and Size properties of the panel are set either to default values or to the values provided during panel creation, making it convenient for developers to work with panels in their games.

PanelElementRenderer and UIManager

Next, we will craft thePanelElementRenderer and UIManager. These classes are pivotal in organizing the rendering logic and managing the UI elements respectively. PanelElementRenderer will contain methods to draw a panel, and UIManager holds a list of panels and orchestrates when and how they are drawn. This separation aligns with the Single Responsibility Principle, making your code modular, organized, and easier to maintain or expand in the future.

The UIManager class also has a method Window_ClientSizeChanged to handle window resizing events. When the window size changes, this method updates the size of all managed panels accordingly, which is a neat way to keep the UI responsive to window size changes.

Step 1: Encapsulation and Single Responsibility Principle (SRP)

PanelElementsRenderer.cs

The Single Responsibility Principle articulates that a class should have one, and only one, reason to change. Applying this principle, it's advantageous to encapsulate the logic related to PanelElement operations within a dedicated class, say PanelElementRenderer, instead of within the Game1.cs. This promotes a cleaner separation of concerns, making the code more modular and easier to manage.

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

namespace HelloWorldXNA {
	public class PanelElementRenderer
	{
		private GraphicsDevice graphicsDevice { get; }
		private Texture2D pixelTexture { get; }


		public PanelElementRenderer(GraphicsDevice gd)
		{
			graphicsDevice = gd;
			pixelTexture = new Texture2D(graphicsDevice, 1, 1);
			pixelTexture.SetData(new[] { Color.White });
		}

		public void DrawPanel(SpriteBatch spriteBatch, PanelElement panel)
		{
			if (panel.IsVisible)
			{
				Rectangle panelRectangle = new Rectangle((int)panel.Position.X, (int)panel.Position.Y, (int)panel.Size.X, (int)panel.Size.Y);

				if (panel.HasBorder)
				{
					DrawFilledRectangle(spriteBatch, panelRectangle, panel.BorderColor);
					panelRectangle.Inflate(-1, -1);
				}

				DrawFilledRectangle(spriteBatch, panelRectangle, panel.BackgroundColor);
			}
		}

		private void DrawFilledRectangle(SpriteBatch spriteBatch, Rectangle rect, Color color)
		{
			spriteBatch.Draw(pixelTexture, rect, color);
		}
	}
}

This allows for Game1.cs to instantiate and utilize PanelElementRenderer for rendering panel elements using

  • The DrawPanel method by taking a SpriteBatch and a PanelElement as arguments It then checks whether the panel is visible using panel.IsVisible.

  • A Rectangle object representing the panel's dimensions is created using the panel's position and size properties.

  • If the panel should have a border (panel.HasBorder is true), it draws the border using the DrawFilledRectangle method, then calls Inflate on panelRectangle to shrink it by 1 pixel on all sides.

  • Finally, it draws the panel's background using the DrawFilledRectangle method.

UIManager.cs

Let's refactor Window_ClientSizeChanged to the UIManager class to handle window resizing events. When the window size changes, this method updates the size of all managed panels accordingly, which is a neat way to keep the UI responsive to window size changes.

using System;
using System.Collections.Generic;
using HelloWorldXNA;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

public class UIManager
{
	private List<PanelElement> _panels = new List<PanelElement>();
	private PanelElementRenderer _renderer;
	private GraphicsDevice _graphicsDevice;

	public UIManager(GraphicsDevice graphicsDevice)
	{
		_renderer = new PanelElementRenderer(graphicsDevice);
		_graphicsDevice = graphicsDevice;
	}

	public void AddPanel(PanelElement panel)
	{
		_panels.Add(panel);
		_panels.Sort((panel1, panel2) => panel1.Layer.CompareTo(panel2.Layer));
	}

	public void Draw(SpriteBatch spriteBatch)
	{
		foreach (var panel in _panels)
		{
			_renderer.DrawPanel(spriteBatch, panel);
		}
	}

	public void Window_ClientSizeChanged(object sender, EventArgs e)
	{
		Vector2 newSize = new Vector2(_graphicsDevice.Viewport.Width, _graphicsDevice.Viewport.Height);
		foreach (var panel in _panels)
		{
			panel.Size = newSize;
		}
	}
}

In this refactoring:

  1. UIManager now has a Window_ClientSizeChanged method which iterates through all PanelElement objects and updates their sizes based on the window size.

  2. In Game1's Initialize method, this.Window.ClientSizeChanged is now subscribed to uiManager.Window_ClientSizeChanged instead of Window_ClientSizeChanged method in Game1.

  3. The Window_ClientSizeChanged method has been removed from Game1 as it's now handled by UIManager.

Class Components:

  1. Fields:

    • _panels: A list that holds all the PanelElement objects managed by the UIManager.

    • _renderer: An instance of PanelElementRenderer used to draw the panels.

  2. Constructor:

    • UIManager(GraphicsDevice graphicsDevice): The constructor initializes the _renderer field with a new instance of PanelElementRenderer, passing the GraphicsDevice object to it.

  3. Methods:

    • AddPanel(PanelElement panel): Adds a new PanelElement to the _panels list.

    • Draw(SpriteBatch spriteBatch): Iterates through all the PanelElement objects in the _panels list and calls _renderer.DrawPanel() to draw each one.

Functionality Breakdown:

  • Initialization: When a UIManager object is created, it initializes a PanelElementRenderer object, which will be used to render panels.

  • Panel Management:

    • AddPanel allows for adding new panels to the UI manager.

    • Draw iterates through these panels, rendering each one using the PanelElementRenderer.

  • Rendering: The actual rendering is delegated to the PanelElementRenderer object, keeping the rendering logic separate from the management logic.

You can then register this method to the ClientSizeChanged event within Game1.cs. This way, the UIManager is responsible for updating the sizes of the panels when the window size changes, which centralizes the UI management logic within the UIManager class.

Implementing the PanelElement in Game1.cs

Now that we have our PanelElement class defined, let's implement it in your Game1.cs file.

Fully Updated Game1.cs

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

namespace YourNameSpaceHere
{
	public class Game1 : Game
	{
		GraphicsDeviceManager graphicsManager;
		SpriteBatch spriteBatch;
		TextElement textElement;
		PhysicsEngine physicsEngine;
		ColorManager colorManager;
		MouseInputManager mouseInputManager;

		private UIManager uiManager;
		private PanelElement panelMain;
		private PanelElement panel2;

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

		protected override void Initialize()
		{
			// Window Attributes
			IsMouseVisible = true;
			Window.AllowUserResizing = true;

			physicsEngine = new PhysicsEngine(graphicsManager);
			colorManager = new ColorManager();
			mouseInputManager = new MouseInputManager();


			// Panels
			panelMain = new PanelElement(Color.LightGray, layer: 1, isVisible: true);
			panel2 = new PanelElement(Color.LightGreen, layer: 2, isVisible: false);

			// TextElements
			textElement = new TextElement(GraphicsDevice, Content, "Click Me!", position: new Vector2(100, 50), panel: panelMain);

			// Instantiate UIManager and add panels to list
			uiManager = new UIManager(GraphicsDevice);
			this.Window.ClientSizeChanged += uiManager.Window_ClientSizeChanged;

			uiManager.AddPanel(panelMain);
			uiManager.AddPanel(panel2);

			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, 200, 50);
			physicsEngine.ApplyMouseEventHandlingToText(textElement, mouseInputManager, isPrecisionClicks: 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();

			// Delegate panel drawing to UIManager
			uiManager.Draw(spriteBatch);

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

Let's break down the significant sections of this code:

Field Declarations:

GraphicsDeviceManager graphicsManager;
SpriteBatch spriteBatch;
TextElement textElement;
PhysicsEngine physicsEngine;
ColorManager colorManager;
MouseInputManager mouseInputManager;

private UIManager uiManager;
private PanelElement panelMain;
private PanelElement panel2;

These fields are various members of Game1 class, representing managers, engine instances, and UI elements that are used throughout the game's lifecycle.

Constructor:

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

The constructor initializes graphicsManager and sets the root directory for content loading.

Initialize Method:

protected override void Initialize()
{
    //...
}

The Initialize method is where various game components and UI elements are instantiated and configured. This includes setting some window attributes, creating instances of PhysicsEngine, ColorManager, MouseInputManager, and some UI elements like PanelElement and TextElement. It also sets up the UIManager and binds a method to the ClientSizeChanged event of the window.

LoadContent Method:

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

LoadContent is where content is loaded into the game. In this code, a new SpriteBatch is created, and a texture is generated for textElement from text.

Update Method:

protected override void Update(GameTime gameTime)
{
    //...
}

Update is called every frame and is where the game's logic is updated. In this method, input is processed, and physics and color updates are applied to textElement.

Draw Method:

protected override void Draw(GameTime gameTime)
{
    //...
}

Draw is also called every frame, and is where the game's rendering occurs. In this method, the graphics device is cleared, SpriteBatch is begun, the UI is drawn by delegating to UIManager, and SpriteBatch is ended.

Updating the PhysicsEngine.cs

You may have noticed that we do not always need to be within the rectangle/on the text when clicking our TextElement this is due to the management of our IsMouseOnNonTransparentPixel() function. To resolve this, we need to update the if condition to the below

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;

	if (x >= 0 && x < textElement.Texture.Width && y >= 0 && y < textElement.Texture.Height)
	{
		int index = x + y * textElement.Texture.Width;
		return pixelColorData[index].A > 0;
	}

	return false;
}

In this corrected version, we first check whether the x and y coordinates are within the bounds of the texture before attempting to calculate the index. This avoids accessing invalid indices and ensures the function works as intended. If the mouse position is outside the texture bounds, the function returns false.

Updating the TextElement.cs

In this updated version we are updating the constructor to handle the passing of the position and a specific panel so the user can specify a location for their TextElement.

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

namespace YourNameSpaceHere
{
	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 PanelElement Panel { get; set; }



		public TextElement(
			GraphicsDevice graphicsDevice,
			ContentManager content,
			string text,
			PanelElement panel = null,
			SpriteFont font = null,
			Vector2? velocity = null,
			Vector2? position = null,
			Color? color = null)
		{
			Text = text;
			Panel = panel;
			if (Panel != null)
			{
				Panel.TextElements.Add(this);  // Automatically add this TextElement to the panel's list of TextElements
			}

			Font = font ?? content.Load<SpriteFont>("Fonts/MyFont");	// Default font if none provided
			Velocity = velocity ?? new Vector2(2, 1);                   // Default velocity if none provided
			Position = position ?? new Vector2(100, 100);				// Default location is none provided
			Color = color ?? Color.White;								// Default color if none provided
			Texture = new Texture2D(graphicsDevice, (int)Font.MeasureString(text).X, (int)Font.MeasureString(text).Y);
		}

		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;
		}
	}
}

Remember you will likely need to adjust your Namespace

Knowledge Check

  1. What is the primary purpose of the PanelElement class in the MonoGame project?

  2. In the PanelElement constructor, what is the default value for the position parameter if it is not provided, and how is this default value set?

  3. What are the key aspects of UI (User Interface) as mentioned in the article?

  4. What is the role of the PanelElementRenderer class in rendering the UI elements?

  5. In the UIManager class, what method is responsible for updating the size of all managed panels when the window size changes, and how is this method triggered?

  6. In the provided PanelElement.cs code, what does the ternary operator (?:) do in the constructor?

Interactive Challenges

Challenge 1: Extend the PanelElement Class

The PanelElement class is a fundamental part of the UI system described in the article. Your challenge is to extend this class to create a new type of panel element, such as a button.

  1. Create a new class called ButtonElement that inherits from PanelElement.

  2. Add new properties to ButtonElement, such as Label (a string to be displayed on the button) and IsClicked (a boolean that tracks whether the button has been clicked).

  3. Override the Draw method to include code that changes the color of the button when IsClicked is true.

  4. Instantiate a ButtonElement in your main program, add it to the UIManager, and test its functionality.

Last updated