Creating a Graphical User Interface

0
308

Creating the GUI Controls

A graphical user interface (GUI) is absolutely essential for a game to be successful, even if that means using nothing more than labels and buttons on the screen that the user can click on.

Sprite Class Improvements

Modifying the Sprite Class

To make the GUI controls more effective, the Sprite class must be tweaked just a little.

  1. We need to change the definition of p_content and p_spriteBatch from private to protected so that they will be accessible to classes that inherit from Sprite. This way, we can load assets and draw without creating new reference variables in every subclass. Open the Sprite class and make the change:
    [code]
    protected ContentManager p_content;
    protected SpriteBatch p_spriteBatch;
    [/code]
  2. Just to be sure we are on the same page despite the changes made to this class in the past, here is the Load() method. Ignore past changes and just note this current version, which shows that the size and origin properties have been moved out of the try block:
    [code]
    public virtual bool Load(string assetName)
    {
    try
    {
    image = p_content.Load<Texture2D>(assetName);
    }
    catch (Exception) { return false; }
    size = new Vector2(image.Width, image.Height);
    origin = new Vector2(image.Width / 2, image.Height / 2);
    return true;
    }
    [/code]
  3. Add an error-handling line to the Draw() method so that it won’t crash the program if the image is null. This is a common verification. Since our GUI controls will be using a few images in interesting ways, we just want to ensure that any image that is not loaded correctly won’t crash the program—instead, it will just not show up.
    [code]
    public virtual void Draw()
    {
    if (!visible) return;
    if (image == null) return;
    . . .
    }
    [/code]

GUI Base Class: Control

All the GUI classes will be found in the GUI.cs source code file for the sake of convenience. Within that file, the classes will be wrapped inside the GameLibrary namespace (the same namespace used by Sprite and Animation).

[code]
namespace GameLibrary
{
. . .
}
[/code]

The base GUI class is called Control, and it is primarily used to create a reference to the ContentManager, SpriteBatch, and SpriteFont objects used in a game—all of which are needed by the GUI. Control inherits from Sprite, so it supplies GUI controls (declared as subclasses of Control) with all the features of Sprite, including loading and drawing. Methods are declared as virtual or override so they can be used and overridden in each subclass. There are certainly more services the base class could provide, such as touch input, but it turns out (during development) that most of that code must reside in each individual class. Listing 20.1 contains the source code for the Control class.

LISTING 20.1 Source Code for the Control Class

[code]
public abstract class Control : Sprite
{
protected SpriteFont p_font;
public Control(ContentManager content, SpriteBatch spriteBatch,
SpriteFont font)
: base(content, spriteBatch)
{
p_font = font;
}
public override bool Load(string filename)
{
return base.Load(filename);
}
public virtual void Update(TouchLocation touch)
{
}
public override void Draw()

{
base.Draw();
}
}
[/code]

Label Control

A Label is the most fundamental type of GUI control, with the simple task of displaying a text message on the screen. This is more important than it might at first seem, because a Label control can be moved anywhere on the screen without affecting the call to Label.Draw() from the main program. This Label class is rather basic, providing a shadow feature with customizable Color properties for the text and shadow. Two Labels will be used in the sample project later in this hour. Listing 20.2 contains the source code for the Label class.

LISTING 20.2 Source Code for the Label Class

[code]
public class Label : Control
{
public string text;
public Color shadowColor;
public Color textColor;
public bool UseShadow;
public Label(ContentManager content, SpriteBatch spriteBatch,
SpriteFont font)
: base(content, spriteBatch, font)
{
text = ““;
color = Color.White;
textColor = Color.White;
shadowColor = Color.Black;
UseShadow = true;
}
public override void Update(TouchLocation touch)
{
base.Update(touch);
}
public override void Draw()
{
if (UseShadow)
{
p_spriteBatch.DrawString(p_font, text,
new Vector2(position.X – 2, position.Y – 2), shadowColor);
}
p_spriteBatch.DrawString(p_font, text, position, textColor);
}
public Vector2 TextSize()
{
return p_font.MeasureString(text);
}
}
[/code]

Button Control

A Button is the second most common type of control needed for a rudimentary GUI system. Our Button class will load a 64×64 bitmap file called button.png (which must be in the content project). The great thing about this is that you can replace the image with one of your own. Due to the way the class works, I recommend using an image with the same dimensions but with your own “skin” theme. The button used in the example this hour is a gray box with a white outline. An important feature for a Button control is to display text and respond to user tap events. Our Button goes further by allowing its background and text colors to be changed independently for a customized look. Listing 20.3 contains the source code for the Button class.

LISTING 20.3 Source Code for the Button Class

[code]
public class Button : Control
{
public string text;
public Color shadowColor;
public Color textColor;
public bool UseShadow;
public bool Tapped;
public Button(ContentManager content, SpriteBatch spriteBatch,
SpriteFont font)
: base(content, spriteBatch, font)
{
text = ““;
color = Color.White;
textColor = Color.White;
shadowColor = Color.Black;
UseShadow = true;
Load(“button”);
}
public override void Update(TouchLocation touch)
{
base.Update(touch);
Tapped = false;
if (touch.State == TouchLocationState.Pressed)
{
Rectangle rect = Boundary();
Vector2 pos = touch.Position;
Point point = new Point((int)pos.X, (int)pos.Y);
if (rect.Contains(point))
{
Tapped = true;
}
}
}
public override void Draw()
{
base.Draw();
Vector2 size = TextSize();
Vector2 pos2 = new Vector2(position.X + 2, position.Y + 2);
Vector2 pivot = new Vector2(size.X / 2, size.Y / 2);
p_spriteBatch.DrawString(p_font, text, position, shadowColor,
0.0f, pivot, 1.0f, SpriteEffects.None, zindex);
p_spriteBatch.DrawString(p_font, text, pos2, textColor, 0.0f, pivot,
1.0f, SpriteEffects.None, zindex);
}
public Vector2 TextSize()
{
return p_font.MeasureString(text);
}
}
[/code]

Horizontal Slider Control

A slider control makes it possible to adjust a setting or to control some aspect of a game directly by the user, and resembles a movable sliding lever on the screen. There are two types of slider: horizontal and vertical. Although one common class could be used for both slider orientations, it would be more coding work, so it is more effective to just separate them into HSlider and VSlider controls. This is definitely a complex type of control compared to Label and Button. HSlider loads three images, so these bitmap files must all be found in the content project for the GUI code to run properly:

  • hslider_bar.png
  • hslider_end.png
  • button.png

Remember, when you are creating your own game using these GUI controls, that you can skin the controls to your own liking. The slider button needn’t be a circle at all! It can be any shape, including a custom image or a picture of a dragon—it doesn’t matter, and it’s up to you!

The left and right end images are shared, so if you create a custom skin for the control, be sure that the end images are interchangeable. The middle piece is a line one (1) pixel wide, scaled to the width of the control (set with the HSlider.Limit property). If the limit is 100, the one-pixel-wide image is scaled by 100 times to reach the edge! The scale as well as other properties are borrowed from the base Sprite class embedded in Control, inherited by HSlider. There isn’t much error handling, so if you try to set Limit to a negative number, it just will not work right or will crash. Listing 20.4 contains the source code for the HSlider class.

LISTING 20.4 Source Code for the HSlider Class

[code]
public class HSlider : Control
{
public bool Moving;
public Vector2 start;
private int p_value;
private int p_limit;
Sprite sprLeftEnd, sprRightEnd, sprBar;
public HSlider(ContentManager content, SpriteBatch spriteBatch,
SpriteFont font)
: base(content, spriteBatch, font)
{
scale = 1.0f;
start = Vector2.Zero;
Load(“slider_tab”);
sprLeftEnd = new Sprite(content, spriteBatch);
sprLeftEnd.Load(“hslider_end”);
sprLeftEnd.origin = new Vector2(3, 16);
sprRightEnd = new Sprite(content, spriteBatch);
sprRightEnd.Load(“hslider_end”);
sprRightEnd.origin = new Vector2(0, 16);
sprBar = new Sprite(content, spriteBatch);
sprBar.Load(“hslider_bar”);
sprBar.origin = new Vector2(0, 16);
Limit = 100;
}
public int Value
{
get { return p_value; }
set
{
p_value = value;
if (p_value < 0) p_value = 0;
if (p_value > p_limit) p_value = p_limit;
position.X = start.X + p_value;
}
}
public int Limit
{
get { return p_limit; }
set
{
p_limit = value;
sprBar.scaleV = new Vector2((float)
(p_limit + this.image.Width+1), 1.0f);
}
}
public override void Update(TouchLocation touch)
{
base.Update(touch);
Moving = false;
if (touch.State == TouchLocationState.Moved)
{
Rectangle rect = Boundary();
Point point = new Point((int)touch.Position.X,
(int)touch.Position.Y);
if (rect.Contains(point))
{
Vector2 relative = Vector2.Zero;
relative.X = touch.Position.X – position.X;
position.X += relative.X;
Value = (int)(position.X – start.X);
if (position.X < start.X)
position.X = start.X;
else if (p_value > p_limit)
position.X -= relative.X;
Moving = true;
}
}
}
public override void Draw()
{
//draw ends
sprLeftEnd.position = new Vector2(start.X – 16, start.Y);
sprLeftEnd.color = this.color;
sprLeftEnd.Draw();
sprRightEnd.position = new Vector2(start.X + 16 + p_limit, start.Y);
sprRightEnd.color = this.color;
sprRightEnd.Draw();
//draw middle bar
sprBar.position = new Vector2(start.X – 16, start.Y);
sprBar.color = this.color;
sprBar.Draw();
//draw sliding circle
base.Draw();
//draw value text
Vector2 size = p_font.MeasureString(p_value.ToString());
p_spriteBatch.DrawString(p_font, p_value.ToString(), position,
Color.Black, 0.0f, new Vector2(size.X/2, size.Y/2), 0.6f,
SpriteEffects.None, 1.0f);
}
public void SetStartPosition(Vector2 pos)
{
position = pos;
start = pos;
}
}
[/code]

Vertical Slider Control

The Vertical Slider control, or VSlider, shares all the same functionality as HSlider, but calculations are shifted 90 degrees in a vertical orientation. So, all the “X” properties used in the HSlider’s functionality become “Y” properties in VSlider in order for it to work properly. Here are the bitmaps required by the control (and note that button.png is shared):

  • vslider_bar.png
  • vslider_end.png
  • button.png

Listing 20.5 contains the source code for the VSlider class.

LISTING 20.5 Source Code for the VSlider Class

[code]
public class VSlider : Control
{
public bool Moving;
public Vector2 start;
private int p_value;
private int p_limit;
Sprite sprTopEnd, sprBottomEnd, sprBar;
public VSlider(ContentManager content, SpriteBatch spriteBatch,
SpriteFont font)
: base(content, spriteBatch, font)
{
scale = 1.0f;
start = Vector2.Zero;
Load(“slider_tab”);
sprTopEnd = new Sprite(content, spriteBatch);
sprTopEnd.Load(“vslider_end”);
sprTopEnd.origin = new Vector2(16, 3);
sprBottomEnd = new Sprite(content, spriteBatch);
sprBottomEnd.Load(“vslider_end”);
sprBottomEnd.origin = new Vector2(16, 0);
sprBar = new Sprite(content, spriteBatch);
sprBar.Load(“vslider_bar”);
sprBar.origin = new Vector2(16, 0);
Limit = 100;
}
public int Value
{
get { return p_value; }
set
{
p_value = value;
if (p_value < 0) p_value = 0;
if (p_value > p_limit) p_value = p_limit;
position.Y = start.Y + p_value;
}
}
public int Limit
{
get { return p_limit; }
set
{
p_limit = value;
sprBar.scaleV = new Vector2(1.0f, (float)
(p_limit + this.image.Height + 1));
}
}
public override void Update(TouchLocation touch)
{
base.Update(touch);
Moving = false;
if (touch.State == TouchLocationState.Moved)
{
Rectangle rect = Boundary();
Point point = new Point((int)touch.Position.X,
(int)touch.Position.Y);
if (rect.Contains(point))
{
Vector2 relative = Vector2.Zero;
relative.Y = touch.Position.Y – position.Y;
position.Y += relative.Y;
Value = (int)(position.Y – start.Y);
if (position.Y < start.Y)
position.Y = start.Y;
else if (p_value > p_limit)
position.Y -= relative.Y;
Moving = true;
}
}
}
public override void Draw()
{
//draw ends
sprTopEnd.position = new Vector2(start.X, start.Y – 16);
sprTopEnd.color = this.color;
sprTopEnd.Draw();
sprBottomEnd.position = new Vector2(start.X, start.Y + 16 + p_limit);
sprBottomEnd.color = this.color;
sprBottomEnd.Draw();
//draw middle bar
sprBar.position = new Vector2(start.X, start.Y – 16);
sprBar.color = this.color;
sprBar.Draw();
//draw sliding circle
base.Draw();
//draw value text
Vector2 size = p_font.MeasureString(p_value.ToString());
p_spriteBatch.DrawString(p_font, p_value.ToString(), position,
Color.Black, 0.0f, new Vector2(size.X / 2, size.Y / 2), 0.6f,
SpriteEffects.None, zindex);
}
public void SetStartPosition(Vector2 pos)
{
position = pos;
start = pos;
}
}
[/code]

Demonstrating the GUI Controls

The initialization code for a GUI demo or a game using GUI controls will always be much more involved and code-intensive than the processing code where the controls are updated and drawn, because there are so many properties involved in creating and customizing a nice-looking, interactive GUI. Our example this hour demonstrates a GUI with Labels, Buttons, HSliders, and VSliders, and is quite functional, as you can see in Figure 20.1. The source code for the GUI Demo program is found in Listing 20.6.

The example demonstrates labels, buttons, and sliders.
FIGURE 20.1 The example demonstrates labels, buttons, and sliders.

On the left is a vertical slider used to adjust the background color. Why? Just to show that the slider works and does something interesting. Maybe in a game a VSlider would be used to adjust the power level of a catapult or an artillery gun. Really, the use for these controls is up to the game’s designer and is just implemented by the programmer (or team). On the right side are three buttons labeled RED, GREEN, and BLUE. Beside each button is a slider.

Clicking a button changes the color component to a random value from 0 to 255, and automatically moves the slider to that location. The slider can also be moved manually, and this in turn will change the button’s color to reflect the change to that color component. The end result of all this color manipulation is seen in the small, unassuming Exit button at the lower-right corner of the screen. Note that the Limit property of both HSlider and VSlider changes its overall size and defines the limits of the sliding button. The three color sliders have a range of 0 to 255, whereas the smaller vertical slider has a range of 0 to 100.

LISTING 20.6 Source Code for the GUI Demo Program

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont font;
Random rand;
TouchLocation oldTouch;
Label lblTitle, lblColor;
Button[] buttons;
HSlider[] hsliders;
VSlider vslider;
Color bgcolor;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
oldTouch = new TouchLocation();
rand = new Random();
bgcolor = Color.CornflowerBlue;
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>(“WascoSans”);
lblTitle = new Label(Content, spriteBatch, font);
lblTitle.text = “Graphical User Interface Demo”;
lblTitle.position = new Vector2(400 – lblTitle.TextSize().X / 2, 0);
//create buttons
buttons = new Button[4];
buttons[0] = new Button(Content, spriteBatch, font);
buttons[0].text = “RED”;
buttons[0].position = new Vector2(400, 150);
buttons[0].textColor = Color.Red;
buttons[0].color = Color.DarkRed;
buttons[0].scaleV = new Vector2(1.5f, 1.0f);
buttons[1] = new Button(Content, spriteBatch, font);
buttons[1].text = “GREEN”;
buttons[1].position = new Vector2(400, 230);
buttons[1].textColor = Color.Green;
buttons[1].color = Color.DarkGreen;
buttons[1].scaleV = new Vector2(1.5f, 1.0f);
buttons[2] = new Button(Content, spriteBatch, font);
buttons[2].text = “BLUE”;
buttons[2].position = new Vector2(400, 310);
buttons[2].textColor = Color.Cyan;
buttons[2].color = Color.DarkCyan;
buttons[2].scaleV = new Vector2(1.5f, 1.0f);
buttons[3] = new Button(Content, spriteBatch, font);
buttons[3].text = “Exit”;
buttons[3].position = new Vector2(750, 450);
buttons[3].scaleV = new Vector2(1.2f, 0.8f);
//create horizontal sliders for color editing
hsliders = new HSlider[3];
hsliders[0] = new HSlider(Content, spriteBatch, font);
hsliders[0].SetStartPosition(new Vector2(500, 150));
hsliders[0].color = Color.Red;
hsliders[0].Limit = 255;
hsliders[1] = new HSlider(Content, spriteBatch, font);
hsliders[1].SetStartPosition(new Vector2(500, 230));
hsliders[1].color = Color.LightGreen;
hsliders[1].Limit = 255;
hsliders[2] = new HSlider(Content, spriteBatch, font);
hsliders[2].SetStartPosition(new Vector2(500, 310));
hsliders[2].color = Color.Cyan;
hsliders[2].Limit = 255;
//create vertical slider for bg color editing
vslider = new VSlider(Content, spriteBatch, font);
vslider.SetStartPosition(new Vector2(140, 170));
vslider.color = Color.Yellow;
vslider.Limit = 100;
//create label for slider
lblColor = new Label(Content, spriteBatch, font);
lblColor.text = “Background Color”;
lblColor.position = new Vector2( 140 – lblColor.TextSize().X/2,
100);
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
TouchCollection touchInput = TouchPanel.GetState();
if (touchInput.Count > 0)
{
TouchLocation touch = touchInput[0];
oldTouch = touch;
lblTitle.Update(touch);
UpdateButtons(touch);
UpdateSliders(touch);
vslider.Update(touch);
lblColor.Update(touch);
}
base.Update(gameTime);
}
void UpdateButtons(TouchLocation touch)
{
//update buttons
int tapped = -1;
for (int n = 0; n < buttons.Length; n++)
{
buttons[n].Update(touch);
if (buttons[n].Tapped) tapped = n;
}
//was a button tapped?
int c = rand.Next(256);
switch (tapped)
{
case 0:
buttons[0].color = new Color(c, 0, 0);
hsliders[0].Value = c;
break;
case 1:
buttons[1].color = new Color(0, c, 0);
hsliders[1].Value = c;
break;
case 2:
buttons[2].color = new Color(0, 0, c);
hsliders[2].Value = c;
break;
case 3:
this.Exit();
break;
}
}
void UpdateSliders(TouchLocation touch)
{
//update horizontal sliders
int moving = -1;
for (int n = 0; n < hsliders.Length; n++)
{
hsliders[n].Update(touch);
if (hsliders[n].Moving) moving = n;
}
switch(moving)
{
case 0:
buttons[0].color = new Color(hsliders[0].Value, 0, 0);
break;
case 1:
buttons[1].color = new Color(0, hsliders[1].Value, 0);
break;
case 2:
buttons[2].color = new Color(0, 0, hsliders[2].Value);
break;
}
//colorize Exit button based on colors
buttons[3].color = new Color(hsliders[0].Value,
hsliders[1].Value, hsliders[2].Value);
//update vertical slider
if (vslider.Moving)
{
bgcolor = Color.CornflowerBlue;
bgcolor.R -= (byte)vslider.Value;
bgcolor.G -= (byte)vslider.Value;
bgcolor.B -= (byte)vslider.Value;
}
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(bgcolor);
spriteBatch.Begin(SpriteSortMode.Deferred,
BlendState.AlphaBlend);
lblTitle.Draw();
foreach (Button b in buttons)
b.Draw();
foreach (HSlider hs in hsliders)
hs.Draw();
vslider.Draw();
lblColor.Draw();
spriteBatch.End();
base.Draw(gameTime);
}
void print(int x, int y, string text, Color color)
{
var pos = new Vector2((float)x, (float)y);
spriteBatch.DrawString(font, text, pos, color);
}
}
[/code]