Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / XAML

Metro: Shuffle

4.83/5 (26 votes)
11 Jul 2012CPOL5 min read 58.2K   2.4K  
A Metro tile puzzle game

Image 1 

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.

Image 2 

The bottom app bar contains a single button which the user can tap/click to shuffle the tiles.

Image 3 

Shuffle button in bottom app bar

The top app bar contains buttons for changing the application's look.

Image 4 

Theme buttons in top app bar

Image 5

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.

Image 6

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.

Image 7

Selecting an image using the file picker

Image 8

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

Image 9

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.

VB.NET
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.
VB.NET
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)

    ' Vertical movement
    If (absVChange > absHChange) Then
        ' Down
        If (vChange > 0) Then
            drctn = Direction.Down
            MoveTileDownwards(e)
        Else ' Up
            drctn = Direction.Up
            MoveTileUpwards(e)
        End If
        ' Horizontal movement
    ElseIf (absHChange > absVChange) Then
        ' Right
        If (hChange > 0) Then
            drctn = Direction.Right
            MoveTileRight(e)
        Else
            ' Left
            drctn = Direction.Left
            MoveTileLeft(e)
        End If
    End If
    KeepInBounds()
    FullTileBlock()
    PartialTileBlock()
    PartialPlusFullTileBlock()
End Sub

The MoveTileDownwards() method does as its name suggests,

VB.NET
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,

VB.NET
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.

Image 10

VB.NET
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 Pages, which are the equivalent of Windows 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.

VB.NET
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.

VB.NET
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 AppBars are defined as follows in MainPage's XAML markup,

XML
<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,

VB.NET
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 Storyboards to change the Fill property of two Paths.

XML
<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 Storyboards that are used to change the theme are defined in the Resources section of the Page.

XML
<Page.Resources>
    ...
    <!-- Storyboards for themes -->
    <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,

VB.NET
''' <summary>
''' Sets the user's preferred theme and writes the user's preference to
''' the app's settings.
''' </summary>
''' <param name="theme">User's preferred theme</param>
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
    ' Write setting
    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.

VB.NET
''' <summary>
''' Checks whether the user has set a preferred theme and sets
''' it when the app is loaded.
''' </summary>
Private Sub LoadTheme()
    Dim localSettings As ApplicationDataContainer = ApplicationData.Current.LocalSettings

    If (localSettings.Containers.ContainsKey("ThemeContainer")) Then
        ' Read setting
        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.

VB.NET
''' <summary>
''' Sets the app's wallpaper when the user selects an image file.
''' </summary>
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

        ' Store the file
        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.

VB.NET
''' <summary>
''' Sets the app's wallpaper when the application is loaded.
''' </summary>
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)