Metro: Shuffle






4.83/5 (26投票s)
一个 Metro 磁贴拼图游戏。
引言
Shuffle 是一款数字拼图游戏,目标是将数字方块按数字顺序排列。由于这是一款 Metro 应用,因此非常注重触摸交互,以模拟真实的方块拼图游戏。用户需要滑动方块到位,移动一个方块可能会导致另一个方块也移动,例如在上图的截图中,向下移动方块 3 也会导致方块 2 向下移动。
Shuffle
Shuffle 包含顶部和底部应用栏,带有用于执行各种任务的按钮。
底部应用栏包含一个按钮,用户可以点击该按钮来打乱方块。
顶部应用栏包含用于更改应用程序外观的按钮。
顶部应用栏左侧的彩色按钮可用于更改应用程序的主题,例如,点击绿色按钮会将应用程序主题更改为绿色。
顶部应用栏右侧的按钮可用于设置或删除应用程序的壁纸。点击“设置壁纸”按钮会显示文件选择器,让用户选择要用作应用壁纸的图像文件。
设置壁纸不会覆盖应用的背景。当用户设置主题或壁纸时,下次打开应用时将会加载它们。
GameTile
每个方块都是一个 UserControl,包含一个只写属性,用于设置显示方块编号的 TextBlock 的 Text 属性。
Public WriteOnly Property TileNumber As Double
Set(value As Double)
NumberTextBlock.Text = value
End Set
End Property
UserControl 还包含一个 Thumb 控件,其 DragDelta 事件处理程序负责处理方块的移动。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
MoveTileDownwards() 方法顾名思义,会向下移动方块。
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() 会移动被移动方块正下方的任何方块。
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
当一个方块移动时,它会检查是否有方块挡住了它的路径。PartialTileBlock() 检查是否有方块部分挡住了它的路径,例如,在下图所示的例子中,方块 7 会阻止方块 3 向上移动。
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 应用使用 Page,这相当于桌面应用程序中的 Window。Shuffle 只包含一个页面,即 MainPage。MainPage 包含一个二维数组,其中存储着 8 个方块显示的 9 个可能的初始坐标。
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}}
LoadTiles() 方法在 MainPage 加载时被调用,并将一个 Canvas 类型的布局容器填充 8 个方块。
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
AppBar 是一个用于显示应用程序特定命令和工具的工具栏。默认情况下它是隐藏的,当用户从屏幕边缘滑动时会显示或隐藏。应用栏可以出现在页面顶部或底部,或两者都有。它被分配给 Page 的 TopAppBar 或 BottomAppBar 属性。
Shuffle 的 AppBar 在 MainPage 的 XAML 标记中定义如下:
<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>
洗牌
点击 Shuffle 按钮会调用 ShuffleTiles() 方法。
Private Sub ShuffleTiles()
rndList.Clear()
GameCanvas.Children.Clear()
LoadTiles()
End Sub
设置主题
不幸的是,在 Metro 中无法引用 DynamicResource。这给 Metro 应用的皮肤/主题化带来了一个棘手的局面。在 Shuffle 中,我更改了构成应用背景的 Image 的 Source 属性值,并使用了一些自定义 Storyboard 来更改两个 Path 的 Fill 属性。
<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>
用于更改主题的 Storyboard 在 Page 的 Resources 部分中定义。
<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>
在 Metro 中,并非所有自定义动画都会默认运行。可能影响性能的动画必须在运行前启用。这些类型的动画被称为“依赖动画”。要启用它们,必须将其 EnableDependentAnimation 属性设置为 True。请注意,上面 XAML 标记中定义的一些动画对象具有此属性设置为 True,特别是那些针对带有 GradientBrush 的 Path 的动画。
现在可以通过调用 SetTheme() 方法来设置主题。
''' <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
请注意,在上面的方法中,我创建了一个名为 ThemeSetting 的应用程序设置,它存储在一个名为 ThemeContainer 的容器中。一旦写入了这个设置,它将在下次启动应用程序时用于加载用户首选的主题。
''' <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
设置壁纸
设置壁纸是通过调用 SetWallpaper() 方法完成的。
''' <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
上面的方法启动了文件选择器。文件选择器是一个界面,允许用户选择一个或多个文件供应用程序打开。由于这里使用文件选择器来显示用户可以设置为壁纸的图片,因此 FileOpenPicker 对象的 ViewMode 被设置为 PickerViewMode.Thumbnail。SuggestedStartLocation 属性指定了 Pictures 库是文件选择器第一次启动时应该检查图片的第一个位置。(在后续用户启动文件选择器时,它将从用户上次检查的目录开始)。我还通过将其添加到 FileTypeFilter 属性返回的列表中来指定文件选择器应处理的文件类型。
当所有必要的文件选择器属性都设置好后,通过调用 Await FileOpenPicker.PickSingleFileAsync() 来显示文件选择器。
一旦文件被打开,我需要跟踪该文件,以便下次启动应用时可以使用它来显示壁纸。要做到这一点,我将文件添加到 FutureAccessList。将文件添加到 FutureAccessList 会返回一个 token,这是一个唯一标识列表中文件的字符串值。要访问下一个应用启动时的文件,我可以将 token 存储在应用的设置中,但还有另一种选择,我将在下一节中解释。
加载壁纸
应用程序加载时会检查用户是否指定了壁纸。这是通过调用 LoadWallpaper() 方法完成的。
''' <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
要设置壁纸,我检索已添加到 FutureAccessList 的文件。这是通过使用列表中第一个条目的 token 来完成的。如果文件被添加到 FutureAccessList,那么该应用程序的 FutureAccessList 将只有一个条目。
结论
在此应用程序中,我使用了微软定义的几项 Metro 功能。以下列表包含与一些已使用功能相关的指南链接;
历史
- 2012 年 7 月 4 日:首次发布
- 2012 年 7 月 9 日:添加了主题和壁纸功能