This two-part tutorial is included in both An iPhone Developer’s Guide to Windows Phone 7 Programming, and Windows Phone 7 Development for Silverlight Programmers. The material is equally relevant to both and so the two series have been temporarily joined.
In part one of this tutorial we will describe and illustrate several animation and graphics techniques while building an arcade game for Windows Phone 7.
The game is called Bird Hunt, the purpose of which is to “shoot” birds flying across the screen by tapping the screen (no living birds were hurt in the creation of this game).
Each time one or more birds are released on screen, the player has 3 shots to try and hit the targets within a certain amount of time. If ten birds are hit, the level advances. As the levels advance, the birds fly faster and become more difficult to shoot. The game is over when the player misses 10 birds.
When the user opens the game options are presented for an easy, medium or hard game
- The easy game has one bird
- The medium game has two birds and a tree obstructing your view
- The hard game has two trees and the birds fly faster
This tutorial was written with Jeff Paries, who created the game we’ll be using and who is the author of Foundation Silverlight 3 Animation . Artwork for the game and for the tutorial was provided by Ryan Loghry.
Jeff will be a guest on Yet Another Podcast where we will discuss the creation of this game and collaboration on the tutorial. The game is open source and two versions of the source code are available. Birdhunt_Blank is the code-along version that we’ll be using in this tutorial. You can also download BirdHunt, the completed version. |
Game Objects
Bird Hunt consists of seven objects:
1) backgroundElements – the grass, trees, etc.
2) duck objects
3) duckIcons – the yellow “rubber ducks” near bottom center used to track hits/misses
4) levelControl – the control at the top left, used to display the current level
5) MainPage – this is where the main application code resides
6) scoringControl – shown top right, tracks score
7) shotCounter – shown bottom left , tracks shots available/fired
All of these objects work together to create the overall game experience. Take a few minutes and examine each of the objects in Visual Studio, or even better, Expression Blend to get a sense of their look and feel. This will help you understand how they function. A brief explanation of each object’s function follows
The Game Objects In Detail
The backgroundElements object contains a base canvas, which contains a canvas object named “backgroundObjects”. The backgroundObjects canvas contains canvases named “treeRight” and “treeLeft”, each of which contains a series of paths that make up tree objects. There are also a series of “grass”, “ground” and “stone” paths that make up the rest of the shapes in the object. The backgroundObjects canvas that contains all of the shapes in this object is scaled slightly along the X-axis to accommodate the application footprint. You can see this on the Scale tab of the Transform pane in Blend.
The duck object contains all the shapes used to make the duck fly, react when shot, and fall. The duck object contains the usual LayoutRoot canvas, which contains a canvas called duckFlyingPoses. This canvas contains canvases with the duck drawn in wingsUp,wingsMid, and wingsDown positions. There is also the duck’s shotReactionPose which is shown upon a successful hit on the duck, as well as the duckFall1 and duckFall2 positions, which will be used to give the duck a “spinning” look as it falls. Finally, there are two elliptical paths – hitZoneBody and hitZoneHead. These are the areas of the duck that can be shot, and the events on these two shapes are used to determine if a shot successfully hits the duck.
In viewing the duck.xaml file, you will notice that all of the poses for the duck are made to be non-visible by default by using the Visibility drop down on the Appearance pane. The poses will have their visibility manipulated by code, but if you would like to view the duck parts, select the appropriate canvas, and change the Visibility from Collapsed to Visible. Remember to reset the visibility back to Collapsed when you’re done exploring.
Note that the duck object also contains 4 storyboards. The storyboards are used to make the duck fly, show the duck’s reaction when shot, show the duck falling, and to manage the duck’s quacking sound.
The duckIcons object contains a LayoutRoot Grid, background Rectangle, TextBlock, and a StackPanel called iconsContainer. The iconsContainer StackPanel contains 10 Canvas objects, each of which has a rubber duck shape inside used to indicate the hit count. Each one of the rubber duck shapes contains paths named with a variation of “duckBody” and “duckbill”, which will have their Fill colors manipulated via code as the game is played.
The levelControl object contains a LayoutRoot Grid, Rectangle, and a StackPanel with two TextBlocks in it. One TextBlock contains the “Level:” messaging, and the other will be changed via code to handle display of the current level.
The MainPage control is where the main game controls are located. Inside of the LayoutRoot Canvas, there is a series of empty Canvas objects that are used to hold the scoring control, ducks, background elements, and icons. These Canvases will have objects written in to them, and serve the purpose of maintaining the desired Z-ordering of the different game elements. The MainPage object also contains the startPage, which is shown in figure 1, as well as some Grid objects called readyCanvas, niceShootingCanvas, and flyAwayCanvas. These Grids contain simple rectangles with game messaging such as “Ready!” and “Nice Shooting”. Finally, there is the gameOverCanvas, which contains some simple game over messaging as well as a Play Again button.
The scoringControl object contains a LayoutRoot Grid object, which has a gradient-filled Rectangle for the background, a StackPanel containing a TextBlock for the “Score:” messaging, and a TextBlock to display the score value.
The shotCounter object has a LayoutRoot Grid that contains a gradient-filled Rectangle for the background, a TextBlock for the “Shots:” messaging, and a StackPanel called “AllShells” which contains 3 canvas objects, each of which contains path objects to draw a shotgun shell shape. The red portion of each shell is named with a variant of “shellCase”, and will be manipulated via code to indicate spent shells as the user fires shots.
Coding the Game Objects
Let’s begin by wiring up the the objects, and then we’ll put it together within the MainPage code behind to get the game running.
The backgroundElements Object
Start Visual Studio 2010 and open the BirdHunt.sln file in the BirdHunt_Blank folder, and open backgroundElements.xaml.cs .
We will need three public methods that allow us to hide the display of obstacles for the easy level, show one obstacle for the medium level, and show two obstacles for the hard level,
public void setEasy() { treeLeft.Visibility = Visibility.Collapsed; treeRight.Visibility = Visibility.Collapsed; } public void setMedium() { treeLeft.Visibility = Visibility.Visible; treeRight.Visibility = Visibility.Collapsed; } public void setHard() { treeLeft.Visibility = Visibility.Visible; treeRight.Visibility = Visibility.Visible; }
The Duck Object
Open duck.xaml.cs and add all of the duck behavior, from quacking to flying to, well to getting shot.
We need the XNA framework for the audio,
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio;
To track the duck’s movement we need its starting x and y coordinates and its velocity on the x and y axis,
public int velX { get; set; } public int velY { get; set; } public int startX { get; set; } public int startY { get; set; }
We’ll add three more properties, to act as flags indicating whether a duck has been shot, if a duck has flown off screen, or if a duck is in “fly away” mode, meaning a user has fired and missed with all 3 shots, or the allotted time to hit the targets has expired.
public bool isShot { get; set; } public bool offScreen { get; set; } public bool flyAway { get; set; }
Finally, four more properties:
- A SoundEffect from the Xna framework, to hold the quacking sound
- An integer to reverse the duck’s direction when it hits a barrier
- An integer to act as framecounter for frame-based animation
- A random number generator
public SoundEffect _quack; private int restitution = -1; private int frameCounter; private Random rand = new Random();
As noted earlier, the XAML for the duck object contains 4 storyboards. In this case, all 4 storyboards are used as timers. The duckFly timer is used to control how fast each duck object’s wings flap. The shotReaction timer displays the duck’s reaction to being shot for .5 seconds. The duckFall timer is used to control the frame-based animation for the duck as it falls, and the quackTimer is used to determine how often to play the audio file. (All four are in duck.xaml):
<UserControl.Resources> <Storyboard x:Name="duckFly" DuratiDuckFallComon="00:00:00.25"/> <Storyboard x:Name="shotReaction" Duration="00:00:00.50"/> <Storyboard x:Name="duckFall" Duration="00:00:00.125"/> <Storyboard x:Name="quackTimer" Duration="00:00:01"/> </UserControl.Resources>
Empty Storyboards such as these are useful, as they raise a Completed event when they finish. This allows us to perform some action and conditionally restart the storyboard to repeat the desired action.
Create event handlers for the Completed event on all four storyboards,
public duck() { InitializeComponent(); duckFly.Completed += duckFly_Completed; shotReaction.Completed += shotReaction_Completed; duckFall.Completed += duckFall_Completed; quackTimer.Completed += quackTimer_Completed; } When duck fly is completed we’ll test the framecounter and set the appropriate image to be visible. We’ll then restart the animation,
private void duckFly_Completed(object sender, EventArgs e) { frameCounter += 1; wingsDown.Visibility = Visibility.Collapsed; wingsMid.Visibility = Visibility.Collapsed; wingsUp.Visibility = Visibility.Collapsed; switch ( frameCounter) { case 1: wingsDown.Visibility = Visibility.Visible; break; case 2: wingsMid.Visibility = Visibility.Visible; break; case 3: wingsUp.Visibility = Visibility.Visible; break; case 4: wingsMid.Visibility = Visibility.Visible; frameCounter = 0; break; } duckFly.Begin(); }
If a duck is shot a reaction is shown for a set amount of time and then duckFall_Completed is called,
private void shotReaction_Completed(
object sender, EventArgs e)
{
frameCounter = 0;
shotReactionPose.Visibility =
Visibility.Collapsed;
this.velY = 5;
duckFall_Completed(null, null);
}
duckFall_Completed is responsible for the duck spiraling downward,
private void duckFall_Completed(object sender, EventArgs e) { frameCounter += 1; duckFall1.Visibility = Visibility.Collapsed; duckFall2.Visibility = Visibility.Collapsed; if (frameCounter == 1) { duckFall1.Visibility = Visibility.Visible; } else if (frameCounter == 2) { duckFall2.Visibility = Visibility.Visible; frameCounter = 0; } duckFall.Begin(); }
The quacking is managed by the final storyboard,
private void quackTimer_Completed(object sender, EventArgs e) { quack(); quackTimer.Begin(); } public void quack() { FrameworkDispatcher.Update(); _quack.Play(); quackTimer.Begin(); } public void setQuack(string whichQuack, int quackLength) { _quack = SoundEffect.FromStream(TitleContainer.OpenStream(whichQuack)); quackTimer.Duration = new TimeSpan(0, 0, quackLength); }
Notice that setQuack is passed a string indicating which sound effect to use, and the duration of the quack is also parameterized.
The RemoveDuck method is used to make the duck invisible after its death spiral is completed.
public void removeDuck() { velX = 0; velY = 0; duckFly.Stop(); duckFall.Stop(); isShot = false; flyAway = false; quackTimer.Stop(); _quack.Dispose(); this.Visibility = Visibility.Collapsed; }
Pay special attention to the penultimate line in which the Quack resource is disposed. Because this is not a managed resource it must be disposed of manually.
The duck method above flaps the duck’s wings but doesn’t move it across the screen. For that we’ll create moveDuck(),
public void moveDuck() { Canvas.SetLeft(this, (Canvas.GetLeft(this)) + velX); Canvas.SetTop(this, (Canvas.GetTop(this)) + velY); if (!isShot && !flyAway) { if (Canvas.GetTop(this) <= 0) { Canvas.SetTop(this, 0); velY *= restitution; } if (Canvas.GetTop(this) >= 350) { Canvas.SetTop(this, 350); velY *= restitution; } if (Canvas.GetLeft(this) <= 0) { Canvas.SetLeft(this, 0); velX *= restitution; duckScale.ScaleX *= -1; } if (Canvas.GetLeft(this) + this.Width >= 800 && velX > 0) { Canvas.SetLeft(this, 800 - this.Width); velX *= restitution; duckScale.ScaleX *= -1; } } else if (flyAway) { if (Canvas.GetTop(this) <= 0) { velY *= restitution; } if (Canvas.GetTop(this) >= 350) { velY *= restitution; } } }
The first two lines move the duck from its current position to a position incremented by vel_X and vel_Y. After this four conditions are checked:
- Has the bird been shot?
- Is the bird in fly-away?
- Has the bird hit a boundary on the sky or the grass?
- Has the bird hit a boundary on either side?
Boundary Checking
The first check is to see if the top of the duck object is <= 0, meaning the duck has reached the top of the screen. If so, the duck is moved to 0 Y, and the Y velocity is reversed by multiplying its value by -1. The second check looks to see if the top of the duck has gone beyond 350Y. This value allows the ducks to fly a bit beneath the top of the grass in the background object, but not to be obscured by flying beneath the ground visuals. If it reaches this value, the duck’s direction is reversed by multiplying Y velocity by -1. The third check looks to see if the duck is <= 0X. If so, the X velocity is reversed by multiplying it by -1. The other task performed here is to scale the duck object by -1X. This scaling effectively reverses the direction in which the duck is drawn. Finally, a check is performed to see if the duck’s current position plus the ducks width is wider than 800 pixels (the width of the game). The reason the duck’s width needs to be added to the current position is because Silverlight measures object positions from the top left. If an object has met this condition, it is positioned to be on screen, and scaled by -1X to reverse the direction in which the duck is drawn. |
FlyAway is handled in the move method (the duck flys off the edge), but the duck being shot is handled by a dedicated method,
public void duckShot() { quackTimer.Stop(); _quack.Dispose(); this.IsHitTestVisible = false; duckFly.Stop(); isShot = true; velX = 0; velY = 0; wingsDown.Visibility = Visibility.Collapsed; wingsMid.Visibility = Visibility.Collapsed; wingsUp.Visibility = Visibility.Collapsed; shotReactionPose.Visibility = Visibility.Visible; shotReaction.Begin(); }
the audio is stopped, and we ensure that the duck can not be shot twice (that would be cruel). Changing the IsHitTestVisible property to false makes the duck invisible to shot.
A shot duck stops and its velocity drops instantly to zero and the duck shot reaction is started.
Duck Icons
The duck icons are used to keep track of whether ducks manage to fly away or are shot. This is done by modifying their color as their state changes, from within the duckIcons class,
private SolidColorBrush hitFill = new SolidColorBrush( Color.FromArgb(255, 255, 63, 0)); private SolidColorBrush missFill = new SolidColorBrush( Color.FromArgb(255, 128, 128, 128)); private SolidColorBrush defaultBodyFill = new SolidColorBrush( Color.FromArgb(255, 255, 223, 0)); private SolidColorBrush defaultBillFill = new SolidColorBrush( Color.FromArgb(255, 255, 190, 0));
We’ll use counters to keep track of how many misses and hits we have, and a couple public methods to initialize and reset the icons,
public int missedCounter { get; set; } public int iconCounter { get; set; } public void initDuckIcons() { missedCounter = 0; iconCounter = 0; } public void resetDuckIcons() { duckBody.Fill = duckBody1.Fill = duckBody2.Fill = duckBody3.Fill = duckBody4.Fill = duckBody5.Fill = duckBody6.Fill = duckBody7.Fill = duckBody8.Fill = duckBody9.Fill = defaultBodyFill; duckBill.Fill = duckBill1.Fill = duckBill2.Fill = duckBill3.Fill = duckBill4.Fill = duckBill5.Fill = duckBill6.Fill = duckBill7.Fill = duckBill8.Fill = duckBill9.Fill = defaultBillFill; }
When the state changes we check the counters and set the colors appropriately,
public void hitIcon() { switch (iconCounter) { case 0: duckBody.Fill = duckBill.Fill = hitFill; break; case 1: duckBody1.Fill = duckBill1.Fill = hitFill; break; case 2: duckBody2.Fill = duckBill2.Fill = hitFill; break; case 3: duckBody3.Fill = duckBill3.Fill = hitFill; break; case 4: duckBody4.Fill = duckBill4.Fill = hitFill; break; case 5: duckBody5.Fill = duckBill5.Fill = hitFill; break; case 6: duckBody6.Fill = duckBill6.Fill = hitFill; break; case 7: duckBody7.Fill = duckBill7.Fill = hitFill; break; case 8: duckBody8.Fill = duckBill8.Fill = hitFill; break; case 9: duckBody9.Fill = duckBill9.Fill = hitFill; break; } iconCounter += 1; } public void missIcon() { switch (iconCounter) { case 0: duckBody.Fill = duckBill.Fill = missFill; break; case 1: duckBody1.Fill = duckBill1.Fill = missFill; break; case 2: duckBody2.Fill = duckBill2.Fill = missFill; break; case 3: duckBody3.Fill = duckBill3.Fill = missFill; break; case 4: duckBody4.Fill = duckBill4.Fill = missFill; break; case 5: duckBody5.Fill = duckBill5.Fill = missFill; break; case 6: duckBody6.Fill = duckBill6.Fill = missFill; break; case 7: duckBody7.Fill = duckBill7.Fill = missFill; break; case 8: duckBody8.Fill = duckBill8.Fill = missFill; break; case 9: duckBody9.Fill = duckBill9.Fill = missFill; break; } iconCounter += 1; missedCounter += 1; }
The Level
coding for the Level control involves nothing more than managing the property and setting the visible value,
public partial class levelControl : UserControl { public void SetMessageLevel( ) { msgLevel.Text = level.ToString(); } private int level = 1; public int Level { get { SetMessageLevel(); return level; } set { SetMessageLevel(); level = value; } } public levelControl() { InitializeComponent(); } }
Scoring Control
Scoring is kept in this control, tracked by the two public properties at the top of the file, and displayed by the msgScore textBlock,
public partial class scoringControl : UserControl { public int pointsForEachDuck { get; set; } public int score { get; set; } public scoringControl() { InitializeComponent(); } public void setScore(string score) { msgScore.Text = score; } public void updateScore(int whichLevel) { score += pointsForEachDuck * whichLevel; msgScore.Text = score.ToString("0000000"); } }
Shot Counter
The shotCounter object has several responsibilities, including tracking how many shots have been fired, playing audio files when shots are fired, and modifying the control visuals to indicate to the user how many shots remain, how many targets have been hit, etc.
The WAV audio files needed for the game are already provided inside the project, in a folder called “sounds”. To play audio in the game, we will be leveraging the XNA Framework, so after double-clicking the shotCounter code-behind to open it for editing, add the following two using statements near the top of the file. These statements make the XNA libraries available to this object.
The tracking of shots fired is managed by coloring the shell cases, much as the ducks are colored in the Duck Icon files.
public int shotsFired = 0; public bool reloading; private SolidColorBrush shotFired; private SolidColorBrush shotReady; private SoundEffect _gunshot; private SoundEffect _reload; private SoundEffect _gottaHurt; private SoundEffect _hail; private int numDucksHit; private string difficultyLevel;
In the constructor we initialize both the counters and the colors,
public shotCounter() { InitializeComponent(); _gunshot = SoundEffect.FromStream(TitleContainer.OpenStream("sounds/gun_shotgun2.wav")); _reload = SoundEffect.FromStream(TitleContainer.OpenStream("sounds/gun_pump_action.wav")); _gottaHurt = SoundEffect.FromStream(TitleContainer.OpenStream("sounds/gotta_hurt.wav")); _hail = SoundEffect.FromStream(TitleContainer.OpenStream("sounds/hail.wav")); shotFired = new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 128, 128, 128)); shotReady = new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 255, 63, 0)); reloadTimer.Completed += new EventHandler(reloadTimer_Completed); doneReloadingTimer.Completed += new EventHandler(doneReloadingTimer_Completed); }
A storyboard is used to manage the various timers, as above,
private void reloadTimer_Completed(object sender, EventArgs e) { _reload.Play(); doneReloadingTimer.Begin(); } private void doneReloadingTimer_Completed(object sender, EventArgs e) { reloading = false; }
Here is the complete shotCounter.xaml.cs for context. The additional methods, updateshots and DoQuip are quite similar to what we’ve seen already,
using System; using System.Windows.Controls; using System.Windows.Media; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; namespace BirdHunt { public partial class shotCounter : UserControl { public int shotsFired; public bool reloading; private SolidColorBrush shotFired; private SolidColorBrush shotReady; private SoundEffect _gunshot; private SoundEffect _reload; private SoundEffect _gottaHurt; private SoundEffect _hail; private int numDucksHit; private string difficultyLevel; public shotCounter() { InitializeComponent(); _gunshot = SoundEffect.FromStream(TitleContainer.OpenStream("sounds/gun_shotgun2.wav")); _reload = SoundEffect.FromStream(TitleContainer.OpenStream("sounds/gun_pump_action.wav")); _gottaHurt = SoundEffect.FromStream(TitleContainer.OpenStream("sounds/gotta_hurt.wav")); _hail = SoundEffect.FromStream(TitleContainer.OpenStream("sounds/hail.wav")); shotFired = new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 128, 128, 128)); shotReady = new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 255, 63, 0)); reloadTimer.Completed += reloadTimer_Completed; doneReloadingTimer.Completed += doneReloadingTimer_Completed; } private void reloadTimer_Completed(object sender, EventArgs e) { _reload.Play(); doneReloadingTimer.Begin(); } private void doneReloadingTimer_Completed(object sender, EventArgs e) { reloading = false; } public void fireShot(int ducksHit, string difficulty) { numDucksHit = ducksHit; difficultyLevel = difficulty; reloading = true; FrameworkDispatcher.Update(); _gunshot.Play(); reloadTimer.Begin(); shotsFired += 1; if (shotsFired > 3) { shotsFired = 3; } updateShots(); doQuip(); } private void updateShots() { switch (shotsFired) { case 0: shellCase_01.Fill = shotReady; shellCase_02.Fill = shotReady; shellCase_03.Fill = shotReady; break; case 1: shellCase_01.Fill = shotFired; shellCase_02.Fill = shotReady; shellCase_03.Fill = shotReady; break; case 2: shellCase_01.Fill = shotFired; shellCase_02.Fill = shotFired; shellCase_03.Fill = shotReady; break; case 3: shellCase_01.Fill = shotFired; shellCase_02.Fill = shotFired; shellCase_03.Fill = shotFired; break; } } private void doQuip() { // two shots fired, two ducks hit, level hard if (numDucksHit == 2 && shotsFired == 2 && difficultyLevel == "hard") { FrameworkDispatcher.Update(); _hail.Play(); } // two shots fired, two ducks hit, level medium if (numDucksHit == 2 && shotsFired == 2 && difficultyLevel == "medium") { FrameworkDispatcher.Update(); _gottaHurt.Play(); } } public void resetShots(int howMany) { shotsFired = howMany; updateShots(); } } }
As it stands, you should be able to compile the code cleanly, although if you run it, there won’t be anything to see since we haven’t instanced any of our objects or created any code to run the game. In the next section, we’ll add the main code that brings all of these objects together and runs the game.
3 Responses to iPhone to Windows Phone 7 – Animation and Games