UWP 异形推箱子 - 第 3 部分





5.00/5 (7投票s)
一个有趣的 UWP 版推箱子游戏实现,展示了一些 XAML 和 C# 6.0 的新特性。第三部分
引言
这是三部分系列文章的第三部分,您将在此系列中探索如何为通用 Windows 平台 (UWP) 实现一个基于 XAML 的游戏。
CellControl
,并考察图像是如何用于表示单元格内容的。您将看到如何通过缓存 Bitmap
对象来减少应用的内存占用。您将了解如何响应应用内的触摸事件,并编写线程安全的异步可等待代码。最后,您将看到如何实现一个 SplitView
来为游戏提供一个滑动菜单。本系列文章链接
检索地图
Game
类利用 IInfrastructure
实现来检索关卡地图。地图信息位于 Sokoban.Launcher 项目的 Levels 目录中。Infrastructure
类的 GetMapAsync
方法中执行的,如列表 1 所示。public async Task<string> GetMapAsync(int levelNumber)
{
string fileUrl = string.Format(@"ms-appx:///{0}Level{1:000}.skbn",
levelDirectory, levelNumber);
StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(new Uri(fileUrl));
string levelText = await FileIO.ReadTextAsync(file);
return levelText;
}
StorageFile
API 来检索文件。探索单元格控件
Cell
对象都与一个 CellControl
实例配对。CellControl
是一个自定义控件,由多个 Image 对象组成;每个 Image 对象代表单元格的类型和内容。Cell
被分配给一个 CellControl
时,CellControl
会订阅 Cell
的 PropertyChanged
事件。当 Cell
的 CellContents
属性发生更改时,会调用 UpdateDisplay
方法。请参阅列表 2。每个 Image 对象的可见性根据 Cell
及其内容进行更新。void UpdateDisplay()
{
wallImage.SetVisibility(cell is WallCell);
floorImage.SetVisibility(cell is FloorCell || cell is GoalCell);
floorHighlightImage.SetVisibility(hasMouse && (cell is FloorCell || cell is GoalCell));
treasureImage.SetVisibility(cell.CellContents is Treasure);
playerImage.SetVisibility(cell.CellContents is Actor);
goalImage.SetVisibility(cell is GoalCell && !(cell.CellContents is Treasure));
goalActiveImage.SetVisibility(cell is GoalCell && cell.CellContents is Treasure);
}
SetVisibility
方法是一个自定义扩展方法,它避免了使用三元表达式来设置 UIElement
的 Visibility
属性。它减少了冗余。请参阅列表 3。
列表 3. UIElementExtensions SetVisibility 方法。
public static void SetVisibility(this UIElement element, bool visible)
{
element.Visibility = visible ? Visibility.Visible : Visibility.Collapsed;
}
换句话说,它将此
wallImage.Visibility = cell is WallCell ? Visibility.Visible : Visibility.Collapsed;
变为此
wallImage.SetVisibility(cell is WallCell);
顺带一提,微软没有借此机会用一个简单的布尔属性替换 UIElement
的 Visibility
属性,这确实很可惜,因为 UWP 中已经对许多不太重要的东西进行了改造。当然,现在引入一个新的可见性枚举值(如 Android 的“Gone”可见性值)可能有点晚了。
此外,SDK 中没有内置的 BooleanToVisibilityConverter
,这意味着新开发者在弄清楚如何根据布尔值隐藏或显示元素时,大部分时间都会卡住。好消息是,随着 Windows 10 创意者更新的发布,x:Bind 现在可以隐式地与 bool 和 Visibility
枚举值相互转换。但这仅在运行创意者更新的计算机和设备上有效。
位图缓存
位图会占用大量内存。如果您的 UWP 应用打算在手机上运行,那么您需要注意将内存使用量降至最低。
注意: 在 Windows Mobile 上,对于内存超过 1GB 的设备,您的应用最多只能使用 390 MB。此限制强制执行,无论有多少系统内存可用,并且如果超出该限制,操作系统将退出您的应用。
为了减少推箱子游戏所需的 RAM 量,BitmapImage
对象被缓存在一个字典中。虽然每个单元格都有自己的一组 Image 对象,但它与其他 CellControls
共享底层的 BitmapImages
。
图像缓存是 CellControl
本身的一个简单的静态 Dictionary
,如下所示。
static readonly Dictionary<string, BitmapImage> imageCache
= new Dictionary<string, BitmapImage>();
当实例化 CellControl
时,也会创建一组 Image
对象。请参阅列表 4。CreateContentImage
方法首先尝试使用 relativeUrl
参数检索图像。如果之前已创建图像,则将其分配给 Image 的 Source
属性。
列表 4. CellControl CreateContentImage 方法
Image CreateContentImage(string relativeUrl)
{
Image image = new Image
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
Stretch = Stretch.Fill
};
BitmapImage sourceImage;
if (!imageCache.TryGetValue(relativeUrl, out sourceImage))
{
Uri imageUri = new Uri(BaseUri, relativeUrl);
sourceImage = new BitmapImage(imageUri);
imageCache[relativeUrl] = sourceImage;
}
image.Source = sourceImage;
return image;
}
响应触摸事件
注意: WinRT 和 UWP 中,一些类型的名称和/或命名空间发生了更改。(WinRT 是我们现在称之为 UWP 的第一个版本)。
其中两个例子是 WPF 的
UIElement MouseEnter
和 MouseExited
事件。它们现在分别是 PointerEntered
和 PointerExited
。PointerEnter
事件,该事件调用 CellControl
的 UpdateDisplay
方法,然后该方法显示单元格高亮图像。使用信号量实现线程安全的异步等待
Actor
类的移动相关代码,使其使用异步等待而不是线程锁定。在实现线程安全的异步代码时,一个挑战是锁原语不允许在异步代码块中使用。它们可能导致死锁。因此,编译器会阻止您执行以下操作:lock (aLock)
{
await DoSomethingAsync();
}
SemaphoreSlim
类来防止异步代码块中的竞态条件。我在 Actor
的 JumpAsync
方法中演示了这一点。请参阅列表 5。moveSemaphore
的 SemaphoreSlim
通过调用其 WaitAsync
方法来获取。请注意,该方法是可等待的。您需要等待该方法才能访问敏感区域。async Task<bool> JumpAsync(Jump jump)
{
bool result = false;
try
{
await moveSemaphore.WaitAsync();
SearchPathFinder searchPathFinder = new SearchPathFinder(Cell, jump.Destination);
if (searchPathFinder.TryFindPath())
{
for (int i = 0; i < searchPathFinder.Route.Length; i++)
{
Move move = searchPathFinder.Route[i];
/* Sleep for the stepDelayMS period. */
await Task.Delay(stepDelayMS).ConfigureAwait(false);
Location moveLocation = Location.GetAdjacentLocation(move.Direction);
Cell toCell = Level[moveLocation];
if (!toCell.TrySetContents(this))
{
throw new SokobanException("Unable to follow route.");
}
MoveCount++;
}
/* Set the undo item. */
Jump newMove = new Jump(searchPathFinder.Route) { Undo = true };
moves.Push(newMove);
result = true;
}
}
finally
{
moveSemaphore.Release();
}
return result;
}
SearchPathFinder
如何找到路径的信息,请参阅 这篇之前的 Silverlight 文章。SemaphoreSlim
类同时支持异步和同步代码块。如果您希望使用同一个 SemaphoreSlim
实例来保护异步和非异步代码块,请使用其同步 Wait
方法。您可以在 Actor
的 DoMove
方法中看到这一点。请参阅列表 6。SemaphoreSlim
对象来保护非可等待块。internal bool DoMove(Move move)
{
try
{
moveSemaphore.Wait();
return DoMoveAux(move);
}
finally
{
moveSemaphore.Release();
}
}
关于图像资源
RadialGradientBrush
,这意味着我无法使用我之前创建的图像资源。因此,我重新开始,在 Photoshop 中创建了图像。我喜欢将资源放在 XAML 中,因为它提供了无损缩放。但是,由于我打算将 Alien Sokoban 移植到 Android 和 iOS,因此使用 .jpg 和 .png 图像是有意义的。但说实话,我真希望 UWP 中能有一个
RadialGradientBrush
。实现滑动菜单
- 撤销移动
- 重做移动
- 重新开始关卡
ICommands
,并且不应与移动命令和撤销/重做移动系统中的 GameCommandBase
类混淆(后者出现得更早)。DelegateCommand
类的实例。DelegateCommand
需要一个 Action
,以及一个可选的 Func
,该 Func 用于评估 Action
是否允许执行。请参阅列表 7。public class DelegateCommand : ICommand
{
readonly Action<object> executeAction;
readonly Func<object, bool> canExecuteAction;
public DelegateCommand(Action<object> executeAction,
Func<object, bool> canExecuteAction = null)
{
this.executeAction = executeAction;
this.canExecuteAction = canExecuteAction;
}
public bool CanExecute(object parameter)
{
return canExecuteAction?.Invoke(parameter) ?? true;
}
public void Execute(object parameter)
{
executeAction?.Invoke(parameter);
}
public event EventHandler CanExecuteChanged;
protected virtual void OnCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
public void RaiseExecuteChanged()
{
OnCanExecuteChanged();
}
}
ICommands
非常有用,因为它们与许多 UI 元素(如 Buttons
)具有亲和力,这些元素会自动利用 CanExecute
方法来自动更改按钮的 IsEnabled
状态。
public Game(IInfrastructure infrastructure)
{
this.infrastructure = ArgumentValidator.AssertNotNull(infrastructure, nameof(infrastructure));
LevelContentBase.SynchronizationContext = infrastructure.SynchronizationContext;
RestartLevelCommand = new DelegateCommand(_ => RestartLevel());
UndoCommand = new DelegateCommand(_ => commandManager.Undo());
RedoCommand = new DelegateCommand(_ => commandManager.Redo());
/* Reset the level number to 0 if a debugger is attached. */
if (Debugger.IsAttached)
{
infrastructure.SaveSetting(levelKey, 0);
}
}
RestartLevelCommand
在执行时调用 RestartLevel
方法。UndoCommand
和 RedoCommand
调用 Game
的 commandManager
的相应 Undo
和 Redo
方法。命令的实现
SplitView
元素用于在按下汉堡按钮时叠加一个菜单窗格。请参阅列表 9。这三个命令绑定到 SplitView.Pane
中的 Button
控件。<SplitView Grid.Row="1" x:Name="splitView"
DisplayMode="Overlay"
OpenPaneLength="320"
PaneBackground="{ThemeResource ApplicationPageBackgroundThemeBrush}"
IsTabStop="False" VerticalAlignment="Stretch" VerticalContentAlignment="Stretch">
<SplitView.Pane>
<StackPanel Background="{ThemeResource ChromeSecondaryBrush}">
<Button Command="{x:Bind Game.RestartLevelCommand}"
Content="Restart Level"
Click="HandleMenuButtonClick"
Style="{ThemeResource MenuItemTextButtonStyle}"/>
<Button Command="{x:Bind Game.UndoCommand}"
Content="Undo"
Click="HandleMenuButtonClick"
Style="{ThemeResource MenuItemTextButtonStyle}"/>
<Button Command="{x:Bind Game.RedoCommand}"
Content="Redo"
Click="HandleMenuButtonClick"
Style="{ThemeResource MenuItemTextButtonStyle}"/>
</StackPanel>
</SplitView.Pane>
<SplitView.Content>
<Grid x:Name="gameCanvasContainer" VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" Background="Transparent">
<Canvas x:Name="gameCanvas" Background="Transparent" />
</Grid>
</SplitView.Content>
</SplitView>
Click
事件也用于在按钮被单击或点击时自动关闭 SplitView
窗格。请参阅列表 10。void HandleMenuButtonClick(object sender, RoutedEventArgs e)
{
splitView.IsPaneOpen = false;
}
ToggleButton
,其 IsChecked
属性绑定到 SplitView
的 IsPaneOpen
属性。请参阅列表 11。ToggleButton
使用文本而非图像来显示汉堡图标。该字符位于 Segoe MDL2 Assets 中,该字体是开箱即用的。<ToggleButton
FontFamily="Segoe MDL2 Assets"
Content=""
Foreground="White"
Background="Transparent"
BorderBrush="Transparent"
TabIndex="1"
AutomationProperties.Name="Navigation"
ToolTipService.ToolTip="Navigation"
IsChecked="{Binding IsPaneOpen, ElementName=splitView, Mode=TwoWay}" />
SplitView
窗格,如图 1 所示。
图 1. 展开的 SplitView 窗格
结论
CellControl
,并考察了图像是如何用于表示单元格内容的。您看到了如何通过缓存 Bitmap
对象来减少应用的内存占用。您看到了如何响应应用内的触摸事件,并编写线程安全的异步可等待代码。最后,您看到了如何实现一个 SplitView
来为游戏提供一个滑动菜单。历史
2016年10月26日
- 首次发布