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 GetMapAsync
method, shown in Listing 1.
Listing 1. GetMapAsync method
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 StorageFile
APIs.
Exploring the Cell Control
When the game is laid out, each Cell
object is married to a CellControl
instance. The CellControl
is a custom control that consists of several Image objects; each representing the type and contents of the cell.
When a Cell
is 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
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 SetVisibility
method 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.
public static void SetVisibility(this UIElement element, bool visible)
{
element.Visibility = visible ? Visibility.Visible : Visibility.Collapsed;
}
In other words, it turns this:
wallImage.Visibility = cell is WallCell ? Visibility.Visible : Visibility.Collapsed;
Into this:
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:
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
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:
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
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];
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++;
}
Jump newMove = new Jump(searchPathFinder.Route) { Undo = true };
moves.Push(newMove);
result = true;
}
}
finally
{
moveSemaphore.Release();
}
return result;
}
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
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
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
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());
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
<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
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
<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}" />
Tapping the hamburger button expands the SplitView
pane, as shown in Figure 1.
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