Rocket Plumber: A rocket engine and spaceship building game I'm making with Julia!

Hi everyone!

For a while I’ve been working on a game built with Julia called Rocket Plumber! Some of you might have seen it already on this other thread where @sylvaticus found the game in the wild, but given I just published a big release (v0.1.0) I wanted to create a post for a proper introduction.

rocketplumber-coolclip-fast

Rocket Plumber is a spaceship and rocket engine building game! At its core is an in-depth fluid network simulation based on electrical circuit solving maths. Pressures, flows, and even the layout of your pipes and components affect how your engines behave. You play in “Explorer Mode”, where you fire your engines to perform manoeuvres in space and encounter randomly generated ships. You must salvage parts and fuel from these ships to keep yourself moving, and the further and faster you go the better parts and fuel you will find. Eventually you will build up a ship powerful enough to perform the massive burn required to ‘escape the galaxy’ and win the game!

I have started a development blog to cover my progress. If you are interested in the circuit simulation stuff check out “RPB#1: Fluid Simulation with Circuits?”, and if you want some more detail on the gameplay take a look at “RPB#2: The Gameplay of Rocket Plumber!”.

As of v0.1.0 Rocket Plumber has moved from just a tech-demo to an early but playable game with a few hours of satisfying learning and progression. It’s far from finished and has some rough edges, but I hope it might be enough to give you a taste of where the game is heading! If you are after a polished game and early access is not your thing, I’d still hold off. But if Rocket Plumber sounds interesting to you, then download it and have a crack. I’m keen to hear what you all think!

  • Check out the game’s source on GitHub!
  • Download stand-alone executables for Windows and Linux! (I’ve created these with JuliaC, and I’d like to see how portable they are)
  • And if you want to stay up to date on releases and the dev-blog, sign up for the mailing list!

approach-finish

Some thoughts on developing with Julia

Given this is a Julia crowd, I figured I’d talk a little about my experiences using Julia for game development which is a somewhat atypical use-case (although less so now, there’s even a commercial game being worked on!).

As for why I chose Julia for this game, I was already using it for technical work as a sort of MATLAB replacement. The game started more as an experiment for that whole fluid-simulation-with-circuit-maths thing, which mostly boils down to linear algebra under the hood. Julia seemed like a great fit for that, so that’s what I used. In the end that hardcore simulation stuff has made up a surprisingly small part of the codebase, once you try and make a game out of it. Julia did also have all the pieces I needed including rendering stuff like GLFW.jl and ModernGL.jl, and given I was building this for fun and to learn I wasn’t particularly interested in an established game engine like Unity anyway. Overall, Julia is just a language I enjoy using so I kept with it.

This has been my first major project with Julia and I’m no expert, so take what I say with a grain of salt. But anyway here are a few things I want to provide my 2 cents on:

Creating portable executable bundles with JuliaC

Of course for a game I can’t expect everyone to install Julia and clone my repo to play, so I need compiled executables. I’ve previously used PackageCompiler for this, and now I’ve recently switched over to JuliaC. I’ve set up a GitHub Actions workflow for the builds, making it easy to get Windows and Linux builds in one go. For RocketPlumber that does require 32GB actions runners as the compilation process has a surprisingly high memory usage. After interacting with some pretty gnarly CMake build systems before, JuliaC doesn’t look half bad in my opinion, and I don’t think distribution needs to be considered a major blocker for Julia’s use in game development.

I’m not actually using --trim at the moment. Trim gets a lot of attention as it’s pretty cool, but I think it’s worth remembering JuliaC works just fine without trimming and you don’t have to make your project trim compatible to distribute it. Of course that means your builds are kind of large. My builds are about 1.3GB on disk, and can be compressed to a 360MB odd zip file for download. That would be pretty bad for something simple like a command line tool, but in the world of 100 plus GB games I don’t see it as that big an issue. For games I actually think having a full fledged Julia environment around could be quite a powerful feature if you think about modding support.

A few other small packaging thoughts.

  • I think the RelocatableFolders.jl package is awesome and I highly recommend using it if you need to get lots of small files like sprites and shaders into your compiled package.
  • Set up PrecompileTools.jl workloads for your package if you are going to compile it. Even just with a handful of these at the top level of your project you can get a massive amount of your code compiled ahead of time. It makes the compiled builds feel really snappy!

Garbage Collection and GC stutters

One thing you need to really think about if you are making games in Julia is garbage collection and allocations. The overall GC overhead isn’t really a problem, but the GC pauses can be really noticeable for the player as hitches and stutters in the gameplay. I made this worse for myself in Rocket Plumber, as all the particle effects make it really noticeable when this occurs.

I wouldn’t recommend trying to write all your code to be non-allocating. In my opinion you lose a lot of the implementation speed and dynamism, that makes Julia nice to use, and a large portion of your code probably hardly ever runs and doesn’t actually contribute much to your overall allocations. My approach has been to instrument my code with TimerOutputs.jl, and use that to track what parts of my code are allocating. It even gives you percentages, it’s almost like a weighted priority list of where to spend your optimisation time! Every now and then I go on an optimisation spree. Most of the fixes aren’t usually that complicated, eliminate dynamic dispatch in a hot loop here, reuse a buffer there. My last one of these optimisation sprees included some more involved work with my data structures, but I was able to reduce the total allocations by over 90%! At some point I plan to write up a dev-blog post about that. I don’t think GC should be considered an insurmountable blocker to Julia’s use in game development, just something to be aware of!

Thanks for reading! If you do end up trying Rocket Plumber there is a feedback email in the game, so let me know what you think! Rocket Plumber is still a work in progress, and I’m keen to get some player thoughts and productive criticisms I can use to guide my future work!

This is awesome, gonna give it a try soon! Thanks for the shoutout on my game as well. I’ve been busy with the demo release but I plan on doing something similar soon and writing a thoughts on working with Julia.

I haven’t gotten to the point of being able to compile with JuliaC either, but plan on carving out some time when things slow down. My CI time is almost an hour, so seeing that your builds only take like 10 minutes makes me want to see how I can get it down myself.

For GC, I have the same experience. I don’t run into it too much, but it can get pretty noticeable. But if I take the time to attack a consistent stutter, usually I can get it to the point where it’s smooth.

Glad to see someone else pushing in this space! Would love to swap notes sometime. Good luck with the project and I’ll be following!!

Love it!

This is great! I appreciate the source being available; it made a really nice porting exercise for my work-in-progress Julia runtime, Jule. It required a few additions to the OpenGL runtime surface and an adapter shim for CLMGUI (which now just works, same as GLMakie). Up and running:

You’ll notice that it is running on Mac… and quite reasonably at 119 FPS (this is an older, M1 Mac). Here’s the really fun part: this is a single-file binary weighing in at 83.8MB, and it was built on Windows and shipped to the Mac.
(ok, the tradeoff is, it’s really Julia – 1.13.0 – and so it precompiles on the first run. This one was also setup not to bring along the dependencies in the bundle, so those downloaded on the first run… about 2 minutes or so. Every run after that… 5 seconds to the launch screen). But, hopefully a good alternative in the future for shipping games and other apps with GUIs. Thanks for sharing the details of your experience in building this; really helpful to understand the range of options for developers who want to ship Julia-based apps.

Thanks @Kyjor, I would definitely be up to compare notes! I’d be particularly interested in how you went integrating with the Steam API, and Steam distribution. I’m not sure if I’m going to try for a steam release yet, but its good to know that all that integration is possible!

On the build times yeah I don’t have anything quantitative but when I switched from PackageCompiler to JuliaC I do think it reduced them. JuliaC also uses a lot more RAM though. I had to close a few chrome tabs the first time I ran it, even on a 32GB machine. That’s why I mostly use actions runners now. Might be worth giving JuliaC without --trim a go to see how it compares for you!

Wow @MDSW cool to see Rocket Plumber running on Arm, thanks for having a play around! I don’t have access to any M-series Macs so that’s not something I have been able to try. Especially interesting as it was crossed over from Windows!

Those small binaries are definitely appealing for distribution. Could you explain a little more about how Jule works? Is it a separate program you have installed that you use to run the binaries, or is it more like a build system similar to JuliaC/PackageCompiler?

Yes, it is a base runtime that hosts an embedded Julia and provides the basic OS hooks and a UI API; the base itself is around 6MB. A Julia install (somewhat reduced by removing unnecessary test files and stripping debug symbols) is compressed and bundled on top. Then your project source (also compressed), and optionally dependencies and artifacts. In theory, you can build a fully standalone executable that you could run offline (though cross-building doesn’t fully work at this extreme in all cases yet – as long as you stick to source for your code and dependencies, it’s fine). Normally, you’ll just include your project and rehydrate all of the dependencies and platform artifacts on the target machine on the first run; this is handled for the user; they just double click (and wait… TTFX still applies).

RocketPlumber allowed me to do an experiment comparing JuliaC and Jule; this is the Windows build – see note below on the size vs the one quoted for Mac (different build options):

JuliaC (v0.1.0, from main) Jule 0.7.3
Build time 11.4 min* 5.1 min clean; 28–51 s using caches
What you ship folder of 4,998 files, 1.4 GB (353 MB zipped) one 218 MB executable (zipping saves 1%)
On disk once installed 1.4 GB 751 MB app + 697 MB shared runtime
First launch to start menu 5.5 s (cold disk cache) 158 s
Later launches 0.8–0.9 s 5.6–6.1 s
Memory in play (working set / private) 577 MB / 2.2 GB 744 MB / 2.3 GB
Frames per second 69 75
Render work per frame (sprites + UI) 2.05 + 0.75 ms 1.41 + 0.69ms
Render-loop allocations per frame (sprites) ~80 KiB ~9 KiB
Simulation step 1.54 ms 1.39 / 2.32 ms

*Built with 3 image-compilation threads instead of the script’s 6, because the machine had only 16 GB.

The 218MB executable was with dependencies (unlike the Mac build, though it is also the case that the Windows and Linux Jule+mounted Julia are larger than the Mac because the debug stripping has less impact). Here’s what makes up the 218MB:

  • 111 MB: Jule and Julia, the same as jule.exe itself.
  • 48 MB: the packages’ source code and the package registry.
  • 59 MB: their artifacts, mostly FFMPEG and its codecs, GTK for the file dialogs, and the image libraries

So the resulting builds with various options are something like the following:

Build options size
Jule + Julia runtime only (--deps=no) 111 MB
+ the packages (--artifacts=no) 159 MB
+ their artifacts (as measured, the default with deps) 218 MB

A few points:

  • This was packaged by Jule, more or less as-is (it required about 30 lines of your source to be changed to use Jule’s GLFW and Imgui adapters, and a few other bits), so it wasn’t written for Jule. As an example, note the inclusion of the GTK file dialogs: Jule provides its own API for this, which get you the native dialogs on each platform. So a little extra weight there. In theory, it could run without any modification; it just wouldn’t use Jule’s windows at all. I was interested in getting it to run somewhat in Jule, so I didn’t try that, but our goal is no-compromises Julia.
  • Bundling is half of what Jule does; you can also just point the runtime at a project directory and run from disk in more of a developer mode. This is more of the design intent – ship malleable tools that users and agents can continue to evolve in a live Julia session.
  • Which also highlights that Jule doesn’t encrypt your source, so if you are trying to obscure that through compilation, it’s not the solution. I looked at encryption, and there is some latent capability to do it, but at some point, your source gets exposed for the build process.
  • That said, you can mount a custom archive 0 Julia build, which in theory could be the output of a JuliaC build, so a hybrid should be possible, but then you would need JuliaC to support cross-compilation for a cross-build to work… I haven’t experimented with this yet.

So cool, congratulations! I look forward to try it out soon :slight_smile:

Very cool to see Julia being used for new, “non-conventional” applications.

I’m happy you found the package useful!

Glad RocketPlumber was useful for your experiment! I’ll definitely keep an eye on Jule as you continue its development. Always good to have more options to package Julia apps!

Thanks for making it, and continuing to update it! I think useful tools like this being so easy to integrate and work with is a big part of why I like working in Julia!

It looks great! And sounds intriguing, any game with Julia.

It’s a trade-off, but does it need to be? Your workaround is pre-allocating, great. Did you know of Bumper.jl? It’s also opt-in but I understand simpler. And brand-new (like the Goose language it’s based on and discussed under here offtopic, a very intriguing language; for games) and might also help you: GitHub - yolhan83/GooseArrays.jl · GitHub See also AllocCheck.jl.

It’s a bit unclear what Julie is and how it achieves this:

1.39 ms is better, but can you explain 2.32 ms? And why all that is better is better? Or conversely?

I haven’t seen it announced, is some Jule.jl forthcoming? I looked and only found here, maybe a false-alarm (in screen-shot filename):

So is this a competitor to JuliaC (and/or PackageCompiler), or some GUI/game tool?

Still does --trim work for you?

Those figures are a range, though really just two data points. JuliaC was only measured once; both are probably (and not surprisingly, thankfully) statistically the same.
Jule is not released yet; there will be no separate Jule.jl – it’s a runtime host, not a package. Yes, that is Jule in the other thread. It’s not a JuliaC competitor; it’s a bundler+GUI host, not a compiler.

Thanks for taking a look @Palli!

I wasn’t aware of either of these, I will keep an eye out for opportunities to give them a go!

I was pretty sure Rocket Plumber isn’t trim compatible but it had been a while since I last tried it so I gave it another go. Currently >1500 errors, about half of which are from my own code and half of which are upstream in my dependencies. I wont rule out ever getting trim working, but its not something I think is likely in the near future.

v0.1.1: New properties inspector and UI scaling release

A Little update on Rocket Plumber. This week I published a new release focused on making Rocket Plumber easier to read and use! Check out the release announcement blog post here RPB#4

The properties inspection panel now shows curated information about each component, instead of a raw dump of the component and circuit maths internals. This should make engaging with the simulation a lot easier, even I am feeling more inclined to optimise things with the information much more obvious!

properties-clips

I also added a UI scaling option, as the default CImGui.jl font was a little hard to read on some higher DPI monitors. Keeping the UI crisp at different scales required some interesting solutions, but I am very happy with the results. If you were struggling to read the text in the last release I would encourage you to give it another try!

ui-scale

There are a few other small changes like a setting for Vsync and autosaves. Check out the full changelog here!

I also have a mailing list if you want release announcements right away!