Stage 1

Development Environment

The first challenge was simply to create a Silverlight application. This is most easily done by downloading the Visual Studio Orcas Beta 1 plus the Silverlight runtime, VS extensions and SDK. I found that the whole development environment ran nicely in Virtual PC.

Orcas creates a Page.Xaml file which hosts the main program. My first task was to simply put everything from the Windows Forms version of Nibbles into Page.Xaml.cs, and hack it until it would compile for Silverlight.

Rectangles and TextBlocks

The WinForms Nibbles made use of FillRectangle for most of the drawing - the snake arena is divided up into a grid of squares. Whenever a snake moved, Invalidate was called, and the Paint routine would fill each square with the appropriate colour depending on whether it was a Snake, Wall or Blank space.

The simplest way to implement this in Silverlight was to add an array of Rectangle objects to the main Canvas.

// Create the array of rectangles
for (int col = 0; col < Columns; col++)
{
    for (int row = 0; row < Rows; row++)
    {
        Rectangle rect = new Rectangle();
        rect.SetValue<double>(Canvas.LeftProperty, col * DefaultBlockSize);
        rect.SetValue<double>(Canvas.TopProperty, row * DefaultBlockSize);
        rect.Width = DefaultBlockSize;
        rect.Height = DefaultBlockSize;
        arena[col, row] = new Cell(CellType.Blank, rect);
        rootElement.Children.Add(rect);
    }
}


The number for the snake to eat, and the Labels that displayed high score information were implemented as TextBlock elements. Like .NET Label controls, all XAML elements handle their own invalidation - you don't need to call anything to get the display to update when you change a property on a visual element.

Silverlight Limitations

Although Silverlight is very similar to WPF, the two are not exactly the same. WPF not only offers more classes, but the objects themselves have more capabilities. Here's a brief list of issues I ran into:
  • Many WPF XAML files use Grid and StackPanel containers and set Margin, Padding, Horizontal and Vertical Alignment properties. You need to remove all these to use existing XAML code in Silverlight.
  • You can't share a SolidColorBrush between two or more Rectangles. So even though I was drawing in just four colours, I needed to make one brush for every one of the 4000 rectangles in the grid. This is because Silverlight doesn't allow the sharing of resources.
  • TextBlock controls do not have any support for text alignment. You have to perform any centering yourself.
  • Polyline (see below) only supports an array of Points, not a PointCollection, making dynamically adding and removing points much more difficult.
  • The Color class does not have any static members for known colours (e.g. White, Blue, Orange etc). You will need to consult a colour chart to get the RGB values yourself.

Timer

SilverNibbles has a timer firing about once every 80ms. The way this is achieved in SilverLight is a little bit unusual. You create a Storyboard with a DoubleAnimation, and then handle its completed event. You can then begin the animation again if you need the timer to keep firing.

Here's the XAML in Page.xaml:

<Canvas.Resources>
    <Storyboard x:Name="timer">
        <DoubleAnimation Duration="00:00:0.08" />
    </Storyboard>
</Canvas.Resources>


Then in the Page.Xaml.cs constructor, we can add a handler and kick off the storyboard:

timer.Completed += new EventHandler(timer_Completed);
timer.Begin();


In SilverNibbles, we leave the timer running permanently, but it only has work to do if the game is actually running:

void timer_Completed(object sender, EventArgs e)
{
    if (arena.GameStatus == GameStatus.Running)
    {
        OnTimerTick(sender,e);
    }
    // restart the timer
    timer.Begin();
}

Keyboard Handling

Nibbles works off the keyboard, which means two things. First, the Silverlight control must have keyboard focus. In the auto-generated HTML, there is some JavaScript to do this for us:

<body onload="document.getElementById('SilverlightControl').focus()">


This works nicely in IE7, but unfortunately does not seem to be reliable in FireFox. Hopefully Microsoft can find a resolution to this.

To subscribe to keyboard events, we simply add an event handler to the KeyDown event. We also have a LostFocus event handler on the main canvas, which allows us to pause the game if the Silverlight control loses focus.

this.LostFocus += new EventHandler(Page_LostFocus);
this.KeyDown += new System.Windows.Input.KeyboardEventHandler(rootElement_KeyDown);


The keyboard handler itself simply receives an integer representing the key code of the key that is pressed. Unfortunately, Silverlight does not provide an enumeration of possible values, so we have created our own Keys enum with keycodes of interest to us:

enum Keys
{
    Return = 13,
    Escape = 27,
    Space = ' ',
    N1 = '1',
    N2 = '2',
    A = 'A',
    D = 'D',
    ...


As can be seen, the platform key code is usually the ASCII value of the character in question. Unfortunately, it appears that the cursor keys do not cause events to be raised at all in the Silverlight control. This meant that Nibbles had to be modified to work with different keys. The keyboard event handler itself can simply cast the PlatformKeyCode to our custom Keys enumeration and take the appropriate action:

void  rootElement_KeyDown(object sender, KeyboardEventArgs args)
{
     Keys key = (Keys)args.PlatformKeyCode;
     ...

The Pause Control

The pause control offered us the first chance to improve the look and feel of the old Nibbles application. First of all, it must display the "Paused" message while the game is in pause. But it was also useful to replace the messageboxes that appeared when snakes died or the game ended.

The pause control was implemented as a custom XAML control. This works slightly differently from the Page.Xaml class as it is constructed differently. Page.Xaml is loaded directly by the Silverlight control, and therefore must contain information about the DLL and class that contain its .NET code:

<Canvas x:Name="parentCanvas"
    ...        
    x:Class="SilverNibbles.Page;assembly=ClientBin/SilverNibbles.dll"
    ...
        >


The Pause class on the other hand has its XAML embedded into the SilverNibbles.dll assembly. Therefore, the x:Class attribute is not required, and the relevant XAML is extracted in the constructor of the PauseControl object. If we want to manipulate any of the objects, we must get references to them by calling FindName on the root element of the loaded XAML.

TextBlock textBlockMessage;
Rectangle rectBorder;

public PauseControl()
{
    System.IO.Stream s = 
        this.GetType().Assembly.GetManifestResourceStream("SilverNibbles.PauseControl.xaml");
    FrameworkElement rootElement = 
        this.InitializeFromXaml(new System.IO.StreamReader(s).ReadToEnd());
    textBlockMessage = (TextBlock) rootElement.FindName("textBlockMessage");
    rectBorder = (Rectangle)rootElement.FindName("rectBorder");
}


The FindName function can be used because we have set the x:Name attributes on the TextBlock and Rectangle in our XAML file:

<Rectangle x:Name="rectBorder" Width="320" Height="140" 
    Stroke="Black" StrokeThickness="4" 
    RadiusX="5" RadiusY="5" Fill="#FFC1C1C1" />
<TextBlock x:Name="textBlockMessage" Width="304" Height="124" 
    Canvas.Left="8" Canvas.Top="8" 
    Text="Welcome To SilverNibbles" TextWrapping="Wrap"/>


Using XAML to construct the look and feel of the Pause control allows us to easily style it. Here we have simply given it a border and rounded corners.

We can also expose a Text property on our Pause control, allowing the pause message to be modified from code that creates the PauseControl.

public string Text
{
    get { return textBlockMessage.Text; }
    set { textBlockMessage.Text = value; }
}


There is however one remaining problem. When the PauseControl is resized, its constituent parts (the rectangle and textBlock) also need to be resized. As the Canvas does not offer us any auto-resizing of child elements, we will have to do it ourselves. Unfortunately though, there is no Canvas Resized event, and the Width and Height properties are not virtual, so cannot be overridden. The solution is found in the example controls supplied with the Silverlight SDK. We will create new Width and Height properties, and pass the new size down to the original properties when they are called. Then we can call our own resizing code for the Canvas' children.

// Sets/gets the Width of the actual control
public new double Width
{
    get { return base.Width; }
    set
    {
        base.Width = value;
        UpdateLayout();
    }
}

// Sets/gets the Height of the actual control
public virtual new double Height
{
    get { return base.Height; }
    set
    {
        base.Height = value;
        UpdateLayout();
    }
}

protected virtual void UpdateLayout()        
{
    rectBorder.Width = Width;
    rectBorder.Height = Height;
    textBlockMessage.Width = Width - 8 * 2;
    textBlockMessage.Height = Height - 8 * 2;
}


Now we have done this, all that remains is for the host canvas to create a PauseControl object, size and position it, and set its text.

pauseControl = new PauseControl();
pauseControl.Width = 380;
pauseControl.Height = 140;
pauseControl.SetValue<double>(Canvas.LeftProperty, (this.Width - pauseControl.Width) / 2);
pauseControl.SetValue<double>(Canvas.TopProperty, (this.Height - pauseControl.Height) / 2);
rootElement.Children.Add(pauseControl);
            
pauseControl.Text =
    String.Format("SilverNibbles 1.03 by Mark Heath\r\n{0}",
    Instructions);


Finally, we are able to show or hide the pause control by setting its visibility. As long as it was the last item to be added to the Canvas, it will appear over the top of anything else drawn on the screen. We set its visibility to Collapsed when we want it to disappear:

pauseControl.Visibility = Visibility.Collapsed;

High Scores

So now we have updated our graphics to display in Siverlight we have a working game, as all the snake detection and positioning logic still works from the original Windows Forms version. The only thing remaining is to find a way to store the high score record.

Silverlight provides a mechanism for persisting files to the local hard disk, called IsolatedStorage. This allows each Silverlight application to save up to 1Mb of data (it is unique to the URL and instance of Silverlight). We will use this to create an XML file containing the high score and the date. Unlike the client application, we will not prompt for a user name, partly because Silverlight doesn't currently have a built-in TextBox, and partly because most users have their own login and hence their own IsolatedStorage.

Saving the record is relatively simple. We get hold of the storage area for our application, and then we create a new stream, overwriting any existing one as we only plan to store a maximum of one high score at the moment. Then we use an XmlWriter to enable us to easily create well-formed XML.

private void SaveRecord()
{
    IsolatedStorageFile file = IsolatedStorageFile.GetUserStoreForApplication();
    IsolatedStorageFileStream stream = new IsolatedStorageFileStream(
                                           "record.xml",
                                           System.IO.FileMode.Create,
                                           file);
    using (XmlWriter writer = XmlWriter.Create(stream))
    {
        writer.WriteStartElement("HighScores");
        writer.WriteStartElement("HighScore");
        writer.WriteAttributeString("Score", recordScore.ToString());
        writer.WriteAttributeString("Date", recordDate.ToLongDateString());
        writer.WriteEndElement();
        writer.WriteEndElement();
    }
}


Loading the record is almost as easy, except we must handle a file not found exception. I have also noticed some other exceptions are thrown in this function on the first run under Windows Vista. I don't yet know what exactly is the cause of this. Reading the XML in is nice and easy thanks to the XmlReader class.

private void LoadRecord()
{
    IsolatedStorageFile file = IsolatedStorageFile.GetUserStoreForApplication();    
    IsolatedStorageFileStream stream = null;
    try
    {
        stream = new IsolatedStorageFileStream(
                     "record.xml",
                     System.IO.FileMode.Open,
                     System.IO.FileAccess.Read,
                     file);
        if (stream != null)
        {
            using (XmlReader reader = XmlReader.Create(stream))
            {
                if (reader.ReadToFollowing("HighScore"))
                {
                    recordScore = Int32.Parse(reader.GetAttribute("Score"));
                    recordDate = DateTime.Parse(reader.GetAttribute("Date"));
                }
            }

        }
    }
    catch (System.IO.FileNotFoundException)
    {
        // this is OK - will be not found first time in
    }
    catch (Exception)
    {
        // first run on Vista seems to have a problem,
        // that doesn't result in a FileNotFoundException
        // - need to work out what this is
    }
}

Last edited May 23, 2007 at 12:42 PM by markheath, version 2

Comments

micaleel Dec 8, 2009 at 8:40 AM 
Nice, I've found this quite helpful. Cheers.