Introduction
Shuffle is a tile puzzle game where the objective is to arrange the numbered tiles in numerical order. Since this is a Metro application there's a huge focus on touch interaction so as to emulate a real tile puzzle game. The user has to slide the tiles into place and moving one tile can cause another tile to move e.g. in the screenshot above moving tile 3 downwards will also resort in tile 2 moving downwards.
Shuffle
Shuffle contains top and bottom app bars with buttons for carrying out various tasks.
The bottom app bar contains a single button which the user can tap/click to shuffle the tiles.
Shuffle button in bottom app bar
The top app bar contains buttons for changing the application's look.
Theme buttons in top app bar
Wallpaper buttons in top app bar
The colored buttons on the left of the top app bar can be used to change the application's theme e.g. clicking/tapping on the green button changes the application's theme to green.
The buttons on the right of the top app bar can be used to set or remove the application's wallpaper. Clicking/tapping on the Set Wallpaper button displays the file picker which enables the user to select an image file to use as the app's wallpaper.
Selecting an image using the file picker
Shuffle with wallpaper
Setting the wallpaper does not override the app's theme. When the user sets the theme or wallpaper it will be loaded the next time the user opens the app.
GameTile
Each tile is a UserControl and contains a Write Only property for setting the Text
property of a TextBlock
that displays a tile's number.
Public WriteOnly Property TileNumber As Double
Set(value As Double)
NumberTextBlock.Text = value
End Set
End Property
The UserControl also contains a
Thumb
control whose
DragDelta
event handler deals with the movement of the tile.
Private Sub GameTileThumb_DragDelta(ByVal sender As Object, ByVal e As DragDeltaEventArgs) _
Handles GameTileThumb.DragDelta
vChange = e.VerticalChange
hChange = e.HorizontalChange
absVChange = Math.Abs(e.VerticalChange)
absHChange = Math.Abs(e.HorizontalChange)
If (absVChange > absHChange) Then
If (vChange > 0) Then
drctn = Direction.Down
MoveTileDownwards(e)
Else
drctn = Direction.Up
MoveTileUpwards(e)
End If
ElseIf (absHChange > absVChange) Then
If (hChange > 0) Then
drctn = Direction.Right
MoveTileRight(e)
Else
drctn = Direction.Left
MoveTileLeft(e)
End If
End If
KeepInBounds()
FullTileBlock()
PartialTileBlock()
PartialPlusFullTileBlock()
End Sub
The MoveTileDownwards()
method does as its name suggests,
Private Sub MoveTileDownwards(ByVal e As DragDeltaEventArgs)
y = Canvas.GetTop(Me)
x = Canvas.GetLeft(Me)
If (x = LOWER_BOUND) OrElse (x = 130) OrElse (x = UPPER_BOUND) Then
Canvas.SetTop(Me, (y + SHIFT))
MoveTileBelow(e)
End If
End Sub
MoveTileBelow()
moves any tile that is positioned directly below the tile being shifted,
Private Sub MoveTileBelow(ByVal e As DragDeltaEventArgs)
For Each tile As GameTile In parentCanvas.Children
tl_X = Canvas.GetLeft(tile)
tl_Y = Canvas.GetTop(tile)
If (tl_X = x) And (tl_Y = (y + TILE_HEIGHT)) Then
tile.GameTileThumb_DragDelta(Nothing, e)
Exit Sub
End If
Next
End Sub
When a tile is moving it checks whether there's a tile blocking its path. PartialTileBlock()
checks whether there is a tile that is partially in its path e.g. in the image below tile 7 will prevent tile 3 from being move upwards.
Private Sub PartialTileBlock()
Select Case drctn
Case Direction.Up
For Each tile As GameTile In parentCanvas.Children
tX = Canvas.GetLeft(tile)
tY = Canvas.GetTop(tile)
If (tY < y) AndAlso (tY = (y - TILE_HEIGHT)) AndAlso (tX > x) AndAlso (tX < (x + TILE_WIDTH)) Then
Canvas.SetTop(Me, tY + TILE_HEIGHT)
Exit Sub
ElseIf (tY < y) AndAlso (tY = (y - TILE_HEIGHT)) AndAlso (tX < x) AndAlso ((tX + TILE_WIDTH) > x) Then
Canvas.SetTop(Me, tY + TILE_HEIGHT)
Exit Sub
End If
Next
Case Direction.Down
For Each tile As GameTile In parentCanvas.Children
tX = Canvas.GetLeft(tile)
tY = Canvas.GetTop(tile)
If (tY > y) AndAlso (tY = (y + TILE_HEIGHT)) AndAlso (tX > x) AndAlso (tX < (x + TILE_WIDTH)) Then
Canvas.SetTop(Me, tY - TILE_HEIGHT)
Exit Sub
ElseIf (tY > y) AndAlso (tY = (y + TILE_HEIGHT)) AndAlso (tX < x) AndAlso ((tX + TILE_WIDTH) > x) Then
Canvas.SetTop(Me, tY - TILE_HEIGHT)
Exit Sub
End If
Next
Case Direction.Left
For Each tile As GameTile In parentCanvas.Children
tX = Canvas.GetLeft(tile)
tY = Canvas.GetTop(tile)
If (tX < x) AndAlso (tX = (x - TILE_HEIGHT)) AndAlso (tY > y) AndAlso (tY < (y + TILE_WIDTH)) Then
Canvas.SetLeft(Me, tX + TILE_HEIGHT)
Exit Sub
ElseIf (tX < x) AndAlso (tX = (x - TILE_HEIGHT)) AndAlso (tY < y) AndAlso ((tY + TILE_WIDTH) > y) Then
Canvas.SetLeft(Me, tX + TILE_HEIGHT)
Exit Sub
End If
Next
Case Direction.Right
For Each tile As GameTile In parentCanvas.Children
tX = Canvas.GetLeft(tile)
tY = Canvas.GetTop(tile)
If (tX > x) AndAlso (tX = (x + TILE_HEIGHT)) AndAlso (tY > y) AndAlso (tY < (y + TILE_WIDTH)) Then
Canvas.SetLeft(Me, tX - TILE_HEIGHT)
Exit Sub
ElseIf (tX > x) AndAlso (tX = (x + TILE_HEIGHT)) AndAlso (tY < y) AndAlso ((tY + TILE_WIDTH) > y) Then
Canvas.SetLeft(Me, tX - TILE_HEIGHT)
Exit Sub
End If
Next
End Select
End Sub
MainPage
Metro apps make use of Page
s, which are the equivalent of Window
s in desktop applications. Shuffle contains only one page, MainPage
. MainPage
contains a two-dimensional array with the 9 possible initial coordinates of the 8 tiles that are displayed.
Private TileCoords()() As Double = New Double(8)() { _
New Double() {0, 0}, New Double() {130, 0}, New Double() {260, 0}, _
New Double() {0, 130}, New Double() {130, 130}, New Double() {260, 130}, _
New Double() {0, 260}, New Double() {130, 260}, New Double() {260, 260}}
The LoadTiles()
method is called when MainPage
is loaded and populates a layout container, of type Canvas
, with 8 tiles.
Private Sub LoadTiles()
Dim rnd As New Random
Dim n As Integer = 1
Do
num = rnd.Next(0, 9)
If (rndList.Contains(num) <> True) Then
rndList.Add(num)
End If
Loop Until rndList.Count = 8
For Each i As Integer In rndList
x = TileCoords(i)(0)
y = TileCoords(i)(1)
gameTile = New GameTile
gameTile.TileNumber = n
Canvas.SetLeft(gameTile, x)
Canvas.SetTop(gameTile, y)
GameCanvas.Children.Add(gameTile)
n += 1
Next
End Sub
AppBar
The AppBar
is a toolbar for displaying application-specific commands and tools. It is hidden by default, and is shown or dismissed when the user swipes from the edge of the screen. An app bar can appear at the top or bottom of the page, or both. It is assigned to a Page
's TopAppBar
or BottomAppBar
property.
Shuffle's AppBar
s are defined as follows in MainPage
's XAML markup,
<Page.TopAppBar>
<AppBar x:Name="tpAppBar" Padding="10,0,10,0" Height="76">
<Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<Button x:Name="GreenButton" Background="#FF03A018" Margin="20,0,20,0" Width="80" Height="40"/>
<Button x:Name="RedButton" Background="#FFBF0303" Margin="0,0,20,0" Width="80" Height="40"/>
<Button x:Name="BlueButton" Background="#FF0C84E8" Margin="0,0,20,0" Width="80" Height="40"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="SetWallpaperButton" Content="Set Wallpaper" Margin="0,0,20,0"/>
<Button x:Name="RemoveWallpaperButton" Content="Remove Wallpaper" Margin="0,0,15,0"/>
</StackPanel>
</Grid>
</AppBar>
</Page.TopAppBar>
...
<Page.BottomAppBar>
<AppBar x:Name="btmAppBar" Padding="10,0,10,0" Height="76">
<Grid>
<Button x:Name="ShuffleButton" Content="Shuffle" HorizontalAlignment="Center"/>
</Grid>
</AppBar>
</Page.BottomAppBar>
Shuffling
Clicking/tapping the Shuffle button calls the ShuffleTiles()
method,
Private Sub ShuffleTiles()
rndList.Clear()
GameCanvas.Children.Clear()
LoadTiles()
End Sub
Setting the Theme
Unfortunately you can't refer to a DynamicResource
in Metro. This creates a rather tricky scenario when it comes to skinning/theming a Metro app. In Shuffle I change the value of the Source
property of an Image
that forms the background of the app and make use of some custom Storyboard
s to change the Fill
property of two Path
s.
<Grid>
<Image x:Name="BackgroundImage" Source="Images/BackgroundBlue.png" Stretch="UniformToFill" Margin="0"/>
<Image x:Name="WallpaperImage" Stretch="UniformToFill" Margin="0"/>
<Grid Name="GameGrid">
<Path x:Name="OuterEdge" Fill="{StaticResource OuterEdgeBrush}"
Stretch="Fill" Width="396" Height="396" Data="..."/>
<Path x:Name="InnerSurface" Fill="#FF005170"
Stretch="Fill" StrokeLineJoin="Round" Stroke="{x:Null}" Data="..."
Height="390" Width="390"/>
...
</Grid>
</Grid>
The Storyboard
s that are used to change the theme are defined in the Resources
section of the Page
.
<Page.Resources>
...
<Storyboard x:Name="BlueThemeStoryboard">
<ColorAnimation Storyboard.TargetName="OuterEdge"
Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)"
To="#FF001F2E" Duration="00:00:00.1" EnableDependentAnimation="True"/>
<ColorAnimation Storyboard.TargetName="OuterEdge"
Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)"
To="#FF003953" Duration="00:00:00.1" EnableDependentAnimation="True"/>
<ColorAnimation Storyboard.TargetName="OuterEdge"
Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)"
To="#FF0075AC" Duration="00:00:00.1" EnableDependentAnimation="True"/>
<ColorAnimation Storyboard.TargetName="InnerSurface"
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
To="#FF005170" Duration="00:00:00.1"/>
</Storyboard>
<Storyboard x:Name="GreenThemeStoryboard">
<ColorAnimation Storyboard.TargetName="OuterEdge"
Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)"
To="#FF042E00" Duration="00:00:00.1" EnableDependentAnimation="True"/>
<ColorAnimation Storyboard.TargetName="OuterEdge"
Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)"
To="#FF085300" Duration="00:00:00.1" EnableDependentAnimation="True"/>
<ColorAnimation Storyboard.TargetName="OuterEdge"
Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)"
To="#FF10AC00" Duration="00:00:00.1" EnableDependentAnimation="True"/>
<ColorAnimation Storyboard.TargetName="InnerSurface"
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
To="#FF0A7000" Duration="00:00:00.1"/>
</Storyboard>
<Storyboard x:Name="RedThemeStoryboard">
<ColorAnimation Storyboard.TargetName="OuterEdge"
Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)"
To="#FF2E0000" Duration="00:00:00.1" EnableDependentAnimation="True"/>
<ColorAnimation Storyboard.TargetName="OuterEdge"
Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)"
To="#FF530000" Duration="00:00:00.1" EnableDependentAnimation="True"/>
<ColorAnimation Storyboard.TargetName="OuterEdge"
Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)"
To="#FFAC0000" Duration="00:00:00.1" EnableDependentAnimation="True"/>
<ColorAnimation Storyboard.TargetName="InnerSurface"
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
To="#FF700000" Duration="00:00:00.1"/>
</Storyboard>
</Page.Resources>
In Metro not all custom animations run by default. Animations that can have a performance impact must be enabled before they run. These types of animation are referred to as dependent animations. To enable them their EnableDependentAnimation
property must be set to True
. Notice that some of the animation objects defined in the XAML markup above have this property set to True
, specifically those targeting the Path
with a GradientBrush
.
The theme can now be set by calling the SetTheme()
method,
Private Sub SetTheme(ByVal theme As String)
Dim localSettings As ApplicationDataContainer = ApplicationData.Current.LocalSettings
localSettings.CreateContainer("ThemeContainer", ApplicationDataCreateDisposition.Always)
Dim bmp As New BitmapImage
Select Case theme
Case "Blue"
bmp.UriSource = New Uri(Me.BaseUri, "/Images/BackgroundBlue.png")
BlueThemeStoryboard.Begin()
Case "Green"
bmp.UriSource = New Uri(Me.BaseUri, "/Images/BackgroundGreen.png")
GreenThemeStoryboard.Begin()
Case "Red"
bmp.UriSource = New Uri(Me.BaseUri, "/Images/BackgroundRed.png")
RedThemeStoryboard.Begin()
End Select
localSettings.Containers("ThemeContainer").Values("ThemeSetting") = theme
BackgroundImage.Source = bmp
End Sub
Notice that in the method above I'm creating an application setting called ThemeSetting that is held in a container named ThemeContainer. Once this setting is written to it is used to load the user's preferred theme the next time the application is launched.
Private Sub LoadTheme()
Dim localSettings As ApplicationDataContainer = ApplicationData.Current.LocalSettings
If (localSettings.Containers.ContainsKey("ThemeContainer")) Then
Dim theme As String = CStr(localSettings.Containers("ThemeContainer").Values("ThemeSetting"))
SetTheme(theme)
End If
End Sub
Setting the Wallpaper
Setting the wallpaper is done by calling the SetWallpaper()
method.
Private Async Sub SetWallpaper()
Dim openPicker As New FileOpenPicker
With openPicker
.ViewMode = PickerViewMode.Thumbnail
.SuggestedStartLocation = PickerLocationId.PicturesLibrary
.FileTypeFilter.Add(".png")
.FileTypeFilter.Add(".jpg")
.FileTypeFilter.Add(".jpeg")
End With
Dim file As StorageFile = Await openPicker.PickSingleFileAsync()
If (file IsNot Nothing) Then
Dim stream As IRandomAccessStream = Await file.OpenAsync(FileAccessMode.Read)
Dim bmp As New BitmapImage
bmp.SetSource(stream)
WallpaperImage.Source = bmp
StorageApplicationPermissions.FutureAccessList.Clear()
Dim token As String = StorageApplicationPermissions.FutureAccessList.Add(file)
End If
End Sub
The method above launches the file picker. The file picker is an interface that lets the user pick one or more files for an app to open. Since here the file picker is used to display pictures, that the user can set as the wallpaper, the ViewMode
of the FileOpenPicker
object is set to PickerViewMode.Thumbnail
. The SuggestedStartLocation
property specifies that the Pictures library is the first place the file picker should check for pictures, when it is launched for the first time. (On subsequent occassions when the user launches the file picker it will start with the last directory the user checked). I also specify the file types the file picker should deal with by adding them to the list returned by the FileTypeFilter
property.
When all the necessary file picker properties have been set the file picker is displayed by calling Await FileOpenPicker.PickSingleFileAsync()
.
Once a file has been opened I need to keep track of the file so that it can be used to display the wallpaper the next time the app is launched. To do this I add the file to the FutureAccessList
. Adding a file to the FutureAccessList
returns a token, which is a string value that uniqely identifies the file in the list. To access the file on the next app launch I could store the token in the app's settings but there is another option, which I'll explain in the next section.
Loading the Wallpaper
When the application is loaded it checks whether the user had specified a wallpaper image. This is done by calling the LoadWallpaper()
method.
Private Async Sub LoadWallpaper()
Dim n As Integer = StorageApplicationPermissions.FutureAccessList.Entries.Count
If (n > 0) Then
Dim firstToken As String = StorageApplicationPermissions.FutureAccessList.Entries.First.Token
Dim retrievedFile As StorageFile = Await StorageApplicationPermissions.FutureAccessList.GetFileAsync(firstToken)
Dim stream As IRandomAccessStream = Await retrievedFile.OpenAsync(FileAccessMode.Read)
Dim bmp As New BitmapImage
bmp.SetSource(stream)
WallpaperImage.Source = bmp
End If
End Sub
To set the wallpaper I retrieve the file that was added to the FutureAccessList
. This is done by using the token of the first entry in the list. This application's FutureAccessList
will have only one entry if a file was added to it.
Conclusion
In the application I've made use of several Metro features whose guidelines have been defined by Microsoft. The following is a list containing links to guidelines that relate to some of the utilized features;
History
- 4th, July 2012: Initial post
- 9th, July 2012: Added theme and wallpaper feature