Darts (Gesture Listener & Flick Gesture)

0
268

Darts is an addictive and slick game that provides a great reason to demonstrate support for the flick gesture (a single-finger, rapid swipe). To make detecting flicks easy, this app leverages a feature in the Silverlight for Windows Phone Toolkit called the gesture listener.

In this one-player game, you throw a dart by flicking the screen. The direction and strength of your flick determines the angle and distance of the throw. Depending on where the dart lands, you could earn 0–60 points with each throw. Try to get the highest score possible with 20 darts!

This game has a lot of potential for easy-to-add enhancements. For some examples, check out my “Hooked on Darts” version of this app in the Windows Phone Marketplace.

Detecting Gestures

The three preceding apps in this part of the book leverage touch points in a straightforward fashion. Often, however, you want to support standard gestures such as flicks, pinch-and-stretch zooming, and rotation. Detecting such gestures based on raw touch point data from the FrameReported event would be a major undertaking.

Fortunately, two options exist that make detecting gestures much easier: Silverlight’s manipulation events and the Silverlight for Windows Phone Toolkit’s gesture listener.

Manipulation Events

Silverlight defines three manipulation events on every UI element:

  • ManipulationStarted
  • ManipulationDelta
  • ManipulationCompleted

These events combine the information from all active touch points and package the data in an easy-to-consume form. ManipulationStarted gets raised when the TouchDown action happens for the first finger. ManipulationDelta gets raised for each TouchMove. ManipulationCompleted gets raised after TouchUp is reported for all fingers.

The ManipulationDelta event gives you information about how the relevant element is expected to be translated or scaled based on a one-finger panning gesture or a two-finger pinch or stretch. The ManipulationDelta and ManipulationCompleted events also provide translation and scale velocities in both X and Y dimensions.

Gesture Listener

Although the manipulation events are helpful for detecting gestures, the Silverlight for Windows Phone Toolkit’s gesture listener raises the bar for simplicity and ease-of-use. In fact, all the remaining apps in this part of the book use the gesture listener instead of the manipulation events.

The GestureListener class exposes nothing but 12 events for detecting 6 types of gestures:

  • Tap (with the Tap event)
  • Double Tap (with the DoubleTap event)
  • Touch & Hold (with the Hold event)
  • Flick (with the Flick event)
  • Pinch & Stretch (with the PinchStarted, PinchDelta, and PinchCompleted events)
  • Drag (with the DragStarted, DragDelta, and DragCompleted events)

The remaining two events— GestureBegin and GestureCompleted— are generic events raised for any gestures, analogous to the ManipulationStarted and ManipulationCompleted events.

The Tap event is similar to MouseLeftButtonUp, except that it only gets raised if there’s no movement between the finger press and release.

A gesture listener can be attached to any element with the GestureService class, which exposes a GestureListener attachable property. For example, the following XAML detects a touch & hold gesture (pressing an element for one second) on a text block:

[code]

<TextBlock …>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener Hold=”GestureListener_Hold”/>
</toolkit:GestureService.GestureListener>
</TextBlock>

[/code]

The gesture listener can cause performance problems!

As of the February 2011 version of the Silverlight for Windows Phone Toolkit, the gesture listener internally uses XNA’s TouchPanel class to detect gestures.However, this can cause performance problems and can even interfere with the input received by other Silverlight controls.Therefore, until these issues are resolved in a future version of the toolkit, you should avoid using the gesture listener (or the XNA TouchPanel) in traditional apps with typical controls.Darts and the other apps in this book that use the gesture listener do not have such problems.

The User Interface

Darts has a main page, an instructions page, and an about page. The latter two pages aren’t interesting and therefore aren’t shown in this chapter, but Listing 40.1 contains the XAML for the main page. The main page has an “intro panel” canvas that does triple duty as a welcome screen, a game-over screen, as well as a paused screen. Underneath the intro panel is a canvas with all the game elements. Figure 40.1 shows the intro panel in its initial state and then the appearance of the page once the user flicks the screen to start the game.

The main page overlays an intro panel on top of the game canvas.
FIGURE 40.1 The main page overlays an intro panel on top of the game canvas.

LISTING 40.1 MainPage.xaml—The User Interface for Darts’Main Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
FontFamily=”Segoe WP Black” FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”White” SupportedOrientations=”Portrait”>
<!– Allow the gesture anywhere on the page –>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener Flick=”GestureListener_Flick”/>
</toolkit:GestureService.GestureListener>
<!– Add several animations to the page’s resource dictionary –>
<phone:PhoneApplicationPage.Resources>
<!– The dart-throwing animation, adjusted from code-behind based on
the angle and velocity of the flick –>
<Storyboard x:Name=”DartThrowStoryboard”
Completed=”DartThrowStoryboard_Completed”>
<!– Animate the horizontal position –>
<DoubleAnimation x:Name=”DartXAnimation”
Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”TranslateX” Duration=”0:0:.5”>
<DoubleAnimation.EasingFunction>
<CircleEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Animate the vertical position –>
<DoubleAnimation x:Name=”DartYAnimation”
Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”TranslateY” Duration=”0:0:.5”>
<DoubleAnimation.EasingFunction>
<CircleEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Animate the horizontal scale –>
<DoubleAnimation x:Name=”DartScaleXAnimation”
Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”ScaleX” By=”1.2”
Duration=”0:0:.25” AutoReverse=”True”>
<DoubleAnimation.EasingFunction>
<CircleEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Animate the vertical scale –>
<DoubleAnimation x:Name=”DartScaleYAnimation”
Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”ScaleY” By=”4”
Duration=”0:0:.25” AutoReverse=”True”>
<DoubleAnimation.EasingFunction>
<CircleEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Leave the dart in its ending position for half a second before returning
it to the bottom of the page for another throw –>
<Storyboard x:Name=”DartDelayStoryboard” Duration=”0:0:.5”
Completed=”DartDelayStoryboard_Completed”/>
<!– Move the dart back to the starting position –>
<Storyboard x:Name=”DartReturnStoryboard”>
<DoubleAnimation Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”TranslateX” To=”0”
Duration=”0:0:.2”>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=”EaseInOut”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”TranslateY” To=”0”
Duration=”0:0:.2”>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=”EaseInOut”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Slide the dart completely off the bottom of the screen –>
<Storyboard x:Name=”DartOffScreenStoryboard”>
<DoubleAnimation Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”TranslateY” To=”130”
Duration=”0:0:.6”>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=”EaseInOut”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Slide the intro panel off the top of the screen –>
<Storyboard x:Name=”IntroOffStoryboard”
Completed=”IntroOffStoryboard_Completed”>
<DoubleAnimation Storyboard.TargetName=”IntroPanelTransform”
Storyboard.TargetProperty=”TranslateY” To=”-800”
Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=”EaseInOut”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Slide the intro panel back onto the screen from up above –>
<Storyboard x:Name=”IntroOnStoryboard”>
<DoubleAnimation Storyboard.TargetName=”IntroPanelTransform”
Storyboard.TargetProperty=”TranslateY” To=”0”
Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=”EaseInOut”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
… more storyboards …
<!– Animate in (then out) a message, such as the # of points just earned –>
<Storyboard x:Name=”ShowMessageStoryboard”
Storyboard.TargetName=”MessageTransform”>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=”TranslateY”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”800”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.5” Value=”430”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<DiscreteDoubleKeyFrame KeyTime=”0:0:2.5” Value=”430”/>
<EasingDoubleKeyFrame KeyTime=”0:0:3” Value=”-800”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<!– The application bar, with two buttons and one menu item –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar BackgroundColor=”#5F3000” ForegroundColor=”White”
Opacity=”.8”>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBarIconButton Text=”delete”
IconUri=”/Shared/Images/appbar.delete.png” Click=”DeleteButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid Background=”#5F3000”>
<Grid.Clip>
<RectangleGeometry Rect=”0,0,480,800”/>
</Grid.Clip>
<!– The game canvas –>
<Canvas>
<!– The dartboard background –>
<Ellipse Width=”480” Height=”480” Fill=”#8000”/>
<!– The dartboard segments –>
<Canvas x:Name=”DartboardSegments”>
<!– Vector art created in Expression Blend. Each region with a different
point value is a distinct Path element (82 total) –>
<Path x:Name=”D1” Data=”M268.269483095,61.50827908C287.000166325,
64.476988515,305.148564115,70.37132529,322.042831735,
78.980320525L240.000027675,240.000437465z” Fill=”Green”
Canvas.Left=”239.417” Canvas.Top=”60.925” Stretch=”Fill”
Width=”83.209” Height=”179.658”/>
… 81 more paths …
</Canvas>
<!– The dartboard numbers –>
<Canvas Opacity=”.5”>
<TextBlock Text=”20” Canvas.Left=”218” Canvas.Top=”1”
FontFamily=”Segoe WP” FontSize=”40”/>
… 19 more text blocks …
</Canvas>
<!– Each time a dart lands, a little black “hole” is placed in this
canvas to mark the position –>
<Canvas x:Name=”HolesCanvas”/>
<!– The dart –>
<Image x:Name=”DartImage” Canvas.Top=”675” Canvas.Left=”202”
Width=”100”>
<Image.RenderTransform>
<CompositeTransform x:Name=”DartTransform” TranslateY=”130”/>
</Image.RenderTransform>
</Image>
</Canvas>
<!– A display for the current score and # of darts remaining –>
<StackPanel x:Name=”ScorePanel” Visibility=”Collapsed”
VerticalAlignment=”Bottom” HorizontalAlignment=”Right”
Margin=”18,64”>
<TextBlock Text=”SCORE” Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”ScoreTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-20,0,0”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”ScoreTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock Text=”DARTS REMAINING”
Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”DartsRemainingTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-20,0,0”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”DartsRemainingTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
</StackPanel>
<!– The welcome/paused/game-over screen –>
<Grid x:Name=”IntroPanel”>
<Grid.RenderTransform>
<CompositeTransform x:Name=”IntroPanelTransform”/>
</Grid.RenderTransform>
<Rectangle Fill=”#5F3000” Opacity=”.6”/>
<!– The title and subtitle –>
<TextBlock Text=”DARTS” Margin=”-19,200,0,0” FontSize=”140”>
<TextBlock.RenderTransform>
<RotateTransform Angle=”-20”/>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock x:Name=”Subtitle” Text=”DO A PRACTICE FLICK TO BEGIN”
Margin=”40,340,-40,0” FontSize=”27”>
<TextBlock.RenderTransform>
<RotateTransform Angle=”-20”/>
</TextBlock.RenderTransform>
</TextBlock>
<!– A display for the best score, average score, and # of games –>
<StackPanel x:Name=”StatsPanel” VerticalAlignment=”Bottom”
HorizontalAlignment=”Right” Margin=”18,64”>
<TextBlock Text=”BEST SCORE” Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”BestScoreTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-20,0,0”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”BestScoreTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock x:Name=”AvgScoreHeaderTextBlock” Text=”AVG SCORE”
Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”AvgScoreTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-20,0,0”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”AvgScoreTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
</StackPanel>
</Grid>
<!– An animated message –>
<TextBlock x:Name=”MessageTextBlock” RenderTransformOrigin=”.5,.5”
FontWeight=”Bold” HorizontalAlignment=”Center” FontSize=”120”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”MessageTransform” TranslateY=”800”/>
</TextBlock.RenderTransform>
</TextBlock>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • To detect the flick gesture, this app handles the Flick event of a gesture listener attached to the whole page.
  • This page contains lots of storyboards for smoothly transitioning elements on and off the screen. The most important one is DartThrowStoryboard, which makes the dart travel based on the flick data. Its first two animations, which alter the dart transform’s TranslateX and TranslateY values, have their By values set in code-behind. The second two animations, which alter ScaleX and ScaleY, make the dart appear to jump out of and then back into the screen in a pseudo-3D arc motion. ScaleY is increased much more than ScaleX to make the image appear to flatten out, because the image already has Y-rotation perspective applied to it. The next section shows how the in-flight dart appears.
  • This app’s dartboard is one of the more interesting chunks of XAML in the whole book. It consists of 82 path elements and 19 text blocks on top of an ellipse. (Although I usually type all my XAML by hand, I created this part in Expression Blend!) Representing each region with a unique score value as a distinct element makes it easy to figure out how many points to award the user when the dart lands. The code-behind performs hit testing to find the topmost element hit by the dart and then awards points accordingly.

    Why are there 82 paths? Because there are 4 for each of the 20 wedges (the outer ring that awards double points, the outer “normal” wedge, the inner ring that awards triple points, and then the inner “normal” wedge), plus the outer and inner bull’s-eyes. Figure 40.2 illustrates how these 82 elements stack to form the final dartboard, as if the elements were separated and viewed at a 3D perspective.

  • The DartImage element doesn’t have its Source assigned. This is done in codebehind, because the source is continually changed between three different images.
  • The root grid has a clip applied to avoid a far-flung dart from causing the screen to go blank. This happens whenever Silverlight tries to render an element that is too far away.
A visualization of the 82 distinct dartboard segments.
FIGURE 40.2 A visualization of the 82 distinct dartboard segments.

The Code-Behind

Listing 40.2 contains the code-behind for the main page.

LISTING 40.2 MainPage.xaml.cs—The Code-Behind for Darts’Main Page

[code]

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
The Code-Behind 885
{
// Persistent settings
Setting<int> bestScore = new Setting<int>(“BestScore”, 0);
Setting<double> avgScore = new Setting<double>(“AvgScore”, 0);
Setting<int> numGames = new Setting<int>(“NumGames”, 0);
// Current game state
int dartsRemaining;
int score;
// Three different dart images
BitmapImage dartStartImageSource =
new BitmapImage(new Uri(“Images/dartStart.png”, UriKind.Relative));
BitmapImage dartBigImageSource =
new BitmapImage(new Uri(“Images/dartBig.png”, UriKind.Relative));
BitmapImage dartSmallImageSource =
new BitmapImage(new Uri(“Images/dartSmall.png”, UriKind.Relative));
public MainPage()
{
InitializeComponent();
// Use the starting dart image
this.DartImage.Source = this.dartStartImageSource;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the persisted values
UpdateStatsLabels();
}
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
// If the game is in progress, pause it rather than exiting the app
if (this.IntroPanel.Visibility == Visibility.Collapsed)
{
this.Subtitle.Text =
“PAUSED.nFLICK TO RESUME THE GAME,nOR PRESS BACK TO EXIT.”;
ShowIntroPanel();
e.Cancel = true;
}
}
// The event handler for flicks
void GestureListener_Flick(object sender, FlickGestureEventArgs e)
{
if (this.IntroPanel.Visibility == Visibility.Visible)
{
// Start a new game if we’re out of darts
if (this.dartsRemaining == 0)
{
// Remove all holes and clear the tracking variables
this.HolesCanvas.Children.Clear();
this.dartsRemaining = 20;
this.score = 0;
// Animate the off-screen dart to the starting position
this.DartReturnStoryboard.Begin();
// Animate a message
this.MessageTextBlock.Text = “GO!”;
this.ShowMessageStoryboard.Begin();
}
// Remove the intro panel (for both new-game and unpause cases)
HideIntroPanel();
}
else
{
// First ensure that a dart throw is not in progress
if (this.DartThrowStoryboard.GetCurrentState() == ClockState.Active ||
this.DartDelayStoryboard.GetCurrentState() == ClockState.Active)
return;
// Throw the dart!
this.DartTransform.TranslateX = 0;
this.DartTransform.TranslateY = 0;
// Here is where the flick data is used
this.DartXAnimation.By = e.HorizontalVelocity / 10;
this.DartYAnimation.By = e.VerticalVelocity / 10;
// The animation scales the image up by as much as 4 vertically, so
// replace the image with a higher-resolution one
this.DartImage.Source = this.dartBigImageSource;
this.DartThrowStoryboard.Begin();
}
}
void IntroOffStoryboard_Completed(object sender, EventArgs e)
{
// The intro panel is now off-screen, so collapse it for better performance
this.IntroPanel.Visibility = Visibility.Collapsed;
}
void DartThrowStoryboard_Completed(object sender, EventArgs e)
{
// The dart has landed, so change the image to a small one
// that looks better that this scale (and is angled like it’s sticking into
// the wall)
this.DartImage.Source = this.dartSmallImageSource;
// Determine the exact point where the tip of the dart has landed
Point point = new Point(
// X: The tip isn’t quite centered in the image, hence the slight offset
Canvas.GetLeft(this.DartImage) + this.DartTransform.TranslateX
+ this.DartImage.Width / 2 – 11.5,
// Y: The tip is at the top edge of the image
Canvas.GetTop(this.DartImage) + this.DartTransform.TranslateY
);
// Place a “hole” where the tip of the dart landed
Ellipse hole = new Ellipse { Fill = new SolidColorBrush(Colors.Black),
Width = 5, Height = 5 };
Canvas.SetLeft(hole, point.X – hole.Width / 2);
Canvas.SetTop(hole, point.Y – hole.Height / 2);
this.HolesCanvas.Children.Add(hole);
// Calculate the score for this throw
int pointsEarned = DetermineScoreAtPoint(point);
// Update the game state and display
this.score += pointsEarned;
this.dartsRemaining–;
UpdateScoreLabels(pointsEarned > 0);
// Let the dart sit for a while before it animates back to the start
this.DartDelayStoryboard.Begin();
if (this.dartsRemaining > 0)
{
// Animate in the score message
this.MessageTextBlock.Text = pointsEarned.ToString();
this.ShowMessageStoryboard.Begin();
}
else
{
// Game over!
// Update the stats
double oldTotal = this.avgScore.Value * this.numGames.Value;
// New average
this.avgScore.Value = (oldTotal + score) / (this.numGames.Value + 1);
// New total number of games
this.numGames.Value++;
this.Subtitle.Text = “FINAL SCORE: “ + this.score + “. FLICK AGAIN!”;
if (this.score > this.bestScore.Value)
{
// New best score
this.bestScore.Value = this.score;
// Animate a best-score message
this.MessageTextBlock.Text = “BEST!”;
this.ShowMessageStoryboard.Begin();
}
ShowIntroPanel();
}
}
void DartDelayStoryboard_Completed(object sender, EventArgs e)
{
// Restore the image
this.DartImage.Source = this.dartStartImageSource;
// Move the dart to the starting position for the next throw,
// or off-screen if the game is over
if (this.dartsRemaining > 0)
this.DartReturnStoryboard.Begin();
else
this.DartOffScreenStoryboard.Begin();
}
int DetermineScoreAtPoint(Point p)
{
// Retrieve all elements inside DartboardSegments that intersect with p
foreach (UIElement element in
VisualTreeHelper.FindElementsInHostCoordinates(p,
this.DartboardSegments))
{
// Fortunately, the list of elements is ordered from top-to-bottom.
// We only care about the top-most element, so we can directly return a
// value based on the first item in this collection.
if (element == this.InnerBull)
return 50;
else if (element == this.OuterBull)
return 25;
else if (element is Path)
{
// The elements are named with a letter and a number.
// The letter is D for double score, T for triple score, or something
// else (A or B) for a normal region.
// The number is the score.
string name = (element as FrameworkElement).Name;
// Retrieve the score from the name
int score = int.Parse(name.Substring(1));
// Apply a multiplier, if applicable
if (name[0] == ‘D’)
score *= 2;
else if (name[0] == ‘T’)
score *= 3;
return score;
}
}
// No relevant element was hit
return 0;
}
void ShowIntroPanel()
{
UpdateStatsLabels();
this.IntroOnStoryboard.Begin();
this.IntroPanel.Visibility = Visibility.Visible;
this.ScorePanel.Visibility = Visibility.Collapsed;
this.ApplicationBar.IsVisible = true;
}
void HideIntroPanel()
{
UpdateScoreLabels(true);
this.IntroOffStoryboard.Begin();
this.ScorePanel.Visibility = Visibility.Visible;
this.ApplicationBar.IsVisible = false;
}
void UpdateStatsLabels()
{
if (this.numGames.Value > 0)
{
this.BestScoreTextBlock.Text = this.bestScore.Value.ToString();
this.AvgScoreTextBlock.Text = this.avgScore.Value.ToString(“0.#”);
if (this.numGames.Value == 1)
this.AvgScoreHeaderTextBlock.Text = “AVG SCORE (1 GAME)”;
else
this.AvgScoreHeaderTextBlock.Text = “AVG SCORE (“ + this.numGames.Value
+ “ GAMES)”;
}
else
{
this.BestScoreTextBlock.Text = “0”;
this.AvgScoreTextBlock.Text = “0”;
this.AvgScoreHeaderTextBlock.Text = “AVG SCORE”;
}
// Animate the textblocks out then in
this.SlideAvgScoreStoryboard.Begin();
this.SlideBestScoreStoryboard.Begin();
}
void UpdateScoreLabels(bool animateScore)
{
this.ScoreTextBlock.Text = this.score.ToString();
this.DartsRemainingTextBlock.Text = this.dartsRemaining.ToString();
// Animate the textblocks out then in
this.SlideDartsRemainingStoryboard.Begin();
if (animateScore)
this.SlideScoreStoryboard.Begin();
}
// Application bar handlers
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void DeleteButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to clear your scores?”,
“Delete history”, MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
this.numGames.Value = 0;
this.bestScore.Value = 0;
UpdateStatsLabels();
}
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Darts”, UriKind.Relative));
}
}
}

[/code]

  • Three dart image sources are created (once) and then assigned as DartImage’s Source value at various stages of the dart-throwing procedure. Figure 40.3 shows all three. The start image looks more vertical, whereas the big and small images have a lot more perspective. The small image is used while the dart is sticking into the dartboard (or wall). The big image is used only while the dart is in flight, because otherwise the up-scaling of the small image would produce a pixelated result. The combination of DartThrowStoryboard’s motion and the image-swapping produces a pretty realistic pseudo-3D dart motion demonstrated in Figure 40.4.
The three dart images are swapped in and out to provide the best-looking result.
FIGURE 40.3 The three dart images are swapped in and out to provide the best-looking result.
The dart’s flight path when it hits a double 3 (6 points).
FIGURE 40.4 The dart’s flight path when it hits a double 3 (6 points).
  • OnBackKeyPress is overridden to show a paused screen when the back key is pressed during a game, canceling the exiting of the app. When the intro panel is shown as a paused screen or game-over screen, the text under the “DARTS” title is updated to explain what is going on, as shown in Figure 40.5.
The intro screen subtitle updates to show that the game is paused or to show the final score.
FIGURE 40.5 The intro screen subtitle updates to show that the game is paused or to show the final score.

 

  • GestureListener_Flick is the flick handler that processes the data from FlickGestureEventArgs and adjusts the two relevant dartthrowing animations appropriately. The Flick event is described in the next section.
  • DetermineScoreAtPoint uses a static method called FindElementsInHostCoordinates on System.Windows.Media. VisualTreeHelper to find the topmost element underneath the passed-in point. (FindElementsInHostCoordinates also exposes an overload that accepts a rectangular region rather than a point.) Because of the naming scheme used by the dartboard elements, the hit element’s name can be converted into the correct point value.

Your game might fail marketplace certification if pressing the Back hardware button doesn’t pause the game!

The Windows Phone 7 Certification Requirements (http://go.microsoft.com/ ?linkid=9730558) state:

For games, when the Back button is pressed during gameplay, the game can choose to present a pause context menu or dialog or navigate the user to the prior menu screen. Pressing the Back button again while in a paused context menu or dialog closes the menu or dialog.

Although this makes the pausing behavior seem optional, I’ve had a game fail certification because it chose not to provide a pausing experience. (And this was a pool game in which pausing is meaningless!) Even if you can get away without doing this, it’s probably a good idea to have your app include this pausing behavior for consistency with user expectations for Windows Phone games.

VisualTreeHelper.FindElementsInHostCoordinates is the best way to determine what element or elements are underneath a specific point or region. Unlike approaches from previous chapters that involve mapping a point into an element’s coordinate space one element at a time, FindElementsInHostCoordinates examines all elements that are children of the passed-in element. (It also examines the passed-in element itself.)

The Flick Event

The FlickGestureEventArgs instance passed to the Flick event exposes the following properties:

  • Angle—A double value expressed in degrees
  • Direction—Either Horizontal or Vertical, revealing the primary direction of the flick
  • HorizontalVelocity—A double value expressing the horizontal magnitude of the 2D velocity vector in pixels per second
  • VerticalVelocity—A double value expressing the vertical magnitude of the 2D velocity vector in pixels per second

This app has no use for the Direction property, which is meant for elements whose motion should be locked to a single direction when the flick occurs. Instead, this app’s dart freely moves in whatever angle the flick reports, even if it’s completely horizontal or downward instead of upward.

Because Listing 40.2 applies the horizontal and vertical velocities as offsets to the dart’s position, it doesn’t even need to check the Angle property. The ratio between the two velocities effectively gives the appropriate angle. When set as offsets to the dart’s position, the velocity values are divided by 10 to give a distance range that works well for this app. This factor was arrived at by trial and error.

A flick event is not raised until the finger has broken contact with the screen. Before then, the finger motion raises drag (and other) events.

If you want to detect flicks without using the gesture listener (perhaps to avoid the problems discussed at the beginning of this chapter), you can use the ManipulationCompleted event defined on all UI elements.The data passed to its handlers includes a FinalVelocities property that is equivalent to the pair of HorizontalVelocity and VerticalVelocity properties exposed to Flick event handlers.

The Finished Product

Darts (Gesture Listener & Flick Gesture)