Blog

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

Nov 30

Written by:
Wednesday, November 30, 2011  RssIcon

It is time to make the game menu built in the previous two blog posts (Part 1, Part 2) more dynamic. Currently all screens are static. When another game screen is opened the change is instant – there is no transition phase between screens.

This is what we want to achieve:

The DigitalRune Animation library provides an animation system for animating objects. In Part 1 and Part 2 the animation system has already been set up and is ready to be used.

Animations can be applied to objects that implement the interface IAnimatableObject and properties that implement IAnimatableProperty<T>. The UI controls, used for rendering the screens, already implement the required interfaces.

Animating the Start Text

First, we want to improve the “Start” screen. When the loading process is complete, the text “Loading…” changes to “Press Start button”. We can animate the text to draw the player’s attention to it. The easiest way is to animate the opacity of the TextBlock.

/// <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);

  // The text should pulse to indicate that a user interaction is required.
  // To achieve this we can animate the opacity of the TextBlock.
  var opacityAnimation = new SingleFromToByAnimation
  {
    From = 1,                             // Animate from opaque (Opacity == 1)
    To = 0.25f,                           // to nearly transparent (Opacity == 0.25)
    Duration = TimeSpan.FromSeconds(0.5), // over a duration of 0.5 seconds.
    EasingFunction = new SineEase { Mode = EasingMode.EaseInOut }
  };

  // A SingleFromToByAnimation plays only once, but the animation should be 
  // played back-and-forth until the user presses a button.
  // We need to wrap the SingleFromToByAnimation in an AnimationClip or TimelineClip.
  // Animation clips can be used to cut and loop other animations.
  var loopingOpacityAnimation = new AnimationClip<float>(opacityAnimation)
  {
    LoopBehavior = LoopBehavior.Oscillate,  // Play back-and-forth.
    Duration = TimeSpan.MaxValue            // Loop forever.
  };

  // We want to apply the animation to the "Opacity" property of the TextBlock.
  // All "game object properties" of a UIControl can be made "animatable".      
  // First, get a handle to the "Opacity" property.
  var opacityProperty = _startTextBlock.Properties.Get<float>(TextBlock.OpacityPropertyId);

  // Then cast the "Opacity" property to an IAnimatableProperty. 
  var animatableOpacityProperty = opacityProperty.AsAnimatable();

  // Start the pulse animation.
  var animationController = _animationService.StartAnimation(loopingOpacityAnimation, animatableOpacityProperty);

  // Enable "automatic recycling". This step is optional. It ensures that the
  // associated resources are recycled when either the animation is stopped or
  // the target object (the TextBlock) is garbage collected.
  // (The associated resources will be reused by future animations, which will
  // reduce the number of required memory allocations at runtime.)
  animationController.AutoRecycle();
}

Playing a Fade-Out Animation

Once the player has pressed the Start button, we want to smoothly fade-out the TextBlock. Instead of triggering the transition from the “Start” screen to the “Menu” screen right away, we first play the fade-out animation.

This is the new update method of the “Start” screen:

private bool _exitAnimationIsPlaying;                 // true if fade-out animation is playing.
private AnimationController _exitAnimationController; // Controls the fade-out animation.

/// <summary>
/// Called every frame when "Start" state is active.
/// </summary>
private void OnUpdateStartScreen(object sender, StateEventArgs eventArgs)
{
  if (_exitAnimationIsPlaying)
    return;

  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)
  {
    // Play a fade-out animation which changes the opacity from its current value to 0.
    var fadeOutAnimation = new SingleFromToByAnimation
    {
      To = 0,                               // Animate the opacity from the current value to 0
      Duration = TimeSpan.FromSeconds(0.5), // over a duration of 0.5 seconds.
    };
    var opacityProperty = _startTextBlock.Properties.Get<float>(TextBlock.OpacityPropertyId).AsAnimatable();
    _exitAnimationController = _animationService.StartAnimation(fadeOutAnimation, opacityProperty);

    // When the fade-out animation finished trigger the transition from the "Start" 
    // screen to the "Menu" screen.
    _exitAnimationController.Completed += (s, e) => _stateMachine.States.ActiveState.Transitions["StartToMenu"].Fire();

    _exitAnimationIsPlaying = true;
  }
}

When we exit the “Start” state, we can stop the animation:

/// <summary>
/// Called when "Start" state is exited.
/// </summary>
private void OnExitStartScreen(object sender, StateEventArgs eventArgs)
{
  // Clean up.
  _exitAnimationController.Stop();
  _exitAnimationController.Recycle();
  _exitAnimationIsPlaying = false;

  _screen.Children.Remove(_startTextBlock);
  _startTextBlock = null;
}

Animating the Menu Items

The menu items should dynamically slide in from the left. To achieve this effect we can animate the opacity and the RenderTranslation of the Button controls. The RenderTranslation is an offset which is only applied during rendering. (It does not affect the layout of the controls on the screen.)

The same animation effect can be applied to the “SubMenu” screen, therefore we put the code in a general helper method:

/// <summary>
/// Called when "Menu" state is entered.
/// </summary>
private void OnEnterMenuScreen(object sender, StateEventArgs eventArgs)
{
  // Show a main menu consisting of several buttons.
  ...

  // Slide the buttons (contained in the stack panel) in from the left to make 
  // the screen look more dynamic.
  AnimateFrom(stackPanel.Children, 0, new Vector2F(-300, 0));
}

/// <summary>
/// Animates the opacity and offset of a group of controls from the specified value to their 
/// current value.
/// </summary>
/// <param name="controls">The UI controls to be animated.</param>
/// <param name="opacity">The initial opacity.</param>
/// <param name="offset">The initial offset.</param>
private void AnimateFrom(IList<UIControl> controls, float opacity, Vector2F offset)
{
  TimeSpan duration = TimeSpan.FromSeconds(0.8);

  // First, let's define the animation that is going to be applied to a control.
  // Animate the "Opacity" from the specified value to its current value.
  var opacityAnimation = new SingleFromToByAnimation
  {
    TargetProperty = "Opacity",
    From = opacity,
    Duration = duration,
    EasingFunction = new CubicEase { Mode = EasingMode.EaseOut },
  };

  // Animate the "RenderTranslation" property from the specified offset to 
  // its current value, which is usually (0, 0).
  var offsetAnimation = new Vector2FFromToByAnimation
  {
    TargetProperty = "RenderTranslation",
    From = offset,
    Duration = duration,
    EasingFunction = new CubicEase { Mode = EasingMode.EaseOut },
  };

  // Group the opacity and offset animation together using a TimelineGroup.
  var timelineGroup = new TimelineGroup();
  timelineGroup.Add(opacityAnimation);
  timelineGroup.Add(offsetAnimation);

  // Run the animation on each control using a slight delay to give the first controls
  // a slight head start.
  var numberOfControls = controls.Count;
  for (int i = 0; i < controls.Count; i++)
  {
    var clip = new TimelineClip(timelineGroup)
    {
      Delay = TimeSpan.FromSeconds(-0.04 * (numberOfControls - i)),
      FillBehavior = FillBehavior.Stop,   // Stop and remove the animation when it is done.
    };
    var animationController = _animationService.StartAnimation(clip, controls[i]);

    // Enable "auto-recycling" to ensure that the animation resources are recycled once
    // the animation stops or the target objects are garbage collected.
    animationController.AutoRecycle();
  }
}

Previously, in the “Start” screen, we have applied the animation directly to the opacity property. In this case we are animating two properties at once. The opacity animation and the position animation are grouped together and the combined animation is applied to the target object. The animation system takes care of the rest and applies the animations to the correct UI control properties.

And so forth…

The remaining animation and screens are similar, so I will skip any further explanations.

This article concludes the three part series. I tried to keep explanations brief and I am aware that I skipped some of the details. I recommend you check out the sample, if you haven’t already. The source code contains a lot of additional comments. It is included in the download package of the DigitalRune Engine (see folder <InstallationFolder>\Samples\DigitalRune.Game.UI\GameStatesSample).

My original goal, when initially writing the sample, was to introduce the StateMachine offered by DigitalRune Game. In Part 2 we have used the state machine to separate the logic of different game screens. But this actually only scratches the surface. The state machine has a lot more to offer, such as nested states, parallel states, timed transitions, etc. I am curious whether you will find it helpful.


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