Those who are following my twitter timeline cannot have missed the fact that I recently published a little game called “Catch’em birds” for Windows Phone 7 in the Marketplace. This was mainly a trial project to see if I could bend MVVMLight far enough to use in an action game (result: yes you can). It’s not exactly a smashing success in terms of downloads and sales, and it does not use any of the Windows Phone exclusive features. So I decided to add a Live Tile showing the overall high score and if possible, the player’s rank. I’ve noticed that some people in the Dutch Windows Phone developer community (I won’t mention names here) tend to get particularly competitive when high scores are involved, so maybe this would boost download. And if not, then at least I’d have a lot of fun developing it.
Catch’em birds features an online leaderboard using the services of Mogade. They have a nice and very simple to use library that you can include in your Windows Phone 7 project. You make an account, you define a game, and you define a leaderboard. This will give you 3 keys that you must include in your App and you are ready to go. Samples on their site are quite adequate so I won’t bore you with a repeat of that.
But now I want to use the same leaderboard services from a Live Tile service. So I defined a solution with an empty ASP.NET Web application project, and deleted all source files except for web.config. In the web.config, I defined four keys:
<appSettings> <add key="gamekey" value="your gamekey here"/> <add key="secret" value="your game secret here"/> <add key="leaderboard" value="your leaderboard key here"/> <add key="baseimageurl" value="Catchembirds/background.png"/> </appSettings>
The first three are the Mogade keys and are supposed to be secret, so you are not going to include them in your tile request, but ‘somewhere safe’. How safe a plain ole’ web.config is, is subject of discussion but not the point of this article.
To make Mogade work, you will need Mogade.Core.dll and Newtonsoft.Json.Net35.dll. Beware: use the full .NET version. This a .NET web application, not a Windows Phone application ;-). You can download these from Mogade or just nick them out of my sample solution. Make a reference to those two dll’s and then it’s simply a case of defining a Generic Handler (ashx) and filling in the ProcessRequest method.
First, I get all the variables I need. My leaderboard is configured to use player name and anonymous live id to recognize the player, so I have to pick those from an URL. The rest comes from the web.config.
var userName = context.Request["username"]; var anid = context.Request["anid"]; var gameKey = ConfigurationManager.AppSettings["gamekey"]; var secret = ConfigurationManager.AppSettings["secret"]; var leaderboard = ConfigurationManager.AppSettings["leaderboard"]; var baseImageUrl = ConfigurationManager.AppSettings["baseImageUrl"];
The second part opens the base image, calls the Mogade service, waits for it to return data, and draws the result on the image. If the user has not played or has no name entered into the game settings yet, there may be no ranking, so that’s optional. To show that it actually works and the tile is indeed updated multiple times I’ve included code to draw a timestamp on the tile as well. This you will never do in real life, of course.
using (var myBitmap = new Bitmap(context.Server.MapPath(baseImageUrl))) { try { var readyCalled = new ManualResetEvent(false); var driver = new Driver(gameKey, secret); // Get high score and user ranking driver.GetLeaderboard(leaderboard, LeaderboardScope.Overall, 1, 50, userName, anid, p => { if (p.Success) { using (var g = Graphics.FromImage(myBitmap)) { var font = new Font("Segoe", 5, FontStyle.Bold); // Draw high score on the tile var points = p.Data.Scores[0].Points; g.DrawString("h: " + points.ToString(), font, Brushes.White, new PointF(10, 5)); // If user has ranking, draw ranking if (p.Data.User != null) { var rank = "r: " + p.Data.User.Rank.ToString(); var size = g.MeasureString(rank, font); g.DrawString(rank.ToString(),font, Brushes.White, new PointF(173 - size.Width - 10, 5)); } // Draw timestamp - for debugging purposes only g.DrawString( String.Format( new CultureInfo("en-US"), "{0:HH:mm\ndd-MM-yyyy}", DateTime.UtcNow), font, Brushes.White, new PointF(10, 110)); } } // Indicate Mogade call is complete readyCalled.Set(); }); // Service waits here for Mogade call to complete readyCalled.WaitOne(); }
To make the code wait for the call to Mogade to complete, I use a ManualResetEvent. Calling its “WaitOne” method basically hangs the main thread until something else – in this case the anonymous callback from the Mogade GetLeaderBoard method – calls it’s Set method. This little trick I learned from Olaf Conijn.
The final part writes the image back to the stream. There are two important things to note here. First, I output the image as a JPEG of pretty low quality. There are three good reasons for that:
- The most important one: I use a ShellTileSchedule to retrieve the tiles, which simply refuses to download anything larger than 80 kb. When I output the tile as PNG, the result is over 120kb and nothing seems to happen. It took me some time to figure out why.
- You hardly see the difference on a small screen between 80kb PNG and 16kb JPEG
- Microsoft have tried very hard to make bandwidth and battery usage of Windows Phone 7 as economical as possible, so be user, battery and data plan friendly and keep this kind of repetitive network access as small as possible.
finally { // Output as JPEG to stay below the 80 kb context.Response.ContentType = "image/jpg"; using (var ms = new MemoryStream()) { // Select JPEG encoder var imgCodec = ImageCodecInfo.GetImageEncoders().Where( codec => codec.FormatID.Equals(ImageFormat.Jpeg.Guid)).FirstOrDefault(); var parameters = new EncoderParameters(); parameters.Param[0] = new EncoderParameter(Encoder.ColorDepth, 8); // Save on memory stream first, then write bytes to Response // Neccesary to get this working on AppHarbor myBitmap.Save(ms, imgCodec, parameters); var bmpBytes = ms.GetBuffer(); ms.Close(); // Add content-length for good measure context.Response.AppendHeader("Content-Length", bmpBytes.Length.ToString()); context.Response.BinaryWrite(bmpBytes); context.Response.Flush(); context.Response.End(); } } } }
The second thing to note is that I use a little detour writing the resulting image to the Response via a MemoryStream. I could just as easy have written it directly to the context.Response.OutputStream – which I originally did - but I host the service on AppHarbor and for some reason this gives me a "A generic error occurred in GDI+" error message. Using the MemoryStream solves that problem.
The result looks something like this:
For my Windows Phone 7 solution I use Mark Monster’s SmartShellTileSchedule – with a little adaption, i.e. I put a try/catch block in the UpdateTileBeforeOperation because that tended to crash on my phone – but not on my wife’s. I still don’t understand why. I initialize it like this:
var mogadeClient = MogadeClient.Initialize(gameId, gameKey); mogadeClient.Update(null); new SmartShellTileSchedule { Interval = UpdateInterval.EveryHour, MaxUpdateCount = 0, RemoteImageUri = new Uri( string.Format( @"http://www.yourservernamehere.com/LiveTile.ashx?username={0}&anid={1}", userName, mogadeClient.GetUniqueIdentifier()), UriKind.Absolute), StartTime = DateTime.Now, Recurrence = UpdateRecurrence.Interval };
gameId, gameKey and userName are all variables loaded from the settings ‘somewhere’ in your phone app.
I had a little doubt whether or not the App Hub test team would object to me sending the anonymous live id as part of the tile request but yesterday my live tile enabled version 1.2.0 of Catch'em birds was certified for the Marketplace, so apparently that's allowed. So now there's no reason not to add a Live Tile to your game as well. Make use of this Windows Phone 7 exclusive feature and get your game pinned to start screens all over the world!Sample code can be downloaded here.
2 comments:
Another nice blog post that will get quite a number of views, I guess.
Quick tip: you could use example.com as the domain in your example instead of somewhere.com; example.com has been specifically reserved for that purpos, a bit like the 555 telephone area code in the US, or the Oceanic airline. ;-)
@peSHir did not know that - but thanks for the suggestion. I changed the sample URL to http://www.yourservernamehere.com/ which does not seem to exist at all ;-)
Post a Comment