Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Build a WPF Interactive Animation Application

0.00/5 (No votes)
19 Jul 2010 1  
An intermediate to advanced article on Building Animations via the WPF Storyboard class

Introduction

This article describes how to make a basic game – a window where bomb user controls drop from the top – to prompt the user to intercept them. To do this, we are going to use Expression Blend 4, as anyone who has ever used this developer tool knows that the Design space is easily switched to an Animation space. WPF applications are sometimes easier to build using Expression Blend. Todd Miranda has a plethora of educational videos about how to use Expression Blend, found at expression.microsoft.com/en-us/cc197141.aspx. I am writing this article to show both the importance of C# in two code-behind files: one for the Main Window, and the other for the bomb, which is a UserControl an added new item. If you are unfamiliar with how this application derives its output, do not be intimidated. Expression Blend 4 functions as a superior vector-based design tool used to gain a stronger hold on both of the Windows Presentation Foundation technology and Silverlight.

Here is a look at the WPF application. Many beginners, me included, develop WPF applications to implement the powerful graphics, animations, and media facilities. This application serves as an amusing little game, or it can serve as a skeleton application for the reader to expand on. So, in my limited knowledge, here is a picture of the Bomb Dropper:

Capture.JPG

Notice the text that states that a bomb is dropped every 1.2 seconds. Each bomb takes 3.4 seconds to fall. You can intercept them by clicking on them. It isn’t difficult to create, configure, and launch an animation programmatically. You just need to create the animation and storyboard objects, add the animations to the storyboard, and start the storyboard. You can perform any cleanup work after your animation ends by reacting to the Storyboard.Completed vent. In this example, every dropped bomb has its own storyboard with two animations. The first animation drops the bomb (by animating the Canvas.Top roperty), and the second animation rotates the bomb slightly back and forth, giving it a realistic wiggle effect. If the user clicks a bomb, these animations are halted and two more take place, to send the bomb careening harmlessly off the side of the Canvas Finally, every time an animation ends, the application checks to see if it represents a bomb that fell down or one that was saved, and updates the count accordingly. Now let’s build it by starting with the Main Window. Of course, when we use Expression Blend 4, we just press the Ctrl-Shift-N key sequence to create a new project. We choose the Standard WPF application choice and give the project a name.

The Main Window contains a two-column Grid. On the left side is a Border lement, which contains the Canvas hat represents the game surface:

<Border Grid.Column="0" BorderBrush="SteelBlue" BorderThickness="1" Margin="5"> 
<Grid> 
<Canvas x:Name="canvasBackground" SizeChanged="canvasBackground_SizeChanged" 
MinWidth="50"> 
<Canvas.Background> 
<RadialGradientBrush> 
<GradientStop Color="AliceBlue" Offset="0"></GradientStop> 
<GradientStop Color="White" Offset="0.7"></GradientStop> 
</RadialGradientBrush> 
</Canvas.Background> 
</Canvas> 
</Grid> 
</Border> 

When the Canvas s sized for the first time or resized (when the user changes the size of the browser window), the following code runs and sets the clipping region:

private void canvasBackground_SizeChanged(object sender, SizeChangedEventArgs e)
{
// Set the clipping region to match the current display region of the Canvas.
RectangleGeometry rect = new RectangleGeometry();
rect.Rect = new Rect(0, 0,
canvasBackground.ActualWidth, canvasBackground.ActualHeight);
canvasBackground.Clip = rect;
}

This is required because otherwise the Canvas raws its children even if they lie outside its display area. In the bomb-dropping game, this would cause the bombs to fly out of the box that delineates the Canvas On the right side of the main window is a panel that shows the game statistics, the current bomb-dropped and bomb-saved count, and a button for starting the game:

<Border Grid.Column="1" BorderBrush="SteelBlue" BorderThickness="1" Margin="5"> 
<Border.Background> 
<RadialGradientBrush GradientOrigin="1,0.7" Center="1,0.7" 
RadiusX="1" RadiusY="1"> 
<GradientStop Color="Orange" Offset="0"></GradientStop> 
<GradientStop Color="White" Offset="1"></GradientStop> 
</RadialGradientBrush> 
</Border.Background> 
<StackPanel Margin="15" VerticalAlignment="Center" HorizontalAlignment="Center"> 
<bomb:Title></bomb:Title> 
<TextBlock x:Name="lblRate" Margin="0,30,0,0" TextWrapping="Wrap" 
FontFamily="Georgia" FontSize="14"></TextBlock> 
<TextBlock x:Name="lblSpeed" Margin="0,30" TextWrapping="Wrap" 
FontFamily="Georgia" FontSize="14"></TextBlock> 
<TextBlock x:Name="lblStatus" TextWrapping="Wrap" 
FontFamily="Georgia" FontSize="20">No bombs have dropped.</TextBlock> 
<Button x:Name="cmdStart" Padding="5" Margin="0,30" Width="80" 
Content="Start Game" Click="cmdStart_Click"></Button> 
</StackPanel> 
</Border> 

You’ll notice that the right-side column contains one unusual ingredient: an element named Title This is a custom user control that shows the BombDropper itle in fiery orange letters. Technically, it’s a piece of vector art, easily drawn with Word Art. To use the Title user control, you need to map your project namespace to an XML namespace, thereby making it available in your Window. Here is what we need to add:

<UserControl x:Class="BombDropper.Page" 
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
xmlns:bomb="clr-namespace:BombDropper;assembly=BombDropper"> 

Now recall when you start a new WPF project, you get a MainWindow.xaml file and a MainWindow.xaml.cs code-behind file. When we go to the Projects tab using Expression Blend, we can right-click the project file, click “Add New Item”, and choose UserControl We, of course, name this control Bomb.xaml. Here is the MainWindow.xaml file:

<Window x:Class="BombDropper.MainWindow" 
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
Title="BombDropper" Height="500" Width="600"> 
<Grid x:Name="LayoutRoot"> 
<Grid.ColumnDefinitions> 
<ColumnDefinition></ColumnDefinition> 
<ColumnDefinition Width="280"></ColumnDefinition> 
</Grid.ColumnDefinitions> 
<Border BorderBrush="SteelBlue" BorderThickness="1" Margin="5"> 
<Grid> 
<Canvas x:Name="canvasBackground" 
	SizeChanged="canvasBackground_SizeChanged" MinWidth="50"> 
<Canvas.Background> 
<RadialGradientBrush> 
<GradientStop Color="AliceBlue" Offset="0"></GradientStop> 
<GradientStop Color="White" Offset="0.7"></GradientStop> 
</RadialGradientBrush> 
</Canvas.Background> 
</Canvas> 
</Grid> 
</Border> 
<Border Grid.Column="1" BorderBrush="SteelBlue" BorderThickness="1" Margin="5"> 
<Border.Background> 
<RadialGradientBrush GradientOrigin="1,0.7" Center="1,0.7" RadiusX="1" RadiusY="1"> 
<GradientStop Color="Orange" Offset="0"></GradientStop> 
<GradientStop Color="White" Offset="1"></GradientStop> 
</RadialGradientBrush> 
</Border.Background> 
<StackPanel Margin="15" VerticalAlignment="Center" HorizontalAlignment="Center"> 
<TextBlock FontFamily="Impact" FontSize="35" 
	Foreground="LightSteelBlue">Bomb Dropper</TextBlock> 
<TextBlock x:Name="lblRate" Margin="0,30,0,0" 
	TextWrapping="Wrap" FontFamily="Georgia" FontSize="14"></TextBlock> 
<TextBlock x:Name="lblSpeed" Margin="0,30" 
	TextWrapping="Wrap" FontFamily="Georgia" FontSize="14"></TextBlock> 
<TextBlock x:Name="lblStatus" TextWrapping="Wrap" 
	FontFamily="Georgia" FontSize="20">No bombs have dropped.</TextBlock> 
<Button x:Name="cmdStart" Padding="5" Margin="0,30" 
	Width="80" Content="Start Game" Click="cmdStart_Click"></Button> 
</StackPanel> 
</Border> 
</Grid> 
</Window> 

Here is the MainWindow.xaml.cs code-behind file:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Media.Animation;
using System.Windows.Threading;

namespace BombDropper
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            bombTimer.Tick += bombTimer_Tick;
        }

        private void canvasBackground_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            RectangleGeometry rect = new RectangleGeometry();
            rect.Rect = new Rect(0, 0, 
		canvasBackground.ActualWidth, canvasBackground.ActualHeight);
            canvasBackground.Clip = rect;
        }

        // "Adjustments" happen periodically, increasing the speed of bomb
        // falling and shortening the time between bombs.
        private DateTime lastAdjustmentTime = DateTime.MinValue;

        // Perform an adjustment every 15 seconds.
        private double secondsBetweenAdjustments = 15;

        // Initially, bombs fall every 1.3 seconds, and hit the ground after 3.5 seconds.
        private double initialSecondsBetweenBombs = 1.3;
        private double initialSecondsToFall = 3.5;
        private double secondsBetweenBombs;
        private double secondsToFall;

        // After every adjustment, shave 0.1 seconds off both.
        private double secondsBetweenBombsReduction = 0.1;
        private double secondsToFallReduction = 0.1;

        // Make it possible to look up a storyboard based on a bomb.
        private Dictionary<bomb,> storyboards = new Dictionary<bomb,>();

        // Fires events on the user interface thread.
        private DispatcherTimer bombTimer = new DispatcherTimer();

        // Start the game.
        private void cmdStart_Click(object sender, RoutedEventArgs e)
        {
            cmdStart.IsEnabled = false;

            // Reset the game.
            droppedCount = 0;
            savedCount = 0;
            secondsBetweenBombs = initialSecondsBetweenBombs;
            secondsToFall = initialSecondsToFall;
            
            // Start bomb dropping events.            
            bombTimer.Interval = TimeSpan.FromSeconds(secondsBetweenBombs);
            bombTimer.Start();
        }

        // Drop a bomb.
        private void bombTimer_Tick(object sender, EventArgs e)
        {
            // Perform an "adjustment" when needed.
            if ((DateTime.Now.Subtract(lastAdjustmentTime).TotalSeconds >
                secondsBetweenAdjustments))
            {
                lastAdjustmentTime = DateTime.Now;
          
                secondsBetweenBombs -= secondsBetweenBombsReduction;
                secondsToFall -= secondsToFallReduction;

                // (Technically, you should check for 0 or negative values.
                // However, in practice these won't occur because the game will
                // always end first.)
                
                // Set the timer to drop the next bomb at the appropriate time.
                bombTimer.Interval = TimeSpan.FromSeconds(secondsBetweenBombs);

                // Update the status message.
                lblRate.Text = String.Format("A bomb is released every {0} seconds.",
                    secondsBetweenBombs);
                lblSpeed.Text = String.Format("Each bomb takes {0} seconds to fall.",
                    secondsToFall);
            }

            // Create the bomb.
            Bomb bomb = new Bomb();
            bomb.IsFalling = true;

            // Position the bomb.            
            Random random = new Random();      
            bomb.SetValue(Canvas.LeftProperty, 
                (double)(random.Next(0, (int)(canvasBackground.ActualWidth - 50))));
            bomb.SetValue(Canvas.TopProperty, -100.0);

            // Attach mouse click event (for defusing the bomb).
            bomb.MouseLeftButtonDown += bomb_MouseLeftButtonDown;
                       
            // Create the animation for the falling bomb.
            Storyboard storyboard = new Storyboard();
            DoubleAnimation fallAnimation = new DoubleAnimation();            
            fallAnimation.To = canvasBackground.ActualHeight;
            fallAnimation.Duration = TimeSpan.FromSeconds(secondsToFall);
            
            Storyboard.SetTarget(fallAnimation, bomb);            
            Storyboard.SetTargetProperty
			(fallAnimation, new PropertyPath("(Canvas.Top)"));
            storyboard.Children.Add(fallAnimation);

            // Create the animation for the bomb "wiggle."
            DoubleAnimation wiggleAnimation = new DoubleAnimation();            
            wiggleAnimation.To = 30;
            wiggleAnimation.Duration = TimeSpan.FromSeconds(0.2);
            wiggleAnimation.RepeatBehavior = RepeatBehavior.Forever;
            wiggleAnimation.AutoReverse = true;
                        
            Storyboard.SetTarget(wiggleAnimation, 
		((TransformGroup)bomb.RenderTransform).Children[0]);
            Storyboard.SetTargetProperty(wiggleAnimation, new PropertyPath("Angle"));
            storyboard.Children.Add(wiggleAnimation);
                                                
            // Add the bomb to the Canvas.
            canvasBackground.Children.Add(bomb);

            // Add the storyboard to the tracking collection.            
            storyboards.Add(bomb, storyboard);

            // Configure and start the storyboard.
            storyboard.Duration = fallAnimation.Duration;
            storyboard.Completed += storyboard_Completed;
            storyboard.Begin();            
        }

        private void bomb_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            // Get the bomb.
            Bomb bomb = (Bomb)sender;
            bomb.IsFalling = false;

            // Get the bomb's current position.
            Storyboard storyboard = storyboards[bomb];
            double currentTop = Canvas.GetTop(bomb);

            // Stop the bomb from falling.
            storyboard.Stop();

            // Reuse the existing storyboard, but with new animations.
            // Send the bomb on a new trajectory by animating Canvas.Top
            // and Canvas.Left.
            storyboard.Children.Clear();
            
            DoubleAnimation riseAnimation = new DoubleAnimation();
            riseAnimation.From = currentTop;
            riseAnimation.To = 0;
            riseAnimation.Duration = TimeSpan.FromSeconds(2);

            Storyboard.SetTarget(riseAnimation, bomb);
            Storyboard.SetTargetProperty(riseAnimation, new PropertyPath("(Canvas.Top)"));
            storyboard.Children.Add(riseAnimation);

            DoubleAnimation slideAnimation = new DoubleAnimation();
            double currentLeft = Canvas.GetLeft(bomb);
            // Throw the bomb off the closest side.
            if (currentLeft < canvasBackground.ActualWidth / 2)
            {
                slideAnimation.To = -100;
            }
            else
            {
                slideAnimation.To = canvasBackground.ActualWidth + 100;
            }
            slideAnimation.Duration = TimeSpan.FromSeconds(1);
            Storyboard.SetTarget(slideAnimation, bomb);
            Storyboard.SetTargetProperty
		(slideAnimation, new PropertyPath("(Canvas.Left)"));
            storyboard.Children.Add(slideAnimation);

            // Start the new animation.
            storyboard.Duration = slideAnimation.Duration;
            storyboard.Begin();
        }

        // Keep track of how many are dropped and stopped.
        private int droppedCount = 0;
        private int savedCount = 0;

        // End the game at maxDropped.
        private int maxDropped = 5;

        private void storyboard_Completed(object sender, EventArgs e)
        {            
            ClockGroup clockGroup = (ClockGroup)sender;
            
            // Get the first animation in the storyboard, and use it to find the
            // bomb that's being animated.
            DoubleAnimation completedAnimation = 
		(DoubleAnimation)clockGroup.Children[0].Timeline;            
            Bomb completedBomb = (Bomb)Storyboard.GetTarget(completedAnimation);
                        
            // Determine if a bomb fell or flew off the Canvas after being clicked.
            if (completedBomb.IsFalling)
            {
                droppedCount++;
            }
            else
            {
                savedCount++;
            }

            // Update the display.
            lblStatus.Text = String.Format("You have dropped {0} bombs and saved {1}.",
                droppedCount, savedCount);
                        
            // Check if it's game over.
            if (droppedCount >= maxDropped)
            {
                bombTimer.Stop();
                lblStatus.Text += "\r\n\r\nGame over.";

                // Find all the storyboards that are underway.
                foreach (KeyValuePair<bomb,> item in storyboards)
                {
                    Storyboard storyboard = item.Value;
                    Bomb bomb = item.Key;

                    storyboard.Stop();
                    canvasBackground.Children.Remove(bomb);
                }
                // Empty the tracking collection.
                storyboards.Clear();                

                // Allow the user to start a new game.
                cmdStart.IsEnabled = true;
            }
            else
            {                
                Storyboard storyboard = (Storyboard)clockGroup.Timeline;
                storyboard.Stop();
                                
                storyboards.Remove(completedBomb);
                canvasBackground.Children.Remove(completedBomb);
            }
        }
   }
}

You can obviously use Visual Studio for this, or any WPF project. Since we are using Expression Blend, we right-click the project icon contained within the Projects tab and choose “Add new Item”. We then choose “UserControl, which is our bomb. In WPF, you can make user controls as class libraries or as sort of an application that is used by exporting it to an application. The Bomb.xaml file is contained in the downloadable source code.

Here is the Bomb.xaml.cs file:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace BombDropper
{
    public partial class Bomb : UserControl
    {
        public Bomb()
        {
            InitializeComponent();
        }

        public bool IsFalling
        {
            get;
            set;
        }
    }
}

For what it is worth, this is still an animation application. Creating an animation is a multistep process. You need to create three separate ingredients: an animation object to perform your animation, a storyboard to manage your animation, and an event handler (an event trigger) to start your storyboard. The storyboard manages the timeline of your animation. You can use a storyboard to group multiple animations, and it also has the ability to control the playback of animation–pausing it, stopping it, and changing its position. But the most basic feature provided by the Storyboard lass is its ability to point to a specific property and specific element using the TargetProperty nd TargetName roperties. In other words, the storyboard bridges the gap between your animation and the property you want to animate.

References

  • MSDN
  • Todd Miranda

History

  • 20th July, 2010: Initial post

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here