Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Silverlight Alien Sokoban

0.00/5 (No votes)
11 Nov 2007 1  
A fun Silverlight implementation of the game Sokoban. Contrasting Silverlight 1.1 and WPF, while showcasing some new features of C# 3.0, Expression Design, Expression Blend, and Visual Studio 2008.
Screenshot - SilverlightGame.jpg

Contents

Introduction

In our last article we discussed the implementation of the game Sokoban using WPF. In this article we will examine the porting of Alien Sokoban to Silverlight, and examine some of the differences between WPF and Silverlight (formally WPF/E). We will look at consuming a web service from Silverlight, creating Silverlight User Controls, hosting a Silverlight project in ASP.NET, and using some of the new Microsoft tools such as Expression Design and Expression Blend; we will also explore some more advanced topics such as multithreading in Silverlight. There have also been quite a few enhancements and new features added to the game.

For those eager to take a look, without having to go through the rigmarole of setting it up locally, I have deployed the game to a server: Play Alien Sokoban.

Background

As the builders say, the larger stones do not lie well without the lesser.

-Plato

In our first article we saw how easy it was to define most of the presentation logic for the game in XAML. This allowed us to keep our code behind mostly free from boiler plate code. WPF has many features that are not present in Silverlight, such as data binding, templating, and styling of controls. Alas our Silverlight version contains considerably more plumbing.

As you probably already know, Silverlight is a browser plugin for providing rich web content. It includes a subset of the capabilities of WPF, and it aims to rival Adobe Flash. I have been interested in Silverlight since I heard about it in 2006. Years ago, I spent a while as a Flash developer/animator, and back then I was writing loosely typed Actionscript for Flash 5. I've never been a big fan of scripting, and I had wanted to do more. Now, like Adobe Flex, Silverlight offers the developer just that. But, having a .NET CLR client side is enough to make most .NET web developers drool.

Silverlight is going to be big, really big. Like Flash, it is cross platform. At present the Silverlight 1.1 Alpha September Refresh plugin is available for Windows and Mac, and the MONO team has stated that it intends to have a Linux version, called Moonlight, available by the end of the year [The Inquirer 2007][Wikipedia 2007]. Version 1.0 of Silverlight offered a light-weight browser plugin, and manipulation of Silverlight at client side was limited to scripting of the DOM using JavaScript. Version 1.1 is a lot more attractive to developers, as it offers .NET on the client side. It has a CLR 2.0 compiled CLR and Type System, a reduced set of BCL classes from .NET Framework 3.5 (including DLINQ). I have, however, encountered a number of noticeably absent classes, which has meant that I have had to do some reimplementation. The 1.1 plugin also includes the new dynamic language runtime (DLR), with support coming for Python, Ruby, VBScript VBx (Visual Basic "10") and JavaScript (ECMAScript), but we won't be exploring that here [Hanselman 2007].

While I believe the excitement that Silverlight is generating is well founded, I wouldn't want to see us plunged back into a web dark age, with a return to gratuitous animation as mentioned in Jakob Nielsen's 2000 polemic article Flash: 99% Bad. Like Flash, Silverlight offers the opportunity to once again decrease usability and accessibility. Do you remember those annoying animated intros! I must confess that I created a few! Yet having the freedom, to develop interactive content, without having to write loads of JavaScript makes me happy. After years of building websites using technologies like Java and ASP.NET, and like many developers, spending an exorbitant amount of time contending with browser differences and bending the HTML model to my will, I rejoice at even the thought of being freed of such a burden.

Part of Microsoft's strategy in producing several new Visual Design IDEs, I refer to the Expression product suite, is obviously to provide distinct separation of the Graphic Design, Animation, and Development aspects of projects. I know from first hand that this is not a bad idea. Whether this is a commercial strategy, I won't speculate, but it makes business sense. In a commercial software house, designers output their designs in the form of vector images etc., and this becomes the input to the developers/animators who then proceed to dissect, reformat and generally squash it into something that, as much as possible stays true to the original design. More often than not, however, last minute changes to the design or tweaking needs to be done, and this throws a spanner in the works, and often causes the animator/developer to have to duplicate his or her undocumented squashing process. The nice thing about using a unified format, in this case XAML, is that there isn't a conversion process between handoffs, and to a limited extent it does empower the developer to make minor changes. But, I can see there being some advantage to having some rudimentary support for Storyboarding within Visual Studio, which it sorely lacks. Until it does, smaller organizations will need to seriously consider providing their developers with Expression Blend.

Getting Started with Silverlight

To work with Silverlight 1.1 within Visual Studio 2008 Beta 2, one must download the Microsoft Silverlight Tools Alpha. That will add a new item to the Visual Studio project types.

Visual Studio Silverlight projects
Figure: Silverlight project types in Visual Studio 2008.

Using a Silverlight Project with Other Regular CLR Projects

In order to debug Silverlight projects hosted within a non-Silverlight project, such as in our case an ASP.NET web application, we must use the Add Silverlight Link from the Visual Studio Project context menu within the Solution Explorer.

Linking to the Silverlight project and hosting it within the ASP.NET project that contains the web service avoids the web service "Cross-Domain problem" that I've heard frequently mentioned.

Add Silverlight Link in Visual Studio
Figure: Add Silverlight Link in Visual Studio.

Once this is done, files including output assemblies from the ClientBin in the Silverlight project will be automatically copied to your hosting project, and they will be kept in sink when one performs a build. Of course, however, content resources used by your Silverlight project, such as media files, will not be copied, and may require a build event copy task, as show below.

Visual Studio Build Event
Figure: Visual Studio Build Event copy task.

How to Play the Game

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.

Controls

Either the keys i, j, k, l or the mouse can be used to control the actor. If the keyboard is 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. There is a new feature in Alien Sokoban: 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, or by holding down the shift key and clicking a square that is in the same row or column as the actor.

Undo and Redo

Use Ctrl+Z and Ctrl+Y to undo and redo moves and jumps.

Level Items

  • Power cubes are to be moved to the goal cells (power stations)
  • The actor is controlled by the user
  • Wall cells cannot be entered by the actor, nor have content placed in the cell
  • Floor cells are where the actor may move, or place power cubes
  • Space cells are normally outside of the walled enclosure of the level grid. They can't contain content

Moving Around

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.

Extending Playability

Alien Sokoban's playability may be extended by creating or modifying the map files located in the Levels directory.

Map File Format

The following characters are used within the map files to signify the structure of a level:

  • "#" Wall
  • " " Empty floor cell
  • "$" Power cell in a power station (or goal)
  • "." Power station (or goal)
  • "@" Actor in a floor cell
  • "!" Space cell

Level Codes

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.

About the Code

Asynchronous Property Changes

To simulate the actor walking across cells in a level, when either a Jump or a Push occurs, the executing thread needs to be put to sleep for a specific duration. For this to work correctly it is done asynchronously. The base class for Cell and CellContents is LevelContentBase. This class provides for the raising of the PropertyChanged event asynchronously. Like WPF, Silverlight uses a single thread of execution with thread affinity. This means that we cannot manipulate UI elements from any thread other than the main application thread. To prevent an InvalidOperationException being thrown from worker threads raising PropertyChanged events, we use a SynchronizationContext instance, initialised during the Page.xaml Page_Loaded handler. This queues calls to the main UI thread as shown in the following excerpt from LevelContentBase.cs.

context.Send(delegate
{
    OnPropertyChanged(new PropertyChangedEventArgs(property));
}, null);

Multithreading in Silverlight

At the moment there is no built-in way to marshal updates to the UI from a background thread. System.Threading.SynchronizationContext is noticeably absent from the Silverlight subset BCL. So, we've had to role our own.

public delegate void SendOrPostCallback(object state);

/// <summary>

/// Emulates System.Threading.SynchronizationContext

/// for Silverlight.

/// </summary>

public class SynchronizationContext
{
    static System.Threading.Thread thread =
        System.Threading.Thread.CurrentThread;
    /* Timer to invoke method calls on the main thread. */
    readonly System.Windows.Browser.HtmlTimer timer =
        new System.Windows.Browser.HtmlTimer();
    readonly List<CallbackState> callbacks = new List<CallbackState>();
    readonly object callbacksLock = new object();

    #region Singleton implementation

    SynchronizationContext()
    {
        thread = System.Threading.Thread.CurrentThread;
        timer.Interval = 1;
        timer.Tick += new EventHandler(timer_Tick);
        timer.Start();
    }

    /// <summary>

    /// Gets the singleton instance.

    /// </summary>

    /// <value>The current.</value>

    public static SynchronizationContext Current
    {
        get
        {
            return Nested.instance;
        }
    }

    class Nested
    {
        /* Explicit static constructor to tell C# compiler
         * not to mark type as beforefieldinit. */
        static Nested()
        {
        }

        internal static readonly SynchronizationContext instance =
            new SynchronizationContext();
    }

    #endregion

    /// <summary>

    /// Triggers the Initialisation of the context.

    /// Method signature set to match

    /// System.Threading.SynchronizationContext.SetContext

    /// Provided here for convenience to avoid code changes

    /// when using this module in a non-Silverlight scenario.

    /// </summary>

    /// <param name="dummy">Dummy parameter

    /// to mimic System.Threading.SynchronizationContext.SetContext</param>

    public static void SetContext(object dummy)
    {
        /* Intentionally left blank. */
    }

    void timer_Tick(object sender, EventArgs e)
    {
        lock(callbacksLock)
        {
            if (callbacks.Count > 0)
            {
                /* Process all waiting callbacks. */
                foreach (CallbackState state in callbacks)
                {
                    state.Callback(state.State);
                }
                callbacks.Clear();
            }
        }
    }

    /// <summary>

    /// Queues the specified <see cref="SendOrPostCallback"/>

    /// to be called from the <see cref="System.Threading.Thread"/>

    /// that this context was created on.

    /// </summary>

    /// <param name="callback">The callback delegate to be called

    /// on the thread that this context was instantiated.</param>

    /// <param name="state">The state that is passed to the

    /// specified callback.</param>

    public virtual void Send(SendOrPostCallback callback, object state)
    {
        if (thread.Equals(System.Threading.Thread.CurrentThread))
        {    /* This is the correct thread. That is, it is not asynchronous
             and can be called immediately. */
            callback(state);
            return;
        }
        lock (callbacksLock)
        {
            callbacks.Add(new CallbackState()
            { Callback = callback, State = state });
        }
    }

    class CallbackState
    {
        public SendOrPostCallback Callback
        {
            get;
            set;
        }
        public object State
        {
            get;
            set;
        }
    }
}

We emulate the System.Threading.SynchronizationContext: calls that would otherwise be executed using a reference to a main UI thread context, are invoked using a timer and a queue. Unfortunately you'll notice that System.Windows.Browser.HtmlTimer is deprecated, and that a new Timer type is on its way. But for now, it does the trick.

Sokoban Web Service

In the previous article we saw our game load map data directly from a predefined local directory. In this version, rather than sending all maps down the wire, we allow them to be requested at runtime via a web service. That is, the Sokoban web service provides the Silverlight game with map data.

Silverlight supports JSON for communicating with web services, and we specify that the web service should use JSON by decorating our SokobanService methods with the ScriptMethod attribute as shown in the following excerpt.

[WebMethod(CacheDuration = cacheTimeoutSeconds)]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public string GetMap(int levelNumber)
{
    string virtualPath = string.Format(@"{0}Level{1:000}.skbn",
        levelDirectory, levelNumber);
    string filePath = Server.MapPath(virtualPath);
    string mapData;

    lock (readerLock)
    {
        using (StreamReader reader = File.OpenText(filePath))
        {
            mapData = reader.ReadToEnd();
        }
    }

    return mapData;
}

You may notice that I have commented parts of the Game class for retrieving map data from the web service asynchronously. This is because the Silverlight 1.1 alpha September Refresh throws an InvalidOperationException if the web service calls are not made from the main UI thread. I hope to see this resolved in a future release.

Silverlight 1.1 Alpha - Keyboard Event Handling

When handling keyboard events, you will notice that there isn't a Silverlight enum for key codes. Instead the KeyboardEventArgs of the KeyUp and KeyDown event use an int value to indicate the key. These key codes are non-specific to any particular operating system, and in fact they are a common subset of all the possible key codes for Macintosh and Microsoft Windows. [Microsoft 2007]

Unfortunately a bug present in the Alpha 1.1 Refresh release causes the arrow keys not to fire the KeyUp event correctly. Instead we have had to change the keyboard mappings to the keys used which are 'I' (up) 'K' (Down) 'J' (Left) 'L' (Right).

Layout Support

Unlike WPF, there is no support for centering text within a TextBlock [Porter 2007]. This was performed manually within the user controls, as the following excerpt demonstrates:

Canvas parent = (Canvas)Parent;
double left = (parent.Width - base.Width) / 2.0;
double top = (parent.Height - base.Height) / 2.0;

User Controls

Silverlight does not include a standard set of controls that you would normally expect to use to build an interactive interface. Expect these to come later. In the mean time we need to come up with our own.

Textbox Control

There is no textbox control included with the Silverlight distribution yet. So I've had to adapt Stephane Tombeur's one for our needs. You will notice also that it is not integrated into the PlayControl.xaml. This is, unfortunately, because there is no provision for the nesting of controls in Silverlight, which raises a serious reusability issue. One may nest canvases, but not controls. [Silverlight 2007]

Hit Testing

To programmatically focus and unfocus the TextBox control one must detect when and where a click occurs on the page when the TextBox has focus. That is, when a user clicks away from the TextBox, the TextBox unfocuses itself. The following excerpt demonstrates how this was done. I'm dubious as to the reliability of this method, but it will suffice.

void root_MouseLeftButtonDown(object sender, MouseEventArgs e)
{
    if (!hasFocus)
    {
        return;
    }
    Point point = e.GetPosition(this);
    if (point.X < 0 || point.X > outline.Width || point.Y < 0
        || point.Y > outline.Height)
    {
        Unfocus();
    }
}

Text Effects

WPF has lots of effects, Silverlight doesn't have any. It's DIY here I'm afraid. There is a simple drop shadow effect in the FeedbackControl which is done using a duplicated TextBlock with a RenderTransform to offset it, and to appear as a shadow.

<TextBlock x:Name="textBlock_MessageShadow" Text="TextBlock">
    <TextBlock.RenderTransform>
        <TransformGroup>
            <ScaleTransform ScaleX="1" ScaleY="1"/>
            <SkewTransform AngleX="0" AngleY="0"/>
            <RotateTransform Angle="0"/>
            <TranslateTransform X="2" Y="2"/>
        </TransformGroup>
    </TextBlock.RenderTransform>
</TextBlock>
<TextBlock x:Name="textBlock_Message" Text="TextBlock" />

Sokoban Project

Game logic is contained within the Orpius.Sokoban project. I had originally intended to add the existing Orpius.Sokoban project from my previous article, and simply reuse that. This was not possible, as Silverlight projects can only reference other Silverlight projects. This means that only assemblies that were especially compiled for Silverlight, i.e. those that come with Silverlight 1.1 or those built by you or others; compiled for the Silverlight CLR, can be referenced. [Zander 2007]

For more information regarding the core Sokoban game project, please see my previous article. There have been a number of enhancements and new features implemented. More on that later.

Animating with Expression Blend

To create an animation within Expression Blend, click the > button in the "Objects and Timeline" window. This will produce a dialogue in which you can define a new Storyboard. A Storyboard consists of a timeline where one defines keyframes to indicate transition points in the animation. Anyone familiar with Flash will be right at home here.
N.B. What you see isn't necessarily what you get. Moreover, I found that animating a TextBlock colour change worked nicely when previewing in Expression Blend, but not in the game. Until I explicitly defined the Foreground colour property of the TextBlock, the animation would not work.

Create a storyboard
Figure: Creating a storyboard in Expression Blend.

Creating a mouseover/mouseout effect is as simple as creating two Storyboards.

Figure: Expression Blend Storyboard Animation.

And then wiring them to the appropriate events in the code behind:

button_RestartLevel.MouseEnter +=
    new MouseEventHandler(button_RestartLevel_MouseEnter);
button_RestartLevel.MouseLeave +=
    new EventHandler(button_RestartLevel_MouseLeave);

The Storyboards are then started thus:

void button_RestartLevel_MouseEnter(object sender, MouseEventArgs e)
{
    restartLevelMouseOverStoryboard.Begin();
}

void button_RestartLevel_MouseLeave(object sender, EventArgs e)
{
    restartLevelMouseOutStoryboard.Begin();
}

Exporting from Expression Design

In our last article, we exported the layers of our Expression Design Cell image to a ResourceDictionary. Unfortunately Silverlight does not use Resource Dictionaries, and instead we export as a Silverlight Canvas, where each layer is exported as a named child Canvas. It works in much the same way, but we must add a little more plumbing in the code behind in order to show, hide, or resize the layers (Canvases).

Exporting from Expression Design for Silverlight
Figure: Exporting from Expression Design for Silverlight.

Scale Transform

In order to use the canvases that are exported from Expression Design, they had to be resized, and to achieve this, a ScaleTransform render was used. By placing a ScaleTransform in a parent Canvas, resizing the parent will cause all child canvases to be resized by the same proportion. To achieve this in XAML we use:

<Canvas.RenderTransform>
    <ScaleTransform x:Name="scale" ScaleX="1" ScaleY="1" />
</Canvas.RenderTransform>

If we do not wish to make modifications to an exported XAML file, lest our changes be lost, we can achieve this programmatically by instantiating a ScaleTransform,

ScaleTransform scale = new ScaleTransform() { ScaleX = 1, ScaleY = 1 };

and then assign it to your target Canvas.

rootCanvas.RenderTransform = scale;

Here the aspect ratio is 1:1. That is, if we now change the ScaleX or ScaleY properties of our ScaleTransform instance, we will effect a resize of the parent and all children by the ratio of 1:X or 1:Y where X and Y are the new ScaleX and ScaleY property values respectively. As an example, say our canvas is 500 pixels wide and we include a ScaleTransform render with ScaleX and ScaleY of both 1. If we then change the ScaleX value to 0.5, the width of the canvas will be reduced to 250 pixels, and the height will remain unchanged; effectively skewing the parent and children.

Sokoban Project Continued

Moves

Moves are delivered to the Actor instance. The actor knows how to perform a move, whether it is a "step" to an adjacent cell, or a "jump or push" to a non-adjacent cell somewhere on the level.

Game Logic Enhancements

Jumping, Pushing, and Path Searching

A Jump is executed by the Actor instance with the help of a SearchPathFinder instance. The Path Search algorithm has been revised from a Depth First search, which would be ok if we weren't interested in finding the best path, to a Breadth First search. (Shiflet 1996) The following excerpt shows the new implementation of the path search algorithm.

bool TryFindPath(Location start)
{
    Queue<Location> queue = new Queue<Location>();
    queue.Enqueue(start);
    Dictionary<Location, Location> previousSteps =
        new Dictionary<Location, Location>();
    Location location;

    while (queue.Count > 0)
    {
        location = queue.Dequeue();

        if (location.Equals(destination))
        {
            Location parentLocation;
            Location childLocation = location;
            List<Move> moves =  new List<Move>();
            while (previousSteps.TryGetValue
                (childLocation, out parentLocation))
            {
                /* Create the move and add to route.*/
                Direction dir = parentLocation.GetDirection(childLocation);
                moves.Add(new Move(dir));
                childLocation = parentLocation;
            }
            moves.Reverse(); /* The moves are back to front. */
            route = moves.ToArray();
            return true;
        }
        else
        {
            for (int i = 0; i < 4; i++)
            {
                Direction direction = GetDirection(i);
                Location neighbour = location.GetAdjacentLocation(direction);
                if (level[neighbour].CanEnter &&
                    !previousSteps.ContainsKey(neighbour))
                {
                    previousSteps.Add(neighbour, location);
                    queue.Enqueue(neighbour);
                }
            }
        }
    }
    return false;
}

New Push Move

In the previous version of the game, the user was only able to push a power cube via the keyboard, or by positioning the actor adjacent to the cube and then clicking on it. Now we are able to "push" a cube using the mouse. This is done using a Shift+click combination, and is implemented in the PushPathFinder class. A PushPathFinder instance calculates a linear route that is obstructed by at most one power cube.

class PushPathFinder
{
    readonly Location destination;
    readonly Cell startCell;
    readonly Level level;
    readonly List<Move> moves = new List<Move>();
    bool treasurePushed;
    Move[] route;

    /// <summary>

    /// Gets the route, or set of steps that map out

    /// the relocation. May be null.

    /// </summary>

    /// <value>The route that an <see cref="Actor"/>

    /// should take to move to destination. May be null.</value>

    public Move[] Route
    {
        get
        {
            return route;
        }
    }

    /// <summary>

    /// Initializes a new instance of the

    /// <see cref="SearchPathFinder"/> class.

    /// </summary>

    /// <param name="start">The start cell. Presumably where

    /// the <see cref="Actor"/> is located.</param>

    /// <param name="destination">Where we would like to get.</param>

    public PushPathFinder(Cell start, Location destination)
    {
        this.destination = destination;
        startCell = start;
        level = start.Level;
    }

    /// <summary>

    /// Tries to find a route to the destination, where the route

    /// may contain one treasure to be pushed.

    /// </summary>

    /// <returns><code>true</code> if a route was found,

    /// <code>false</code> otherwise.</returns>

    public bool TryFindPath()
    {
        Location current = startCell.Location;
        if (current.ColumnNumber != destination.ColumnNumber &&
            current.RowNumber != destination.RowNumber)
        {
            return false;
        }
        moves.Clear();

        bool horizontal = current.RowNumber == destination.RowNumber;

        /* We loop through cells in the path.
         If a cell in the path contains a treasure, it will be pushed.
         If there are more than one treasure in the path
         the push will fail, i.e. return false. */
        if (horizontal)
        {
            int rowNumber = current.RowNumber;

            if (current.ColumnNumber < destination.ColumnNumber)
            {
                for (int i = current.ColumnNumber + 1;
                    i <= destination.ColumnNumber; i++)
                {
                    Cell cell = level[rowNumber, i];
                    if (!Enter(cell, Direction.Right))
                    {
                        return false;
                    }
                }
            }
            else
            {
                for (int i = current.ColumnNumber - 1;
                    i >= destination.ColumnNumber; i--)
                {
                    Cell cell = level[rowNumber, i];
                    if (!Enter(cell, Direction.Left))
                    {
                        return false;
                    }
                }
            }
        }
        else /* Vertical path. */
        {
            int columnNumber = current.ColumnNumber;

            if (current.RowNumber < destination.RowNumber)
            {
                for (int i = current.RowNumber + 1;
                    i <= destination.RowNumber; i++)
                {
                    Cell cell = level[i, columnNumber];
                    if (!Enter(cell, Direction.Down))
                    {
                        return false;
                    }
                }
            }
            else
            {
                for (int i = current.RowNumber - 1;
                    i >= destination.RowNumber; i--)
                {
                    Cell cell = level[i, columnNumber];
                    if (!Enter(cell, Direction.Up))
                    {
                        return false;
                    }
                }
            }
        }

        route = moves.ToArray();
        moves.Clear();
        return true;
    }

    /// <summary>

    /// Tests whether we can enter the specified cell

    /// from the specified direction.

    /// </summary>

    /// <param name="cell">The cell.</param>

    /// <param name="fromDirection">From direction.</param>

    /// <returns></returns>

    bool Enter(Cell cell, Direction fromDirection)
    {
        bool result = false;
        if (cell.CanEnter)
        {
            result = true;
        }
        else if (!treasurePushed && cell.CellContents is Treasure)
        {
            treasurePushed = true;
            result = true;
        }

        /* If we were successful and this isn't the last cell where
         we have previously pushed a treasure, then we add it to moves.
         The reason we check for the last cell and treasurePushed
         is because we want the treasure to be located at the destination
         at the end of the move. */
        if (result && !(cell.Location == destination && treasurePushed))
        {
            Move move = new Move(fromDirection);
            moves.Add(move);
        }

        return result;
    }
}

The Push is then enacted by the Actor not unlike a Jump, as the following excerpt shows.

public partial class Actor
{
    /// <summary>

    /// Attempt to relocate to the another cell

    /// using the route in the specified <see cref="Push"/> instance.

    /// </summary>

    /// <param name="push">The push.</param>

    /// <returns><code>true</code> if the move succeeded,

    /// <code>false</code> otherwise.</returns>

    internal bool DoMove(Push push)
    {
        bool result = false;

        if (push.Undo)
        {
            lock (moveLock)
            {
                for (int i = push.Route.Length - 1; i >= 0; i--)
                {
                    Move move = push.Route[i];
                    Location moveLocation = Location.GetAdjacentLocation
                        (push.Route[i].Direction.GetOppositeDirection());
                    Cell toCell = Level[moveLocation];
                    Cell fromCell = this.Cell;
                    if (!toCell.TrySetContents(this))
                    {
                        throw new SokobanException("Unable to follow route.");
                    }
                    if (move.PushedContents != null)
                    {
                        if (!fromCell.TrySetContents(move.PushedContents))
                        {
                            throw new SokobanException
                        ("Unable to undo set contents.");
                        }
                    }
                }
                MoveCount -= push.Route.Length;
                result = true;
            }
        }
        else
        {
            WaitCallback callback = delegate(object state)
            {
                #region Anonymous Push method.
                lock (moveLock)
                {
                    PushPathFinder pathFinder =
                    new PushPathFinder(Cell, push.Destination);
                    if (pathFinder.TryFindPath())
                    {
                        for (int i = 0; i < pathFinder.Route.Length; i++)
                        {
                            Move move = pathFinder.Route[i];

                            /* Sleep for the stepDelay period. */
                            Thread.Sleep(stepDelay);
                            Location moveLocation =
                            Location.GetAdjacentLocation(move.Direction);
                            Cell toCell = Level[moveLocation];
                            if (!toCell.CanEnter &&
                            toCell.CellContents is Treasure)
                            {
                                move.PushedContents = toCell.CellContents;
                                if (!toCell.TryPushContents(move.Direction))
                                {
                                    throw new SokobanException("Unable to push contents.");
                                }
                            }
                            if (!toCell.TrySetContents(this))
                            {
                                throw new SokobanException
                        ("Unable to follow route.");
                            }
                            MoveCount++;
                        }
                        /* Set the undo item. */
                        Push newMove = new Push(pathFinder.Route)
                        { Undo = true };
                        moves.Push(newMove);
                        result = true;
                    }
                }
                #endregion
            };
            ThreadPool.QueueUserWorkItem(callback);
        }
        return result;
    }
}

Future Enhancements

  • Actor walking should be able to redirect in mid jump/push, rather than having to block to completion
  • Preloading of content, including audio
  • Adding some animation to actor/cubes etc.
  • Creating a level editor

Conclusion

In this article we looked at the porting of a WPF application to Silverlight, and highlighted some of the differences between the two technologies. We looked at consuming a web service from Silverlight, creating Silverlight User Controls, hosting a Silverlight project in ASP.NET, and using some of the new Microsoft tools such as Expression Design and Expression Blend, and some more advanced topics such as multithreading. We also examined some of the shortcomings of the Alpha 1.1 Refresh, and how they can be worked around. As with any alpha software, it is important to realise that it will undoubtedly change a lot before its production release. At present, however, Silverlight still promises a strong platform on which to build rich web content.

I hope you find this project useful. If so, then you may like to rate it and/or leave feedback below.

Credits

References

History

  • November 2007: First release

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here