Blog

How to Build an Animated, State-Based Game Menu in XNA (Part 2)

Nov 29

Written by:
Tuesday, November 29, 2011  RssIcon

This is the second part of the step-by-step tutorial for building a simple game menu in XNA. Previously we have set up the project, the game loop, and added the UI theme. Now it’s time to implement the game logic.

As a reminder, here is what the final application looks like:

I have to admit, it is not very exciting – but it is still a good example to demonstrate some of the basics. The final source code is included in the DigitalRune Engine package (see folder <InstallationFolder>\Samples\DigitalRune.Game.UI\GameStatesSample).

Preparing UI Rendering

The MainComponent, created in the first part, will manage the different screens and render the UI using DigitalRune Game UI.

Here is the setup required for rendering a UI screen:

public class MainComponent : DrawableGameComponent
{
  // Services used by this game component.
  private readonly IInputService _inputService;
  private readonly IUIService _uiService;
  private readonly IAnimationService _animationService;

  // The UI screen renders our controls, such as text labels, buttons, etc.
  private UIScreen _screen;

  public MainComponent(Game game)
    : base(game)
  {
    // Get the required services from the game's service provider.
    _inputService = (IInputService)game.Services.GetService(typeof(IInputService));
    _uiService = (IUIService)game.Services.GetService(typeof(IUIService));
    _animationService = (IAnimationService)game.Services.GetService(typeof(IAnimationService));
  }

  protected override void LoadContent()
  {
    // Load a UI theme, which defines the appearance and default values of UI controls.
    var theme = Game.Content.Load<Theme>("Theme");

    // Create a UI renderer, which uses the theme info to renderer UI controls.
    var renderer = new UIRenderer(Game, theme);

    // Create a UIScreen and add it to the UI service. The screen is the root of 
    // the tree of UI controls. Each screen can have its own renderer. 
    _screen = new UIScreen("Default", renderer)
    {
      // Make the screen transparent.
      Background = new Color(0, 0, 0, 0),
    };

    // Add the screen to the UI service.
    _uiService.Screens.Add(_screen);

    base.LoadContent();
  }

  public override void Draw(GameTime gameTime)
  {
    // Clear background.
    GraphicsDevice.Clear(new Color(50, 50, 50));

    // Draw the UI screen. 
    _screen.Draw(gameTime);
  }
}

So far only a blank screen is rendered because no controls have been added to the screen. (The animation service is not yet used, but it will be relevant in part 3.)

Managing States using a State-Machine

Before we implement the different game screens, let’s think about how to control the current state of the game. The game has the following states:

GameStatesSample

  • Loading … The application is starting and assets are loading in the background.
  • Start … Loading has finished, the game is waiting for the user to press the Start button.
  • Menu … The main menu.
  • SubMenu … A dummy sub-menu.
  • Game … A placeholder for the actual gameplay.

This state chart is rather simple and should not be difficult to implement. But in more complex games it can be pretty challenging to keep track of all possible states. So, what is the best way to manage the states of a game? There is clearly no one right answer to this question.

DigitalRune Game provides one possible solution: a StateMachine. This helper class is a general implementation of the UML state machine view. Whether the states represent different game screens, different states of a character AI, etc., the state machine offers one way to organize states and manage transitions between them.

The StateMachine contains States and Transitions. A state provides three events: Enter is raised when the state is entered, Update is called once per frame when the state is active, and Exit is raised when the state is left.

A Transition represents the change from one state to another. Transitions can be triggered by calling the method Fire().

First, the state machine needs to be created and when the game component is active the state machine needs to be updated once per frame. Add the following code to the MainComponent.

private StateMachine _stateMachine;

public override void Initialize()
{
  _stateMachine = new StateMachine();
}

public override void Update(GameTime gameTime)
{
  _stateMachine.Update(gameTime.ElapsedGameTime);
}

Now it is time to implement the individual states.

The Loading Screen

The “Loading” state is the initial state. Let’s add the state to the state machine in MainComponent.Initialize():

public override void Initialize()
{
  _stateMachine = new StateMachine();

  var loadingState = new State { Name = "Loading" };
  loadingState.Enter += OnEnterLoadingScreen;
  loadingState.Exit += OnExitLoadingScreen;

  _stateMachine.States.Add(loadingState);
}

Here is the logic of the “Loading” screen:

private TextBlock _loadingTextBlock;  // Shows the text "Loading...".

/// <summary>
/// Called when "Loading" state is entered.
/// </summary>
private void OnEnterLoadingScreen(object sender, StateEventArgs eventArgs)
{
  // Show the text "Loading..." centered on the screen.
  _loadingTextBlock = new TextBlock
  {
    Name = "LoadingTextBlock",    // Control names are optional - but very helpful for debugging!
    Text = "Loading...",
    HorizontalAlignment = HorizontalAlignment.Center,
    VerticalAlignment = VerticalAlignment.Center,
  };
  _screen.Children.Add(_loadingTextBlock);

  // Start loading assets in the background.
  Parallel.StartBackground(LoadAssets);
}

/// <summary>
/// Loads all required assets.
/// </summary>
private void LoadAssets()
{
  // To simulate a loading process we simply wait for 2 seconds.
  Thread.Sleep(TimeSpan.FromSeconds(2));
}

/// <summary>
/// Called when "Loading" state is exited.
/// </summary>
private void OnExitLoadingScreen(object sender, StateEventArgs eventArgs)
{
  // Clean up.
  _screen.Children.Remove(_loadingTextBlock);
  _loadingTextBlock = null;
}

When the state is entered, i.e. when the game starts, the text “Loading…” is displayed and a background process is started which loads all required assets.

But so far the state never changes. Let’s see how we can switch from one state to another.

In Initalize() add the next state and define a transition between the two states.

public override void Initialize()
{
  ...

  var startState = new State { Name = "Start" };
  _stateMachine.States.Add(startState);
    
  var loadingToStartTransition = new Transition
  {
    Name = "LoadingToStart", 
    TargetState = startState,
  };
  loadingState.Transitions.Add(loadingToStartTransition);  
}

The newly created transition can be used to change from the “Loading” state to the “Start” state. The transition can be triggered by calling:

loadingToStartTransition.Fire();

Or, if we are currently in the “Loading” state, we can write:

_stateMachine.States.ActiveState.Transitions["LoadingToStart"].Fire();

However, in this case the transition should automatically trigger when the background loading process finishes. We cannot call Fire() in the LoadAssets() method because the method runs asynchronously in a background thread. The simplest way to signal the end of the loading process is to set a flag in LoadAssets().

private volatile bool _allAssetsLoaded;

private void LoadAssets()
{
  // To simulate a loading process we simply wait for 2 seconds.
  Thread.Sleep(TimeSpan.FromSeconds(2));
  _allAssetsLoaded = true;
}

Transitions can be configured to fire automatically if a certain condition is fulfilled – which is exactly what we need in this case. Add the following parameters to the transition:

public override void Initialize()
{
  ...
  
  var loadingToStartTransition = new Transition
  {
    Name = "LoadingToStart", 
    TargetState = startState,
    FireAlways = true,
    Guard = () => _allAssetsLoaded,
  };
  loadingState.Transitions.Add(loadingToStartTransition);  
}

The Guard is the condition that needs to be fulfilled to enable the transition and FireAlways tells the transition to trigger automatically. So as soon as the background thread sets the flag, the game changes from “Loading” to “Start”.

The Start Screen

The purpose of the Start screen is to decide which gamepad should be assigned to the first player. To do this we prompt the user to press the Start button.

Let’s register the required event handlers for the “Start” state, add the next state (“Menu”) and define a transition between the two states.

public override void Initialize()
{
  ...

  var startState = new State { Name = "Start" };
  startState.Enter += OnEnterStartScreen;
  startState.Update += OnUpdateStartScreen;
  startState.Exit += OnExitStartScreen;
  _stateMachine.States.Add(startState);

  var menuState = new State { Name = "Menu" };
  _stateMachine.States.Add(menuState);
    
  var startToMenuTransition = new Transition
  {
    Name = "StartToMenu",
    TargetState = menuState,
  };
  startState.Transitions.Add(startToMenuTransition);
}

Now that everything is wired up, we can implement the “Start” screen:

private TextBlock _startTextBlock;  // Shows the text "Press Start button".

/// <summary>
/// Called when "Start" state is entered.
/// </summary>
private void OnEnterStartScreen(object sender, StateEventArgs eventArgs)
{
  // Show the "Press Start button" text centered on the screen.
  _startTextBlock = new TextBlock
  {
    Name = "StartTextBlock",
    Text = "Press Start button",
    HorizontalAlignment = HorizontalAlignment.Center,
    VerticalAlignment = VerticalAlignment.Center,
  };
  _screen.Children.Add(_startTextBlock);
}

/// <summary>
/// Called every frame when "Start" state is active.
/// </summary>
private void OnUpdateStartScreen(object sender, StateEventArgs eventArgs)
{
  bool transitionToMenu = false;

  // Check if the user presses A or START on any connected gamepad.
  for (var controller = PlayerIndex.One; controller <= PlayerIndex.Four; controller++)
  {
    if (_inputService.IsDown(Buttons.A, controller) || _inputService.IsDown(Buttons.Start, controller))
    {
      // A or START was pressed. Assign this controller to the first "logical player".
      _inputService.SetLogicalPlayer(LogicalPlayerIndex.One, controller);
      transitionToMenu = true;
    }
  }

  if (_inputService.IsDown(MouseButtons.Left)
      || _inputService.IsDown(Keys.Enter)
      || _inputService.IsDown(Keys.Escape)
      || _inputService.IsDown(Keys.Space))
  {
    // The users has pressed the left mouse button or a key on the keyboard. 
    if (!_inputService.GetLogicalPlayer(LogicalPlayerIndex.One).HasValue)
    {
      // No controller has been assigned to the first "logical player". Maybe 
      // there is no gamepad connected.
      // --> Just guess which controller is the primary player and continue.
      _inputService.SetLogicalPlayer(LogicalPlayerIndex.One, PlayerIndex.One);
    }

    transitionToMenu = true;
  }

  if (transitionToMenu)
  {
    _stateMachine.States.ActiveState.Transitions["StartToMenu"].Fire();
  }
}

/// <summary>
/// Called when "Start" state is exited.
/// </summary>
private void OnExitStartScreen(object sender, StateEventArgs eventArgs)
{
  // Clean up.
  _screen.Children.Remove(_startTextBlock);
  _startTextBlock = null;
}

The Menu Screen

The “Menu” screen shows the main menu and presents a few options for the player to choose from. A lot of this is similar to the previous screens, therefore I will skip the repetitive parts and focus on the interesting stuff.

The menu is a Window with a vertical StackPanel containing the menu items (Button controls):

private void OnEnterMenuScreen(object sender, StateEventArgs eventArgs)
{
  // Show a main menu consisting of several buttons.

  // The user should be able to select individual buttons by using the 
  // D-pad on the gamepad or the arrow keys. Therefore we need to create
  // a Window. A Window manages the currently selected ("focused") control 
  // and automatically handles focus movement.
  // In this example the Window is invisible (no chrome) and stretches across 
  // the entire screen.
  _menuWindow = new Window
  {
    HorizontalAlignment = HorizontalAlignment.Stretch,
    VerticalAlignment = VerticalAlignment.Stretch,
  };
  _screen.Children.Add(_menuWindow);

  // The content of the Window is a vertical StackPanel containing several buttons.
  var stackPanel = new StackPanel
  {
    Orientation = Orientation.Vertical,
    HorizontalAlignment = HorizontalAlignment.Left,
    VerticalAlignment = VerticalAlignment.Bottom,
    Margin = new Vector4F(150, 0, 0, 200)
  };
  _menuWindow.Content = stackPanel;

  // The "Start" button starts the "Game" state.
  var startButton = new Button
  {
    Name = "StartButton",
    Content = new TextBlock { Text = "Start" },
    FocusWhenMouseOver = true,
  };
  startButton.Click += OnStartButtonClicked;

  // The buttons "Sub menu 1" and "Sub menu 2" show a dummy sub-menu.
  var subMenu1Button = new Button
  {
    Name = "SubMenu1Button",
    Content = new TextBlock { Text = "Sub-menu 1" },
    FocusWhenMouseOver = true,
  };
  subMenu1Button.Click += OnSubMenuButtonClicked;

  var subMenu2Button = new Button
  {
    Name = "SubMenu2Button",
    Content = new TextBlock { Text = "Sub-menu 2" },
    FocusWhenMouseOver = true,
  };
  subMenu2Button.Click += OnSubMenuButtonClicked;

  // The "Exit" button closes the application.
  var exitButton = new Button
  {
    Name = "ExitButton",
    Content = new TextBlock { Text = "Exit" },
    FocusWhenMouseOver = true,
  };
  exitButton.Click += OnExitButtonClicked;

  stackPanel.Children.Add(startButton);
  stackPanel.Children.Add(subMenu1Button);
  stackPanel.Children.Add(subMenu2Button);
  stackPanel.Children.Add(exitButton);

  // By default, the first button should be selected.
  startButton.Focus();
}

The buttons have a Click event, which occurs when the user activates a button by pressing A on the gamepad, ENTER or SPACE on the keyboard, or clicks the control with the left mouse button. The event handlers simply trigger a transition when executed. The event handler of the “Exit” button calls Game.Exit() to terminate the application.

The Game Screen and the SubMenu Screen…

The remaining screens are similar, so I will omit the implementation for the sake of brevity. For all details check out the full source of the final application.

Next up: Dynamic Transitions using Animations

In the next (last) article in this series we will add animations to make the UI more appealing.


Your name:
Gravatar Preview
Your email:
(Optional) Email used only to show Gravatar.
Your website:
Title:
Comment:
Security Code
CAPTCHA image
Enter the code shown above in the box below
Add Comment   Cancel 

Article Collection

A collection of the most useful blog articles can be found here:

Article Collection
(on Documentation page
)