瓷片的诱惑
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.8 数字拼图游戏设计

本节介绍一款数字拼图游戏的设计,如图2-21所示为该游戏运行时的情况。程序启动后,可以单击“Start”按钮,打乱数字的顺序,然后通过移动数字块来重新排列数字,当15个数字排列整齐后,游戏完成并会给出所用的时间与移动次数。

本游戏中应用了本章介绍的页面布局,如Grid面板的使用及元素的显示与隐藏。以下是程序的设计过程。

(1)启动Visual Studio Express 2010 for Windows Phone。在Windows操作系统中,单击“开始”→“所有程序”→“Microsoft Visual Studio 2010 Express” →“Microsoft Visual Studio 2010 Express for Windows Phone”。

图2-21 数字拼图(Number puzzle)运行情况

(2)新建应用程序项目。在“Start Page”页上,单击“New Project…”或者选择“File” →“New Project…”命令。在新建工程“New Project”窗口中,选择左侧项目模板为“Other Languages”→“Visual Basic”,然后在中间的项目模板列表中选择“Windows phoneApplication”,设置项目名称为“Puzzle”,指定项目文件存放的路径。单击“OK”按钮,创建新应用程序项目。

(3)选择“Windows phonePlatform”。选择应用程序运行的操作系统平台为Windows phoneos7.1。单击“OK”按钮,进入项目设计窗口。

(4)修改“MainPage.xaml”文件。将系统默认提供的MainPage.xaml,修改如下:

XAML代码:MainPage.xaml

<phone:PhoneApplicationPage
   x:Class="Puzzle.MainPage"
   …
   shell:SystemTray.IsVisible="True">
   <!--LayoutRoot is the root grid where all page content is placed-->
   <Grid x:Name="LayoutRoot" Background="Transparent">
      <Grid.RowDefinitions>
         <RowDefinition Height="Auto"/>
         <RowDefinition Height="Auto"/>
         <RowDefinition Height="489*" />
      </Grid.RowDefinitions>
      <!--TitlePanel contains the name of the application and page title-->
      <StackPanel x:Name="DouDouSoft" Grid.Row="0" Margin="12,17,0,28">
         <TextBlock x:Name="ApplicationTitle" Text="DouDouSoft" Style="{StaticResource PhoneTextNormalStyle}"/>
         <TextBlock x:Name="PageTitle" Text="Number puzzle" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}" FontSize="56" />
      </StackPanel>
   <!--ContentPanel-place additional content here-->
      <Grid Name="GameContainer" Width="400" Height="400" Grid.Row="2" Margin="40" VerticalAlignment="Top">
         <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100" />
            <ColumnDefinition Width="100" />
            <ColumnDefinition Width="100" />
            <ColumnDefinition Width="100" />
         </Grid.ColumnDefinitions>
         <Grid.RowDefinitions>
            <RowDefinition Height="100" />
            <RowDefinition Height="100" />
            <RowDefinition Height="100" />
            <RowDefinition Height="100" />
         </Grid.RowDefinitions>
      </Grid>
      <Grid Grid.Row="1" Height="70" HorizontalAlignment="Center" Margin="2" Name="Grid1" VerticalAlignment="Center" Width="460">
         <Grid.ColumnDefinitions>
            <ColumnDefinition Width="120*" />
            <ColumnDefinition Width="157*" />
            <ColumnDefinition Width="183*" />
         </Grid.ColumnDefinitions>
      <Button Content="Start" Grid.Row="0" Height="76" HorizontalAlignment="Center" Margin="6" Name="Button1" VerticalAlignment="Center" Width="160" Grid.Column="1" />
         <local:TimeDisplay x:Name="TotalTimeDisplay" Grid.Column="2" mdigitWidth="18" HorizontalAlignment="Right" Margin="0,0,12,0" FontSize="{StaticResource PhoneFontSizeLarge}" VerticalAlignment="Bottom" />
      </Grid>
   </Grid>
</phone:PhoneApplicationPage>

(5)添加程序引用的名称空间。本程序需要使用集合和Timer对象,因此,需要引用System.ComponentModel和System.Windows.Threading两个名称空间。在程序代码编辑窗口中打开MainPage.xaml.vb文件,在代码顶部加入如下代码:

Imports System.ComponentModel
Imports System.Windows.Threading

(6)添加页面级公共变量。MainPage.xaml.vb文件代码中,用到多个页面级公共变量,在MainPage.xaml.vb文件的Public Sub New()之前,添加以下公共变量的定义代码:

Dim isstarting As Boolean=False '标记游戏是否开始,True表示开始,False表示未开始
Public TileList As New List(Of tile)(15)
Dim timer As New DispatcherTimer()With {.Interval=TimeSpan.FromSeconds(0.1)}
Dim TotalTime As New TimeSpan '记录总计用时
Dim Starttime As New DateTime '记录开始时间
Dim MoveCount As Integer=0 '移动次数

(7)定义页面载入事件。下面的代码首先通过两层For循环,往Grid面板(名称为“GameContainer”)中添加15个数字块tile,tile是一个用户自定义类(代码见tile.xaml和tile.xaml.vb),每个数字块标记值为1~15的数字,第16个数字块单独添加,其number值为-1。

Private Sub PhoneApplicationPage_Loaded(sender As System.Object,e As System.Windows.RoutedEventArgs)Handles MyBase.Loaded
       Dim tt As tile 'tile是一个用户自定义类,用于定义可移动的数字块
       Dim x As Integer
       Dim y As Integer
       Dim i As Integer
       '通过循环,加入15个数字块,每行放4个
       For x=0 To 14
           tt=New tile
           tt.number=x+1
           i=x \4
           y=x Mod 4
           tt.x=i
           tt.y=y
           Me.GameContainer.Children.Add(tt)'将数字方块添加到Grid面板中
           Grid.SetRow(tt,i)'设置在Grid面板中的行位置
           Grid.SetColumn(tt,y)'设置在Grid面板中的列位置
           TileList.Add(tt)'将数字方块添加到集合中
       Next
       '加入第十六数字块,与前面15个不同的是:这一块的数字是-1
       tt=New tile
       tt.number=-1
       tt.x=3
       tt.y=3
       Me.GameContainer.Children.Add(tt)
       Grid.SetRow(tt,3)
       Grid.SetColumn(tt,3)
       TileList.Add(tt)
       AddHandler timer.Tick,AddressOf Timer_Tick '绑定timer对象的事件句柄
    End Sub

(8)定义“Start”按钮的事件代码。单击“Start”按钮,页面上的数字块顺序会被打乱,程序进入开始游戏状态,游戏状态记录在公共变量isstarting中。通过随机函数生成16以内的两个整数值,当这两个数不相等时,交换以这两个数字为Index值的对应数字块的number值,实现打乱。代码如下:

Private Sub Button1_Click(sender As System.Object,e As System.Windows.RoutedEventArgs)Handles Button1.Click
       Dim rnd As Random=New Random(System.DateTime.Now.Second)
       Dim n As Integer=0
       For n=0 To 99
          Dim n1 As Integer=rnd.Next(16)
          Dim n2 As Integer=rnd.Next(16)
          '随机取16以内的两整数值,当这两个数不相等时,交换这两个数字为索引的对应数'字块的number值,实现打乱
          If(n1 <> n2)Then
             Dim tmp As Integer=TileList(n1).number TileList(n1).number=TileList(n2).number
             TileList(n2).number=tmp
          End If
       Next
       '调用refreshtile刷新页面数字块,使页面呈现打乱状态
       refreshtile()
       '记录游戏状态为开始状态
       Me.isstarting=True
       '记录游戏开始时的时间
       Starttime=DateTime.UtcNow
       '显示用时记录器,用时记录器详细代码见TimeDisplay.xaml和'TimeDisplay.xaml.vb
       If Me.TotalTimeDisplay.Visibility=Windows.Visibility.Collapsed Then
          Me.TotalTimeDisplay.Visibility=Windows.Visibility.Visible
       End If
      '停止timer对象,清零总用时,然后重新开始,主要是为了在多次游戏中,用于清除上'次游戏的用时
      Me.timer.Stop()reset()
      Me.timer.Start()
   End Sub

(9)定义reset()重置总用时子过程。该子过程非常简单,只完成将总用时清零操作,代码如下:

Private Sub reset()
       Me.TotalTime=TimeSpan.Zero
   End Sub

(10)定义触屏操作代码。用户在屏幕上触击数字块,以移动数字块。代码如下:

Private Sub PhoneApplicationPage_ManipulationStarted(sender As System.Object,e As System.Windows.Input.ManipulationStartedEventArgs)Handles MyBase.ManipulationStarted
       Dim tt As tile
       '判断触击的是否是数字块,数字块Tile外部用Border包裹,名称起始字符串为'Tborder,当这两条件满足时,执行移动代码
       If(TypeOf e.ManipulationContainer Is Border And e.ManipulationContainer.GetValue(FrameworkElement.NameProperty).ToString().StartsWith("Tborder"))Then
            Dim b1 As Border=e.ManipulationContainer
            tt=b1.Parent
            '判断是否可移动和移动的方向,如果可移动,按可移动方向移动。
            Dim ii As Integer=Me.CanMovePiece(tt.number)
            If ii=1 Then
               tmove(tt,TileList(4 * tt.x+tt.y-4))
            End If
            If ii=2 Then
               tmove(tt,TileList(4 * tt.x+tt.y+1))
            End If
            If ii=3 Then
               tmove(tt,TileList(4 *(tt.x+1)+tt.y))
            End If
            If ii=4 Then
               tmove(tt,TileList(4 * tt.x+tt.y-1))
            End If
         End If
         e.Complete()
         e.Handled=True
         '判断是否完成,如完成提示完成信息
         If iscompleted()Then
            isstarting=False
            timer.Stop()
            MessageBox.Show("祝贺您!您成功了!" & Environment.NewLine & " 用时:" & Me.TotalTime.ToString & Environment.NewLine & " 移动次数:" & MoveCount & "下","游戏完成",MessageBoxButton.OK)
            Me.TotalTimeDisplay.Visibility=Windows.Visibility.Visible
         End If
      End Sub

(11)定义是否可移动判断函数。比较空白数字块(其number值为-1)与被触击数字块之间的位置,来判别是否可移动,以及移动的方向。代码如下:

Public Function CanMovePiece(ByVal num As Integer)As Integer
      'num参数保存当前被触击数字块的数值
      Dim totalPieces As Integer=15
      Dim CurrentTileindex As Integer=-1 '当前被触击数字块的Index值
      Dim emptyTileindex As Integer=-1 '空白数字块的Index值
      Dim i As Integer=0
      If Me.isstarting=False Then
         Return 0
      End If
      For i=0 To totalPieces
         If(TileList(i).number=num)Then 'number=num,即为被触击块
            CurrentTileindex=i
         ElseIf TileList(i).number=-1 Then 'number=-1,即为空白块
            emptyTileindex=i
         End If
      Next
      If((CurrentTileindex=emptyTileindex+1)Or(CurrentTileindex=emptyTileindex-1)Or(CurrentTileindex=emptyTileindex+4)Or(CurrentTileindex=emptyTileindex-4))Then
          If(CurrentTileindex+1=emptyTileindex)Then
             Return 2 '可向右移动
          ElseIf(CurrentTileindex-1=emptyTileindex)Then
             Return 4 '可向左移动
          ElseIf(CurrentTileindex-4=emptyTileindex)Then
             Return 1 '可向上移动
          ElseIf(CurrentTileindex+4=emptyTileindex)Then
             Return 3 '可向下移动
          End If
       End If
       Return 0 '0表示不可移动
    End Function

(12)定义移动函数。数字块移动函数通过将两个数字块的值交换,实现移动。参数t1、t2分别代表要互换的数字块。

Public Function tmove(ByVal t1 As tile,ByVal t2 As tile)As Boolean
       If t2.number=-1 Then
          Dim t3 As New Integer
          t3=t2.number
          t2.number=t1.number
          t1.number=t3
          MoveCount=MoveCount+1 '统计移动次数
          refreshtile()
          Return True
       Else
          Return False
       End If
    End Function

(13)定义游戏是否完成函数。当数字块集合中,所有数字块的Index(索引值)与number值(即显示的数值)相符时,即表示游戏完成。

Private Function iscompleted()As Boolean
       If isstarting Then '游戏处于开始状态时才进行判断
          Dim iscompleted1 As Boolean=True
          Dim i As Integer=0
          For i=0 To 14
   '如果数字块集合中,Index与number不相等,则未完成,如果全部相符,则表示已排列完成
              If i <>(TileList(i).number-1)Then iscompleted1=False
              End If
          Next
          Return(iscompleted1)
       Else
          Return False
       End If
    End Function

(14)定义timer执行函数。定时器timer每隔一定时间,更新总用时。代码如下(包括显示总用时的子过程):

Private Sub Timer_Tick(sender As Object,e As EventArgs)
       Dim Pasttime As TimeSpan=DateTime.UtcNow-Me.Starttime
       Me.Starttime=Me.Starttime+Pasttime
       Me.TotalTime=Me.TotalTime+Pasttime
       ShowCurrentTime()
End Sub
Private Sub ShowCurrentTime()
       Me.TotalTimeDisplay.mtime=Me.totalTime
End Sub

(15)定义总用时显视器的可视特性。总用时显示在右上角,有时候变动的时间会干扰用户。因此,程序允许用户在需要时可以通过单击总用时显示器,达到隐藏总用时显示器的特性。代码如下:

Private Sub TotalTimeDisplay_MouseLeftButtonDown(sender As System.Object,e As System.Windows.Input.MouseButtonEventArgs)Handles TotalTimeDisplay.MouseLeftButtonDown
       Me.TotalTimeDisplay.Visibility=Windows.Visibility.Collapsed
End Sub

上述完成的是MainPage.xaml.vb中的程序代码定义。接下来,需要创建两个程序用到的自定义控件。

(16)创建数字块控件。在解决方案管理器窗口中,用鼠标右键单击“Puzzle”项目,在弹出的快捷菜单中选择“Add…”→“New Item…”命令,在“New Item”对话框中,选择“Windows phoneUser Control”,Name为“tile.xaml”。双击打开“tile.xaml”文件,修改XAML代码如下。

XAML代码:tile.xaml

<UserControl x:Class="Puzzle.tile"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
   xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
   mc:Ignorable="d"
   FontFamily="{StaticResource PhoneFontFamilyNormal}"
   FontSize="{StaticResource PhoneFontSizeNormal}"
   Foreground="{StaticResource PhoneForegroundBrush}"
   d:DesignHeight="100" d:DesignWidth="100">
   <Border Width="99" Height="99" Name="Tborder" Background="Blue">
     <TextBlock Name="nu" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="36" />
   </Border>
</UserControl>

修改tile.xaml.vb程序代码如下。

VB.NET代码:tile.xaml.vb

Partial Public Class tile
    Inherits UserControl
    Private_x As Integer '记录在Grid面板中的行位置
    Public Property x()As Integer
       Get
           Return_x
       End Get
       Set(ByVal value As Integer)
           _x=value
       End Set
    End Property
    Private_y As Integer '记录在Grid面板中的列位置
    Public Property y()As Integer
       Get
           Return_y
       End Get
       Set(ByVal value As Integer)
           _y=value
       End Set
    End Property
    Private_number As Integer '数据块显示的数字
    Public Property number()As Integer
       Get
           Return_number
       End Get
       Set(ByVal value As Integer)
           _number=value
           '如果不是空白块,设置数字块颜色为系统前景色,并显示数字
           If number <>-1 Then
              Me.nu.Text=number.ToString
              Me.Tborder.Background=New SolidColorBrush(Application.Current.Resources("PhoneAccentColor"))
           Else '是空白块,不显示数字,且颜色为系统背景色
              Me.nu.Text=""
              Dim clr As Color=Application.Current.Resources("PhoneBackgroundColor")
              Me.Tborder.Background=New SolidColorBrush(clr)
           End If
       End Set
    End Property
    Public Sub New()
       InitializeComponent()
    End Sub
 End Class

(17)创建时间显示器控件。与数字块控件类似,TimeDisplay.xaml代码如下。

XAML代码:TimeDisplay.xaml

<UserControl x:Class="Puzzle.TimeDisplay"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
VerticalAlignment="Center">
   <StackPanel x:Name="LayoutRoot" Orientation="Horizontal"/>
</UserControl>

修改TimeDisplay.xaml.vb代码如下:

Imports System
Imports System.ComponentModel
Imports System.Globalization
Imports System.Windows
Imports System.Windows.Controls
Partial Public Class TimeDisplay
    Inherits UserControl
    Dim digitWidth As Integer
    Dim time As TimeSpan
    Public Sub New()
       InitializeComponent()
       If(DesignerProperties.IsInDesignTool)Then
          Dim textblock As New TextBlock
          textblock.Text="0:00.0"
          Me.LayoutRoot.Children.Add(textblock)
       End If
    End Sub
    Public Property mdigitWidth()As Integer
       Get
          Return digitWidth
       End Get
       Set(ByVal value As Integer)
          digitWidth=value
          mtime=time
       End Set
    End Property
    Public Property mtime()As TimeSpan
       Get
       Return time
       End Get
       Set(ByVal value As TimeSpan)
       Me.LayoutRoot.Children.Clear()
       Dim minutesString As String=value.Minutes.ToString()
       For i As Integer=0 To minutesString.Length-1
          AddDigitString(minutesString(i).ToString())
       Next
       Me.LayoutRoot.Children.Add(New TextBlock()With {.Text=":"})
       AddDigitString((value.Seconds \10).ToString())
       AddDigitString((value.Seconds Mod 10).ToString())
       Me.LayoutRoot.Children.Add(New TextBlock()With {.Text=CultureInfo.CurrentUICulture.NumberFormat.NumberDecimalSeparator})
       AddDigitString((value.Milliseconds \100).ToString())
       time=value
       End Set
    End Property
    Private Sub AddDigitString(ByVal digitString As String)
       Dim border As New Border()With {.Width=Me.digitWidth}
       border.Child=New TextBlock()With {.Text=digitString,.HorizontalAlignment=HorizontalAlignment.Center}
       Me.LayoutRoot.Children.Add(border)
    End Sub
 End Class

(18)更换程序图标。程序默认图标没有特色,可以根据需要更换为其他图标。本例中,ApplicationIcon.jpg和Background.jpg分别更换为如图2-22所示图标。

图2-22 ApplicationIcon.jpg和Background.jpg更换的图标