Original Article Posted on Jesse Liberty's Blog
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 this second part of the tutorial, we will describe how to wire up the MainPage.xaml.cs file and bring all of the objects in the Bird Hunt game together into a running application
To review: 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
Recall that 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.
We’re going to be using XNA for some sound effects, so add the following to your using statements:
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
We begin by creating instances of each of the objects being used in the game. Prior to the MainPage()
constructor, add the following code:
private readonly BackgroundElements _backgroundElements = new BackgroundElements();
private readonly duckIcons _duckIcons = new duckIcons();
private readonly SoundEffect _fieldSound;
private readonly SoundEffect _kickAss;
private readonly levelControl _levelControl = new levelControl();
private readonly ScoringControl _scoringControl = new ScoringControl();
private readonly ShotCounter _shotCounter = new ShotCounter();
In addition to a random number generator, we’re going to set up member variables for the difficulty level(“easy”, “medium”, or “hard”), the number of ducks hit, the speed of the duck’s flapping, whether or not the hard level has been initialized, some minimum and maximum X and Y velocity settings, the number of targets to generate, the number of targets on screen, the total number of targets hit and the total number of targets missed,
private readonly Random _rand = new Random();
private string _difficulty;
private int _ducksHit;
private TimeSpan _flapSpeed;
private bool _hardLevelInitialized;
private int _maxXVel;
private int _maxYVel;
private int _minXVel;
private int _minYVel;
private int _numTargets;
private Duck[] _targets;
private int _targetsOnScreen;
private int _totalTargetsHit;
private int _totalTargetsMissed;
The Constructor
Begin coding inside the constructor by adding the _backgroundElements
instance to the backgroundElements
Canvas object. Follow that with the _shotCounter
, _duckIcons
, _scoringControl
, and _levelControl
objects.
backgroundElements.Children.Add(_backgroundElements);
Canvas.SetLeft(_shotCounter, 10);
iconsCanvas.Children.Add(_shotCounter);
Canvas.SetLeft(_duckIcons, 155);
iconsCanvas.Children.Add(_duckIcons);
_duckIcons.initDuckIcons();
Canvas.SetLeft(_scoringControl, 600);
Canvas.SetTop(_scoringControl, 10);
scoringCanvas.Children.Add(_scoringControl);
Canvas.SetLeft(_levelControl, 10);
Canvas.SetTop(_levelControl, 10);
scoringCanvas.Children.Add(_levelControl);
Create event handlers for the gameLoop storyboard, difficulty buttons shown at the beginning of the game, the duckCanvas MouseLeftButtonDown
event, and the Loaded event,
gameLoop.Completed += GameLoopCompleted;
btnEasy.Click += BtnEasyClick;
btnMedium.Click += BtnMediumClick;
btnHard.Click += BtnHardClick;
duckCanvas.MouseLeftButtonDown += DuckCanvasMouseLeftButtonDown;
_levelControl.Level = 1;
Loaded += MainPage_Loaded;
instructionsShow.Click += InstructionsShowClick;
instructionsClose.Click += InstructionsCloseClick;
playAgain.Click += PlayAgainClick;
levelMessageTimer.Completed += LevelMessageTimerCompleted;
audioTimer.Completed += AudioTimerCompleted;
levelTimer.Completed += LevelTimerCompleted;
readyMessage.Completed += ReadyMessageCompleted;
closeInstructionsStoryboard.Completed += CloseInstructionsStoryboardCompleted;
_kickAss = SoundEffect.FromStream(TitleContainer.OpenStream("sounds/out_of_gum.wav"));
_fieldSound = SoundEffect.FromStream(TitleContainer.OpenStream(
"sounds/fieldAmbientSounds.wav"));
Methods
The Loaded event handler is raised when the Silverlight application has been loaded (surprise!).
This event is here for a single purpose – performance in the WP7 emulator. While your mileage may vary, the game play runs smoother in some cases when the frame rate counter is enabled. Therefore, if you are experiencing jumpy animations and generally poor performance, try enabling the frame rate counter. If your laptop performs well, you may omit the following method.
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
Application.Current.Host.Settings.EnableFrameRateCounter = true;
}
The event handler for the easy, medium and hard buttons are very similar. Each of the three methods begins by setting the _backgroundElements object to the appropriate level – recall that we have trees to obscure the view that can be enabled based on the level.
The _min and _max velocity variables are used to set the minimum and maximum speeds at which the targets may be moving across the screen, and _numTargets to determine how many targets will be generated on screen when a level starts.
Following that is a string to reference the level selected, and the point value we want to specify for each target that is hit. The _flapSpeed
variable is a TimeSpan object that determines how fast the ducks flap their wings. The faster they move across the screen, the faster they should be flapping. The Duration property of the levelTimer determines how long the player has to shoot the targets on screen before the targets are allowed to fly away. We finish up by calling a few methods to clear and reset our game screen.
private void BtnEasyClick(object sender, RoutedEventArgs e)
{
_backgroundElements.SetEasy();
_minXVel = 1;
_maxXVel = 1;
_minYVel = 1;
_maxYVel = 1;
_numTargets = 1;
_difficulty = "easy";
_scoringControl.PointsForEachDuck = 100;
_flapSpeed = new TimeSpan(0, 0, 0, 0, 250);
levelTimer.Duration = new TimeSpan(0, 0, 15);
ResetScoreAndShots();
_duckIcons.initDuckIcons();
_duckIcons.resetDuckIcons();
InitObjects();
}
private void BtnMediumClick(object sender, RoutedEventArgs e)
{
_backgroundElements.SetMedium();
_minXVel = 2;
_maxXVel = 4;
_minYVel = 2;
_maxYVel = 4;
_numTargets = 2;
_difficulty = "medium";
_scoringControl.PointsForEachDuck = 500;
_flapSpeed = new TimeSpan(0, 0, 0, 0, 175);
levelTimer.Duration = new TimeSpan(0, 0, 10);
ResetScoreAndShots();
_duckIcons.initDuckIcons();
_duckIcons.resetDuckIcons();
InitObjects();
}
private void BtnHardClick(object sender, RoutedEventArgs e)
{
_backgroundElements.SetHard();
_minXVel = 4;
_maxXVel = 6;
_minYVel = 4;
_maxYVel = 6;
_numTargets = 2;
_difficulty = "hard";
_scoringControl.PointsForEachDuck = 2500;
_flapSpeed = new TimeSpan(0, 0, 0, 0, 100);
levelTimer.Duration = new TimeSpan(0, 0, 7);
ResetScoreAndShots();
_duckIcons.initDuckIcons();
_duckIcons.resetDuckIcons();
InitObjects();
}
The _duckIcons.initDuckIcons()
and _duckIcons.resetDuckIcons()
methods already exist since we already have the _duckIcons
object in our project. However, the ResetScoreAndShots()
and InitObjects()
methods do not.
The ResetScoreAndShots()
method is fairly straightforward. It resets the number of shots fired, the current score being stored, and the score being displayed in the _scoringControl
object.
The InitObjects()
method is more complex. This method is what sets up our objects for use in the game. This method starts out by initializing our _targets
array of Duck objects to the length determined by _numTargets
, and then using a for loop to generate the new Duck objects. Recall that _numTargets
was set in the button click events for the level,
private void ResetScoreAndShots()
{
_shotCounter.ShotsFired = 0;
_scoringControl.Score = 0;
_scoringControl.SetScore("0000000")
}
private void InitObjects()
{
_targets = new Duck[_numTargets];
for (var i = 0; i < _numTargets; i++)
{
_targets[i] = new Duck();
}
}
Iterate through the array of targets and assign the _flapSpeed, and use our random number generator to determine the minimum and maximum X and Y velocities for the target. Following that, randomize the start X and Y location for the object.
The StartX was a random number between 0 and 800, but we want the ducks to start out off screen, so we’ll perform a quick check here – if StartX is <400, we’ll start the duck on the left side of the screen by setting its position to 0 – the width of the duck object. Otherwise, we know it starts on the right side of the screen, so we’ll flip the X scale to turn the duck around, set the duck’s position off screen to the right, and make sure the X velocity is set to a negative number to cause the duck to fly to the left.
for (var i = 0; i < _targets.Length; i++)
{
_targets[i].duckFly.Duration = _flapSpeed;
_targets[i].VelY = _rand.Next(_minYVel, _maxYVel);
_targets[i].VelX = _rand.Next(_minXVel, _maxXVel);
_targets[i].StartY = _rand.Next(Convert.ToInt16(LayoutRoot.Height - 200));
_targets[i].StartX = _rand.Next(800);
if (_targets[i].StartX <= 400)
{
_targets[i].StartX = 0 - Convert.ToInt16(_targets[i].Width);
}
else
{
_targets[i].duckScale.ScaleX *= -1;
_targets[i].StartX = 801;
_targets[i].VelX *= -1;
}
Set up an event handler for the hitzones on the duck objects (we’ll write this method in a moment), and position the duck on screen based on the values we just generated. Then handle the assignment of the quacking sounds for the ducks. We don’t want each duck to have the same sound effect, so the first one gets assigned one sound, and the second one gets assigned a different sound. We know that our maximum number of targets is 2. If that number were to increase, this code would need to be refactored to change the sound for every other duck,
_targets[i].MouseLeftButtonDown += HitZoneMouseLeftButtonDown;
Canvas.SetLeft(_targets[i], _targets[i].StartX);
Canvas.SetTop(_targets[i], _targets[i].StartY);
if (i == 0)
{
_targets[i].SetQuack("sounds/duck01.wav", 5);
}
else
{
_targets[i].SetQuack("sounds/duck02.wav", 2);
}
_targetsOnScreen += 1;
duckCanvas.Children.Add(_targets[i]);
_targets[i].duckFly.Begin();
}
startPage.Visibility = Visibility.Collapsed;
readyCanvas.Visibility = Visibility.Visible;
The last check performed is to see if the player has selected the hard level. If so, we initialize the hard level by playing an audio quip. This is handled this way because the audio quip should only be played once, not every time targets are released on screen when the hard level is played. The audioTimer in this case delays the gameplay for 4 seconds, which is the time necessary for the audio to play. If we’re not in hard mode, we can start the readyMessage
timer that will throw us into the game loop.
if (_difficulty == "hard" && !_hardLevelInitialized)
{
FrameworkDispatcher.Update();
_kickAss.Play();
_hardLevelInitialized = true;
audioTimer.Duration = new TimeSpan(0, 0, 4);
audioTimer.Begin();
}
else
{
readyMessage.Begin();
}
}
Inside of the previous method, we set up an event handler to determine if our target was hit. The method starts out by checking to see if the shot counter is in reloading mode. If the shotgun is reloading, a target cannot be shot. If the gun is not reloading, we’ll increment the _ducksHit
variable, call out to the _scoringControl
object to update our score, call out to the _duckIcons
object to update our duckIcon
display, and increment our _totalTargetsHit
variable.
Following that, we need to determine which target was hit, so we capture that using sender. If our target object is not null, we’ll call the DuckShot()
method for that object, which as you recall, will send the duck into a death spiral. Close out this method by calling the CheckShotLimit()
method.
private void HitZoneMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (!_shotCounter.Reloading)
{
_ducksHit += 1;
_scoringControl.UpdateScore(_levelControl.Level);
_duckIcons.hitIcon();
_totalTargetsHit += 1;
var whichDuckIsHit = sender as Duck;
if (whichDuckIsHit != null) whichDuckIsHit.DuckShot();
CheckShotLimit();
}
}
The CheckShotLimit()
method, tracks how many shots have been fired against how many targets have been hit. If the number of shots fired is 3, and the player has not hit all of the targets on the screen, the game stops the level timer, shows the flyAwayCanvas (“Fly Away!”), sets each duck to fly away mode, and increments the _totalTargetsMissed
variable.
If all of the ducks on screen have been hit, the levelTimer is stopped, and a “Nice Shooting!” message is displayed.
private void CheckShotLimit()
{
if (_shotCounter.ShotsFired == 3 && _ducksHit < _numTargets)
{
levelTimer.Stop();
for (var i = 0; i < _targets.Length; i++)
{
if (!_targets[i].IsShot)
{
flyAwayCanvas.Visibility = Visibility.Visible;
_targets[i].FlyAway = true;
_totalTargetsMissed += 1;
}
}
}
if (_ducksHit == _targets.Length)
{
levelTimer.Stop();
niceShootingCanvas.Visibility = Visibility.Visible;
}
}
A lot of the game logic is in place at this point, but we worked away from the remaining event handlers in the constructor, so let’s get back to those by writing up the GameLoopCompleted
method. This method is called when the gameLoop timer expires. Because the gameLoop timer is an empty storyboard, it expires as soon as it is started, raising the Completed event. By restarting the timer, a running loop is created within the game.
The first part of this method physically moves the ducks on screen. Remember there are two animations at work with the ducks – one to make their wings flap, then the game loop that moves them on screen. This is done by calling the MoveDuck()
method for each duck. If a duck has been shot and is in its death spiral, a check is done to see if the duck has fallen off screen, and remove it if this is the case. The removal of objects is handled by the RemoveObjects()
method.
If no targets remain on screen, the game loop is stopped, and a call is made to the DoLevel() method (which we will also code up momentarily). Otherwise, we know our targets are still on screen and active, so we can restart the gameLoop timer.
The RemoveObjects()
method is called when we have a duck that needs to be removed from the game, either because it has been shot and fallen off screen, or because it was allowed to fly away. Notice that this method is passed an integer, which represents the object’s location within the _targets array.
This method begins with an if statement to see if the object has been shot. If not, a call is made to the missIcon()
method on the _duckIcons object. Following that, a check is made to see if the player has missed a total of 10 ducks or more. If so, the messaging timer is stopped, and a call is made to the GameOverMan()
method to end the game. Otherwise, the game play can continue, but the selected target is removed from play by calling the Remove()
duck method on the duck object, and the _targetsOnScreen
variable is decremented. The DoLevel()
method is called when the targets on screen have cleared out either from being shot, or by flying away. The first thing this method does is clear the children of the duckCanvas. Next, the levelTimer and gameLoop are stopped, and the flyAwayCanvas
is hidden. The _ducksHit
variable is reset, as is the _shotCounter
object.
private void GameLoopCompleted(object sender, EventArgs e)
{
for (var i = 0; i < _targets.Length; i++)
{
_targets[i].MoveDuck();
if (Canvas.GetTop(_targets[i]) >=
duckCanvas.Height && _targets[i].IsShot)
{
RemoveObjects(i);
}
if (_targets[i].FlyAway)
{
if (Canvas.GetLeft(_targets[i]) <=
-_targets[i].ActualWidth)
{
RemoveObjects(i);
}
if (Canvas.GetLeft(_targets[i]) >
duckCanvas.Width)
{
RemoveObjects(i);
}
}
}
if (_targetsOnScreen == 0)
{
gameLoop.Stop();
DoLevel();
}
else
{
gameLoop.Begin();
}
}
private void RemoveObjects(int i)
{
if (!_targets[i].IsShot)
{
_duckIcons.missIcon();
if (_duckIcons.missedCounter >= 10)
{
levelMessageTimer.Stop();
GameOverMan();
}
}
_targets[i].RemoveDuck();
_targetsOnScreen -= 1;
}
private void DoLevel()
{
duckCanvas.Children.Clear();
levelTimer.Stop();
gameLoop.Stop();
flyAwayCanvas.Visibility = Visibility.Collapsed;
_ducksHit = 0;
_shotCounter.ResetShots(0);
}
The following code increases the speed of the game play a bit if the player has gone through 10 targets. The game is sped up (speeded up? spud up?) regardless of whether or not the targets were all hit. The _totalTargetsHit
variable is reset for the next set of targets, and the _levelControl
object is incremented. The icon counter is reset, as is the look and feel of the icons themselves.
if (_duckIcons.iconCounter == 10)
{
_minXVel += 1;
_maxXVel += 1;
_minYVel += 1;
_maxYVel += 1;
_totalTargetsHit = 0;
_levelControl.Level += 1;
_duckIcons.iconCounter = 0;
_duckIcons.resetDuckIcons();
}
The method concludes with a little housekeeping. The _levelControl
is updated to display the current level, and any potential visible messaging canvas objects are collapsed. The “Ready!” message is displayed, and the levelMessageTimer
is started to prepare the player for the level.
_levelControl.SetMessageLevel();
flyAwayCanvas.Visibility = Visibility.Collapsed;
niceShootingCanvas.Visibility = Visibility.Collapsed;
readyCanvas.Visibility = Visibility.Visible;
levelMessageTimer.Begin();
The GameOverMan()
method is called when the end of the game is reached. This method clears the duckCanvas, stops messaging timers, and resets the _targetsOnScreen
and _hardLevelInitialized
variables. It then hides the messaging canvases before displaying the gameOverCanvas
and Play Again button.
private void GameOverMan()
{
duckCanvas.Children.Clear();
readyMessage.Stop();
flyAwayMessage.Stop();
levelMessageTimer.Stop();
levelTimer.Stop();
_targetsOnScreen = 0;
_hardLevelInitialized = false;
readyCanvas.Visibility = Visibility.Collapsed;
niceShootingCanvas.Visibility = Visibility.Collapsed;
flyAwayCanvas.Visibility = Visibility.Collapsed;
gameOverCanvas.Visibility = Visibility.Visible;
playAgain.Visibility = Visibility.Visible;
}
OK, back to wiring up our events! Next, we have the DuckCanvasMouseLeftbuttonDown
method. This method is straightforward – it checks to see if the shotgun is reloading. If not, it calls the FireShot()
method of the _shotCounter
objects, and then calls the CheckShotLimit()
method we wrote a few moments ago.
Next are the methods to handle the instructions. InstructionsShowClick
is called when the Instructions link is selected on the main screen. This method makes the two controls that contain the instructions visible, and then plays the openInstructionsStoryboard
. The instructionsCloseClick
method plays the closeInstructionsStoryboard
, and the CloseInstructionsStoryboardCompleted
event that is raised when the instructions are hidden simply sets the visibility of the instructions back to Collapsed.
The PlayAgainClick
method is called when a player selects the Play Again button after the game has ended. The purpose of this method is to reset all of the objects in the game to their default state in order to prepare the game to be played again,
private void DuckCanvasMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (!_shotCounter.Reloading)
{
_shotCounter.FireShot(_ducksHit, _difficulty);
CheckShotLimit();
}
}
private void InstructionsShowClick(object sender, RoutedEventArgs e)
{
Instructions.Visibility = Visibility.Visible;
stackPanelInstructions.Visibility = Visibility.Visible;
openInstructionsStoryboard.Begin();
}
private void InstructionsCloseClick(object sender, RoutedEventArgs e)
{
closeInstructionsStoryboard.Begin();
}
private void CloseInstructionsStoryboardCompleted(object sender, EventArgs e)
{
Instructions.Visibility = Visibility.Collapsed;
}
private void PlayAgainClick(object sender, RoutedEventArgs e)
{
gameLoop.Stop();
startPage.Visibility = Visibility.Visible;
gameOverCanvas.Visibility = Visibility.Collapsed;
for (var i = 0; i < _targets.Length; i++)
{
_targets[i].RemoveDuck();
duckCanvas.Children.Remove(_targets[i]);
}
_difficulty = "";
_levelControl.Level = 1;
_shotCounter.ShotsFired = 0;
_ducksHit = 0;
_totalTargetsHit = 0;
_totalTargetsMissed = 0;
_targetsOnScreen = 0;
_duckIcons.initDuckIcons();
_scoringControl.Score = 0;
_scoringControl.PointsForEachDuck = 0;
_scoringControl.SetScore("0000000");
}
Finally, we need event handlers for the various Completed events for the messaging in the game and the levelTimer that controls how long a player has to attempt their shots. The LevelTimerCompleted
method is called when a player has run out of time to attempt their shots. This method displays the “Fly Away!” messaging, and sets the FlyAway flag for any targets to true in addition to incrementing the _totalTargetsMissed
variable.
The ReadyMessageCompleted
method calls after the game screen has been prepped, and the player is given a head’s up that the level is about to begin (“Ready!”). This method hides the messaging and begins the timer that controls how long the player has to take their shots. It begins the background ambient sound, and starts the Quack sound effect for each target on the level. Finally, the gameLoop is started to begin the game play.
private void LevelMessageTimerCompleted(object sender, EventArgs e)
{
niceShootingCanvas.Visibility = Visibility.Collapsed;
InitObjects();
}
private void AudioTimerCompleted(object sender, EventArgs e)
{
readyMessage.Begin();
}
private void LevelTimerCompleted(object sender, EventArgs e)
{
for (var i = 0; i < _targets.Length; i++)
{
if (!_targets[i].IsShot)
{
flyAwayCanvas.Visibility = Visibility.Visible;
_targets[i].FlyAway = true;
_totalTargetsMissed += 1;
}
}
private void ReadyMessageCompleted(object sender, EventArgs e)
{
readyCanvas.Visibility = Visibility.Collapsed;
levelTimer.Begin();
FrameworkDispatcher.Update();
_fieldSound.Play();
for (var i = 0; i < _targets.Length; i++)
{
_targets[i].Quack();
}
gameLoop.Begin();
}