I've been working on a GUI for my game. So far I've used code to create objects that look like "forms" (not to be confused with winforms, I'm using monogame). Also I created buttons. But now I'm having difficulty creating a checkbox. So here is my CheckBoxGUI
class:
// take a look into the recent edits for this post if you need to see my old code.
My TextOutliner
class used to draw text with borders/outlines:
// take a look into the recent edits for this post if you need to see my old code.
And finally, here's how I use the CheckBoxGUI
class in my TitleScreen
:
// take a look into the recent edits for this post if you need to see my old code.
Here is my problem (at least that's where I think it is, in the method that handles the checkbox logic):
// take a look into the recent edits for this post if you need to see my old code.
And finally, Draw()
:
// take a look into the recent edits for this post if you need to see my old code.
So the method CheckBoxInputTS
allows me to check/uncheck a CheckBoxGUI item with mouse clicks, but I can't actually attach any functionality to this. For instance I can do the following inside that same method:
// take a look into the recent edits for this post if you need to see my old code.
Perhaps I'm missing something simple, or I don't understand how to actually make further improvements into the code I already have to make it support different functionalities for the checkBoxes
... However, when I test this, if I open the window and check the box the window closses and won't open again. The IsOpen
member of the WindowGUI
class controls whether the window is visible or not through an if-statement in the class' Draw()
method.
The same thing happens if I try to use other examples for the checkboxes... Once checkBox.IsChecked
has a user-given value I can't change it.
If someone can find what I'm doing wrong and help me clean-up this code, it will be much appreciated. Thanks in advance!
Edit: Here's what I have so far, based on the proposed answer:
public class CheckBoxGUI : GUIElement
{
Texture2D checkBoxTexEmpty, checkBoxTexSelected;
public Rectangle CheckBoxTxtRect { get; set; }
public Rectangle CheckBoxMiddleRect { get; set; }
public bool IsChecked { get; set; }
public event Action<CheckBoxGUI> CheckedChanged;
public CheckBoxGUI(Rectangle rectangle, string text, bool isDisabled, ContentManager content)
: base(rectangle, text, isDisabled, content)
{
CheckBoxTxtRect = new Rectangle((Bounds.X + 19), (Bounds.Y - 3), ((int)FontName.MeasureString(Text).X), ((int)FontName.MeasureString(Text).Y));
CheckBoxMiddleRect = new Rectangle((Bounds.X + 16), Bounds.Y, 4, 16);
if (checkBoxTexEmpty == null) checkBoxTexEmpty = content.Load<Texture2D>(Game1.CheckBoxPath + @"0") as Texture2D;
if (checkBoxTexSelected == null) checkBoxTexSelected = content.Load<Texture2D>(Game1.CheckBoxPath + @"1") as Texture2D;
}
public void OnCheckedChanged()
{
var h = CheckedChanged;
if (h != null)
h(this);
}
public override void UnloadContent()
{
base.UnloadContent();
if (checkBoxTexEmpty != null) checkBoxTexEmpty = null;
if (checkBoxTexSelected != null) checkBoxTexSelected = null;
}
public override void Update(GameTime gameTime)
{
base.Update(gameTime);
if (!Game1.IsSoundDisabled)
{
if ((IsHovered) && (!IsDisabled))
{
if (InputManager.IsLeftClicked())
{
ClickSFX.Play();
IsHovered = false;
}
}
}
if (IsClicked) OnCheckedChanged();
}
public void Draw(SpriteBatch spriteBatch)
{
if ((FontName != null) && ((Text != string.Empty) && (Text != null)))
{
if (IsChecked) spriteBatch.Draw(checkBoxTexSelected, Bounds, Color.White);
else if (IsDisabled) spriteBatch.Draw(checkBoxTexEmpty, Bounds, Color.Gray);
else spriteBatch.Draw(checkBoxTexEmpty, Bounds, Color.Gray);
TextOutliner.DrawBorderedText(spriteBatch, FontName, Text, CheckBoxTxtRect.X, CheckBoxTxtRect.Y, ForeColor);
}
}
}
And then the GUIElement
class:
public abstract class GUIElement
{
protected SpriteFont FontName { get; set; }
protected string Text { get; set; }
protected SoundEffect ClickSFX { get; set; }
public Rectangle Bounds { get; set; }
public Color ForeColor { get; set; }
public Color BackColor { get; set; }
public bool IsDisabled { get; set; }
public bool IsHovered { get; set; }
public bool IsClicked { get; set; }
public GUIElement(Rectangle newBounds, string newText, bool isDisabled, ContentManager content)
{
Bounds = newBounds;
Text = newText;
IsDisabled = isDisabled;
FontName = Game1.GameFontSmall;
ForeColor = Color.White;
BackColor = Color.White;
ClickSFX = content.Load<SoundEffect>(Game1.BGSoundPath + @"1") as SoundEffect;
}
public virtual void UnloadContent()
{
if (Bounds != Rectangle.Empty) Bounds = Rectangle.Empty;
if (FontName != null) FontName = null;
if (Text != string.Empty) Text = string.Empty;
if (ClickSFX != null) ClickSFX = null;
}
public virtual void Update(GameTime gameTime)
{
if (!IsDisabled)
{
if (Bounds.Contains(InputManager.MouseRect))
{
if (InputManager.IsLeftClicked()) IsClicked = true;
ForeColor = Color.Yellow;
IsHovered = true;
}
else if (!Bounds.Contains(InputManager.MouseRect))
{
IsHovered = false;
ForeColor = Color.White;
}
}
else ForeColor = Color.Gray;
}
}
And this is my usage:
// Fields
readonly List<GUIElement> elements = new List<GUIElement>();
CheckBoxGUI chk;
bool check = true;
string text;
// LoadContent()
chk = new CheckBoxGUI(new Rectangle(800, 200, 16, 16), "Hide FPS", false, content);
chk.CheckedChanged += cb => check = cb.IsChecked;
elements.Add(chk);
// UnloadContent()
foreach (GUIElement element in elements) element.UnloadContent();
// Update()
for (int i = 0; i < elements.Count; i++) elements[i].Update(gameTime);
foreach (CheckBoxGUI chk in elements) chk.Update(gameTime);
// Draw()
if (chk.IsChecked) check = true;
else check = false;
if (check) text = "True";
else text = "False";
spriteBatch.DrawString(Game1.GameFontLarge, text, new Vector2(800, 400), Color.White);
if (optionsWindow.IsOpen) chk.Draw(spriteBatch);
As an alternative to what @craftworkgames suggested, you can also pass a delegate through the checkbox constructor, and invoke it directly. The difference between this and events is marginal; an event is basically a list of delegates (i.e. managed pointers to functions), and "firing an event" simply calls all handlers you attached (using +=
) in succession. Using events will provide a more uniform way (at least more comfortable to .NET developers), but I will also mention some other aspects in my answer.
This is the general idea:
public class CheckBoxGUI : GUIElement
{
public bool IsChecked { get; set; }
// instead of using the event, you can use a single delegate
readonly Action<CheckBoxGUI> OnCheckedChanged;
public CheckBoxGUI(Action<CheckBoxGUI> onCheckedChanged, Rectangle rectangle)
: base(rectangle) // we need to pass the rectangle to the base class
{
OnCheckedChanged = onCheckedChanged;
}
public override bool Update(GameTime gameTime, InputState inputState)
{
if (WasClicked(inputState)) // imaginary method
{
// if we are here, it means we need to handle the inputState
IsChecked = !IsChecked;
// this method will be invoked
OnCheckedChanged(this);
return true;
}
else
{
return false;
}
}
... drawing methods, load/unload content
}
When you instantiate the checkbox, you can simply pass an anonymous method which will get called on state change:
var checkbox = new CheckBoxGUI(
// when state is changed, `win.IsOpen` will be set to a new value
cb => win.IsOpen = cb.IsChecked,
new Rectangle(0, 0, 100, 100)
);
(btw, the difference between this approach and events is rather negligible:)
// if you had a 'CheckedChanged' event, instantiation would look something like this
var checkbox = new CheckBoxGUI(...);
checkbox.CheckedChanged += cb => win.IsOpen = cb.IsChecked;
Create a base gui class with shared functionality
I would also extract most functionality into a base class. This is the same reason Control
class exists as the base class in WinForms, and you will be repeating this over and over again unless you use a base class.
If nothing else, all elements have a bounding rectangle and the same way of checking for mouse clicks:
public class GUIElement
{
public Rectangle Bounds { get; set; }
public GUIElement(Rectangle rect)
{
Bounds = rect;
}
public virtual bool Update(GameTime game, InputState inputState)
{
// if another element already handled this click,
// no need to bother
if (inputState.Handled)
return false;
// you should actually check if mouse was both clicked and released
// within these bounds, but this is just a demo:
if (!inputState.Pressed)
return false;
// within bounds?
if (!this.Bounds.Contains(inputState.Position))
return false;
// mark as handled: we don't want this event to
// propagate further
inputState.Handled = true;
return true;
}
public virtual void Draw(GameTime gameTime, SpriteBatch sb) { }
}
When you decide that you want your mouse clicks to be triggered on mouse-up events (like it's usually done), you only have a single place in your code where this change should happen.
Btw. this class should also implement other properties shared between all your UI elements. You don't need to specify their values through the constructor, but you must specify defaults. This is similar to what WinForms does:
public class GUIElement
{
public Rectangle Bounds { get; set; }
public SpriteFont Font { get; set; }
public Color ForeColor { get; set; }
public Color BackColor { get; set; }
// make sure you specify all defaults inside the constructor
// (except for Bounds, of course)
}
To abstract the mouse/touch/gamepad input, I have used a rather simplistic InputState
above (you might want to extend it later):
public class InputState
{
// true if this event has already been handled
public bool Handled;
// true if mouse is being held down
public bool Pressed;
// mouse position
public Point Position;
}
Inheriting from the base class
With this in place, your CheckBoxGUI
now inherits the goodies from GUIElement
:
// this is the class from above
public class CheckBoxGUI : GUIElement
{
public bool IsChecked { get; set; }
readonly Action<CheckBoxGUI> OnCheckedChanged;
public CheckBoxGUI(Action<CheckBoxGUI> onCheckedChanged, Rectangle rectangle)
: base(rectangle) // we need to pass the rectangle to the base class
{
OnCheckedChanged = onCheckedChanged;
}
// Note that this method returns bool, unlike 'void Update'.
// Also, intersections should be handled here, not outside.
public override bool Update(GameTime gameTime, InputState inputState)
{
var handled = base.Update(gameTime, inputState);
if (!handled)
return false;
// if we are here, it means we need to handle the inputState
IsChecked = !IsChecked;
OnCheckedChanged(this);
return true;
}
... drawing methods, load/unload content
}
Your game/scene class
At the beginning of your main Game (or Scene) update method, you only need to create the InputState
instance and call HandleInput
for all elements:
public void Update(GameTime gameTime)
{
var mouse = Mouse.GetState();
var inputState = new InputState()
{
Pressed = mouse.LeftButton == ButtonState.Pressed,
Position = mouse.Position
};
foreach (var element in this.Elements)
{
element.Update(gameTime, inputState);
}
}
Event based alternative
Using the event-based approach, you would change the check box to:
// this is the class from above
public class CheckBoxGUI : GUIElement
{
public bool IsChecked { get; set; }
public event Action<CheckBoxGUI> CheckedChanged;
protected virtual void OnCheckedChanged()
{
var h = CheckedChanged;
if (h != null)
h(this);
}
public CheckBoxGUI(Rectangle rectangle)
: base(rectangle) // we need to pass the rectangle to the base class
{ }
// the rest of the class remains the same
}
And instantiate it:
var cb = new CheckBoxGUI(new Rectangle(0, 0, 100, 100));
cb.CheckedChanged += cb => win.IsOpen = cb.IsChecked;
Update
This is how your Update
call stack should look like:
Game.Update
TitleScreen.Update
TitleScreen.Update
element.Update
for gui elements (not just checkboxes)GuiElement.Update
CheckBoxGUI.Update
If you are doing things right, you should have a list of gui elements in your screen class, not actual instances. Screen should not know or care which exact type of the element is being drawn:
readonly List<GuiElement> elements = new LIst<GuiElement>();
var chk = new CheckBoxGUI(new Rectangle(800, 200, 16, 16), "Window", false, content);
chk.CheckedChanged += cb => win.IsOpen = cb.IsChecked;
// from now on, your checkbox is just a "gui element" which knows how to
// update itself and draw itself
elements.Add(chk);
// your screen update method
foreach (var e in elements)
e.Update(gameTime);
// your screen draw method
foreach (var e in elements)
e.Draw(gameTime);