WormRace Game using WPF and C#

Introduction

While leisurely browsing the net I came across a flash game called worm race game. As name suggests, the worm race is about a race between worms. The player is supposed to select one of the available worms and place a bet on it, then the race begins and if the winner is the worm the player selected then the player wins otherwise he loses. Somehow I felt it was a good game to learn animations in WPF by developing this game. So I attempted to write a similar game using animations in WPF.

At the top level of the design of this game are the following tasks:
  • Making a sprite for the worm
  • Animating the body of the worm to make it appear to be crawling
  • Making a sprite into a user control
  • Animating the worm from the start point to the end point
  • A panel to place bets
  • A notification window that shows what to do next and the results of the game
  • Logic to select worms and decide winners

In the process of developing this game, I used and learned the following concepts in WPF:

  • Making a UserConrol from a collection of controls
  • Adding new/custom properties to a usercontrol
  • Property animations and keyframeanimations in WPF
  • Control Templates
  • Styles

The game looks as in the following figure:

wormrace2.png
 
Making a sprite for the worm

Sprites could be made by taking bitmaps (raster based) or drawing them ourselves (vector based). While sprites based on bitmaps are more realistic, they are relatively harder to implement and consume more resources than simple vector-based drawings and I wanted to use property animations of WPF so I chose vector-based sprites, simply made of some circles.

 

worm.png
 

Animating the body of the worm to make it appear to be crawling
 
Now in the sprite we need to make circles 2 and 3 move slightly up and down. To do this we use keyframe animations provided by WPF.
 
KeyFrameAnimations 
 
If the property you are animating needs to go through a series of changes and you need to define the state of the property at each of those stages, then we need to use keyframeanimation. Keyframes are specific snapshots of your property at various times during the length of the animation. The following code shows the  animation required for the worm to appear to be crawling (body up position while crawling)
 

<DoubleAnimationUsingKeyFramesStoryboard.TargetProperty=
"(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)"

Storyboard.TargetName="ellipse">

               <EasingDoubleKeyFrameKeyTime="0"Value="0"/>

               <EasingDoubleKeyFrameKeyTime="0:0:1"Value="-1"/>

           </DoubleAnimationUsingKeyFrames>

           <DoubleAnimationUsingKeyFramesStoryboard.TargetProperty=
"(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.Y)"
 

 

Storyboard.TargetName="ellipse">

               <EasingDoubleKeyFrameKeyTime="0"Value="0"/>

               <EasingDoubleKeyFrameKeyTime="0:0:1"Value="-13"/>

           </DoubleAnimationUsingKeyFrames>


 

worman.png
 

 


Here the circle moves back by 1 unit (TranslateTransform.x) in a second and moves back by 13 units (TranslateTrasform.Y) in 1 second. We animate the circles up and down from their current position (wherever it may be) using a translate transform, instead of a property based animation, that we can use if we know the "From" value of the property and the "To" value is fixed. We will see that in the other animation section.

4. Making a sprite into a usercontrol
Now we need at least two worms for a race and four to make it look better. If we were to write XAML for each one, we will have same XAML repeated all over the source code, so we need some reusable code/datatype/class from which we can create various instances of a worm and also set the color of each worm. There are several ways to do this; it can be class based on frameworkelement or shape or it could be made into a usercontrol. Usercontrols are extremely popular in WPF. UserControl derives from ContentControl so the visual tree defining the control is generally defined in XAML as the control's Content property. The codebehind file might define a couple more custom properties and handles events and interactions. In other words it is an easy way of taking a collection of existing controls and bundling them together as a new resusable single control.
 
A Worm user control is made of a transparent border, four ellipses for a body, two small ellipses for eyes and a storyboard for animating the body. Storyboard is a way to wrap animations in XAML and allows interactive controls over animation (we can start, stop and pause animations using a storyboard). The XAML used for the Worm usercontrol is in worm.xaml.
 
Adding custom properties to the usercontrol
 
Now we need to have a property to specify the color of the worm once and it should change the fill color of all the four ellipses that make the body of the worm. So we need to add a custom property called "WormColor" to this user control. this involves three steps.
 
A. Registering a dependency property
 
The following will register a dependency property:

public static readonly DependencyProperty WormColorProperty = DependencyProperty.Register("WormColor", typeof(Brush), typeof(Worm), new
       FrameworkPropertyMetadata(new PropertyChangedCallback(OnWormColorChanged)));
 

By convention, the DependencyProperty is named <PropertyName>Property; in this case the property name is wormcolor so the dependency property becomes "WormColorProperty". To create a new dependency property we must call the "Register" method with the following arguments:
  1. The Name of the property: "WormColor"
  2. Type of data held by the property: typeof(Brush)
  3. The Name of the class registering the property: typeof(Worm)
  4. Additional things about the property, including a callback: FrameworkPropertyMetadata(new PropertyChangedCallback(OnWormColorChanged))

Even though all we want is to register a callback that needs to be called when the property value changes, we must do it using a "FrameworkPropertyMetadata" instance only and there is no direct way of registering it.

B. Implementing dependency property

public Brush WormColor

{

   get { return (Brush)GetValue(WormColorProperty); }

   set { SetValue(WormColorProperty,value); }

}


The GetValue and SetValue references the dictionary of property values held by the object. The key is the dependency property we registered 
(WormColorProperty) and is a combination of name and the behavior associated with the property. These properties should exist even if there are no objects and all objects of the usercontrol class should see this property so they always need to be static. 
 
C. Implementing the callback
 

   private static void OnWormColorChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)

    {

        Worm wrm = (Worm)sender;

        Brush b = wrm.WormColor;

        wrm.ellipse.Fill = b;

        wrm.ellipse1.Fill = b;

        wrm.ellipse3.Fill = b;

        wrm.ellipse4.Fill = b;

    }

 
OnWormColorChanged is the handler that is called whenever the "wormcolor" property changes, this must be static because the dependency property is static.
 
And the object that invoked this callback can be obtained by casting "sender" (Worm wrm = (Worm)sender). Once you have an instance, filling the ellipses to change the color of the worm is trivial.
 
Now we have the user control ready. We now can easily create various instances of the worm, as in:
 
<local:Worm x:Name="YellowWorm" WormColor="Yellow" />
<local:Worm x:Name="RedWorm" WormColor="Red" />
<local:Worm x:Name="WoodWorm" WormColor="BurlyWood"/>
<local:Worm x:Name="GreenWorm"/>

 
fourworms.png
 
5. Animating a worm from start point to end point
 
The Main layout chosen for this game is a grid and various worms are placed in various rows of a grid, however to move a worm from one place to another it is much more convenient to have a layout with absolute positioning like a canvas, so we wrap it in a canvas as in the following:
 

<CanvasGrid.Row="1">

<local:Wormx:Name="YellowWorm"WormColor="Yellow"/>

</Canvas>


Once we have a canvas, we can use simple property-based animation to move the worm from start the point to the end point.

Property-based animation
 
Property-based animation provides a way to directly modify the value of a dependency property over an interval of time. For example, to make a drawing of a rectangle fade out of view, you can modify its Opacity property in an animation. To make it grow or shrink, you can modify its Width and Height properties. so creating the animation you want becomes a matter of determining the properties you need to modify.
 
For a property to have animation capabilities, it must meet the following requirements:
  • It must be a dependency property.
  • It must belong to a class that inherits from DependencyObject and implements the IAnimatable interface.
  • There must be a compatible animation data type available.

This means that to animate a dependency property, you need to have an animation class that supports its data type. For example, the Width property of a rectangle uses the double data type. To animate it, you use the DoubleAnimation class. However, the Color property of a SolidColorBrush object uses the color structure, so it requires the ColorAnimation class. If WPF doesn't provide the data type you want to use, you can create your own animation class for that data type.


Property-based animation and keyframe animation differ in the way they generate intermediate values of a property during a selected time interval. Property-based animation takes "From" and "To" values and uses linear interpolation to generate intermediate values and is smooth. On the other hand key frame animations change property values abruptly at specified times, and are often used when changing certain data types, such as a string.

Using property animation to move a worm

We need to move a worm horizontally along a canvas, so we need to change the "Canvas.Left" property of the worm from 0 to 750 (could be anything) over some random interval of time. "Canvas.Left" is a Double so we need to use "DoubleAnimation". The following code moves a worm from 0 to 750 somewhere between 10 and 15 seconds. 

da =new DoubleAnimation(0, 750,TimeSpan.FromSeconds(rnd.Next(10, 15) % 15));

da.AccelerationRatio = rnd.NextDouble();

da.Completed += da_Completed;

da.Name ="YellowWorm";

YellowWorm.BeginAnimation(Canvas.LeftProperty, da);
 

There are several important properties of propertyanimation; they are:
  • Duration: Sets the length of time the animation runs, from start to finish, as a Duration object (TimeSpan.FromSeconds(rnd.Next(10, 15) % 15))
  • SpeedRatio: Increases or decreases the speed of the animation. The default value is 1
  • AccelerationRatio and DecelerationRatio: Makes an animation nonlinear, so it starts off slow and then speeds up (by increasing AccelerationRatio) or slows down at the end (by increasing the DecelerationRatio). Both values are set from 0 to 1
  • AutoReverse: If this is set to true, the animation will play out in reverse once it is complete, reversing to the original value
  • FillBehavior: Determines what happens when the animation finishes. Usually, it keeps the property fixed at the ending value
  • RepeatBehavior: Allows you to repeat an animation a specific number of times.

Of these we have used only AccelerationRatio so that worms show some random behavior during the course of race. In other words a worm may start fast initially bit slow down after a while, which makes the race a little more interesting.
 

6. Panel to place bets

The top panel of the grid is reserved for the start and the row of buttons to place various amounts of bets. Round buttons look better than rectangles (inspired from gold coins). We can make buttons round by changing the control template of the button and using a style to apply it for all buttons. We can also use triggers to generate some animations when the mouse hovers on buttons and the user clicks on it.
 

The XAML looks like the following and is explained in the other article: http://www.c-sharpcorner.com/UploadFile/52f7b2/9534/
 

<Style TargetType="Button">

    <Setter Property="Template">

        <Setter.Value>

            <ControlTemplate TargetType="Button">

                <Grid>

                    <!--Border BorderThickness="4" BorderBrush="DarkGray" CornerRadius="10"-->

                    <Rectangle x:Name="mainButton" Fill="{TemplateBinding Background}" Stroke="DarkGray" StrokeThickness="5" RadiusX="25" RadiusY="25">

                    </Rectangle>

                    <!--/Border-->

                    <ContentPresenter Margin="2" HorizontalAlignment="Center" VerticalAlignment="Center" />

                    <Rectangle x:Name="buttonHoverButton" Opacity="0" RadiusX="25" RadiusY="25">

                        <Rectangle.Fill>

                            <RadialGradientBrush>

                                <GradientStop Color="Transparent" Offset="1"/>

                                <GradientStop Color="Indigo" Offset="0.9"/>

                                <GradientStop Color="Blue" Offset="0.8"/>

                                <GradientStop Color="LawnGreen" Offset="0.7"/>

                                <GradientStop Color="Yellow" Offset="0.6"/>

                                <GradientStop Color="Orange" Offset="0.5"/>

                                <GradientStop Color="Violet" Offset="0.4"/>

                                <GradientStop Color="Transparent" Offset="0"/>

                            </RadialGradientBrush>

                        </Rectangle.Fill>

                    </Rectangle>

                </Grid>

                <ControlTemplate.Triggers>

                    <Trigger Property="Button.IsMouseOver" Value="True">

                        <Setter Property="BitmapEffect">

                            <Setter.Value>

                                <OuterGlowBitmapEffect GlowColor="Violet" GlowSize="10"/>

                            </Setter.Value>

                        </Setter>

                    </Trigger>

                    <Trigger Property="Button.IsPressed" Value="True" >

                        <Setter TargetName="mainButton" Property="Fill" Value="Gold"/>

                    </Trigger>

                    <EventTrigger RoutedEvent="Button.MouseEnter">

                        <EventTrigger.Actions>

                            <BeginStoryboard>

                                <Storyboard>

                                    <DoubleAnimation Storyboard.TargetName="buttonHoverButton" Storyboard.TargetProperty="Opacity" To="1"

Duration="0:0:0.25" />

                                </Storyboard>

                            </BeginStoryboard>

                        </EventTrigger.Actions>

                    </EventTrigger>

                    <EventTrigger RoutedEvent="Button.MouseLeave">

                        <EventTrigger.Actions>

                            <BeginStoryboard>

                                <Storyboard>

                                    <DoubleAnimation Storyboard.TargetName="buttonHoverButton"

Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:0.5"/>

                                </Storyboard>

                            </BeginStoryboard>

                        </EventTrigger.Actions>

                    </EventTrigger>

                </ControlTemplate.Triggers>

            </ControlTemplate>

        </Setter.Value>

    </Setter>

</Style>

Logic to select worms and decide winners

Selecting Worm

We need to handle various mouse-related events of the worm for selecting a worm; they are:

  • MouseEnter: this handler is invoked when the user hovers over a worm and here we make the transparent border of the worm visible so the highlighting rectangle appears around the worm and the details related to the worm will appear in the notification window.
  • MouseLeave: it clears the border.
  • MouseLeft Down : this will note the selected worm and moves the game state from "selectworm" to "selectbet" so that the user can place a bet.
Similarly mouse events on buttons record the bet amount.
 
The Game uses a simple state machine to track various states (SelectWorm, SelectBet, ReadyToStart, RaceInProgress and RaceEnd ) explained below, using a switch case and generates the appropriate messages in the Notification window (TextBox) below the game.
 
notif.png
 
Most of the code is self-explanatory.
 
Conclusion

WPF makes animation very easy to implement by abstracting several internal details like threading, timers etc.. that are hard to implement. It is almost as good as scripting (especially for people coming from a C++ background). Finally I will conclude with instructions of how to play the game, which is:
  1. hover the mouse over worms to get details of worms
  2. select the worm
  3. select the amount
  4. click start
  5. on completion of a race you will see results in the notification window
  6. start changes to Reset
  7. click on Reset to play another race

Any out of order actions generate error messages. I hope this article helps in getting an overview of WPF animations and provides some enjoyment while playing the game.