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

UWP Alien Sokoban - Part 2

4.98/5 (15 votes)
18 Oct 2016CPOL7 min read 19K  
A fun UWP implementation of the game Sokoban, demonstrating some new features of XAML and C# 6.0. Part 2

Image 1

Introduction

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

In part 1 you looked at creating a cross-platform compatible class library in which to place game logic. We contrasted some of the differences between file linking, shared projects, portable class libraries, and the new .NET standard. Finally you saw how the Game page is implemented, and we briefly looked at the new x:Bind markup extension.
 
In this part we look at wiring up the the sokoban Game object to the app’s main page. You see how to play sound effects using the UWP MediaElement control. Finally, you explore how the game grid is populated with custom cell controls.

Links to articles in this series:

Wiring up the Game Page

The Game class, within the Sokoban project, is the entry point for the game logic, and effectively the ViewModel of the main page.
 
The Sokoban project is a PCL project and all code contained therein is platform agnostic. However, the Game class requires several abstracted platform features to function correctly. Firstly, it requires a means to test if it is running on the UI thread, so as to avoid invoking calls unnecessarily back on the UI thread. The Portable Class Library project does not have the APIs needed to do that.  So, the Sokoban Game class expects an instance of a class implementing a custom ISynchronizationContext interface.
 
ISynchronizationContext has two members:
  • bool InvokeRequired { get; }
  • void InvokeIfRequired(Action action)
 
NOTE: For this project I provide a UWP specific implementation of ISynchronizationContext. In the next article you see a Xamarin Forms targeted implementation.
 
The UWP implementation of ISynchronizationContext is the UISynchronizationContext class. See Listing 1.
 
Listing 1. UISynchronizationContext class
C#
class UISynchronizationContext : ISynchronizationContext
 {
  CoreDispatcher dispatcher;
  readonly object initializationLock = new object();
 
  public bool InvokeRequired
  {
   get
   {
    EnsureInitialized();
 
    return !dispatcher.HasThreadAccess;
   }
  }
 
  public void InvokeIfRequired(Action action)
  {
   EnsureInitialized();
 
   if (InvokeRequired)
   {
 
#pragma warning disable 4014
    dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => action());
#pragma warning restore 4014
   }
   else
   {
    action();
   }
  }
 
  bool initialized;
 
  void EnsureInitialized()
  {
   if (initialized)
   {
    return;
   }
 
   lock (initializationLock)
   {
    if (initialized)
    {
     return;
    }
 
    try
    {
     dispatcher = CoreApplication.MainView.CoreWindow.Dispatcher;
    }
    catch (InvalidOperationException ex)
    {
     throw new Exception("Initialize called from non-UI thread.", ex);
    }
 
    initialized = dispatcher != null;
   }
  }
 }
UISynchronizationContext uses the CoreDispatcher, retrieved via CoreApplication.MainView.CoreWindow.Dispatcher
The dispatcher must be retrieved on the UI thread, otherwise an InvalidOperationException is thrown.
 
MainPage.xaml.cs is where the Game object is instanciated. See Listing 2.
 
The LevelCodesGenerator class allows you to generate a unique set of level codes for your game. By uncommenting the commented lines in the MainPage constructor you generate the LevelCode class which can then replace the existing LevelCode class in the Sokoban project.
 
Listing 2. MainPage constructor
C#
public MainPage()
{
 /* Uncomment to generate the LevelCode class. */
 //LevelCodesGenerator.GenerateLevelCodes();
 
 InitializeComponent();
 
 var synchronizationContext = new UISynchronizationContext();
 var audioClips = new AudioClips(rootGrid, synchronizationContext);
 
 var infrastructure = new Infrastructure(synchronizationContext, audioClips);
 infrastructure.Initialize();
 
 Game = new Game(infrastructure);
 DataContext = Game;
 
 Loading += HandleLoading;
}
The Game object requires an object implementing the IInfrastructure interface. IInfrastructure could well be called ‘IPlatformSpecificStuff’. IInfrastructure is an abstraction over the platform specific APIs to retrieve map data from the file system (or elsewhere), to store and retrieve app settings, and to play sound effects. IInfrastructure has the following members:
  • int LevelCount { get; }
  • Task<string> GetMapAsync(int levelNumber);
  • ISynchronizationContext SynchronizationContext { get; }
  • void SaveSetting(string key, object setting);
  • bool TryGetSetting(string key, out object setting);
  • T GetSetting<T>(string key, T defaultValue);
  • void PlayAudioClip(AudioClip audioClip);
Please take a look at the IInfrastructure code file for the member descriptions.

In this article we implement this interface for UWP. We’ll do the same for iOS and Android when we go on to support those platforms in a later article.

Playing Sound Effects

Sound effects bring a game alive. In this project, the AudioClips class performs the playback of a known set of audio files.
 
You use the Windows.UI.Xaml.Controls.MediaElement to playback an audio file. See Listing 3.
 
Listing 3. AudioClips class
C#
class AudioClips
{
 readonly ISynchronizationContext synchronizationContext;
 readonly Panel page;
 
 internal AudioClips(Panel page, ISynchronizationContext synchronizationContext)
 {
  this.page = ArgumentValidator.AssertNotNull(page, nameof(page));
  this.synchronizationContext = ArgumentValidator.AssertNotNull(
      synchronizationContext, nameof(synchronizationContext));
 
  Uri baseUri = page.BaseUri;
 
  const string audioDir = "/Audio/";
  levelIntroductionElement = CreateElement(new Uri(baseUri, audioDir + "LevelIntroduction.mp3"));
  /* MediaElement requires a moment to load its media. Calling Play immediately fails.
   * You can either subscribe to the MediaOpened event or have it auto-play. */
  levelIntroductionElement.AutoPlay = true;
 
  gameCompleteElement = CreateElement(new Uri(baseUri, audioDir + "GameComplete.mp3"));
  levelCompleteElement = CreateElement(new Uri(baseUri, audioDir + "LevelComplete.mp3"));
  footstepElement = CreateElement(new Uri(baseUri, audioDir + "Footstep.mp3"));
  treasurePushElement = CreateElement(new Uri(baseUri, audioDir + "TreasurePush.mp3"));
  treasureOnGoalElement = CreateElement(new Uri(baseUri, audioDir + "TreasureOnGoal.mp3"));
 }
 
 MediaElement CreateElement(Uri uri)
 {
  var result = new MediaElement { Source = uri, AutoPlay = false };
  page.Children.Add(result);
  return result;
 }
 
 readonly MediaElement levelIntroductionElement;
 readonly MediaElement gameCompleteElement;
 readonly MediaElement levelCompleteElement;
 readonly MediaElement footstepElement;
 readonly MediaElement treasurePushElement;
 readonly MediaElement treasureOnGoalElement;
 
 void Play(MediaElement element)
 {
  synchronizationContext.InvokeIfRequired(() =>
  {
   element.Position = TimeSpan.Zero;
   element.Play();
  });
 }
 
 internal void Play(AudioClip audioClip)
 {
  switch (audioClip)
  {
   case AudioClip.Footstep:
    Play(footstepElement);
    return;
   case AudioClip.GameComplete:
    Play(gameCompleteElement);
    return;
   case AudioClip.LevelComplete:
    Play(levelCompleteElement);
    return;
   case AudioClip.LevelIntroduction:
    Play(levelIntroductionElement);
    return;
   case AudioClip.TreasureOnGoal:
    Play(treasureOnGoalElement);
    return;
   case AudioClip.TreasurePush:
    Play(treasurePushElement);
    return;
  }
 }
}
It feels a little strange using a visual element to play a sound effect audio file. But, that’s how it’s done in UWP. Windows Phone Silverlight allowed access to some XNA APIs for audio playback, which was a tad more convenient.
 
NOTE: Even though we don’t need or want a visual representation of each sound effect in our user interface, we still need to add the MediaElement to the visual tree, or the MediaElement won’t work.
 
We pass the AudioClips class the root Grid control from the main page. The AudioClips object then adds its MediaElement controls to the grid and we’re good to go.
 
NOTE: Before a clip can be played it must be loaded by the MediaElement control. If you call Play on the MediaElement before it’s loaded, then playback of the clip will be missed. You can do two things to ensure playback.
  • Subscribe to the MediaElements MediaOpened event to know when the MediaElement is ready. Or
  • Set the AutoPlay property of the MediaElement to true. AutoPlay causes the MediaElement to begin playback as soon as it is ready. This is what I did with the introduction clip to ensure it plays when the app is launched.
 
Because MediaElement derives from DependencyObject, all calls to it must be dispatched from the UI thread. The Game object performs some activities on a threadpool thread, which means that if it chooses to play an audio clip from a background thread, the call must be invoked on the UI thread. Hence the use of the ISynchronizationContext.InvokeIfRequired method.

Laying Out the Game Grid

We use the MainPage’s Loading event to trigger laying out of the game grid. See Listing 4.
 
The Loading event may occur multiple times during the app’s lifecycle. Therefore a loaded flag is used to limit execution to one time.
 
Listing 4. HandleLoading method.
C#
void HandleLoading(FrameworkElement sender, object args)
{
 if (loaded)
 {
  return;
 }
 
 loaded = true;
 
 StartGame();
 
 var window = Window.Current;
 window.CoreWindow.KeyUp += HandleKeyUp;
 window.SizeChanged += HandleWindowResized;
}
When the app’s Window is resized, the game grid needs to be laid out again. The Window's SizeChanged event triggers that. See Listing 5.

The size of the game container element does not change until after this event is raised, therefore we queue the LayOutLevel call using the Page’s Dispatcher. If we didn’t do that, the LayOutLevel would use the previous dimensions.
 
NOTE: The BeginInvoke method of the Dispatcher class in WPF and Silverlight has been replaced by a RunAsync method. You’d be forgiven for thinking that the delegate (in this case LayOutLevel) is executed on a Threadpool thread, but it isn’t. It’s not the same as Task.RunAsync, although the name would imply otherwise.
 
Listing 5. HandleWindowResized method.
C#
async void HandleWindowResized(object sender, WindowSizeChangedEventArgs e)
{
 await Dispatcher.RunAsync(CoreDispatcherPriority.High, LayOutLevel);
}
When a key is pressed, the user may be using the keyboard to play the game (rather than the mouse). Keystrokes are passed to the Game’s ProcessKeyPressed method and the game object decides what to do. See Listing 6.
 
Listing 6. HandleKeyUp method.
C#
void HandleKeyUp(CoreWindow sender, KeyEventArgs e)
{
 var state = CoreWindow.GetForCurrentThread().GetKeyState(VirtualKey.Control);
 bool shiftPressed = (state & CoreVirtualKeyStates.Down) == CoreVirtualKeyStates.Down;
 
 Game.ProcessKeyPressed(e.VirtualKey.ToKeyboardKey(), shiftPressed);
}
 
The VirtualKey enum is platform specific, so we can’t pass that to the platform agnostic Game object. Instead we use the custom extension method ToKeyboardKey to map the actual key to a key within the game’s KeyboardKey enumeration.

Starting the Game

The MainPage StartGame method (See Listing 7)  subscribes to the Game’s PropertyChanged event.
 
Listing 7. MainPage StartGame method
C#
void StartGame()
{
 Game.PropertyChanged -= HandleGamePropertyChanged;
 Game.PropertyChanged += HandleGamePropertyChanged;
   
 Game.Start();
}
The <font face="Courier New">MainPage</font>’s <font face="Courier New">ProcessGameStateChanged </font>method is called when the Game’s GameState property changes. See Listing 8.
 
Listing 8. HandleGamePropertyChanged method
C#
void HandleGamePropertyChanged(object sender, PropertyChangedEventArgs e)
{
 switch (e.PropertyName)
 {
  case nameof(Game.GameState):
   ProcessGameStateChanged();
   break;
 }
}
When the Game’s state changes from Loading to Running, the game must be laid out. See Listing 9.

This may serve as an extensibility point where you could enrich the game by adding animations and so forth for the various game states.
 
Listing 9. MainPage ProcessGameStateChanged method
C#
void ProcessGameStateChanged()
{
 switch (Game.GameState)
 {
  case GameState.Loading:
   break;
  case GameState.GameOver:
   break;
  case GameState.Running:
   if (gameState == GameState.Loading)
   {
    InitialiseLevel();
   }
   break;
  case GameState.LevelCompleted:
   break;
  case GameState.GameCompleted:
   break;
 }
 
 gameState = Game.GameState;
}
The MainPage’s InitializeLevel method unsubscribes to each cell’s various events and removes all cell controls from the gameCanvas element, before calling the LayOutLevel method. See Listing 10.
 
Listing 10. MainPage InitializeLevel method.
C#
void InitialiseLevel()
{
 foreach (CellControl control in controlDictionary.Values)
 {
  DetachCellControl(control);
 }
 
 controlDictionary.Clear();
 
 gameCanvas.Children.Clear();
 
 LayOutLevel();
 
 levelCodeTextBox.Text = Game.LevelCode;
}
NOTE: The levelCodeTextBox element’s Text property should really be bound to a Game property. The reason it isn't is because I reused the logic from a previous article and didn’t get around to refactoring it.
 
A game Level is essentially a two dimensional array of game cells. See Listing 11. In the LayOutLevel method the size of the game cells are maximized to encompass the available space.
 
We associate each cell with a CellControl instance using a Dictionary<Cell, CellControl>. CellControl objects are added to the game Canvas at the appropriate position.
 
Listing 11. MainPage LayOutLevel method
C#
void LayOutLevel()
{
 Level level = Game.Level;
 int rowCount = level.RowCount;
 int columnCount = level.ColumnCount;
   
 /* Calculate cell size and offset. */
 double availableWidth = gameCanvasContainer.ActualWidth;
 double availableHeight = gameCanvasContainer.ActualHeight - topContentGrid.Height;
 int cellWidthMax = (int)(availableWidth / columnCount);
 int cellHeightMax = (int)(availableHeight / rowCount);
 int cellSize = Math.Min(cellWidthMax, cellHeightMax);
 
 int gameHeight = rowCount * cellSize;
 int gameWidth = columnCount * cellSize;
 
 int leftStart = 0;
 double left = leftStart;
 double top = (availableHeight - gameHeight) / 2;
 
 /* Add CellControls to represent each Game Cell. */
 for (int row = 0; row < rowCount; row++)
 {
  for (int column = 0; column < columnCount; column++)
  {
   Cell cell = Game.Level[row, column];
 
   CellControl cellControl;
   if (!controlDictionary.TryGetValue(cell, out cellControl))
   {
    cellControl = new CellControl(cell);
    controlDictionary[cell] = cellControl;
 
    DetachCellControl(cellControl);
    AttachCellControl(cellControl);
 
    gameCanvas.Children.Add(cellControl);
   }
 
   cellControl.SetValue(Canvas.LeftProperty, left);
   cellControl.SetValue(Canvas.TopProperty, top);
   cellControl.Width = cellSize;
   cellControl.Height = cellSize;
 
   left += cellSize;
  }
 
  left = leftStart;
  top += cellSize;
 }
 
 gameCanvas.Width = gameWidth;
 gameCanvas.Height = gameHeight;
}
 

Conclusion

In this article we 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 the next part you see how the map data is retrieved from the file system. We 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 how to write thread-safe awaitable code. Finally, you see how to implement a SplitView to provide a slide in menu for the game.
 
I hope you find this project useful. If so, then please rate it and/or leave feedback below.

History

  • October 14 2016
    • First published

License

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