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日
- 首次发布

