Postmortem Part 2: Getting It Together


I went all-in on splitting RiftBreak into independent components. Not only is each gameplay segment separate, but also the main menu, game over screen, error handler (I’ll get into that later), and video player.


The launcher presents a fake DRM façade inspired specifically by the StarForce “protection system” thing I remember from Silent Hunter III, but it’s actually the heart of the whole operation. I used Windows Forms for this, targeting .NET Core instead of .NET Framework (yes, that’s supported now) because that’s the direction .NET is going but really mainly because it’s possible to build a fully self-contained, single-file executable. Windows Forms is fast and easy to work with but provides limited options for styling- I don’t have as much experience with WPF and figured I didn’t have time to finagle it into working.

It also leaks memory and steals CPU cycles.

No, really! There’s code in the launcher that deliberately eats a CPU core and leaks memory, to add to the so-bad-it’s-good-ness. Or maybe just badness. This is scaled by the detected hardware and sometimes disabled. I think I had it set up to only steal resources if you had more than 4GB of RAM and at least 4 CPU cores, and then steal 1MB of RAM per second per 8GB and 1 core for every 4. I kind of screwed this up and the way it’s set up essentially gives you a fixed timer to complete the game before your system becomes unusable.

It turns out that there’s an issue with Windows Defender that causes it to eat RAM like crazy under certain workloads including my RAM virus, so I had to issue instructions on how to disable it (I made it a config file option because I was worried this would happen) and ended up disabling it by default in 1.1. This didn’t come up on either of my test machines, and I’ll get into some of my testing woes later.

As it turns out, there was also an accidental CPU core stealing going on. The launcher has to run other programs and wait for them to exit, then do something. I did this the laziest way possible, by busywaiting on a thread and checking if the process has exited. In version 1.1 I would fix this by changing it to poll slowly and sleep (actually Task.Delay) between polls.

Because of one or both or perhaps another issue, at least one tester experienced a hard crash because of my game. Sorry, that’s on me.

For the most part, there isn’t much communication between components. The launcher starts a component, waits for it to exit, and checks its exit code. This is I guess where the hackiness begins. For one, I’ve used the exit code 0 to indicate an abnormal exit and various integers to describe success, failure, and in the case of the main menu selection of chapters. And while Unity and .NET Core executables return exit codes normally, I was unable to do so from nw.js for some reason. Fortunately I already knew about this before the jam- I would have torn my hair out otherwise. Just before exiting, the nw.js-based components (including an RPG Maker MV segment) save a file with an exit code in it which is then read by the launcher. I’ll talk about where that file is and some of the other issues with data files near the end of this segment.

The launcher itself is a giant hilarious mess of async-await, switch cases, and flow control via exceptions. I’m not sure if I’ll ever release the full source but you can see some of it here. The Drm class contains fake DRM stuff as well as legitimate utility methods and that’s another technical decision I regret. I’m not exactly happy with it, but it works, it’s not a complete nightmare to maintain, and I was very tired when I wrote most of it. For those of you who aren’t familiar with async-await, it’s kind of like Coroutines in Unity but way better and lets you write asynchronous code almost like synchronous code. It would have been much, much more difficult to write this without it.

The game is divided into chapters, as I’ve mentioned before. These aren’t always a single program but might be a short sequence, usually playing a video and then running the gameplay sequence. Async/await makes this very easy code to write.

I went with Unity for the main menu. I deliberated for a while over using nw.js or using Unity for this. Once I decided to treat the mandatory splash screen as a feature, not a bug (hey, it’s so bad it’s good), that pushed me towards Unity. I decided to make it really nice, with new graphics (mostly nine-patches I made fresh in Photoshop), transition effects and sounds. The moving “dust” effect is made with a particle system that sprays particles past the camera. The chapter preview images are just stock images, colorized and sometimes tweaked in Photoshop.

It’s probably the nicest main menu I’ve ever made, and it’s for a so bad it’s good game. I think I felt more willing to make it fancy because it’s a fairly simple menu, with just a few buttons and a chapter select. I didn’t have to worry about building a huge menu system with options menus and such and making it all both look nice and work properly. For this I wanted to make a good impression but didn’t have to get a ton of stuff to work.

Fun fact: the main menu uses CommonCore Core (the core part of my framework) despite basically using none of the functionality of it. I could have copy/pasted a few methods and saved myself some install size and some trouble.

Another fun fact: The chapter buttons are generated dynamically. There’s a JSON file with the data and a bunch of images in Resources, and even provision for loading overrides at runtime. I never used runtime overrides, only changed a few things after initial setup and probably could have just hardcoded everything.

Oh well.

The video player is just a simple nw.js app. Originally I was going to use VLC, but I didn’t think it would give me the customization and control I wanted and I pondered that question for a while before realizing that I could just use what basically amounts to a web page. The VP9 video codec is actually pretty efficient, somewhere between h.264 and h.265 and quite good at low bit rates, but there’s limited support for hardware encoding and even software encoding is very slow (ffmpeg libvpx-vp9 versus x264). I did most of the transcoding (usually from h.264 Premiere Pro exports) on my gaming rig as it has much better sustained performance than my laptop.

The game over screen is also nw.js. It’s really just a dirt simple web page. I don’t have much to say about it.

The error handler (aka crash handler, I’ve used both names and screwed up some of the code because of it) is another Windows Forms .NET Core component. It’s inspired by the infamously confusing Abort/Retry/Continue prompt in DOS. In this case the Abort/Retry/Continue generally makes sense, or at least I thought it did. The chapter you have attempted to play has errored out (usually because it doesn’t exist), so would you like to abort (exit the game/return to main menu), retry (restart the chapter) or continue (skip the chapter)? I think this was more clear in my mind to be honest.

I got a bit worried that people would just exit the game the first time the error handler appeared, so I ended up modifying the wording a little bit. I think it worked out because as far as I can tell most people who played the game played more than one chapter. I also made sure the fatal error handler (same executable, pass an argument, it changes the appearance a bit) was visually distinct to make it clear moving past that one wasn’t possible.

The “RiftBreak.exe” executable in the root folder really is just a dummy executable. It’s the Win32 GUI application template from Visual Studio slightly modified, because I barely know Win32 and generally don’t enjoy programming in it. It starts up, displays the prompt, and exits.

While I did get the thing to work, getting there was, um, “fun”. Debugging sucked. There are a few reasons for this:

  • There are a lot of “moving parts”; disparate software components that have to work together
  • All the components need to be present and at least working enough to return exit codes in order to test other parts
  • Debugging Windows Forms apps on .NET Core isn’t as robust as debugging Windows Forms apps on .NET Framework, especially when async is involved.
  • The launcher is slow to start up (half deliberate fakery, half because it’s doing stuff) and we need to go through for virtually everything
  • I only had access to two computers plus a virtual machine to test on, all with similar software and OS configurations

Using .NET Core, fortunately, meant that we could build self-contained executables that didn’t rely on having a framework installed on the computer, although I’d end up pulling back on this a bit with the 1.1 update.

I think the most trouble I had was actually with handling cleanup when closing the launcher. We needed to cancel the big huge asynchronous Task that runs the game switching logic, which we can do with CancellationToken. But we need to make sure the child processes are closed, and importantly need to delay actually exiting the application before the task has completed cancelling which I think was the hardest part and the part I hadn’t expected.

I’m primarily a .NET developer, so that’s why I went with .NET so much. I have some familiarity with web technologies and nw.js in particular, and I knew I could do certain things with those easily so I went that way where I thought it would make sense. Of course, my main project is Unity and I have quite a bit of experience with that as well.

The first released version of RiftBreak didn’t make any attempt to share runtimes. There are something like 4 copies of nw.js and 3 single-file executables including their own partial .NET Core runtimes. I figured the resulting large filesize was fine for a jam and maybe would contribute to the so bad it’s good ness of the game.

Between that and the insanely large intro video I ended up almost slamming into the 1GB itch.io upload limit, and I had to be very careful about adding the last little bonuses late into the jam. Ultimately I squeezed by with room to spare at 830MB.

The updated 1.1 version shares an nw.js runtime and uses a global .NET Core runtime (except for the launcher which still includes the runtime). It also reduces the size of some of the video cutscenes. Altogether I was able to get the compressed size down to under 500MB. That isn’t the only change- 1.1 also fixes some bugs and issues (including the accidental CPU cycle stealing) and adds a few new bits and pieces. The resource leaking is also disabled by default because of the aforementioned issues.

Data is saved to the same place as all my games: roaming app data, XCVG Systems folder, project name/codename subfolder. In this case it ends up as %AppData%/XCVG Systems/Arkansas/. I ended up creating a “session” folder for temporary data although it was only ever used for the nw.js exit code files.

Speaking of which, one big downside to making a game that’s actually multiple games tied together with duct tape and hay wire is that huge mess it makes. In addition to the main/shared Arkansas data folder (which I was able to direct some, but far from all, of the game data into), there are separate folders for the Unity segments as well as the nw.js segments, some contained in the XCVG Systems folder and others spread across local and roaming AppData.

Such is the price of progress. Or something like that.

Also, I did some playing around with nw.js versions and wasn’t careful with package names so I ended up with some of the nw.js apps being the “same” and complaining of the profile being too new or too old or something.

I deliberately did not implement saving anything except which chapter the player had previously completed. Config isn’t shared either. Both of these things would be doable in theory, but I put them aside to limit scope and actually stand a chance in hell of finishing the jam on time.

Whew! I think I’ve gone through all the important stuff about how RiftBreak goes together. Next time, I’ll talk about how I built each individual section, some of the things that went well and some of the things that didn’t.

Get RiftBreak

Leave a comment

Log in with itch.io to leave a comment.