Skip to main content

Developing a Casual Game with Siverlight 2 – Part 4

Author: Joel Neubeck – Director of Technology / Silverlight MVP
Blog: http://joel.neubeck.net/

Expression Newsletter, subscribe now to get yours.

In this series of articles, we are exploring the process of designing and building a casual online game in Silverlight 2 (SL2). Throughout the series we target the interactive developer, as we construct our own version of the classic 1980s game “sabotage”. The premise of this game is quite simple; get as many points as possible by shooting down parachuters and helicopters before the enemy destroys your bunker.

Module 4: Animations and Sound

In our first three modules we constructed our framework, developed our game mechanics, and began to visualize how our spites looked within our game. In this fourth article, we will add some animations to illustrate our helicopter flying and our paratrooper falling, while attaching some sounds that trigger when bullets are fired and strike our target.

Animating on the “tick”?

We all know that an animation is nothing more than a visual effect. In Silverlight, and more importantly this game, we incorporate a few different techniques to animate game elements. The first you have already seen back in Module 2 and 3. Anytime we move an object across the screen were animate on the tick. Take, for example, the bullet fired from the gun. When the user clicks the left mouse button, we insert a bullet on to our display stack, and begin animating with a very specific velocity (direction and speed). Our movement is updated on the screen by attaching a handler to the CompositionTarget.Rendering event. Back in older version of SL we would have created a timer and “on tick” we would have moved our sprites. CompositionTarget.Rendering makes a lot more sense since it is fired just before each frame is rendered. One thing you should not forget to consider is Framerate. In a game like ours, we don’t need a lot of frames, so we can explicitly set a lower maximum frame rate, let's say 32 fps. With a game as simple as this, we should always get at least 32 fps. As such, our movement will have a fairly consistent speed. Now if we had not set a maximum frame rate, we might get as high as 60fps, causing the Rendering event to be fired nearly twice as often. The result would be a very inconsistent speed of movement. To compensate for this, we must adjust our speed based on the elapsed time between ticks.

Our falling paratrooper

To illustrate animating on the tick, let's take a look at our falling paratrooper. The animation of moving the paratrooper from the helicopter to the ground is done on each fire of the Rendering event. When our trooper is placed into the scene, we set a few properties on our sprites model to help controls the decent. The first is the starting position of the trooper; next is its direction (X=0, Y=1), while the last is the speed (1.25). Each of these properties is set on the model's constructor and used to define the velocity of our trooper.

Look Back: In Module 2 we learned that direction was calculated by determining the Sin (X) and Cosine (Y) of a specified angle in radians. Since our Paratrooper must obey gravity, the angle is 0 degrees or straight down.

To calculate our velocity we simply take our speed and multiply it times our direction.

Velocity = new Vector(speed * direction.X, speed * direction.Y);

On each rendering event, the trouper's Move method is called and the position of the trooper is updated by adding its current position to our calculated velocity. As our trooper approaches the ground, we create the effect of deceleration by changing our trooper’s velocity by maintaining the same direction, but decreasing its speed. Figure 1 show how this deceleration is calculated. To emphasize this deceleration we could also add an additional animation in the Paratrooper sprite that simulated the trooper pulling down on the parachute ropes to flare the chute. To add this effect, all we would need to do is create an additional event and fire it when we are within the 20 pixels of the ground.

Public new void Move(FrameworkElement container)
{
    if (this.Moving)
    {
        //as the trooper approaches the ground we want to decelerate
        if (Position.Y + Height >= container.Height - 20 & this.State != States.Falling)
        {
            Velocity = Vector.Multiply(base.Direction, (base.Speed/3));
        }
        Vector newPosition = CalculateNewPosition(container, true);
        UpdatePosition(newPosition);
        this.OnMoved();
    }
}

Figure 1 - Decrease trooper’s velocity

The Storyboard

Silverlight uses a property based animation model know as a Storyboard. This model is referred to as time-based, where you are modifying the value of a property over an interval of time. You set the initial state, the final state and the duration of time it should take to animate, and Silverlight will calculate the frame rate.

Helicopter Tail Rotor

Figure 2 - Helicopter Tail Rotor

The first animation we will take on is one of the simplest, the tail rotor. For this animation, all we want is the rotor to spin clockwise as our sprite is flying across the sky. The XAML in Figure 3 is what we created in Blend to achieve the affect.

<Storyboard x:Name="Tail" RepeatBehavior="Forever">
   <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
      Storyboard.TagetName="tail"
      Storyboard.TargetProperty="(UIElement.RenderTransform.
                                 (TransformGroup.Children[2].
                                 RotateTransform.Angle)">
      <SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
      <SplineDoubleKeyFrame KeyTime="00:00:00.1000000" Value="180"/>
      <SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="360"/>
   </DoubleAnimationUsingKeyFrames>
</Storyboard>

Figure 3 - Tail rotor XAML

Note that the “RepeatBehavior” of the Storyboard is set to “Forever”. This tells Silverlight to repeat the animation indefinitely.

Paratrooper

In most real casual games a single sprite can be comprised of numerous animations that correspond to different behaviors the sprite will exhibit. In our game or paratrooper is comprised of 7 different storyboards.

  • Descend – Used when the trooper is falling from the helicopter
  • DescendCanopy – Used when we want to break parachute from trooper.
  • DeployCanopy - Used when the trooper first exits the helicopter to deploy parachute.
  • ShotCanopy – When a bullet hits the canopy, it breaks off of trooper
  • Spin – When the canopy has been shot, the trooper begins to spin out of control as it falls to the ground
  • Explode – When a bullet strikes the body of the paratrooper, he explodes
  • Crash – Used when the paratrooper is spinning and impacts the ground

Of these behaviors, the most complicated animation is Explode. In this animation we create an exploding visualization by showing a series of stars that appear to start big in the center and move outward. Figure 4 shows a few of the important frames of this animation. One thing we did to make this animation a bit more dynamic was to have each star pulse. Within the Star UserControl we simply have a single storyboard that pulses the star from 100% opacity to 20%, then back to 100% again. In the code-behind on this UserControl, we attach to the Loaded Event and being the Pulse animation. Each time that our Paratroopers model fires a DeathCollision event, we play this animation within our view.

Figure 4 - Explode Sequence of frames

Sound

No game would be a game without some sounds. Sounds are super simple! In Silverlight 2, the way in which you attach and play a sound is by inserting a MediaElement into your display stack and setting its source equal to an audio file.  Silverlight supports multiple formats, but we tend to use either .mp3 or .wma. For this example they are embedded .wma.

In this game we have chosen to manage all of our sounds directly within the view in which the sound will be used. If a game has a ton of sounds that get used all over the place, then the developer might benefit from a single class that implements more of a factory pattern to access the sound. In that situation you may choose to use a Singleton to ensure the minimum number of instances of each sound are stored in memory. Since our game does a good job of reusing each sprite, we are certain that we will not have an excessive number of sounds being loaded into memory.

The sound of impact

Let's look at an example of implementing a sound. In the case of my Paratrooper, we have two sounds we would like to play. The first will be used when a bullet strikes the trooper and the second if the trooper crashes into the ground. To make this happen, the first thing we must do is instantiate both MediaElements in our view's constructor. Once they're instantiated, we can add each as a child of our LayoutRoot. Remembering back to module 2, you might recall that each time a bullet strikes our trooper, we fire a DeathCollision event. It is within this event handler that we will play our sound. Figure 5 shows the code from our Paratrooper view we will use to play our sounds. Note that we explicitly set the Position of each MediaElement to ensure that the sound starts playing at the beginning with no hesitation.

public Paratrooper(Models.Paratrooper model)
{
  InitializeComponent();
  . . . . .
   _model.DeathCollision += new Game.Models.CollisionHandler(_model_DeathCollision);
  . . . . .
  //instantiate sounds
  _bulletHitSound.Source = new Uri("../Sounds/bulletbody.wma", UriKind.Relative);
  _bulletHitSound.Volume = 1.0;
  _bulletHitSound.IsMuted = false;
  _bulletHitSound.AutoPlay = false;
  this.LayoutRoot.Children.Add(_bulletHitSound);
  _smooshSound.Source = new Uri("../Sounds/waterballoon.wma", UriKind.Relative);
  _smooshSound.Volume = 1.0;
  _smooshSound.IsMuted = false;
  _smooshSound.AutoPlay = false;
  this.LayoutRoot.Children.Add(_smooshSound);
}
void _model_DeathCollision(object sender, Game.Models.ISprite sprite)
{
    this.Descend.Stop();
    this.Explode.Begin();
    _bulletHitSound.Stop();
    _bulletHitSound.Position = TimeSpan.FromMilliseconds(0);
    _bulletHitSound.Play();
    this.Explode.Completed += new EventHandler(Explode_Completed);
}

Figure 5 - Paratrooper View

Conclusion

With these techniques you can create virtually every effect needed in a casual game. In the next article, we will focus on what it takes to initialize our game, download assets and deploy it to users in an easy to update way. Thank you and see you next time.

Joel Neubeck, Director of Technology, Terralever