design

Developers guide to writing a simple Silverlight game

Introduction

In order to learn a little about Silverlight and how to co-ordinate development with a designer I joined forces with Hannah at Clear Breeze Design to write a fun little game called Olop. I thought I'd share some of the problems and tips we discovered along the way.

Olop's iceflow rescue

The Game Loop

The 'game loop' is the typical mechanism used to bring all the parts of a game in synch and display them. The difficulty with developing a game loop is to ensure that you create such a synchronization point. I'd considered using a timer (which is possible) but previously I've found them to be unreliable given a typical "desktop timer" resolution especially as stresses on the system can result in timer messages disappearing (or never arriving) from the queue. So it was of great interest that I read Bill Reiss' Silverlight Games 101 about using a Storyboard with a duration of 0. What I like about this solution is that it's at the level of the animation engine so, hopefully, many of the timer resolution issues should already be taken care of (however it is an assumption nevertheless). NB. If you do read the tutorial you'll see that Bill has included the dispatch timer method as an alternative.

I've included the initialization of the basic game loop here;

void Page_Loaded(object sender, RoutedEventArgs e)

{

StoryboardGameLoop storyBoardLoop = new StoryboardGameLoop(this);

this.gameLoop = storyBoardLoop;

storyBoardLoop.Update += new GameLoop.UpdateHandler(storyBoardLoop_Update);

}

NB Since RC0 was released creating the storyboard is easier and there is a new render event which can be used as yet another alternative

So the synchronization of the game is handled by the Update event on the storyboard, so now we've got the basic foundations for a game. However, one of the problems with using non-game dedicated hardware/software is that other things can take priority. We've all seen it, even when playing DirectX games in full screen, suddenly everything slows as the Virus checker kicks in or an email arrives. Bill uses a smart technique where the game loop times the delta between the current and the last Update. The idea is that you can use this duration to calculate the "missing" frames. E.g. You fire a shot and it moves smoothly from point A->B. Suddenly there is a delay of 1 frame where we should have rendered the shot at C, but because of the delay we didn't get chance to. Therefore when the next Update arrives we calculate that now we should be at D. The technique is great but in my experience when the game did suffer such a delay the stutter was amplified rather than disguised so I ended up completely removing the compensation. I also recently attended a Silverlight User Group where Richard Costal & Pete McGann gave a great talk about recreating Manic Minor in Silverlight. They used the delta method so I was interested in seeing how the game played. What I discovered was that for me the game suffered from the same glitches and the delta made things doubly worse. Not only did it become more noticable (as it did for Olop) but because they're using pixel-perfect detection the characters would skip detection. The made it very difficult to play because the characters would move into obstacles, miss platforms, etc. I guess like anything in software you need to see what works best for you rather than take some bloggers point of view ;). NB. It played great on the PC though.

Keyboard support

The funny thing with Silverlight is the simple things are often the hardest. Although it's easy to catch the keyboard event you have to take special care to capture them (especially when the key is constantly pressed) when the system is stuttering around. Again Bill's keyboard handler is excellent so I happily used it and can recommend it.

Drawing sprites onto the screen

The fun stuff, actually drawing stuff onto the screen. Silverlight may not be a games engine but creating sprites is a breeze. The technique I used was to ask the graphic designer (Hannah Watkins from Clear Breeze Design) to create sprites as individual User Controls in Blend. Then the game can dynamically create (and maintain) the sprites as you would any user control. It's easy ;) Ok maybe it's not obvious so here's a cut down snippet from Olop.

Dynamically adding controls to the game surface

private void CreateCharacters(NonPlayerSprite[] characters, double top, double left, NonPlayerMovementMode nonPlayerMovementMode,

NonPlayerBombMode nonPlayerBombMode)

{

   for (int characterCount = 0; characterCount < characters.Length; characterCount++)

   {

     NonPlayerSprite character = characters[characterCount];

     this.nonPlayerCharacters.Add(character);

     this.gameSurface.Children.Add(character);

   }

}

The important part to remember is to always add the control (in this case 'character') to the control tree (this.gameSurface.Children.Add), otherwise you won't see anything! What I haven't shown is the code to create the control/sprite itself. This is because the game changes the characters depending upon the level, therefore I used a Factory pattern to deliver the user controls based upon the level the player has achieved.

Level factory

The pattern is pretty simple, pass in the level and the code returns the set of characters to display;

public static List<LevelCharacterSet> CharactersForLevel(int level)

{

   List<LevelCharacterSet> characterSets = new List<LevelCharacterSet>();

?

   Can[] cans = { new Can(), new Can(), new Can(), new Can(), new Can() };

   LevelCharacterSet levelCharacterSet = new LevelCharacterSet(

     cans, NonPlayerMovementMode.NoVerticalMovement, NonPlayerBombMode.None,

     new Point(100, 30));

?

What I tried to do with the factory is find a way to return the characters in a generic way where the caller just deals with a LevelCharacterSet rather than needing some ugly set of ifs or a large switch statement. To aid in this I created a class hierarchy of;

User Control - > Sprite -> NonPlayerSprite

This created a practical problem with Visual Studio. When you create a user control Visual Studio creates a number of files for you; the .xaml and .cs are for you to edit but the .g.cs is generated to hold all the plumbing that you shouldn't be, or want to be, messing with. It provides this nice separation through the use of partial classes, however this is where the problem lies. For example, when I created the IceShard user control the .cs contained the following code;

public partial class IceShard : UserControl

However I want to deal with NonPlayerSprite not just User Control so I changed it to;

public partial class IceShard : NonPlayerSprite

Unfortunately Visual Studio isn't "watching" that closely, so when I compiled I got an error complaining that you can't have different base classes. The problem is with the hidden .g.cs file which still contains the code to derive from UserControl. But where is this pesky file? Well you can find it lurking under obj\<configuration>\<control filename>.g.cs . So in my case that was obj\Debug\IceShard.g.cs

To make life easy I kept the file open in Visual Studio as it's quite easy to make a change to the xaml and for Visual Studio to simply overwrite the code with UserControl again. I'm not sure if Microsoft have fixed this issue yet, but just in case they haven't just keep the file open and/or try to avoid accidentally poking the original xaml file.

That all sounds like effort so why not just stick with User Controls?

Sprite & NonPlayerSprite

Silverlight, and its WPF older brother, use xaml as the declarative language of choice. It's way beyond this little article to discuss the many merits of xaml but there are some parts I find annoying. One of these is accessing dependancy properties. For example, to make decisions about a character you must know some basic facts such as the position and size of the root canvas of the user control. Most xaml guidance recommend wrapping dependency properties with your own "standard" property. Now I could have written this position & size properties on each user control but they all share that code so why not go the base class route? However, not all sprites are the same. The monsters in the game all share properties about how they move, how they attack, etc. which are not relevant to other characters such as the player. Therefore, the NonPlayerSprite came into existence. So the advantages of having these single points of maintenance far out-way the niggling problems of changing the .g.cs files.

So we can create sprites (rather than just User Controls), add them to the control tree and handle keyboard input, but that doesn't make for a very interesting game.

Animations

The first worry I had was how to animate the characters, specifically the Olop - the player. Traditionally I'd have used a set of sprite states and drawn them in sequence as the user moves the character, but Silverlight already has animations built in so could I use them? Again I turned to Hannah to supply the storyboards of the character moving. It then became a trivial task to issue the Begin when the user moved the character. In OO fashion I let the character (user control) look after starting the animation when the Move() method was called. One gotcha with this is if the animation takes longer to complete than two Move requests the animation will look clipped and unnatural. To protect against this I added a little guard which is only removed in the storyboards Completed event;

...

if (!this.animationRunning)

{

   this.animationRunning = true;

   this.BearWalk.Begin();

}

?

void BearWalk_Completed(object sender, EventArgs e)

{

   this.animationRunning = false;

}

Another worry I had was how was I going to keep all the different animations running in synch', and yet allow them to run concurrently and independently? Fortunately Silverlight just takes care of this for me, are they sure it isn't a game framework?

Once the move animations were working I could turn my attention to the "fun" storyboards. To start this I just added some code into the characters to randomly select a storyboard, given that each language has it's own random generator I thought I'd show how I did that;

// in the constructor...

this.random = new Random(this.GetHashCode());

?

private void FunnyAnimation()

{

   if (!isAnimated && random.Next(100) == 5)

   {

     int animationFrame = random.Next(animations.Length);

     string animationName = animations[animationFrame];

     Storyboard storyboard = (Storyboard)this.Resources[animationName];

     storyboard.AutoReverse = true;

     storyboard.Completed += this.storyBoardCompleted;

     storyboard.Begin();

     isAnimated = true;

   }

}

As normal, the secret to a good random number is to seed the generator with a unique-ish value. If you don't you may end up with all your monsters playing out exactly the same animation at the same time, more synchronized dancing the gaming.

olop chrome

Shrinking ice

Olop needed a way to show that you were running out of time/lives so we plumped for a slowly decaying chunk of ice. Hannah supplied me with another user control with lots of interesting paths and gradient fills, but how to make it shrink? Simply changing the dimensions of the outer canvas saw the ice shrink towards the middle, a nice affect but not the gauge we were hoping for. So I turned to RenderTransform with ScaleTranform in the y-axis. This was fine but still the whole canvas shrank towards the middle. The key to solving this problem was discovering the RenderTransformOrigin property. Setting this allowed me to anchor the transform and to finally produce the gauge effect;

Xaml

<Canvas x:Name="LayoutRoot">

<Canvas x:Name="Ice" RenderTransformOrigin="0,1" VerticalAlignment="Bottom">

<Canvas.RenderTransform>

<TransformGroup>

   <ScaleTransform x:Name="IceValue" ScaleY="-1.0"/>

</TransformGroup>

</Canvas.RenderTransform>

?

.cs

this.IceValue.ScaleY = -1 * newIceValue;

Collision detection

Ah my old friend (and enemy), collision detection. This is where you realise Silverlight isn't a game framework because there is no out-of-the-box hit test. Ok not to worry, what we do have is the rather strange concept of the canvas. Like a container, a canvas is used to group a set of controls and help position them. But unlike a container the controls are not constrained by their canvas, you can draw off the canvas...weird. So really the name canvas is a misnomer, it's really a grouping & relative positioning container...Ok maybe canvas will do ;) But what this means is that the canvas can provide a nice, if primative, form of collision detection. Using the canvas and the detectors bounding box means we can not only provide a very efficient hit test but we can also easily exclude graphical assets (such as smoke or spray) from creating a collision. Just in case you're not familiar with such tests this is the one I used;

public static bool Collision(Sprite sprite1, Sprite sprite2)

{

   double left1 = (double)sprite1.GetValue(Canvas.LeftProperty);

   double left2 = (double)sprite2.GetValue(Canvas.LeftProperty);

   double top1 = (double)sprite1.GetValue(Canvas.TopProperty);

   double top2 = (double)sprite2.GetValue(Canvas.TopProperty);

   double actualWidth = sprite1.ActualWidth;

   double actualHeight = sprite1.ActualHeight;

   GetActualSizesForTransformedSprite(sprite1, ref actualWidth, ref actualHeight);

   //See if the sprite rectangles overlap

   bool collision = !(left1 > left2 + sprite2.ActualWidth

   || left1 + actualWidth < left2

   || top1 > top2 + sprite2.ActualHeight

   || top1 + actualHeight < top2);

   return collision;

}

The observant amongst you may have noticed GetActualSizesForTransformedSprite, this represented my biggest problem. One of our characters is a big jet plane that acts as a end of level boss. Although it moves around the screen using the same techniques as any other character the plane also zooms in and out using a transform. This caused a big problem with the collision detection. When I asked for it's actual size Silverlight would always tell me the size of the sprite in it's normal form, regardless of if it was currently fully zoomed in (or out). I have to thank dcstraw from the Silverlight.net forums for the core of the solution;

//game controller

private static void GetActualSizesForTransformedSprite(Sprite sprite1, ref double actualWidth,

   ref double actualHeight)

{

   PlaneControl plane = sprite1 as PlaneControl;

   if (plane != null)

   {

     Rect bounds = plane.GetRenderBounds(Application.Current.RootVisual);

     actualWidth = bounds.Width;

     actualHeight = bounds.Height;

   }

}

?

// Plane control

public Rect GetRenderBounds(UIElement relativeTo)

{

   var transform = LayoutRoot.TransformToVisual(relativeTo);

   var topLeft = transform.Transform(new Point(0, 0));

   var topRight = transform.Transform(new Point(ActualWidth, 0));

   var bottomRight = transform.Transform(new Point(ActualWidth, ActualHeight));

   var bottomLeft = transform.Transform(new Point(0, ActualHeight));

   double boundsLeft = Math.Min(topLeft.X, Math.Min(topRight.X,

      Math.Min(bottomRight.X, bottomLeft.X)));

   double boundsTop = Math.Min(topLeft.Y, Math.Min(topRight.Y,

     Math.Min(bottomRight.Y, bottomLeft.Y)));

   double boundsRight = Math.Max(topLeft.X, Math.Max(topRight.X,

     Math.Max(bottomRight.X, bottomLeft.X)));

   double boundsBottom = Math.Max(topLeft.Y, Math.Max(topRight.Y,

     Math.Max(bottomRight.Y, bottomLeft.Y)));

return new Rect(boundsLeft, boundsTop, boundsRight - boundsLeft,

   boundsBottom - boundsTop);

}

Miscellaneous issues and tips

I covered the significant issues I ran into, but I thought I'd use this section to touch on a few lesser problems and a few tips.

numbers

Screen Layout

For those that don't know xaml, by default, it increases the z-order as you go down the page. So if you want a fake dialog/canvas to appear above the rest of the page then you can position it further down the page. Of course you can just set the z-order ;)

Creating numbers in a non-standard font

Olop is a fun little game and so we felt the standard fonts just weren't quirky enough (although Hannah will hate me saying quirky). So I asked her to produce a set of numbers from 0-9, stacked on top of each other. This was a single number control. I then created a score control that contained four number controls. It was then fairly easy to expose a number property to the control which displayed the correct number.

Debugging

Silverlight isn't blessed with the best error messages and it can become frustrating. In my experience the majority of problems were from unsupported xaml where I'd tried to use a non-supported WPF element. These errors are typically raised very early in the life cycle, and usually originate from InitializeComponent. However, if you want to debug exactly where the problem is, this is my tip. Find the call to InitializeComponent in your .cs file and drill into it. Then remove/comment out the [System.Diagnostics.DebuggerNonUserCodeAttribute()] and set a breakpoint in the code. This way you can narrow down the element that is causing the problem.

Problems playing sounds

The first problem I had with playing sounds was attempting to play a .wav file. Silverlight wasn't exactly forthcoming with the error message but I quickly realised .wav just isn't supported. So I turned from trusty wav to even more trustworthy mp3, surly PCs and Macs alike love mp3s don't they? I created a little demo of a shot sound and the mp3 played hurray. Put the sound into the game and nothing. Odd. After some playing around the sound would, infrequently, play. Odd. Rapidly causing the sound to play seemed to work but none of this was useful to the game. Eventually I converted the mp3 to wma and everything worked fine. I then received confirmation from the forums (from none other than Bill Reiss) that there is a flaw with playing small mp3s. So my advice...use wma for small files.

Resources

In order to play sounds I needed to load the sound, duh. But where to load them from, and what to load them with (sounds like a track from Blur). I didn't want any network latency so I thought the best option was to pack the sounds into the xap file. I must confess I got a bit confused between the choices; Embedded Resource or Resource sir? Well it turns out that I needed neither of those, in fact I needed to set the sound files to Content and do not copy. I could then load the sound by;

MediaElement mediaElement = new MediaElement();

mediaElement.AutoPlay = false;

Uri resourceUri = new Uri(resourcePath, UriKind.Relative);

StreamResourceInfo resource = Application.GetResourceStream(resourceUri);

mediaElement.SetSource(resource.Stream);

this.gameSurface.Children.Add(mediaElement);


Persisting user information

One of the interesting features of Silverlight is its ability to persist information to the user local file store via Isolated Storage. Whilst not a new feature to Windows developers it certainly provides an easier persistence mechanism for browsers than cookies - plus converting to WPF is easy. I used Isolated Storage to store the user's high score (I'd love to use a High Score service but sometimes you just need to get something out there). Typically I use Xml to store settings data and I felt no reason to change here. However, Silverlight doesn't include the typical Xml classes so I turned to serialization to help me out. By using the DataContract attributes I decorated my Settings class to explain how and what I wanted to be persisted;

[DataContract]

public class Settings

{

   [DataMember]

   public double HighScore { get; set; }

 }

?

Once you have an object to serialize you need to get Silverlight to do the work;

...

using (IsolatedStorageFile isolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication())

{

   using (IsolatedStorageFileStream settingsFileStream =

     isolatedStorageFile.OpenFile("settings.xml", System.IO.FileMode.Create))

   {

     Type typeToSerialize = objectToSave.GetType();

     DataContractSerializer dataContractSerializer = new DataContractSerializer(typeToSerialize);

     dataContractSerializer.WriteObject(settingsFileStream, objectToSave);

   }

}

?

Once saved, you need to retrieve the values;

using (IsolatedStorageFile isolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication())

{

   if (isolatedStorageFile.FileExists("settings.xml"))

   {

     using (IsolatedStorageFileStream settingsFileStream =

       isolatedStorageFile.OpenFile("settings.xml", System.IO.FileMode.OpenOrCreate))

     {

       if (settingsFileStream.Length > 0)

       {

         DataContractSerializer dataContractSerializer = new DataContractSerializer(typeOfObjectToLoad);

         return dataContractSerializer.ReadObject(settingsFileStream);

...

The method did catch me out. Initially I used Create or Append which leaves the original settings file on disk and appends to it. The problem is that if the file gets smaller it leaves bits of Xml tags from the previous save in the file and Xml really doesn't like random tags floating around! So I switched to Create to ensure the file is purged before writing.

Tip: To check the contents of the isolated file set a breakpoint on the GetUserStoreForApplication code and it will tell you where the file is located on the disk. You can then navigate to the location and examine the file.

Problems with Xaml from Blend

One of the reasons for starting this project was to get first hand experience of the "new" workflow process for a developer and designer, I.e using Visual Studio and Blend with xaml as the common denominator. The first problem was the naming of elements. Blend makes it relatively easy to create lots of controls and paths, storyboards, etc. whilst still making it easy to navigate around the control tree which is great from a designers point view. However, once in Visual Studio all I get is chunk of semi-meaningless xaml with no way of associating a xaml element with the rather poor preview pane. So the first lesson we learnt was name the items in Blend even though this isn't strictly helpful to the Blend user, it's a must for the developer. Conversely sometimes Blend isn't that great at helping with problems in the xaml and it's particularly naughty at creating unnecessary elements. So there were a few occasions where we switched to xaml view in Blend to easily correct some problems. However, Blend lacks Visual Studio's xaml edit so you loose the ability to do the simple things, such as collapsing islands or ctrl+] to navigate to start/end of tags. Second lesson, use the xaml edit to correct flaws in Blend, and if there is a lot of work then do this in Visual Studio. The third lesson is for the developer to install Blend, sometimes you need it to navigate the xaml, but don't be tempted to edit using it, you're not a designer, you're not a designer.....Lesson four, the canvas offset. When you create controls in Blend you can happily ignore the root canvas. This can result in the developer getting a user control where the bulk of the graphics are at some huge offset to the root canvas. To avoid this always give the root canvas a default colour while developing the assets, that way you can immediately see what the offset will be.

Well that's all folks, hope you found this useful, and why not have a quick go at helping Olop save the ice. Paulio.

Return to site