I recently spent a couple days porting Owen Deery and Gabriel Verdon‘s recent XBLIG release, Bytown Lumberjack, to HTML5 using my .NET to JavaScript compiler, JSIL. This post will go into the process I used and give you some insight into the problems I had to solve to get the game working in-browser, and hopefully help you understand what might be involved in bringing XNA games of your own to the web.
You can play the HTML5 port of the game here.
To begin with, Owen generously gave me access to the source repository for the game, so I checked it out and made sure that it built and ran successfully in Visual Studio. After that, I copied the folder of one of my existing demos and made some minor changes to the HTML & CSS to make it appropriate for the new game – swapping out the title, adjusting the size of the canvas, updating the controls text at the bottom, etc. The next step was to create a configuration file for the game – again, I copied an existing configuration file and made minor adjustments to point it to the right location and adjust the settings for the XNA content processor. With that done, the final step was to actually feed the game to JSIL. To save myself some grief, I made a batch file to run JSILc on the game and its configuration file:
pushd C:\Users\Kevin\Documents\Projects\JSIL\bin JSILc "C:\Users\Kevin\Documents\Projects\lumberjack\LumberjackPC.sln" "C:\Users\Kevin\Documents\Projects\JSIL\jsil.org\demos\Lumberjack\Lumberjack.jsilconfig" popd
At this point the first issue revealed itself: The game used some XACT features that the open-source XACT parser I’m using, XapParse, didn’t support. I opened it up in VS and made the appropriate changes to support the features (none of them required significant code changes, just some tweaks to the parser), and then ran JSILc again. This time, the game successfully translated to JS and produced usable manifests.
Next, I edited the HTML to add the manifests that were generated by JSILc:
<script src="Lumberjack.exe.manifest.js" type="text/javascript"></script> <script src="LumberjackContent.contentproj.manifest.js" type="text/javascript"></script>
This part requires a little human know-how since JSILc will often output manifests that you don’t need. For example, if your game has custom content processors, you’ll get a manifest for those, but you don’t need to include that manifest because the game itself will automatically pull in those DLLs.
I started a local web server for testing (python -m http.server to the rescue!) and pointed Chrome at it. (Normally IE is my starting point, since you can run it against file:// and it has a good debugger, but it has audio bugs that I knew would cause problems for Lumberjack.)
The game started and rendered part of the title screen, but I had lots and lots of missing external methods and properties to implement. I stopped the game and made a copy of the console output and started implementing all the missing methods.
My Valiant Battle With The Title Screen
One of the first obstacles was that I hadn’t implemented support for the XNA Effect class, and Lumberjack was using it to do a bloom filter on the game’s visuals. This ended up being easy to implement; all I needed to do was stub out the relevant methods/properties with dummy code. To get it working I also had to extend the XNA content processor in JSILc so that it could copy Effect XNB files to the game’s output directory, to make them available to load. I also stubbed out a handful of missing XACT and GamePad APIs. After this I started the game up and got a mostly complete rendering of the title screen, but the game immediately crashed after that and there were clearly things wrong with the rendering.
To fix the crash, I dug into the error and discovered that the game depended on a XNA behavior I hadn’t implemented. In XNA, when you add a GameComponent to the Game.Components collection, the component’s Initialize method will automatically be called if the game is already running. Because I wasn’t doing this, the component was then accessing uninitialized members containing useful things like textures and shader instances. Reproducing this behavior took a few lines of code and fixed the crash.
JSIL.ImplementExternals("Microsoft.Xna.Framework.GameComponentCollection", function ($) { $.RawMethod(false, "$internalCtor", function (game) { this._game = game; this._ctor(); }); $.RawMethod(false, "$OnItemAdded", function (item) { if (this._game.initialized) { if (typeof (item.Initialize) === "function") item.Initialize(); } }); });
Starting the game up again meant it got a bit further and I hit a new crash, this one caused by an edge case in JSIL: The game was casting a nullable enumeration value directly to integer, and JSIL had no support for that. It turned out to be easy to fix, and once it was fixed the game got further in the title screen and I was able to identify more missing externals and stub them in to reduce the number of error messages being spammed to the console.
Another start of the game revealed that it relied on some math APIs that I hadn’t implemented like Math.Exp and MathHelper.Lerp, so I added those in along with some other APIs being used on the title screen.
Reading some of the code for the title screen revealed another JSIL bug: the combination of casting, enumerations and by-reference passing of values was causing JSIL’s support for ref/out parameters to break. This ended up being relatively simple to fix as well.
Wherein An Alpha Channel Is Premultiplied
At this point, the title screen was working, so I started digging into getting it to behave and look the way the actual game’s title screen did. The first thing I did was fix the support for mouse events in JSIL, so that I could interact with the menu using my mouse. I also noticed that the text being rendered on the main menu was badly misaligned, so I fixed that too. Next, I started digging into the rendering issues.
The rendering issues turned out to be caused by two specific problems: First, the game was making use of BlendStates other than AlphaBlend, and I hadn’t implemented any support for configuring blend modes. The second problem was that the Bloom implementation relied on particular pixel shaders, and as JSIL still used pure canvas, there wasn’t a way to implement those shaders. This resulted in the title screen looking blurry and brighter than it should have been.
Implementing BlendState ended up being pretty straightforward – I just had to update GraphicsDevice and SpriteBatch to track BlendStates and add a little bit of logic to apply them to the canvas (by setting globalCompositeOperation to copy, source-over or lighter appropriately).
I spent a bit trying to reproduce the bloom effect using canvas, but eventually it became clear that it would require pixel shaders, so I took the easy way out and entirely stubbed out the component responsible with a Proxy:
namespace Lumberjack.Proxies { [JSProxy( typeof(BloomPostprocess.BloomComponent), )] public class BloomComponentProxy { protected void LoadContent () {} protected void UnloadContent () {} public void BeginDraw() {} public void Draw (GameTime gameTime) {} } }
And then I added the proxy library to the config file so that it would be applied to Lumberjack’s code during translation:
"Assemblies": { "Proxies": [ "C:\\Users\\Kevin\\Documents\\Projects\\lumberjack\\Lumberjack\\Lumberjack\\bin\\x86\\Debug\\Proxies.dll"
Running the game through JSILc again produced a title screen without the broken bloom effect, and appropriately stubbed out code:
/* Implementation from BloomPostprocess.BloomComponent */ $.Method({Static:false, Public:true }, "BeginDraw", $sig.get(89342, null, [], []), function BeginDraw () { } );
Starting up the title screen and playing with it a little revealed that while it was mostly working, it looked a bit strange. One of the bugs responsible turned out to be my implementation of Color.Multiply – it turned out that Lumberjack was passing alpha values below 0 and larger than 1 to it, and I was producing positively strange looking colors in response. It was also using SpriteBatch‘s rotation parameter, so I implemented that as well.
Next, I noticed that while the title screen rendered correctly, the transition that played when the game loaded seemed broken compared to the real game. This turned out to be due to a problem with my framerate balancing code that caused the transition to go from beginning to end in a single frame.
Next, I tried opening the Options screen from the main menu, and immediately got a crash. This crash turned out to be caused by invoking GameComponent.Draw on a component without ever calling Update on it, which would happen if the game was running below 60fps due to framerate regulation.
XML: Is It Giving Your Children Cancer? News At Eleven
At this point the title screen was completely working, so I clicked ‘New Game’ and was greeted by a Loading screen followed immediately by a crash. This was a tough one: Lumberjack uses XmlSerializer for levels and saved games. I spent an hour or so fiddling with possible alternatives to XmlSerializer – XNA’s IntermediateSerializer turned out to not be an option because of piss-poor documentation and missing support for critical XmlSerializer features, while JSON wasn’t an option due to the amount of XML involved and the amount of functionality provided by XmlSerializer that I’d have to duplicate anyway.
Looking through the documentation and disassembled code for XmlSerializer didn’t give me much hope. In the past, I’d tangled with it and come away from the experience with little desire to use it again. After a few minutes of despair, however, inspiration struck: SGEN! SGEN is a Microsoft utility designed to improve the performance of XmlSerializer by producing compiled code that can convert each of the types in your application to/from XML. This has the awesome side effect of making the actual XmlSerializer implementation from the .NET runtime library entirely unnecessary and only needing XmlReader/XmlWriter to work.
I added a post-build event to the Lumberjack game project to run it through SGEN and generate code for the type that I cared about:
"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\NETFX 4.0 Tools\sgen.exe" lumberjack.exe /t:Lumberjack.Level /force
And then I updated the batch file to feed the output into JSILc:
JSILc "C:\Users\Kevin\Documents\Projects\lumberjack\LumberjackPC.sln" "C:\Users\Kevin\Documents\Projects\lumberjack\Lumberjack\Lumberjack\bin\x86\Debug\Lumberjack.XmlSerializers.dll" "C:\Users\Kevin\Documents\Projects\JSIL\jsil.org\demos\Lumberjack\Lumberjack.jsilconfig"
Another run of JSILc produced solid-looking JS output that seemed like it would be able to load levels once I implemented the necessary external methods, so I started by hacking up the generated JS for Lumberjack to make it directly invoke the code produced by SGEN. Attempting to start the game provided me with a long list of missing XmlReader APIs, and then the browser hung because of an infinite loop inside of XmlSerializer. That’s a start!
Now came the tough part: Implementing XmlReader. I set up a simple test harness to run XML test cases in C# and produce JS that I could run in the browser to compare my implementation, and started implementing features one or two at a time.
I spent a few hours hacking on XML, taking heavy advantage of the DOMParser class built into modern web browsers to do some of the heavy lifting for me, and periodically checking with Lumberjack to see whether the level loader would get further and hit more missing external methods. Eventually, I implemented the last few necessary methods and the game successfully loaded its first level. The little intro story sequence appeared, running very slowly, and I clicked through it… only to be greeted by another crash!
Gameplay: Do Your Games Really Need It? A Comparative Analysis
Luckily, the crash was a trivial one – my implementation of negation for Vectors was broken, so I fixed it. I hit a serious bug in JSIL that was breaking the game’s collision detection code as soon as the level began. The problem turned out to be that break statements were being omitted from nested switch statements. The fix turned out to be extremely trivial, and yet another example of me being simultaneously too clever and lazy for my own good. While digging through the JavaScript output to identify the switch statement problem, I noticed a few other issues and ended up cleaning up some problems in the JSIL type system to produce better code, because I’m easily distracted.
Wait, wasn’t I porting an XNA game? Oh right. Lots of XNA APIs were being used by the game once the level began, so I went through the painstaking process of implementing most of some of almost all of some of them. In particular, despite being a 2D game it made heavy use of 4×4 transformation matrices, so I had to expand my stubbed implementation of those to be closer to reality.
Distracting, poorly placed aside in an extremely long blog post
At this point I feel compelled to point out that when implementing stuff like XNA library functions, I am aided tremendously by a recently added JSIL feature called GenerateSkeletonsForStubbedAssemblies. If you turn this configuration flag on, any stubbed assemblies it translates will be generated as helpful skeletons, like so:
JSIL.ImplementExternals("Microsoft.Xna.Framework.Graphics.Texture2D", function ($) { $.Method({Static:false, Public:false}, ".ctor", (new JSIL.MethodSignature(null, [], [])), function _ctor () { throw new Error('Not implemented'); } ); $.Method({Static:false, Public:true }, ".ctor", (new JSIL.MethodSignature(null, [ $asms[3].TypeRef("Microsoft.Xna.Framework.Graphics.GraphicsDevice"), $.Int32, $.Int32 ], [])), function _ctor (graphicsDevice, width, height) { throw new Error('Not implemented'); } );
The skeleton can basically be used as a template for your own implementation of the library – you can copy-paste individual methods or entire classes, and replace the throw statements with your own method implementations (or just a big ugly // FIXME comment). It is an understatement to call this feature a time-saver, just like having your very own undead minions to do hard work for you, but without the clacking of bones and the corpses.
Equally distracting end of distracting aside
Stuttering Turns Out To Be Kind Of Terrible In A Video Game
Shockingly, at this point, the game was working. I could move around the level and attack enemies and get attacked by enemies. I decided to take a short break and test out all the other JSIL test cases, to see if I had broken any of them. Unsurprisingly, I had, so I spent a bit fixing the issues I had introduced, and adding test cases where appropriate.
While the game worked, The problem was, it ran pretty slowly, and in particular, it stuttered really badly when certain things happened – the fades in the opening story sequence were very slow, and any time you hit an enemy or got hit by an enemy, the game would hang for nearly half a second. Some profiling in the Chrome developer tools revealed the culprit to be $jsilxna.getCachedMultipliedImage. I started by fiddling a bit with the code to make it slightly faster and skip calling it in spots where it was unnecessary, along with fixing some related rendering problems. This made the game run a bit faster, but it was still doing some fundamentally unacceptable stuff in order to render the scene. Brute-force optimization wasn’t going to cut it, so I spent a while and figured out a better way to implement that function.
And now, more than you ever wanted to know about GL_MULTIPLY
The purpose of $jsilxna.getCachedMultipliedImage is to simulate color multiplication in HTML5 canvas. Color multiplication is an extremely handy tool used very often in modern games – in OpenGL, it’s known as GL_MULTIPLY and in Direct3D it doesn’t even have a name anymore because you just put it in your pixel shader and forget about it. Typically, in 3D environments you achieve this by attaching a color to each vertex, and multiplying your texture colors by the vertex colors – hence the name MULTIPLY. As always, Canvas doesn’t have a feature like this – it doesn’t really have any features at all except, oddly, for the ability to render blurry drop shadows – so I have to emulate it with a terrible hack.
Up until this point, the hack JSIL used worked like this: Make a copy of the source image, and multiply all the pixels by the specified color, and then draw the copy instead of the source image. As you might expect, this is expensive. And slow. And also very unkind to your RAM. It had been sufficient for the occasional uses of color multiplication in my demos so far, but not for the extensive use of the feature in Lumberjack.
Eventually, I realized that I could implement this feature in Canvas with less significant memory use and lower CPU overhead by splitting it up into smaller pieces. Instead of creating a unique image for each multiplication color, I could create a single set of four images for each image I wanted to multiply, each one of the output images corresponding to a single color channel from the source image:
Once I had those four images, to render any given image multiplied by any given color:
- First, render the black image (generated from the alpha channel) using the source-over blend mode, and using the color’s alpha as opacity. This produces the same results as if the source image were actually solid black with an alpha channel.
- Next, for each color channel, render the image for that color channel using the lighter blend mode, and using that color channel as opacity. This means that I will be adding the contents of the red channel to the (now black) framebuffer, and multiplying the red channel by the amount of red in the multiply color.
Following these steps produces roughly the same result as color multiplication in Direct3D/OpenGL, and removes the need to generate a unique image for each unique color. It’s still slower than real 3D, but it’s dramatically faster and it reduces the memory usage significantly.
OK, enough about GL_MULTIPLY, seriously
The Hard Part Is Done! Now It’s Not Unplayable, Just Slow
With that optimization applied, the game ran pretty well – still some stuttering when it had to generate cached images for multiplication, but dramatically less stuttering. In Chrome, it ran at or close to 60FPS almost all of the time, the physics and audio worked, and I could kill enemies and complete levels! I spent a little time profiling it with the Chrome developer tools, but didn’t see anything in JSIL to fix, so I switched to testing it in other browsers and fixing miscellaneous bugs.
At last, after much procrastination, the time had come: Canvas was too slow and I was going to do something about it. I quickly and decisively located a library that somebody had written to solve this problem for me, and then painstakingly dropped it into the JSIL Libraries directory. Hey, look, the game runs faster now! Pleased with the results of my hard work, I hacked up webgl-2d to support some missing features and expose direct access to the equivalent of GL_MULTIPLY, so that I wouldn’t need to cache the channel images at all. With the channel images gone, almost all the stuttering was gone too! The only remaining performance issues were problems with the actual game code.
Thus, I decided to screw around with things that didn’t matter for my own amusement. Behold the fruits of my labour:
Thanks for reading! I hope this long, confusing post has given you some ideas on how to use JSIL and some insight into the challenges you might face. Feel free to comment, or email me with any questions!