Creating a visual selection control from scratch
Friday, March 29th, 2013In this post we will extend the DrawableGameComponent to create a control which will let the user select one of a number of possible options.
Here you can see what we want to achieve:
The different options will be implemented as one big picture, where only the currently selected option is displayed in the control.
How we will build the control
- Subclass DrawableGameComponent
- Add basic properties for
- positioning the control (Vector2)
- the menutexture (Texture2D)
- the number of possible selections (int)
- the currently selected index (int)
- the SpriteBatch to draw to
- Add functions for manipulating the selected index
- MoveToNext()
- MoveToPrevious()
- Override base Update() and Draw() to react to keyboard input and change selection
After building this control in a basic version, which "blinks" from selection to selection, we will improve it, so the selection slides from one selected item to the next.
Subclassing DrawableGameComponent
The benefits of using DrawableGameComponent for basic game mechanics like this one is that once the gameobject is created and added to the Game object's Components collection, our component will automatically have Update() called (if our object's Enabled property is true) and Draw() called (if our object's Visible property is true). This way we can leave a lot of the "housekeeping" to the XNA engine.
So go ahead, whip up a new XNA solution in your favorite editor and add a new class "VisualSelectionControl", which inherits DrawableGameComponent:
public class VisualSelectionControl : DrawableGameComponent { }
When subclassing another class which doesn't have a default constructor (a "default constructor" is also known as an "empty constructor" or a "parameterless constructor"), we often choose to pass the parameters for the superclass' constructor to the subclass' constructor, so the subclass can pass the parameters on to the superclass'. This means that our class will not compile as it is right now, because we can't create a VisualSelectionControl without creating a DrawableGameComponent. And the DrawableGameComponent wants a Game object in its constructor - or it just won't play :).
So we add a constructor to our VisualSelectionControl which takes a Game object, which we pass on to DrawableGameComponent's constructor:
public class VisualSelectionControl : DrawableGameComponent { //creates a VisualSelectionControl by first creating a DrawableGameComponent //The constructor takes a Game object, and passes it on to the superclass (the "base" class) public VisualSelectionControl (Game game) : base(game) { } }
Add basic properties
Since we already know what properties we need for our control to work (se item 2 in "How we will build our control"), let's add them to the class, and as parameters to the constructor, so we can't instantiate a VisualSelectionControl object without these variables:
public class VisualSelectionControl : DrawableGameComponent { //the position (top-left corner) of the menu public Vector2 Position { get; set; } //the spritebatch to draw to public SpriteBatch SpriteBatch { get; set; } //stores the entire menu in one big picture public Texture2D MenuTexture { get; private set; } //how many selectable items the texture should be split into public int NumberOfPossibleSelections { get; private set; } //Currently selected index public int CurrentIndex { get; set; } //creates a VisualSelectionControl by first creating a DrawableGameComponent //takes a Game object, and passes it on to the superclass (DrawableGameComponent) public VisualSelectionControl(Game game, Vector2 position, SpriteBatch spriteBatch, Texture2D menuTexture, int numberOfPossibleSelections) : base(game) { //store parameters Position = position; SpriteBatch = spriteBatch; MenuTexture = menuTexture; NumberOfPossibleSelections = numberOfPossibleSelections; } }
As you can see we've omitted the CurrentIndex property from the parameters to the constructor, since we're okay with that defaulting to zero when the control is created.
All we need to do now for our control to work is implement the Draw method on the control, so we can see what is selected.
Since the Draw method already exists on the DrawableGameComponent, we override it:
public override void Draw(GameTime gameTime) { //call the superclass' implementation of the method we're overriding base.Draw(gameTime); //calculate the height of one menuitem int heightOfOneMenuItem = MenuTexture.Height / NumberOfPossibleSelections; //get the source rectangle we want, based on current index Rectangle sourceRect = new Rectangle(0, heightOfOneMenuItem* CurrentIndex, (int)MenuTexture.Width, heightOfOneMenuItem); //draw to spritebatch, at control's position, from the sourcerectangle SpriteBatch.Draw(MenuTexture, Position, sourceRect, Color.White); }
Calling overridden method from overriding method
As you can see, we call the base class' Draw() method from our new Draw() method. This is important to do whenever you override a method from a class where you're not entirely sure what is going on inside the overriden method. This is to ensure that the functionality in the parent object remains intact.
Then we add our own code to calculate how tall one menuitem is based on the complete menutexture and the number of items in the menu. Finally we create a source Rectangle, which we use to render only that part of our texture to the screen.
Test it in the Game class
Go ahead and download the Doom difficulty menu image (right click and save)
and add it to your content project.
Then add code to the LoadContent() method of the Game class, to load the texture and instantiate a VisualSelectionControl, which we add to the Game object's Components collection
public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; VisualSelectionControl _selectionControl; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); Texture2D _levelselectionTexture = Content.Load<Texture2D>("doom_difficulty"); _selectionControl = new VisualSelectionControl(this, Vector2.One * 100, spriteBatch, _levelselectionTexture, 5); Components.Add(_selectionControl); }
Adding the VisualSelectionControl to the Game.Components collection ensures (as we've been over) that our component will automatically:
- have Update() called if our object's Enabled property is true (Enabled is inherited from GameComponent)
- have Draw() called if our object's Visible property is true (Visible is inherited from DrawableGameComponent)
BUT - if we run the game now, we will get an exception, as our control tries to draw to the SpriteBatch, and we haven't called SpriteBatch.Begin. So let's change the Draw() method in the Game class, to ensure that we call SpriteBatch.Begin() before calling the Draw() of all GameComponents, and SpriteBatch.End() afterwards:
protected override void Draw(GameTime gameTime) { //clear the screen using any color you like (I like blood red) GraphicsDevice.Clear(Color.DarkRed); //begin drawing spriteBatch.Begin(); //call the Game object's Draw() method, //which draws all visible DrawableGameComponents in the Components collection base.Draw(gameTime); //end drawing spriteBatch.End(); }
Run the program - and voilà:
To test the menu control, you can try setting the CurrentIndex to 2 in the LoadContent method
_selectionControl.CurrentIndex = 2;
That should change the selected index, which in turn will make the calculations in Draw() create a different source Rectangle, rendering a different part of the Texture2D:
Add functions for manipulating the selected index
Now the good thing about having a property
//Currently selected index public int CurrentIndex { get; set; }
instead of a public variable
//Currently selected index public int CurrentIndex;
is that we've encapsulated the data inside with accessors, so we have full control over reading and writing to and from the variable. We will use that now, to ensure we only store valid values.
First we change the automatic property to a property with a backing variable:
//Currently selected index private int _currentIndex; public int CurrentIndex { get { return _currentIndex; } set { _currentIndex = value; } }
and then we only set the _currentIndex variable, if the value we get is valid
set { //ensure only valid values are set if (value >= 0 &amp;&amp; value < NumberOfPossibleSelections) { _currentIndex = value; }
This way we can easily add two helpermethods for moving step by step:
public void MoveToPrevious() { CurrentIndex--; } public void MoveToNext() { CurrentIndex++; }
The code in the CurrentIndex property's Set will ensure we don't go beyond the boundaries of the menu.
Add code to handle Keys.Up and Keys.Down presses
The choice of where to implement this functionality is up to you. It can go in your Game class, or in the control we're making. The problem with creating it in the control, is that it will listen to the KeyboardState all the time, and many different controls in the same game could start reacting to the same keypresses. But you can easily set the VisibleSelectionControl's Enabled property to false and by that make sure the Update() method of the control is not called. So we're going to go ahead and implement keyboard handling in the control for now. First we will implement simple keyboard handling with a flaw. Then we will look at why it doesn't work, and improve it by adding a little more code.
Naïve implementation of keyboard handling
We add a variable to the control for storing the keyboard's state:
//stores the state of the keyboard private KeyboardState _currentKeyboardState;
Then we make a naïve implementation of reacting to the keyboard input in the Update() method:
public override void Update(GameTime gameTime) { //get the keyboard's state _currentKeyboardState = Keyboard.GetState(); //call the superclass' implementation of the method we're overriding base.Update(gameTime); //use input to change state if (_currentKeyboardState.IsKeyDown(Keys.Up)) { MoveToPrevious(); } if (_currentKeyboardState.IsKeyDown(Keys.Down)) { MoveToNext(); } }
Go ahead and try it out. You will only see the first and last options in the menu.
"Why?" you ask?
Because the Update() method is called arond sixty times per second, and you probably won't release the Up or Down key fast enough to only trigger MoveToPrevious()/MoveToNext() once or twice, but five or ten times. So we have to only move one step, right when the key is pressed.
We do this by storing the keyboard's state from the previous Update() and then only acting on a new press, i.e. when the key's last state was UP and it is now DOWN.
Improved keyboard handling
So go ahead and update your code:
//stores the state of the keyboard private KeyboardState _currentKeyboardState, _previousKeyboardState; public override void Update(GameTime gameTime) { //get the keyboard's state _currentKeyboardState = Keyboard.GetState(); //call the superclass' implementation of the method we're overriding base.Update(gameTime); //use input to change state if (_currentKeyboardState.IsKeyDown(Keys.Up) &amp;&amp; _previousKeyboardState.IsKeyUp(Keys.Up)) { MoveToPrevious(); } if (_currentKeyboardState.IsKeyDown(Keys.Down) &amp;&amp; _previousKeyboardState.IsKeyUp(Keys.Down)) { MoveToNext(); } //store this Update()'s keyboard state for use in next Update _previousKeyboardState = _currentKeyboardState; }
and try it out again. Now you should only trigger movement when you first press the key, and have to release it again to make another move - COOL huh?
Not good enough you say?
So you think it would be nicer to have a smooth, scrolling motion - huh? Well okay - for a final encore, let's implement scrolling and call it a day
Whenever you want movement in a game (even in a menu) you can make a variable to store the amount you want to change an object's position. Then for every update you can add that amount to the position of whatever you want moved. In our case, we want the source rectangle (the place we grab the visible part of the menu from) to move gradually from where it was, to a position based on the new CurrentIndex.
To do that, all we need is to store how far we are from where we want to be, and then gradually diminish that distance every Update.
This technique can be used for a LOT of other animations as well in a game, just so you know
Okay - so let's implement that...
Store where we are currently at, so we can calculate how to get to where we want to be
Since we want the source rectangle to move gradually now, we want to store its current position in a variable, so we can manipulate it in every Update().
So go ahead and move the source rectangle into its own member variable on the control. The position of the source rectangle will change every update and move towards the currently selected index.
//store the source rectangle private Rectangle _sourceRectangle;
And since we will be needing the height of one menu item more places than one now, move it into its own member variable as well
//stores the height of one menu item private float _heightOfOneMenuItem;
and initialize both of those variables in the constructor:
//calculate the height of one menuitem _heightOfOneMenuItem = MenuTexture.Height / NumberOfPossibleSelections; //create the source rectangle _sourceRectangle = new Rectangle(0, 0, MenuTexture.Width, (int)_heightOfOneMenuItem);
All we need to do now is move the source rectangle towards its destination in the Update() method:
//calculate where the sourcerectangle is supposed to be on the Y axis of the texture float sourceRectangleYDestination = _heightOfOneMenuItem * CurrentIndex; //get the difference between where it is now and where it is supposed to be float differenceOnYAxis = sourceRectangleYDestination - _sourceRectangle.Top; //calculate a thrirty percent movement float thirtyPercentMovement = differenceOnYAxis * .3f; //move the source rectangle _sourceRectangle.Offset(0, (int)thirtyPercentMovement);
Here we calculate what the difference is between where the source rectangle is now and where it should be. Then we move the source rectangle thirty percent of the distance every Update(). That percentage will result in a slowing down as the absolute distance in pixels gets smaller (30 % of 200 pixels is 60, but 30 % of 40 pixels is only 12 )
This section of code is a functional unit, which performs one operation, so it is a perfect candidate for moving into its own function, so we can call it from the Update() method:
MoveSourceRectangleTowardsDestination();
This method of cutting parts of your code out and making them into their own well-named methods is a good idea. It keeps your code readable for both yourself and others as it gets more complex.
That's it!
We're done - we've got a control which works, and we can use it with very few lines of code in any game.
Your next assigment is to create a control which let's the user slide selections sideways Have fun!
Here's the final code:
public class VisualSelectionControl : DrawableGameComponent { #region Variables and properties //the position (top-left corner) of the menu public Vector2 Position { get; set; } //the spritebatch to draw to public SpriteBatch SpriteBatch { get; set; } //stores the entire menu in one big picture public Texture2D MenuTexture { get; private set; } //how many selectable items the texture should be split into public int NumberOfPossibleSelections { get; private set; } //Currently selected index private int _currentIndex; //stores the states of the keyboard private KeyboardState _currentKeyboardState, _previousKeyboardState; //store the source rectangle private Rectangle _sourceRectangle; //stores the height of one menu item private float _heightOfOneMenuItem; public int CurrentIndex { get { return _currentIndex; } //ensures only valid values are set set { if (value >= 0 &amp;&amp; value < NumberOfPossibleSelections) { //calculate the height of one menuitem int heightOfOneMenuItem = MenuTexture.Height / NumberOfPossibleSelections; _currentIndex = value; } } } #endregion #region Constructor //creates a VisualSelectionControl by first creating a DrawableGameComponent //takes a Game object, and passes it on to the superclass (DrawableGameComponent) public VisualSelectionControl(Game game, Vector2 position, SpriteBatch spriteBatch, Texture2D menuTexture, int numberOfPossibleSelections) : base(game) { //store parameters Position = position; SpriteBatch = spriteBatch; MenuTexture = menuTexture; NumberOfPossibleSelections = numberOfPossibleSelections; //calculate the height of one menuitem _heightOfOneMenuItem = MenuTexture.Height / NumberOfPossibleSelections; //create the source rectangle _sourceRectangle = new Rectangle(0, 0, MenuTexture.Width, (int)_heightOfOneMenuItem); } #endregion #region Draw and Update public override void Draw(GameTime gameTime) { //call the superclass' implementation of the method we're overriding base.Draw(gameTime); //draw to spritebatch, at control's position, from the sourcerectangle SpriteBatch.Draw(MenuTexture, Position, _sourceRectangle, Color.White); } public override void Update(GameTime gameTime) { //get the keyboard's state _currentKeyboardState = Keyboard.GetState(); //call the superclass' implementation of the method we're overriding base.Update(gameTime); //use input to change state if (_currentKeyboardState.IsKeyDown(Keys.Up) &amp;&amp; _previousKeyboardState.IsKeyUp(Keys.Up)) { MoveToPrevious(); } if (_currentKeyboardState.IsKeyDown(Keys.Down) &amp;&amp; _previousKeyboardState.IsKeyUp(Keys.Down)) { MoveToNext(); } MoveSourceRectangleTowardsDestination(); //store this Update()'s keyboard state for use in next Update _previousKeyboardState = _currentKeyboardState; } #endregion #region Helpermethods private void MoveSourceRectangleTowardsDestination() { //calculate where the sourcerectangle is supposed to be on the Y axis of the texture float sourceRectangleYDestination = _heightOfOneMenuItem * CurrentIndex; //get the difference between where it is now and where it is supposed to be float differenceOnYAxis = sourceRectangleYDestination - _sourceRectangle.Top; //calculate a thrirty percent movement float thirtyPercentMovement = differenceOnYAxis * .3f; //move the source rectangle _sourceRectangle.Offset(0, (int)thirtyPercentMovement); } public void MoveToPrevious() { CurrentIndex--; } public void MoveToNext() { CurrentIndex++; } #endregion }
And for the Game class:
public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; VisualSelectionControl _selectionControl; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); Texture2D _levelselectionTexture = Content.Load<Texture2D>("doom_difficulty"); _selectionControl = new VisualSelectionControl(this, Vector2.One * 100, spriteBatch, _levelselectionTexture, 5); Components.Add(_selectionControl); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.DarkRed); spriteBatch.Begin(); base.Draw(gameTime); spriteBatch.End(); } }
Class diagram
Here you can see how our VisualSelectionControl inherits the DrawableGameComponent, which in turn inherits the GameComponent
Source code for download
Here's the code