![]() |
Platforms, Frameworks & Libraries »
Windows Presentation Foundation »
Data Binding
Intermediate
License: The BSD License
WPF Alien SokobanBy Daniel VaughanA fun implementation of the game Sokoban, written to showcase some features of WPF, C# 3.0, Expression Design, and Visual Studio 2008. |
C# 3.0, Windows, .NET 3.5, XAML, WPF, VS2008, Dev, Design
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
The downloadable WPF Sokoban project is intended to be an instructional yet fun introduction to creating WPF applications, and an introduction to some of the new features of .NET 3.5. It is inspired by Sacha Barber's excellent article WPF: The Classic Snakes WPF'ed. This article provides an overview of data binding with WPF: styling and templating of controls. In a later article I intend to port this application to Silverlight, in order to explore some of the differences between the two technologies.
If you must play, decide on three things at the start: the rules of the game, the stakes, and the quitting time.
-Chinese proverb
Sokoban, like many other brain teasing and time consuming puzzles, can be enjoyable to play but difficult to quit. It has few rules, and can, at times, be challenging. The game was invented by Hiroyuki Imabayashi in 1980. It is somewhat ubiquitous, and can be found on game consoles, mobile phones, and according to the Wikipedia entry, Canon power shots digital cameras.
Push the blue power cubes (treasures) onto the yellow power stations. When all cubes are seated in the power stations, our alien friend will be transported to the next level. Only one power cube may be pushed at a time, and they cannot be pulled.
Once a new level has been attained, make note of the level code. This may be used to later return to that level.
Either the arrow keys or the mouse can be used to control the actor. If the arrow keys are used, then the actor is able to move up, down, left, or right. If the mouse is used, then the actor will attempt to traverse the level grid to a point clicked by the user. The mouse can be used to move a power cube by positioning the actor adjacent to the power cube and then by single clicking on it.
Use Ctrl+Z and Ctrl+Y to undo and redo moves and jumps.
The actor may move either in a single step to an adjacent floor cell, or with a jump to any reachable floor cell on the level grid. In the case of a jump, the application will calculate the best path i.e. that which is comprised of the fewest number of moves.
Alien Sokoban's playability may be extended by creating or modifying the map files located in the Levels directory.
The following characters are used within the map files to signify the structure of a level:
This application displays a game level using a System.Windows.Controls.Grid populated with buttons. That is, each cell on the grid contains a button. We use a Style for each button in conjunction with Style triggers to display the cell and any cell contents.
The following excerpt from MainWindow.xaml shows how this is accomplished:
<Style x:Key="Cell" TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Grid>
<!-- The cell, -->
<Rectangle Width="40" Height="40"
Style="{DynamicResource CellStyle}" />
<!-- and its content. -->
<Rectangle Style="{DynamicResource CellContentStyle}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Style triggers change the way the cell is displayed when the Cell or CellContents, which is databound to the button, changes. The following excerpt shows how the Actor is set to display when the CellContents.Name property changes to Actor.
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<!-- If the cell contains the Actor. -->
<Condition Binding="{Binding Path=CellContents.Name}" Value="Actor" />
</MultiDataTrigger.Conditions>
<Setter Property="Fill" Value="{StaticResource PlayerCellContentBrush}"/>
</MultiDataTrigger>
Evaluation of the trigger occurs when the PropertyChanged event of the CellContent class is raised. The DataContext of the button is set to an Orpius.Sokoban.Cell when the level is first initialized in the InitialiseLevel method of the MainWindow codebehind. In this case, the databinding is one way only. That is, the UI does not change the Cell instance. The DataContext of, in this case, a button, is merely some object instance that has implemented the INotifyPropertyChanged interface. When a property changes in the DataContext, it is reflected in the UI. For a more detailed explanation of WPF data binding see Data Binding on MSDN. Please note that implementing the INotifyPropertyChanged interface is not required for data binding, but this allows changes to a DataContext to be reflected in the UI. A single Orpius.Sokoban.Game instance is used for the life of the application. It is specified in XAML as the following excerpt shows:
<Sokoban:Game x:Key="sokobanGame"/>
When the MainWindow is initialized, an instance of the Game class is created and made available as a window resource. We are then able to arbitrarily bind data to the instance, as shown in the following excerpt:
<Label Name="label_Moves" Style="{StaticResource CenterLabels}"
Content="{Binding Level.Actor.MoveCount}"/>
Here, the Level is a property of the Game Window.Resource just mentioned. The Game instance is also used in the codebehind, and is privately exposed as a property like so:
Game Game
{
get
{
return (Game)TryFindResource("sokobanGame");
}
}
All game logic is contained within the Orpius.Sokoban project. This approach was chosen to provide clear separation of game logic and presentation logic, so that we may easily reuse the game logic for a technology other than WPF.
As the previous diagram shows, a Game has, at most, one Level at any time. Each Level has a collection of Cells, with each Cell having zero or one CellContents instances.
The Cell is the base implementation for all cells used in the game.
Cell class and concrete implementations A Cell may hold a single instance of the CellContents class. That is, an Actor or a Treasure may be present in a cell at any one time. Not both.
CellContents base class and its two concrete implementations: Treasure and Actor Each modification of the Game instance in the presentation layer project is done via a CommandBase instance passed to the CommandManager, and is an implementation of the Command Pattern. This allows us to undo and redo moves that the actor has performed. A Jump is considered a single command, and thus is undone in a single step. The CommandManager maintains a Stack of undoable and redoable commands.
CommandManager and related classes Moves are delivered to the Actor instance. The actor knows how to perform a move, whether it is a move to an adjacent cell, or a jump to a non-adjacent cell somewhere on the level.
The number of moves is accumulated by the Actor instance. A jump is considered to be a set of moves, and therefore a jump will increase the number of moves by one or greater. Undoing a move or jump decreases the move count accordingly.
MoveBase class and associated concrete classes A Jump is executed by the Actor instance with the help of a PathSearch instance. The PathSearch attempts to recursively locate the shortest path to the Jump.Destination location. The recursive path search method is presented here:
bool Step(Location location, int steps)
{
bool found = false;
steps++;
if (location.Equals(destination))
{
if (minSteps == 0 || steps < minSteps)
{
route = new Move[steps];
minSteps = steps;
foundPath = true;
arrayIndex = minSteps - 1;
}
else
{
return false;
}
}
else
{
for (int i = 0; i < 4; i++)
{
Direction direction = GetDirection(i);
Location neighbour = location.GetAdjacentLocation(direction);
int traversedLength = traversed.Count;
if (level[neighbour].CanEnter
&& IsNewLocation(neighbour)
&& Step(neighbour, steps)) /* Recursive call. */
{
route[arrayIndex--] = new Move(direction);
found = true;
}
traversed.RemoveRange(traversedLength,
traversed.Count - traversedLength);
}
return found;
}
return foundPath;
}
The efficiency of this method should be improved. For levels with large open areas, it is too slow. It has been listed as a future enhancement.
Expression Design was used to create all of the images seen in the game.
Rather than mocking up the whole game board in Photoshop, I chose to experiment with the design within Expression Design and export it as I went. It was certainly less efficient doing it this way, as time was wasted exporting and viewing repeatedly. The process was somewhat streamlined though: by exporting as a Resource Dictionary one is able to create a brush for each layer in the document.
Within the App.xaml file, a ResourceDictionary element was created for each Expression Design exported document.
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ResourceDictionaries/Cell.xaml" />
<ResourceDictionary Source="ResourceDictionaries/Banner.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
Then a cell could be styled within a trigger, like so:
<Setter Property="Fill" Value="{StaticResource WallCellBrush}"/>
There are two actions that take place within the application that occur asynchronously. The first is level loading. In order to compensate for slow loading of map file data, such as in a web application, loading of level data is done asynchronously. The second is that in order to simulate the actor walking across a level, when a Jump occurs, the executing thread needs to be put to sleep for a specific duration. This is also done asynchronously. As with Windows Forms programming, WPF uses a single thread of execution with thread affinity. The base class for Cell and CellContents is LevelContentBase. This class provides for the raising of the PropertyChanged event asynchronously; not using the main UI thread. This is done by initialising a SynchronizationContext instance with the SynchronizationContext.Current property during instantiation, and sending calls to the main UI thread as shown in the following excerpt from LevelContentBase.cs.
context.Send(delegate
{
OnPropertyChanged(new PropertyChangedEventArgs(property));
}, null);
Level codes are used to jump to a level after it has been successfully completed. The LevelCode class has 500 pregenerated level codes available. Although there are only 51 levels, if more level files are added (*.skbn files in the Levels directory), the level codes will open up. There are some static methods in the LevelCode class to regenerate the level codes if necessary.
The aim of this project was to explore WPF, and to employ some of the new language features of C# 3.5 including extension methods, and automatic properties, and object initialisers. WPF makes it remarkably easy to rapidly create complex GUI interfaces, without having to write lots of plumbing code. In my next article, I intend to port "Alien Sokoban" to Silverlight.
I hope you find this project useful. If so, then you may like to rate it and/or leave feedback below.
| You must Sign In to use this message board. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 16 Jun 2008 Editor: Sean Ewington |
Copyright 2007 by Daniel Vaughan Everything else Copyright © CodeProject, 1999-2009 Web19 | Advertise on the Code Project |