porting THREES to luxe, part one

sign up sign in

This post related to the web version of THREES, which I ported to luxe last year. You can play it here if you haven't: http://play.threesgame.com/

Part one of this postportem will cover some of the more general things involved in the port to web rather than being overly luxe or snõwkit specific.

It's always tricky to know where to start with these type of posts, so I'll start with the reasoning and ideals for the port.

The recon phase

When Asher mentioned the idea of a web version of the game to me, he had already made a working version of the game using Unity - since the Unity WebGL preview builds were around, it made a lot of sense to give it a try.

I won't speak for Asher but the results weren't great, the loading before you even see a loading bar was in the megabytes range. It was many more megabytes later before the game showed up. The preloader was pulling down more than 10x the size of all of the assets in the game before the user saw one of them.

This post isn't about Unity but rather the goals of the port - obviously the size was at the forefront of concern. Not just because of the waiting time for a game like THREES being highly relevant, but the cost to host a game to many thousands of users is quickly multiplied, and hosting costs easily eclipse the value of the port.

Because of the existing build, Asher and Greg had already exported a bunch of the assets in a simplified manner, and made the source code and assets available to me to have a look through. As far as preparing goes, it was pretty much ready to dig in from a code standpoint. I spent a couple days pouring through the game code trying to better understand the parts that made up the game, and making notes about the structure and composition.

I won't go into how long it took to find where A connected to B in some cases - but it's worth mentioning that Asher did a pretty decent job of separation of concerns on the code side. The same can't really be said about the project structure overall, but that's not a viable comparison because Unity dictates that in many cases.

Having made and ported many things let me throw in a tip here, keeping the core gameplay code out of the structures imposed by Unity (or any engine) are a boon if you ever intend for anyone else to understand your project code and structure. This may appear "obvious" to experienced developers (many of whom still get that wrong), but keep it in mind. THREES code in this aspect was great, but more on the code later.

The inception

Since I consider THREES a big deal, I wanted to be sure that I could commit to the project properly and not let Asher (and Greg, visually) down with regards to the quality of the port. I spent a bit of time experimenting in isolation, answering any of the worrying unknowns for me. Since the port was to be simplified a tad for the benefit of the full experience on mobile, this part didn't take all that long - and I started messing with the "real" version during that phase too, starting with the menus. Since I have work to do on Drifter, I floated the idea of spending some time on the port to Colin (Drifter lead dev) and set forward on getting it done.

I started by building the menus from scratch, since the previous logic of the UI was pretty specific to Unity workflow, it made more sense to me to start off on the right foot. I implemented a lot of the UI specifics, and then simply assigned them a key on the keyboard. When you push a key, the preview icon animates, another makes the menu slide left, or slides right. This is a pretty useful way to show progress, and Asher was pretty excited to see some already. We could also finalize some higher level details, and make sure we were on the same page before I got too far into the code. I'll mention more on that later.

The port

I set up a bunch of workflow related things and got to work on the game code, like this seemingly helpful task (this is a joke task, don't make lists like that). Since the image below shows a date, it's worth mentioning we didn't have any particular timeline specified sometimes (i.e release date and start date don't give you much information about the work involved).

code

My method of porting when it comes to code that is very similar (like the same language or languages that translate easily) is to set my goal on compilation of as much of the existing code as possible.

brute port
For cross platform ports in c++ for example, you'd often find that the primary goal is just building the code (the next one would be linking it). During this phase, you'd be making very easy-to-find notes of all the code that you remove, shield, comment out, or add in order for that to happen. Nothing else matters at this point except a brute force rampage through to something that compiles. From there you go backward and flesh out the rest.

For games or apps, I look for the entry point of the project, and copy paste everything into place (commented out unless the code has similarity), and then port that piece at a time. This allows me to quickly block in the framework for the rest of the code to fit into and I have something to debug against and validate. I assign the sections a priority if needed, and when the code is compiling I solve any blocking issues first. It's important to note that this process becomes more incremental when you're more experienced at it. You learn to discern which code would compile first, and which code is just a rabbit hole, allowing you to get code that you can validate sooner.

Two additional notes: Don't leave the hardest or unsolvable stuff till last if you can avoid it, motivation and movement are critical resources and there's a nice balance of easy and hard work to pit against each other for your time. And secondly, I said the word validate more than once. Code compiling is only half way there. Don't make assumptions about validity and you'll save many hours in the long run.

translating concepts
Within a very short time, I had around 90% of what appeared to be the game code ported, and most of the UI tied up into their necessary structure. Because Haxe and C# are quite similar in many ways, a lot of the todo sections were either calling empty functions in game code (to be added later) or were things that needed redesigning/rearchitecting because of the engine differences.

One important goal was to minimize the amount of difference between the original code and my own.

There's many reasons for this, if you run into a bug, you can compare using a diff tool and see if you're missing something obvious. If the original game gets updated for some fixes, or you find a bug in the original code, up or downstreaming that change is trivial. I also do this to ensure that the person I do the port for has the ability to fully understand the code already, so that they aren't dependent on me a month later to solve some minor issue, and are given control and agency over the port (which I much prefer).

By this point, there was very little (if any) code that needed translating because I had already solved the harder things earlier doing the mash-code-everywhere phase.

One notable example for interest sake is the idea of a Coroutine. I opted for a simple approach – which is to just translate the Coroutine in place – using a timer. A timer with a repeating callback behaves a lot like cooperative multitasking in the sense that it gives you a slice of time alongside the existing game loop.

Since coroutines are like mini state machines, I refactored the code in a minimal way to fit into that model, so that the actual code that does the work didn't deviate much. Once you pick an approach and stick to it, every Coroutine you find is easy to tackle. It's also worth mentioning that nested coroutines can get confusing fast, you need to be able to reason about what's happening. If you're not well familiar with the concepts at play it's very easy to get confused, even when you are! In some cases it is definitely better to isolate them for clarity alone, and leave them flattened.

Some advice in porting things like a coroutine, try to make sure you understand the code intent very well before tackling more challenging portions. If you're unsure just separate the code and try to make it easy to reason about first. When it's working correctly you can refactor with better understanding, but understanding what the code is trying to do should be the first priority (especially when it's not your own code). As you get more experienced you rarely need to understand the code intent, unless it's not working, but it could save you hours of frustration by prioritizing that during ports (it has for me).

Indecipherable bugs
These type of bugs are the best.
The code is 1:1 compared, there are literally no differences.
... Why isn't working?

Here's where the intent in the code and higher levels becomes critical. You have to understand what the author is trying to do, in order to validate the implementation details.

Also, validate your assumptions first. For me this is the fastest way to solve these problems. In THREES would you assume that the grid puts top left as 0,0? Would you assume the game code is instead upside down logically? No? Well it is.

The game logic is vertically flipped because of coordinate space differences. I had assumed that the 2D space represented by the original game treated Y a specific way and it didn't - and because of that, it's game logic treated indexing a certain way.

An easy thing to miss and an even easier thing to fix, but makes for a difficult thing to track down later on. The symptoms were all over the place - tiles merging when they shouldn't, tiles flipping to the wrong values - all stemmed from the fact the game was treating the grid upside down and my visual representation was treating them as top left.

The simplest path forward when you're stuck is one step at a time, validate every simple super obvious thing, in order. Assumptions are often where the most time is lost/wasted, for me.

Haxe / C# differences
Another indecipherable bug came from a subtle but critical difference in the ported code. I was very careful with this during the translating but missed one or two places (and foolishly didn't mark these places in code).

The difference is that C# treats a certain variable by value and in Haxe, everything except primitives are treated as a reference. This meant that some code modified a value that it considered transient and was instead modifying the actual persistent game state. That also broke things in magnificently confusing ways.

I spent a few too many hours stepping through the code and comparing the 100's of relevant lines because I didn't clearly mark places during the translation phase. Next time I'll be sure to mark anything that looks remotely like value semantics with a note to validate later! Validate all the things.

rendering

As mentioned above, here's an example of some WIP code earlier on, which lets me demonstrate a lot of the specifics of the UI without the rest of the game having to exist yet. By the end of the initial set of work, a ton of the interactions were implemented and triggered via keys, I have 16 bound keys just for explicit action testing. This helps find numerous bugs, and also allows showing progress during ports.

override function onkeyup( e:KeyEvent ) {  
        if(e.keycode == Key.key_b) { shortBoard = !shortBoard; SetGridSize( Threes.settings.kStartSize ); ShowGrid(true); }
        if(e.keycode == Key.key_w) topmenu(0);
        if(e.keycode == Key.key_e) topmenu(1);
        if(e.keycode == Key.key_l) move(1);
        if(e.keycode == Key.key_j) move(0);
        if(e.keycode == Key.key_c) Luxe.camera.zoom = Luxe.camera.zoom == 0.25 ? 1 : 0.25;
    }

Here's a number of the examples. Spawning particles, firing menu actions when the button is held down, UI animations, game state changes, and so on. One of these has a bug, even.

Of course I always throw in tons of visual debugging. I started the port by putting in a screenshot of the original game at my testing resolution, and made sure that all the UI was resizable, and made sure it was pixel perfect before moving on. When I hold shift in the debug build, and move my mouse horizontally, various levels of debug UI will show up to help me validate the details precisely.

Exact rendering details or structuring specific to luxe are for part 2.

assets

For the majority of the port, I developed the game using loose assets (not packed into an atlas) and preloaded them up front.

Initially I worked at 1x scale but kept in mind high-DPI in the code, and made sure I had the necessary sources for the 2x resolution assets nearby. Much later in the port I added a flag for packed assets, to start testing the more final build.

Once packed, I tested the idea of loading a high-DPI specific pack file when detecting the device has a scaling factor. However it's possible (at least on Mac) that when moving the window to a non high-DPI screen the scaling factor went away and the assets would be in the wrong scale.

I mostly fixed that issue in the loading and resizing scaling factors because it would matter later, but my main concerns related to simplicity. Adding complexity into the steps of your build workflow can throw a wrench at you when you're tired or under pressure (like at release), where a fix that should take 2 minutes takes a half hour because you make simple mistakes or skip a step. I simplified by packing the high-DPI assets only in the final build.

Once we had already set up the game for release and were testing things, Asher asked if it was plausible to test on mobile as well (something we had left as a wishlist item). Testing on mobile showed some odd performance characteristics that I didn't have a lot of time to investigate, so I decided to eliminate the low hanging fruit first; like using a texture atlas (sprite sheet). It's worth noting here the gain wasn't for draw calls (that's handled by the luxe batching renderer) but rather for state changes between textures on mobile WebGL. That and some other fixes worked well.

The code in luxe is more malleable to me than most, I know this, but is designed to make it easy to change to atlases quite easily. Adding the atlases took less time in code than it did to pack them using TexturePacker.

Once packed and atlased, the size of the 2x and 1x assets weren't actually over my size budget goal, but I still opted for sticking with just the high-DPI assets for simplicity on the release timeframe, because I hadn't fixed the situation of changing scaling factor mid game yet.

build size

The final build came down to the following (gzip compressed, the way the content is delivered):

  • assets.parcel 365.56 KB
  • Threes.min.js 161.99 KB
  • index, other 12.99 KB

That makes the download size around 540.54 KB with only 2 small files (requests) dependent before showing the loading screen - the code + the index.

Funny enough, for some reason I didn't build with dead code elimination (-dce full, I typically do) which would have shaved off 280 KB from the code before compressing. Before dce, the uglified code is 740 KB, and after 457 KB. I also didn't use closure compiler advanced mode which also could shave a couple of KB off.

Since I was well under an ideal size budget and a bit pressed for time at this point, I left those for the next time I do a release build!

Conclusion

In part two, I'll go into a bit more detail on how I handed things like the cameras, states, and so on.


This post was posted to patreon before being published here, as a bonus. Thanks to everyone for the support.

Tags
threespostportemportluxesnowkitdev