构建 Windows Phone 7 拼图游戏






4.99/5 (88投票s)
利用新的 Windows Phone 7 开发工具抢占先机。学习如何为 WP7 平台创建 Silverlight 版的 Sokoban 游戏。
我制作了一个游戏视频。
请注意,在视频中,当我提到“点击”时,指的是在模拟器中使用鼠标,这相当于在真实设备上按下屏幕。
目录
引言
这篇文章对我来说有点离题,内容也会比我通常写的文章轻松一些。本文介绍的技术是上周才发布的,所以希望大家能谅解我没有对每个功能都进行深入探讨。我们将初步了解 Windows Phone 7,并使用 Silverlight 和 XNA 框架音频 API 的功能来创建一个益智游戏。
背景
上周在 MIX10 上,微软发布了新的 Windows Phone 7 开发工具。和许多开发者一样,我对此感到非常高兴。上周一在拉斯维加斯开始的 MIX10 开发者大会 2010,微软将重点放在了其 Microsoft Windows 7 手机及其开发工具上。我整个周末都在编写这个游戏,并尝试使用新的 WP7 功能。结论是?我真的很喜欢它。它是一个熟悉的环境,Silverlight 和 XNA,我相信它将对手机市场产生重大影响。和许多开发者一样,我曾有点嫉妒 iPhone 开发者,他们制作那些有趣的小应用。看起来他们能从像 iFart 这样荒谬的应用中赚大钱。我相信这很大程度上是炒作,如今的市场竞争非常激烈。但即便如此,我也想尝试一下那种场景,但遗憾的是,我既没有时间也没有兴趣去用 Objective-c 进入苹果的世界。是的,Novell 有它的 iPhone Touch,我也曾考虑过尝试一下。但即便如此,我认为我最终会感觉自己像在试图将一个方形块推入一个圆形孔。
WP7 登场。现在微软真是功不可没。是的,我知道我听起来像个粉丝,但我很高兴这些东西出现了。2008 年,我曾预测 Silverlight(第一个使用 .NET 的版本)1.1 版本会非常流行。好吧,那是个显而易见的预测,但现在很明显,Windows Phone 将会产生巨大影响。我们有熟悉的 Visual Studio 开发环境,一项构建灵活和动态界面的绝佳技术——Silverlight,以及最后是 应用商店。所有这些都是完美风暴的要素!
所以,为了尝试 WP7,我带回了我那个像 Mike Wazowski 的角色,再次制作 Alien Sokoban!几年前,我曾为另一篇文章中的 Silverlight 编写了大部分游戏逻辑。那时 Silverlight for .NET 还没有正式发布,它是 1.1 版本,我们甚至还没有内置的文本框;我们不得不自己编写。世事变迁,如今随着最新版本的 Silverlight,我们有了大量新功能。摄像头、VSM、COM 互操作、多播流,等等等等。(但遗憾的是,仍然没有内置的菜单控件!)。
Windows Phone 7 使用 Silverlight 3。开发工具包含一个模拟器,实际上它是一个虚拟机。这很棒,因为这就像拥有了手机,但又没有手机,这很有帮助,因为我没有手机。Visual Studio 中有完整的开发者集成;我们有调试器,可以在代码编辑器中将鼠标悬停在变量上,等等,就像在 Silverlight 中一样。
入门
如果您是 Silverlight 新手,我建议您先学习相关知识。这里有许多 CodeProject 上精彩的入门 Silverlight 文章,可以帮助您入门。
WP7 工具的安装
安装Windows Phone 开发工具 CTP,并且不要忘记阅读发行说明。请注意,如果您有 Visual Studio 2010 RC(请注意,您需要 RC 或更高版本,并且不能在同一台机器上安装 beta 版本),则该工具将集成到现有的 Visual Studio 安装中,否则将安装 Visual Studio 2010 Express。
安装程序将自动下载并安装所需的组件。
如果您希望使用 Expression Blend 4 进行 WP7 开发,虽然本文不需要,但可以从 Christian Schormann 的博客下载并安装适用于 Windows Phone 的 Expression Blend 4 附加组件。
安装完成后,请重启并启动 Visual Studio 2010。
然后,您就可以开始工作了!
游戏 GUI
主用户界面由一个 PhoneApplicationPage
提供。这是创建新的 Windows Phone 应用程序时默认的宿主控件。
开发者在 Visual Studio 2010 中处理 Windows Phone 应用时会感到非常自在。在这里,我们看到可以直接使用设计器,它提供了工作手机区域的可视化表示以供参考。
页面方向
在 Windows Phone 应用程序中托管应用程序最明显的影响之一是如何处理布局方向,特别是检测方向变化以及我们是否认为 UI 与特定方向兼容。PhoneApplicationPage
有一个可设置的属性名为 SupportedOrientations
。这个 enum
值可以是 Landscape
、Portrait
或 PortraitOrLandscape
。通过在 XAML 或代码中为其赋值,我们可以限制应用程序的显示方式。对于 Alien Sokoban,我选择了 PortraitOrLandscape
,因为我想让用户能够根据游戏网格的尺寸旋转它。需要注意的是,无法从用户代码设置方向,因为它被标记为SecurityCritical。当我们通过 Reflector 浏览到 Page.Orientation
属性时,可以看到这一点。
SecurityCritical
是大多数 Silverlight 开发者熟悉的属性。这意味着如果我们尝试调用它,将会引发异常,因此它是不允许访问的。您可以在这里找到更多关于 Silverlight 代码安全模型的信息。
请注意,这是 Silverlight 安全,并非 Windows Phone 特有。这充分说明了 Windows Phone 使用的是完整版 Silverlight,而不是 Silverlight Lite。
从 Reflector 中,我们可以看到 Page.Orientation
不是依赖属性(也不会引发 PropertyChanged
事件),其其他属性也不是。因此,绑定到 PhoneApplicationPage
属性是行不通的。
方向可见性转换器
不经过一些额外的代码旁置工作,无法立即检测和更新页面元素。我想根据方向隐藏“Alien Sokoban”标题,因为它在纵向模式下太宽了。我可以简单地在 OrientationChanged
事件处理程序中设置 Visibility,但我选择在 MainPage
上创建了一个新的依赖属性,该属性在调用 OrientationChanged
事件处理程序时更新。为了转换方向值,我创建了一个 IValueConverter
,并在 XAML 中使用它。当 Page.Orientation
属性发生变化时,标题 TextBlock
的 Visibility 的绑定也会随之改变。
<TextBlock Visibility="{Binding ElementName=Page, Path=PageOrientation,
Converter={StaticResource OrientationToVisibilityConverter},
ConverterParameter= Landscape}" .../>
使用 ConverterParameter
“Landscape
” 来指定我们希望仅在 Page.Orientation
属性是风景模式(包括 Landscape
、LandscapeLeft
或 LandscapeRight
)时显示 TextBlock
。以下是 OrientationToVisibilityConverter
的一个片段,展示了转换器如何将 PageOrientation
值转换为 Visibility
值。
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
var orientation = (PageOrientation)value;
string showWhenOrientation = parameter.ToString().ToLower();
bool show = false;
switch (orientation)
{
case PageOrientation.Portrait:
case PageOrientation.PortraitDown:
case PageOrientation.PortraitUp:
show = showWhenOrientation == "vertical";
break;
case PageOrientation.Landscape:
case PageOrientation.LandscapeLeft:
case PageOrientation.LandscapeRight:
show = showWhenOrientation == "landscape";
break;
}
return show ? Visibility.Visible : Visibility.Collapsed;
}
使用 XNA 框架音频 API
我非常惊喜地发现使用 XNA 框架播放音效非常容易。它适用于需要即时播放的短样本。但请注意,它对格式要求很严格。我发现只有 PCM 格式的 wav 文件受支持。我使用了GoldWave 将所有音频保存为 PCM 格式。对于较长的片段,使用更节省空间的格式(如 mp3)更有意义,但这需要使用 MediaElement
控件。
所有音效都在 MainPage.xaml.cs 代码旁边定义,如下面的摘录所示:
readonly SoundEffect footStepSoundEffect =
SoundEffect.FromStream(TitleContainer.OpenStream("Audio/Footstep.wav"));
然后,我们可以这样播放音效:
footStepSoundEffect.Play();
请注意,所有打算由 XNA 框架播放的音频文件的 Build Action 都必须设置为 Content。
过去在使用 Silverlight 中的 MediaPlayer
元素时,我注意到在排队媒体时可能会有短暂的延迟。使用 XNA 框架,播放是即时的;这是音效的关键要求。
主页面绑定
在实例化期间,MainPage
将一个 Game
实例分配给其 DataContext
。它的大部分活动都由数据绑定和 Game 属性更改控制。MainPage
在此完整提供:
<phoneNavigation:PhoneApplicationPage
x:Class="DanielVaughan.Sokoban.UI.MainPage"
x:Name="Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phoneNavigation="clr-namespace:Microsoft.Phone.Controls;
assembly=Microsoft.Phone.Controls.Navigation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:DanielVaughan.Sokoban.UI"
mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="800"
SupportedOrientations="PortraitOrLandscape"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}" Orientation="Landscape">
<phoneNavigation:PhoneApplicationPage.Resources>
<controls:OrientationToVisibilityConverter
x:Key="OrientationToVisibilityConverter" />
<Style x:Key="CenterLabels" TargetType="TextBlock">
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontSize" Value="18"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style x:Key="ToolBarWebdings" TargetType="Button">
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
<GradientStop Color="#FF9AFF95" Offset="0.21"/>
<GradientStop Color="#FF5DD757" Offset="0.589"/>
<GradientStop Color="#FF99FF93" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="FontFamily" Value="Webdings"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Padding" Value="5 "/>
</Style>
<Style x:Key="OrdinaryButton" TargetType="Button">
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
<GradientStop Color="#FF9AFF95" Offset="0.21"/>
<GradientStop Color="#FF5DD757" Offset="0.589"/>
<GradientStop Color="#FF99FF93" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Padding" Value="5 "/>
</Style>
</phoneNavigation:PhoneApplicationPage.Resources>
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<controls:BackgroundControl Opacity=".3" />
<Border VerticalAlignment="Top" Grid.Row="0" Height="60"
BorderBrush="#FFFFE63E" CornerRadius="10,10,10,10"
BorderThickness="2,2,2,2" Margin="0,0,0,0">
<Border.Background>
<LinearGradientBrush EndPoint="0.5,-1.389"
StartPoint="0.5,2.389" SpreadMethod="Pad">
<GradientStop Color="#FFFF9900" Offset="1"/>
<GradientStop Color="#FFFF9900" Offset="0.58"/>
<GradientStop Color="#FFFFFFFF" Offset="0"/>
</LinearGradientBrush>
</Border.Background>
<Grid>
<Rectangle Stroke="{x:Null}" Margin="5,3,5,18"
RadiusX="10" RadiusY="10" Opacity="0.41">
<Rectangle.Fill>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFECECEC" Offset="0"/>
<GradientStop Color="#FFFFFFFF" Offset="1"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<StackPanel Height="50" x:Name="stackPanel1" Margin="15,0,5,0"
VerticalAlignment="Center" HorizontalAlignment="Stretch"
Width="Auto" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Foreground="White" Text="Code:" TextWrapping="Wrap"/>
<TextBox Text="{Binding Path=LevelCode, Mode=OneWay}"
x:Name="textBox_LevelCode" MaxLength="5"
Opacity="0.4" Width="110" TextAlignment="Center"
VerticalAlignment="Center"
HorizontalContentAlignment="Center"
GotFocus="TextBox_LevelCode_GotFocus"
LostFocus="TextBox_LevelCode_LostFocus"
KeyUp="TextBox_LevelCode_KeyUp"
Background="White" />
<Button Style="{StaticResource ToolBarWebdings}"
Margin="0,-10,0,0" Height="10" Content=""
Click="Button_Undo_Click"/>
<Button Style="{StaticResource ToolBarWebdings}"
Margin="0,-10,0,0" Height="10" Content=""
Click="Button_Redo_Click"/>
<TextBlock Visibility="{Binding ElementName=Page,
Path=PageOrientation,
Converter={StaticResource OrientationToVisibilityConverter},
ConverterParameter=Landscape}"
VerticalAlignment="Center" HorizontalAlignment="Center"
TextAlignment="Center" Foreground="White"
Text="Alien Sokoban"
FontFamily="Tahoma" FontSize="36" Margin="45,0,0,0"/>
</StackPanel>
<Grid HorizontalAlignment="Right">
<StackPanel Orientation="Horizontal">
<StackPanel VerticalAlignment="Center">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource CenterLabels}"
Text="Level "/>
<TextBlock x:Name="label_LevelNumber"
Style="{StaticResource CenterLabels}"
Text="{Binding Path=Level.LevelNumber}"/>
<TextBlock Style=
"{StaticResource CenterLabels}" Text="/"/>
<TextBlock Style="{StaticResource CenterLabels}"
Text="{Binding Path=LevelCount}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource CenterLabels}"
Text="Moves "/>
<TextBlock x:Name="label_Moves"
Style="{StaticResource CenterLabels}"
Text="{Binding Path=Level.Actor.MoveCount}"/>
</StackPanel>
</StackPanel>
<Button Style="{StaticResource ToolBarWebdings}"
Margin="0,-8,0,0"
Height="10" x:Name="button_RestartLevel" Width="80"
Click="Button_RestartLevel_Click"
IsTabStop="False" Content=""
HorizontalAlignment="Right" >
<ToolTipService.ToolTip>
<ToolTip Content="Restart"></ToolTip>
</ToolTipService.ToolTip>
</Button>
</StackPanel>
</Grid>
</Grid>
</Border>
<!-- The Game grid. -->
<Border Grid.Row="1" Padding="5" BorderBrush="#919292" CornerRadius="12"
BorderThickness="0" Background="Transparent">
<Grid x:Name="grid_Game" />
</Border>
<Grid x:Name="textBlock_PressAnyKey"
Background="#006DCAC1"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.RowSpan="2">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Stretch"
Background="#556DCAC1">
<TextBlock x:Name="feedbackControl"
VerticalAlignment="Center"
HorizontalAlignment="Center" Text="TextBlock" FontSize="32"/>
<Button Style="{StaticResource OrdinaryButton}" Content="Continue"
VerticalAlignment="Center" HorizontalAlignment="Center"
Click="Button_Continue_Click"/>
</StackPanel>
</Grid>
</Grid>
</phoneNavigation:PhoneApplicationPage>
游戏网格中的每个单元格都用一个 CellControl
填充。这发生在 MainPage
的 InitializeLevel
方法中。
void InitialiseLevel()
{
cellControls.Clear();
commandManager.Clear();
grid_Game.Children.Clear();
grid_Game.RowDefinitions.Clear();
grid_Game.ColumnDefinitions.Clear();
for (int i = 0; i < Game.Level.RowCount; i++)
{
grid_Game.RowDefinitions.Add(new RowDefinition());
}
for (int i = 0; i < Game.Level.ColumnCount; i++)
{
grid_Game.ColumnDefinitions.Add(new ColumnDefinition());
}
var cellSize = CalculateCellSize();
for (int row = 0; row < Game.Level.RowCount; row++)
{
for (int column = 0; column < Game.Level.ColumnCount; column++)
{
Cell cell = Game.Level[row, column];
cell.PropertyChanged += cell_PropertyChanged;
CellControl cellControl = new CellControl(cell);
cellControl.MaxHeight = cellControl.MaxWidth = cellSize;
cellControl.Click += Cell_Click;
Grid.SetColumn(cellControl, column);
Grid.SetRow(cellControl, row);
grid_Game.Children.Add(cellControl);
cellControls.Add(cellControl);
}
}
/* Play the intro audio clip. */
PlayAudioClip(introSoundEffect);
/* Listen for actor property changes. */
//Game.Level.Actor.PropertyChanged += Actor_PropertyChanged;
RefreshGameGrid();
}
通常,我不建议将这种 UI 逻辑代码放在代码旁边,因为我更喜欢使用 MVVM 方法。但是,为了方便起见,我采用了这种方法。
CellControl
被分配了一个 Game 单元格,并根据单元格的状态改变其外观。如果被认为是墙,它显示一个灰色方块,等等。
游戏玩法
请参阅游戏视频以了解游戏玩法概览。游戏逻辑也已在此处和此处进行了文档记录。
手机屏幕键盘
Windows Phone 7 允许开发人员指定用户可以通过屏幕键盘输入的数据类型。它也是上下文感知的,并且当 TextBox
获得焦点时会放大到 TextBox
。要更改键盘以使其更适合正在输入的数据,可以使用 InputScope
。
例如,要指定对默认屏幕键盘(如上所示)的修改,请将 InputScope
元素应用于 TextBox
。
<TextBox>
<TextBox.InputScope>
<InputScope>
<InputScope.Names>
<InputScopeName NameValue="EmailNameOrAddress"/>
</InputScope.Names>
</InputScope>
</TextBox.InputScope>
</TextBox>
通过使用 EmailNameOrAddress NameValue
,显示的键盘包含 QWERTY 字符以及 .com 和 @ 符号。
以下是有效输入范围值列表:
SIP 布局 | XAML 或枚举值 | SIP 描述 |
默认值 | 默认和其他标准输入范围值 | 标准 QWERTY 键盘 |
文本 | 文本 |
具有自动更正和文本建议等功能的标准文本 |
Web | Url |
用户键入 URL |
电子邮件地址 | EmailSmtpAddress |
用户键入电子邮件地址 |
电子邮件名称或地址 | EmailNameOrAddress |
用户键入电子邮件名称或地址 |
映射 | 映射 |
用户键入要在地图上搜索的位置 |
电话号码 | TelephoneNumber |
用户键入电话号码 |
搜索 | 搜索 |
用户键入搜索查询 |
SMS 联系人 | NameOrPhoneNumber |
用户在 SMS 收件人字段中键入 |
聊天 | 聊天 |
使用缩写等智能功能的文本输入 |
来源:Don's Expression Blend Blog
结论
在本文中,我们学习了如何使用 Silverlight 3 创建 Windows Phone 益智游戏。我们研究了 PageOrientation
及其在控制布局中的应用。我们检查了屏幕键盘,以及如何根据上下文选择不同的键盘,还简要介绍了 XNA 框架音频 API 及其在播放音效中的用途。
我对这个平台的未来感到非常兴奋。在短暂接触之后,我已经感觉很适应了。希望您觉得这个项目有用。如果觉得有用,请帮忙给它评分和/或在下方留下反馈。这将帮助我改进下一篇文章。
历史
- 2010 年 3 月
- 出版日期
- 2010 年 7 月
- 更新为 Windows Phone 7 Beta