Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / UWP

UWP Alien Sokoban - Part 3

5.00/5 (7 votes)
26 Oct 2016CPOL8 min read 11K  
A fun UWP implementation of the game Sokoban, demonstrating some new features of XAML and C# 6.0. Part 3

Image 1

Introduction

This is part 3 of a 3 part series in which you explore how to implement a XAML based game for the Universal Windows Platform (UWP).

In part 2 you looked at wiring up the the sokoban Game to the app’s main page. You saw how to play sound effects using the UWP MediaElement control. Finally, you explored how the game grid is populated with custom cell controls.
 
In this part you see how the map data is retrieved from the file system. You look closer at the custom CellControl and examine how images are used to represent cell contents. You see how to reduce the memory footprint of your app by caching Bitmap objects. You see how to respond to touch events within an app and at writing creating thread-safe awaitable code. Finally, you see how to implement a SplitView to provide a slide in menu for the game.

Links to articles in this series:

Retrieving a Map

In the game, maps are stored in ascii files. Each character in a file represents a cell. For more information regarding the map format, please see this previous article.
 
The Game class leverages the IInfrastructure implementation to retrieve level maps. Map information is located in the Levels directory of the Sokoban.Launcher project.
 
Each map file has its Build Action set to Content, which causes the map to be placed in the app package at build time, making it accessible to the app. Retrieving a map is performed in the Infrastructure class’s GetMapAsyncmethod, shown in Listing 1.
 
Listing 1. GetMapAsync method
C#
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;
}
We construct a URI for the map using the ms-appx:/// protocol prefix. We then retrieve the file using the asynchronous StorageFileAPIs.

Exploring the Cell Control

When the game is laid out, each Cell object is married to a CellControlinstance. The CellControl is a custom control that consists of several Image objects; each representing the type and contents of the cell.
 
When a Cellis assigned to a CellControl, the CellControl subscribes to the PropertyChanged event of the Cell. When the Cell’s CellContents property changes the UpdateDisplay method is called. See Listing 2. The visibility of each Image object is updated according to the Cell and its contents.
 
Listing 2. CellControl UpdateDisplay method
C#
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);
}

The SetVisibilitymethod is a custom extension method that alleviates the need for a ternary expression to set Visibility property of the UIElement. It reduces verbosity. See Listing 3.

Listing 3. UIElementExtensions SetVisibility method.

C#
public static void SetVisibility(this UIElement element, bool visible)
{
    element.Visibility = visible ? Visibility.Visible : Visibility.Collapsed;
}

In other words, it turns this:

C#
wallImage.Visibility = cell is WallCell ? Visibility.Visible : Visibility.Collapsed;

Into this:

C#
wallImage.SetVisibility(cell is WallCell);

As an aside, it’s a pity Microsoft didn’t take the opportunity to replace the UIElement’s Visibility property with a simpler boolean property, since many less important things have been overhauled in UWP. Surely, it’s a bit late to introduce a new visibility enum value (such as Android’s ‘Gone’ visibility value).

Moreover, there is no built-in BooleanToVisibilityConverter in the SDK, which means that new developers predominately get stuck figuring out how to hide or reveal an element based on a boolean value. The good news is that with the Windows 10 Anniversary Update, x:Bind now implicitly converts to and from a bool and a Visibility enum value. This will, however, only work on machines and devices running the Anniversary update.

Bitmap Caching

Bitmaps can take up large amounts of memory. If you intend your UWP app to run on a phone then you need to be mindful of keeping your memory usage to a minimum.

NOTE: On Windows Mobile, for devices with over 1GB of RAM, your app can use no more than 390 MB. This limit is imposed regardless of how much system memory is available, and if you exceed it, the OS will exit your app.

To reduce the amount of RAM that the Sokoban game needs, BitmapImage objects are cached in a dictionary. While each cell has its own set of Image objects, it shares the underlying BitmapImages with other CellControls.

The image cache is a simple static Dictionary in the CellControl itself, as shown:

C#
static readonly Dictionary<string, BitmapImage> imageCache
   = new Dictionary<string, BitmapImage>();

When a CellControl is instantiated, a set of Image objects are also created. See Listing 4. The CreateContentImage method first attempts to retrieve an image using the relativeUrl parameter. If an Image has previously been created, it is assigned to the Source property of the Image.

Listing 4. CellControl CreateContentImage method

C#
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;
}

Responding to Touch Events

There are various touch and keyboard actions that control our little alien friend. Firstly, tapping on any square causes the alien to attempt to walk to the location using the shortest path it can find. I detailed this in a previous article.
 
NOTE: More than a few types have had their names and/or namespaces changed in WinRT and UWP. (WinRT was the first incarnation of what we now call UWP)
Two such cases are the WPF UIElement MouseEnter and MouseExited events. They they are now PointerEntered and PointerExited respectively.
 
When the user mouses over a cell the PointerEnter event is raised, which invokes the CellControl’s UpdateDisplay method, which then reveals the cell highlight image.

Implementing Thread-Safe Async Await with Semaphores

I’ve updated the Actor class’s movement related code to use async await rather than thread locking. One challenge when implementing threadsafe async code is that lock primitives are not allowed in async code blocks. They can lead to deadlocks. So the compiler prevents you from doing the following:
C#
lock (aLock)
{
    await DoSomethingAsync();
}
You can, however, use the SemaphoreSlim class to prevent race conditions in an async block. I demonstrate this in the Actor’s JumpAsync method. See Listing 5.
 
The SemaphoreSlim named moveSemaphore is engaged by calling its WaitAsync method. Notice that the method is awaitable. You need to await the method to prevent access to the sensitive region.
 
Listing 5. Actor JumpAsync method
C#
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;
}
For information on how a move is performed, and in particular how the SearchPathFinder locates a path, please see this previous Silverlight article.
 
The SemaphoreSlim class supports both asynchronous and synchronous code blocks. If you wish to protect both async and non-async code blocks using the same SemaphoreSlim instance, use its synchronous Wait method. You can see this demonstrated in the Actor’s DoMove method. See Listing 6.
 
Here we rely on the same SemaphoreSlim object to protect a non-awaitable block.
 
Listing 6. Actor DoMove method
C#
internal bool DoMove(Move move)
{
 try
 {
  moveSemaphore.Wait();
 
  return DoMoveAux(move);
 }
 finally
 {
  moveSemaphore.Release();
 }
}

A Note About Image Assets

The first articles made use of Expression Design to create the assets for the game. Expression Design no longer exists as a product. Moreover, UWP does not support the RadialGradientBrush, which meant I couldn't use the image assets I previously created. So, I went back to the drawing board and created the images in Photoshop.
I liked having the assets in XAML because of the lossless scaling that afforded. But, since I intend to also port Alien Sokoban to Android and iOS, using .jpg and .png images makes sense. But boy, I’d sure like to see a RadialGradientBrush in the UWP.

Implementing a Slide-In Menu

The game has the following three commands that a user can activate via a menu in the game:
  • Undo Move
  • Redo Move
  • Restart Level
These commands are implemented as ICommands and are not to be confused with move commands and the GameCommandBase class in the undo redo move system (that predates them).
 
The three commands are instances of a custom DelegateCommand class.
 
NOTE: I considered referencing the Calcium framework, to give me access to all of the usual infrastructure I rely on for most projects. I decided, however, to keep the code in the project simple and approachable.
 
DelegateCommand requires an Action and optionally a Func that evaluates if the Action is allowed to execute. See Listing 7.
 
Listing 7. DelegateCommand class
C#
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 are useful because they share affinity with many UI elements, such as Buttons, which automatically leverage the CanExecute method to automatically change the IsEnabled state of the button.

 
The Game class, in the Sokoban project, creates the three commands in its constructor. See Listing 8.
 
Listing 8. Game constructor
C#
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);
 }
}
The RestartLevelCommand calls the RestartLevel method when it is executed.

The UndoCommand and RedoCommand call the respective Undo and Redo methods of the Game’s commandManager.

Materializing the Commands

A SplitView element is used to overlay a menu pane when a hamburger button is pressed. See Listing 9. The three commands are bound to Button controls in the SplitView.Pane.
 
Listing 9. MainPage.xaml SplitView Excerpt
XML
<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>
The Button’s Click event is also leveraged to automatically close the SplitView pane when the button is clicked or tapped. See Listing 10.
 
Listing 10. MainPage HandleButtonClick method
C#
void HandleMenuButtonClick(object sender, RoutedEventArgs e)
{
 splitView.IsPaneOpen = false;
}
The hamburger button is a ToggleButton whose IsChecked property is bound to the IsPaneOpen property of the SplitView. See Listing 11.
 
The ToggleButton doesn’t use an image but rather text to display the hamburger icon. The character is located in the Segoe MDL2 Assets, which is supported out of the box.
 
Listing 11. Hamburger ToggleButton
XML
<ToggleButton
 FontFamily="Segoe MDL2 Assets"
 Content="&#xE700;"
 Foreground="White"
 Background="Transparent"
 BorderBrush="Transparent"
 TabIndex="1"
 AutomationProperties.Name="Navigation"
 ToolTipService.ToolTip="Navigation"
 IsChecked="{Binding IsPaneOpen, ElementName=splitView, Mode=TwoWay}" />
Tapping the hamburger button expands the SplitView pane, as shown in Figure 1.
 
Image 2

Figure 1. Expanded SplitView Pane

Conclusion

In this article you saw how the map data is retrieved from the file system. We looked closer at the custom CellControl and examined how images are used to represent cell contents. You saw how to reduce the memory footprint of your app by caching Bitmap objects. You saw how to respond to touch events within an app and at writing thread-safe awaitable code. Finally, you saw how to implement a SplitView to provide a slide-in menu for the game.
 
In the next series we take a look porting Sokoban to Xamarin Forms, in which we leverage our platform-agnostic Sokoban project. I hope you’ll join me.
 
I hope you find this project useful. If so, then please rate it and/or leave feedback below.

History

October 26 2016

  • First published

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)