UWP 外星推箱子 - 第一部分






4.95/5 (12投票s)
一款有趣的 UWP 推箱子实现,展示了 XAML 和 C# 6.0 的一些新特性。第一部分
系列介绍
在本系列三篇文章中,您将了解如何为通用 Windows 平台 (UWP) 实现基于 XAML 的游戏。我们将探讨 WPF、Silverlight 和 UWP 之间的一些差异。您还将了解如何为游戏逻辑创建跨平台的可移植类库,以及如何使用信号量为可等待代码块提供线程安全。我们将介绍如何利用 x:Bind 以及如何使用
MediaElement
控件播放音效。最后,您将了解如何使用 SplitView
为游戏提供一个滑动菜单。
在接下来的系列文章中(在本三部分系列之后),您将了解如何使用 Xamarin Forms 实现跨平台。
本系列文章链接
本文内容
在第一部分中,我们将着眼于创建一个跨平台兼容的类库来放置游戏逻辑。我们将对比文件链接、共享项目、可移植类库以及新的 .NET 标准之间的差异。最后,您将了解
Game
页面的实现方式,并简要介绍新的 x:Bind 标记扩展。背景
难以置信,我早在 2007 年就为 WPF 和 Silverlight 编写了推箱子游戏的第一个版本。变化太大了。WPF 在企业领域仍然健在,而 Silverlight 几乎已经消失;但其遗产仍在延续。
过去几个月,我一直在使用 Xamarin Android,通过 Xamarin Android 工具将 Surfy Browser 移植到 Android。近距离接触 Windows 和 Android 平台是一次有趣的经历。对了,如果您有兴趣,可以查看 Surfy Browser for Android Beta 版本,地址是 https://play.google.com/apps/testing/com.outcoder.browser
再次使用“Windows XAML”进行此项目是一件令人愉快的事情。在我看来,Visual Studio 和微软的 Windows 开发工具组合是无与伦比的。
此项目的游戏逻辑在 第一篇文章 中有详细介绍。我建议您先快速浏览一下那篇文章(它相当短),以了解游戏的基本原理。
在本系列文章中,推箱子游戏逻辑基本保持不变。我在此基础上更新了核心游戏代码,将线程池调用替换为异步方法,以及 C# 6.0 属性 lambda 等。但这些改动大部分是表面的。
更实质性的改动是我对主视图所做的。我对其进行了重构,并将大部分逻辑移到了
Game
类中。您可以将推箱子项目中的 Game
类视为主视图的 ViewModel。使用 PCL 项目实现跨平台代码共享
在开始这个项目时,我决定同时涵盖 UWP 和 Xamarin Forms。因此,我选择将游戏逻辑移到一个可移植类库 (PCL) 中。尽管 Xamarin Forms 现在支持 UWP,但我的初衷是先构建一个“原生”UWP 应用,然后再将其移植到 Xamarin Forms。
选择跨平台代码共享策略时,您有多种选择
- 文件链接
- 共享项目
- 可移植类库 (PCL)
- .NET Standard
现在,我们简要了解一下每种选项。
文件链接
文件链接已经存在很多年了。我依稀记得十年前在添加按钮上发现了那个难以捉摸的小下拉按钮。请参见图 1。

图 1. 添加链接文件。
使用文件链接的优点在于其灵活性。您可以将任何文件包含到项目中,并使用预处理器指令来启用或禁用代码文件中的内容行。
文件链接的主要缺点是它可能很繁琐;在添加或重命名链接文件时,您必须手动确保在每个链接到该文件的项目中都更新链接。
过去,有一些 Visual Studio 扩展提供了基于文件后缀的自动链接。但 Visual Studio 团队后来并未继续支持它们。
共享项目
共享项目不产生任何构建产物;共享项目不会生成程序集。共享项目类似于自动链接功能。共享项目中的任何内容都会合并到引用项目中。共享项目是一个不可否认的有用工具;它支持预处理器指令,因此您可以包含或排除特定于平台的代码,并且无需进行繁琐的链接维护。
然而,缺点是充斥着预处理器指令的项目可能会成为维护的噩梦。细微的 API 差异可能导致文件膨胀。部分类有助于缓解这种情况,但它仍然会增加内部复杂性。
可移植类库
可移植类库允许您使用相同的输出程序集来定位多个平台。从一开始,这是一个非常有吸引力的选择。然而,这需要付出代价:API 表面是所有框架的交集。您定位的平台越多,可用的 API 就越少。
PCL 允许您定位一组已知的框架。从图 2 中可以看出,Xamarin Forms PCL 项目模板为您提供了相当广泛的平台,包括 iOS 和 Android。尽管未列出,但 UWP 也受支持。

图 2. PCL 支持的平台
要创建新的跨平台兼容类库,请在“新建项目”对话框的“跨平台”节点下选择“类库 (Xamarin.Forms)”,如图 3 所示。

图 3. 创建新的 PCL 项目。
定位 .NET 标准
前面的小节都引向了这里。微软认识到了 PCL 的优点和缺点,特别是每次新 .NET 平台出现时,都会带来新的组合复杂性。
.NET 标准旨在统一和拓宽 API 表面,以便您可以在跨平台库中使用更多 API,而无需排除不包含所需 API 集合的平台。它不像 PCL 那样强迫您使用最低公分母。
您可以在以下地址找到有关 .NET 标准的更多信息
- https://docs.microsoft.com/en-us/dotnet/articles/standard/library
- https://blogs.msdn.microsoft.com/dotnet/2016/09/26/introducing-net-standard/
有些人可能对尝试通过创建新标准来统一 .NET 框架的分支持怀疑态度。但我认为它以有效的方式解决了 PCL 的局限性。这是微软持续投入的一个有前景的方法。
选择跨平台代码共享策略时的次要考虑因素
除了每种代码共享方法的明显优缺点外,还有工具方面的影响。特别是,我发现 Visual Studio(带有 Resharper)在使用 PCL 和共享项目类型时可能会变得混乱。我偶尔会发现符号无法解析,尽管您的解决方案可能可以编译,但 Visual Studio 的错误列表中可能会出现一系列未识别的符号错误。Intellisense 也可能失灵。
提示: 解决错误的类型解析失败的一种方法是关闭解决方案并删除解决方案的 .suo 文件。重新打开解决方案将消除那些恼人的错误,至少在一段时间内是这样。
为何选择 PCL?
我选择 PCL 作为推箱子游戏逻辑的原因,而不是其他方法,是因为为了易读性,我绝对想避免使用预处理器指令。这排除了共享项目和链接。 .NET 标准正在发生一些变化,尽管它看起来很有前景,但我发现了一些关于 NuGet 和在 Xamarin Forms 解决方案中将 PCL 转换为 .NET 标准的问题。然而,我认为它是创建 .NET 跨平台应用程序的最佳选择,并且它将在此方面取代 PCL。
在 XAML 中实现游戏页面
MainPage.xaml 布局包括一个顶部工具栏部分,一个中央游戏区域和滑动菜单部分(使用 UWP
SplitView
控件实现)。还有一个覆盖层,用于在关卡完成时部分隐藏关卡,并向用户显示“按任意键继续”的消息。请参见列表 1。列表 1. MainPage.xaml 内容网格。
<Grid x:Name="rootGrid" Background="{ThemeResource GameBackgroundBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid x:Name="topContentGrid"
HorizontalAlignment="Stretch"
Background="{ThemeResource ChromePrimaryBrush}"
Height="50" Padding="0,0,12,0">
...
</Grid>
<SplitView Grid.Row="1" x:Name="splitView"
DisplayMode="Overlay"
OpenPaneLength="320"
PaneBackground="{ThemeResource ApplicationPageBackgroundThemeBrush}"
IsTabStop="False">
<SplitView.Pane>
<StackPanel Background="{ThemeResource ChromeSecondaryBrush}">
...
</StackPanel>
</SplitView.Pane>
<SplitView.Content>
<Canvas x:Name="gameCanvas" Background="Transparent" />
</SplitView.Content>
</SplitView>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
PointerReleased="HandleFeedbackControlPointerReleased"
Background="{StaticResource GameShadeBrush}" Grid.RowSpan="3"
Visibility="{x:Bind Game.FeedbackVisible,
Converter={StaticResource BooleanToVisibilityConverter}, Mode=OneWay}">
<TextBlock
Text="{x:Bind Game.FeedbackMessage, Mode=OneWay}"
FontSize="{StaticResource TextStyleExtraLargeFontSize}"
Foreground="White"
Visibility="{x:Bind Game.ContinuePromptVisible,
Converter={StaticResource BooleanToVisibilityConverter}, Mode=OneWay}"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
</Grid>
</Grid>
使用 ThemeResources 为您的应用设置样式
Theme resources 是 UWP 中一项新的 XAML 功能。Theme resources 是独立的样式、笔刷和其他 UI 元素集合,可以在运行时应用、切换和重新应用。它们与 StaticResources 非常相似。但是,StaticResources 仅在应用程序首次加载 XAML 时进行评估,而 ThemeResources 集合可以随时与其他集合进行交换。
UWP 中有三组
ThemeResources
。它们是 Light 、 Dark 和 HighContrast 。您可以使用应用程序 App.xaml 文件中 Application
元素的 RequestedTheme
属性来指示您的应用程序选择特定的主题,如下所示。<Application
x:Class="Outcoder.Sokoban.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Outcoder.Sokoban"
RequestedTheme="Light">
RequestedTheme
可以是 Light 或 Dark 。请注意,没有 HighContrast 值,因为如果用户通过“设置”>“辅助功能”>“高对比度”配置了高对比度设置,操作系统会自动应用高对比度。包含主题资源的资源字典可以放置在 Application.Resources 元素内。请参见以下示例。
<Application.Resources>
<ResourceDictionary>
<local:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="ChromePrimaryBrush" Color="#ff9a00" />
...
您也可以将
ResourceDictionary
放置在解决方案的其他位置,并使用 ResourceDictionary
的 Source
属性进行引用,如下所示。<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary Source="FooDictionary.xaml" x:Key="Light" />
此外,
ResourceDictionary
的 MergedDictionaries
属性允许您将不同文件中的分散资源合并在一起,如下所示。<ResourceDictionary x:Key="Light">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="FooDictionary1.xaml" />
<ResourceDictionary Source="FooDictionary2.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
尽管似乎有三种支持的主题类型,实际上还有第四种:Default 主题。如果您的某个资源在所有主题中都相同,您可以将其放在一个
ResourceDictionary
中,并将其 x:Key 值设置为 Default ,如下所示。<ResourceDictionary x:Key="Default">
如果应用程序在 Light、Dark 或 HighContrast 主题中找不到资源,那么它会在您的 Default 主题中查找。
使用
ThemeResource
标记扩展在页面、控件或模板中引用主题资源,如下所示。<Grid x:Name="rootGrid" Background="{ThemeResource GameBackgroundBrush}">
Visual Studio 为
ThemeResources
提供智能感知,就像它为 StaticResource
项提供一样。ThemeResources
提供了一种简单的方法,可以根据用户选择的操作系统主题即时更改应用程序的外观和感觉。注意: 不要在主题资源中引用另一个
ThemeResource
。相反,请使用 StaticResources
标记扩展。 解析错误和循环依赖 可能会导致不可预测的结果。有一个例外:引用内置主题资源是可以的。理解 x:Bind 标记扩展
任何熟悉 WPF 或 Silverlight 的人都会告诉你,XAML 中的数据绑定是使用 Binding 标记扩展完成的。如果您不熟悉 XAML 中的数据绑定,数据绑定是一种更新 UI 元素状态以匹配另一个对象状态(反之亦然)的方法,而无需编写代码来执行更新。数据绑定是创建高度可维护的 XAML UI 的关键要素,也是 XAML UI 开发如此有趣的原因之一。
Binding 标记扩展使用运行时检查来映射对象属性。事实证明,运行时检查是有性能成本的。在大多数情况下,成本微不足道且难以察觉。然而,在处理动态填充列表项的长列表时,流畅的滚动需要快速填充项数据。虽然虚拟化(仅在可见项上填充)可以在一定程度上减轻成本,但在某些情况下,运行时检查可能会导致 UI 明显滞后。
因此,微软引入了 x:Bind 标记扩展,这在 UWP XAML 应用中是独有的。x:Bind 在开发时生成代码,从而消除了对运行时检查的需要。它更快。它还附带编译时绑定验证,并支持对绑定代码设置断点,这在可维护性方面是一大进步。
x:Bind 的缺点是您需要显式绑定到视图的属性或子属性。尽管有这个要求,我还是建议尽可能使用 x:Bind。
在可下载示例代码的 Sokoban.Launcher.Uwp 项目的 MainPage.xaml.cs 文件中,您可以看到 Sokoban
Game
对象被公开为页面的公共属性,如下所示。public Game Game { get; }
这允许页面直接 x:Bind 到
Game
属性,例如关卡号,如以下示例所示。<TextBlock Text="{x:Bind Game.Level.LevelNumber, Mode=OneWay, FallbackValue=0}" />
注意: 与 Binding 标记扩展不同(其 Mode 属性因控件而异),x:Bind 的默认模式是 OneTime 。当您的绑定表达式似乎不起作用时,请尽量记住这一点。我个人更希望将此值设置为 OneWay ,因为我认为这是最常见的情况。
结论
在本文中,我们着眼于创建一个跨平台兼容的类库来放置游戏逻辑。我们对比了文件链接、共享项目、可移植类库以及新的 .NET 标准之间的差异。最后,我们了解了 Game 页面的实现方式,并简要介绍了新的 x:Bind 标记扩展。
在本系列的 下一部分 中,我们将着眼于将推箱子
Game
与应用程序的主页面连接起来。您将了解如何使用 UWP MediaElement
控件播放音效。最后,您将探索如何用自定义单元格控件填充游戏网格。我希望这个项目对您有用。如果觉得有用,请评分和/或在下方留言。
历史
- 2016 年 10 月 14 日
- 首次发布