Developing a Casual Game with Silverlight 2 – Part 5
Author: Joel Neubeck, Director of Technology/Silverlight MVP
Web Site: http://joel.neubeck.net/
subscribe nowto get yours.
In this series of articles, we are exploring the process of designing and building a casual online game in Silverlight 2 (SL2). Throughout the series we target the interactive developer, as we construct our own version of the classic 1980s game Sabotage. The premise of this game is quite simple: get as many points as possible by shooting down parachuters and helicopters before the enemy destroys your bunker.
Module 5: Initialization and Deployment
In our first four modules we constructed our framework, developed our game mechanics, and made our sprites come to life. In this fifth article, we will step outside of this specific game, and talk about initialization and deployment strategies.
Any time you build a game, your first priority is game play, but you have to give thought to the entire user experience. How you load your assets can have a big effect on adoption. If you make your users wait a frustrating amount of time to see what the game will be like, they might not stay and grow to love your game.
As hard as we all try, often game assets can get pretty large in size. It’s not uncommon for a complex sprite that has various frame-by-frame sequences to take up a few megabytes. Throw in a few sounds and additional assemblies, and a single XAP could be 6-7 MB. The last thing we would want to do is make our users download this entire package to get started.
Multiple Game Assemblies
There are many possible approaches to make this process asynchronous. One I have used in the past is to break the game into multiple assemblies. The first is very small; it contains only the controller, models and game shell. The second contains your game sprites and sounds. If your game has multiple levels, it might even make sense to break this apart even further and have the first half of the levels in one assembly, and the final half in the second. In both cases, Figure 1 illustrates how the loading sequence might flow.
Figure 1. Game Loading Sequence
When users hit our game for the first time, Silverlight returns to them the results of Page.xaml from our primary Game.xap. Once loaded, our controller is instantiated and the controller’s Initialize method is called. A reference to Page.xaml is passed to the controller via constructor dependency injection. Within the controller’s Initialize method, we begin the downloading of our splash screen. I always think it is a great idea to have the ability to insert some type of interstitial screen before a user goes into the downloading process. You want this screen to be fast-loading and have little delay in getting something visual in front of your user. This makes it very important that the splash.xaml is small. I would shoot for 100k or less.
Now a lot of people ask, why not just include it with the primary Game.xap? By all means you could, but one of the advantages you get from grabbing it asynchronously is that you can swap it in and out at random. Imagine if your game were on a commercial site and you wanted to insert an interstitial ad. It would be pretty simple to read from an initParam the location of the splash.xap and interchange what the users see when they first load the game.
Controller Downloads Splash
private UserControl _splashPage;
private Assembly _splashAssembly;
private const string SPLASHXAP = "Splash.xap";
private const string SPLASHDLL = "Splash.dll";
private const string SPLASHCTRL = "Splash.MainPage";
. . . . .
public void Initialize()
WebClient wc = new WebClient();
wc.OpenReadCompleted += new OpenReadCompletedEventHandler(wc_OpenReadCompleted);
wc.OpenReadAsync(new Uri(SPLASHXAP, UriKind.Relative), SPLASHDLL);
void wc_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
StreamResourceInfo sri = new StreamResourceInfo(e.Result as Stream, null);
String sURI = e.UserState as String;
StreamResourceInfo assemblySri = Application.GetResourceStream(sri,
new Uri(sURI, UriKind.Relative));
AssemblyPart assemblyPart = new AssemblyPart();
Assembly a = assemblyPart.Load(assemblySri.Stream);
_splashAssembly = a;
_splashPage = _splashAssembly.CreateInstance(SPLASHCTRL) as UserControl;
ISplash splash = _splashPage as ISplash;
splash.Completed += new EventHandler(splash_Completed);
//insert the loading screen
_loadingView = new Game.Views.Loading();
_loadingView.Completed += new EventHandler(_loadingView_Completed);
Figure 2. Controller downloads Spalsh.xap
Let’s break down what is happing in Figure 2. The first two fields will hold our splash assembly and our MainPage UserControl, while the three constants, SPLASHXAP, SPLASHDLL and SPLASHCTRL, will be used to reference the correct XAP, assembly and UserControl. From time to time you might have a solution that has more then one assembly in a XAP. In this case, you could have your code read the AppManifest as a way to discover each individual assembly name.
Next in Figure 2, we have our controller’s Initialize method. This will be used to instantiate a WebClient to fetch our Splash.xap. Since I intend to control both the splash and game, it’s fair to assume that the XAP will reside relative to the game. If it were required, I could have easily made this an absolute location.
Since everything you fetch in Silverlight is asynchronous, our OpenReadCompleted event handler will be needed for all of the heavy lifting. The first part of this handler is pretty straightforward: we cast our e.Results as a stream and use this to instantiate a StreamResourceInfo object. StreamResourceInfo is a special object in WPF and Silverlight that simply contains a stream. Using this object we will load our stream as an assembly and create an instance of the MainPage UserControl. At this point we can add the splash screen to our main page view.
Once the splash screen has been loaded on to our stage, we have to find a way to know when it is done animating. There are a few ways we could do this. First, we could use a DispatcherTimer and after a set number of seconds, we could use the timer’s “Tick” event to signal us to continue loading the game. The obvious downside of this approach is that we take away the ability for the splash screen to be on stage as long as it needs to. A second and much better approach would be to consume a custom event from our splash class that gets fired whenever it knows it is done animating. I certainly prefer the second approach, but it poses an interesting challenge. Since our assembly is loaded at runtime, we don’t have a strong reference to it. Without a strong reference, it is more difficult to subscribe to the Splash.Completed Event. Now we could certainly use more reflection, find the event and subscribe. The downside is that we will need to make some assumptions around event naming. A much simpler approach is to force our Splash class to implement an interface defined within our primary game assembly. The interface will be our contract, ensuring that both sides play by the same rules. Figures 3 and 4 show what this would look like.
public interface ISplash
event EventHandler Completed;
Figure 3. Game.ISplash interface
public partial class MainPage : UserControl, Game.ISplash
. . . .
Figure 4. Splash interface implementation
By using an interface, we can simply cast our UserControl to ISplash and allow our controller to subscribe to the Splash.Completed event. Within this completed event handler, we can continue our loading sequence.
Assets from Additional XAP
For most games we will have a ton of game assets. These will include our sounds and sprites. To ensure that our users have the best experience, we should always include a loading screen that visualizes the progress of acquiring these assets. Silverlight has a great little ProgressBar control that can be used to show users the progress of data being downloaded. For my game it’s a bit of overkill, since all of my assets are less than 120K. Nonetheless, let’s go through some of the changes required when you have multiple views coming from an external assembly.
First, we will need to create a new Silverlight project to hold all of our assets. I have chosen to call the project Games.Asset and place all of my sprites in the Game.Assets.Views namespace. Second, we need to add code to our controller to download the new XAP and create an instance of the Game.Assets assembly. Figure 5 show that the process is nearly identical to how we grabbed our splash.xap.
void splash_Completed(object sender, EventArgs e)
//remove the splash screen
_splashPage = null;
//download our Games.Assets assembly
WebClient wc1 = new WebClient();
wc1.OpenReadCompleted += new OpenReadCompletedEventHandler(wc1_OpenReadCompleted);
wc1.DownloadProgressChanged += new
wc1.OpenReadAsync(new Uri(ASSETSXAP, UriKind.Relative), ASSETSDLL);
Figure 5. Download Games.Assets XAP
Once the XAP is fetched, we will need to make quite a few changes to how we will instantiate each view within our controller. Figure 6 show an example of the additional code required to create a view that comes out of our Game.Assets assembly.
private void PlaceGround()
for (int i = 0; i < 4; i++)
Models.Block model = new Models.Block(new Vector(i * 100, 350));
model.Moving = false;
//Views.Ground ground = new Game.Views.Ground(model);
UserControl ground = _AssetsAssembly.CreateInstance("Game.Assets.Views.Ground")
ground.Width = 100;
ground.Height = 50;
PropertyInfo inf = ground.GetType().GetProperty("Model");
inf.SetValue(ground, model, null);
TrackModel(model, ref _gridMoving);
Figure 6. Create View from downloaded assembly
Notice how we have used reflection to create an instance of our Ground view and cast it to its base class, UserControl. Since we have done a good job of ensuring that our views don’t have any crazy custom code-behind logic, it is acceptable to keep all of our reference to sprites as UserControls. There is one exception, and that is our Model property. Back in Module 1 we demonstrated the use of constructor dependency injection as a way to pass our view, a reference to our model. In light of our new way of creating a view, we will now need to use a bit more reflection to access the setter of our public Model property. We repeat this same approach each time we require an additional view be added to our game.
I encourage everyone to take a second and download the sample project. To refactor our solution to support multiple XAPs requires a bit of code reorganization. We did not add a ton of code to our solution, but certainly changed the order in which things get instantiated and added to the display stack.
Thank you and see you next time.
Joel Neubeck, Silverlight MVP