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:
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)
{
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
{
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;
}
private DateTime lastAdjustmentTime = DateTime.MinValue;
private double secondsBetweenAdjustments = 15;
private double initialSecondsBetweenBombs = 1.3;
private double initialSecondsToFall = 3.5;
private double secondsBetweenBombs;
private double secondsToFall;
private double secondsBetweenBombsReduction = 0.1;
private double secondsToFallReduction = 0.1;
private Dictionary<bomb,> storyboards = new Dictionary<bomb,>();
private DispatcherTimer bombTimer = new DispatcherTimer();
private void cmdStart_Click(object sender, RoutedEventArgs e)
{
cmdStart.IsEnabled = false;
droppedCount = 0;
savedCount = 0;
secondsBetweenBombs = initialSecondsBetweenBombs;
secondsToFall = initialSecondsToFall;
bombTimer.Interval = TimeSpan.FromSeconds(secondsBetweenBombs);
bombTimer.Start();
}
private void bombTimer_Tick(object sender, EventArgs e)
{
if ((DateTime.Now.Subtract(lastAdjustmentTime).TotalSeconds >
secondsBetweenAdjustments))
{
lastAdjustmentTime = DateTime.Now;
secondsBetweenBombs -= secondsBetweenBombsReduction;
secondsToFall -= secondsToFallReduction;
bombTimer.Interval = TimeSpan.FromSeconds(secondsBetweenBombs);
lblRate.Text = String.Format("A bomb is released every {0} seconds.",
secondsBetweenBombs);
lblSpeed.Text = String.Format("Each bomb takes {0} seconds to fall.",
secondsToFall);
}
Bomb bomb = new Bomb();
bomb.IsFalling = true;
Random random = new Random();
bomb.SetValue(Canvas.LeftProperty,
(double)(random.Next(0, (int)(canvasBackground.ActualWidth - 50))));
bomb.SetValue(Canvas.TopProperty, -100.0);
bomb.MouseLeftButtonDown += bomb_MouseLeftButtonDown;
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);
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);
canvasBackground.Children.Add(bomb);
storyboards.Add(bomb, storyboard);
storyboard.Duration = fallAnimation.Duration;
storyboard.Completed += storyboard_Completed;
storyboard.Begin();
}
private void bomb_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Bomb bomb = (Bomb)sender;
bomb.IsFalling = false;
Storyboard storyboard = storyboards[bomb];
double currentTop = Canvas.GetTop(bomb);
storyboard.Stop();
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);
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);
storyboard.Duration = slideAnimation.Duration;
storyboard.Begin();
}
private int droppedCount = 0;
private int savedCount = 0;
private int maxDropped = 5;
private void storyboard_Completed(object sender, EventArgs e)
{
ClockGroup clockGroup = (ClockGroup)sender;
DoubleAnimation completedAnimation =
(DoubleAnimation)clockGroup.Children[0].Timeline;
Bomb completedBomb = (Bomb)Storyboard.GetTarget(completedAnimation);
if (completedBomb.IsFalling)
{
droppedCount++;
}
else
{
savedCount++;
}
lblStatus.Text = String.Format("You have dropped {0} bombs and saved {1}.",
droppedCount, savedCount);
if (droppedCount >= maxDropped)
{
bombTimer.Stop();
lblStatus.Text += "\r\n\r\nGame over.";
foreach (KeyValuePair<bomb,> item in storyboards)
{
Storyboard storyboard = item.Value;
Bomb bomb = item.Key;
storyboard.Stop();
canvasBackground.Children.Remove(bomb);
}
storyboards.Clear();
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
History
- 20th July, 2010: Initial post