LD42Postmortem

One of my favorite game jams is Ludum Dare: It’s just you, a theme, and 48 hours to make a game. Pure and challenging, and I’ve participated in several of them from time to time with a variety of tools. Usually my go-to is Unity3D, though arguably my best game was in Python+Pygame. This time though, for Ludum Dare 42 on August 2018, I finally had both the energy and ability to write my game in Rust, using ggez. So I decided to write about it!

Since ggez was originally intended to be used for things like game jams, this is pretty nice! The game jam theme was Running Out Of Space, and so I turned it around and made a game called Running In To Space. I didn’t manage to get it quite done, but it worked out pretty well anyway, and I’ll probably go back and expand the game more. The source code is, naturally, available on Github. I used a lot of other crates other than ggez though, and this is the first time I’ve really combined most of them in a practical game, so I thought I’d write about how it worked out and the good and bad things I ran into.

Tools used

So to start, here’s the full list of dependencies:

# Game stuff
ggez = "0.4"
ggez-goodies = { git = "https://github.com/ggez/ggez-goodies", rev = "aacd12867d886b0331478ab616dcb35e337643a8" }
specs = "0.12"
specs-derive = "0.2"
nalgebra = "0.16"
ncollide2d = "0.17"
warmy = "0.7"

# Utility stuff
log = "0.4"
fern = {version = "0.5", features = ["colored"] }
chrono = "0.4"
failure = "0.1"
rand = "0.5"

A brief overview:

  • ggez is a lightweight 2d game framework. Its goal is to handle basic graphics, windowing, input, resources and sound in a nice way that provides the basics without getting in your way. I’m the chief maintainer of ggez but to my shame I haven’t actually used it for a game since version 0.3.
  • ggez-goodies is a set of small add-on tools I’ve written for ggez. They tend to be more opinionated and higher-level than what ggez alone provides.
  • specs is probably the premier entity-component-system framework in Rust. An ECS is a tool for organizing game data in an efficient and flexible way.
  • nalgebra and ncollide are a vector math library and a collision detection framework, respectively.
  • warmy is a resource management system that does resource hotloading.

So, how did things work out?

ggez

ggez was great because I mostly didn’t notice it. I wanted to use the current devel branch (which will become version 0.5 when it’s finished), but in the weeks leading up to it I was too busy to fix a few math bugs necessary to get it into a truly usable state. So I sucked it up and used the current stable version, 0.4.3, and… it was flawless. My requirements weren’t exactly high, but it did everything I needed it to do: windowing, setup, input, and 2D drawing. Everything was nice and, well, ez. It’s really pleasant to have a framework that makes the easy stuff easy, especially in a game jam where you don’t spend much time worrying about annoying edge cases of design like “what if the user resizes the screen” and “what if the user’s graphics card doesn’t support multisampling.” (Full disclosure, ggez becomes a lot less flawless if you start to use some of the more advanced features.) I am naturally biased on this point since I wrote large chunks of ggez so naturally I know what it can do and how to make it do what I want, but still. It was really nice to use it and have it go well.

Also, writing games is way more fun than writing ggez, at this point. Sometimes ggez feels like 90% of it is subtle math bugs and hardware incompatibilities, but then when I actually use it I get to say “oh wait, it mostly Just Works.”

ggez is quite sparse and low-level though, and intentionally so. I have a crate called ggez-goodies that intends to build a few useful tools on top of it: more abstract input handling, a scene manager, a particle system, stuff like that. Unfortunately it gets maintained basically when it occurs to me that something needs updating, and really is not very polished (as you can see from me linking to a single particular commit in the dependency listing above). It would be great if I had the time and energy to keep it up to date, but as it is… well. The scene manager works pretty darn well, at least. The input system is kind of a mess that needs to be cleaned up, which is in the process of happening. The camera code and particles I didn’t even bother trying to use for this game. SOMEDAY, I will bring it up to the same standard as ggez. And maybe even come up with a sensible name.

I also started my game off from the ggez game template project I made, which is basically 500 lines of skeleton code that ties together ggez, ggez-goodies’s scene and input systems, specs and warmy. I really wish it was less than 500 lines, but frankly it was a huge time-saver and the basic structure remained entirely unaltered through the course of the game jam. Figuring out a structure that worked really well for the type of games I tend to like to make (usually 2D oldschool-y things) took a lot of work, but once it’s done, it’s pretty much done. More on how that works later.

nalgebra + ncollide2d

My game involves things touching each other and an ad-hoc physics simulation that has a lot of non-realistic rules, so I wanted collision detection without a full physics engine attached. I could have written my own fairly easily, but frankly writing basic collision detection, like doing integrals by hand, is one of those things where the less I have to do it in life the happier I am. ncollide2d had a large rewrite a few months back and seemed to fit the bill, so I decided to give it a spin. And since I was using ncollide2d, I wound up using nalgebra as a matter of course.

(ggez uses nalgebra internally anyway, and version 0.4.3 exports it as ggez::nalgebra, and uses nalgebra types for all its vector and point inputs. This is expedient but kind of a little awful; one of the big changes for 0.5.0 is using mint for all external API types instead so the user can use whatever math library they want.)

This was my biggest mistake. I’d never used ncollide2d before, and as anyone who has successfully (or unsuccessfully) done a game jam can tell you, when you have 48 hours to make a game you do not want to spend 5 of those hours figuring out how to make your tools work. But I did anyway. I prooooobably could have written my own collision detection more quickly, but… well, by the time you’re 3 hours in you’re kind of in “make or break” mode.

Fortunately, in the end I mostly made it. Even without learning it before hand, ncollide2d is pretty dope. It worked great in practice once I got it working, fast and smooth and reasonably easy to work with. I was bitten by no bugs, I was in dire need of no missing features. Having a dedicated collision detection library separate from the physics engine is awesome. sebcrozet’s docs are great as always, my only complaint might be that they tend to be very bottom-up so you don’t get to see what the whole system working together looks like until you’re 3/4 of the way through them.

One weird and kind of uniquely Rustic part of this was… well, ggez 0.4.3 uses nalgebra 0.14. My game uses ncollide2d 0.17, which requires nalgebra 0.16. So I had two different versions of nalgebra linked into my program, and my program had to use them both. And, miraculously… this wasn’t a big deal. It helped that ggez only uses a few of nalgebra’s types (points and vectors) and only in a few very specific places (drawing and mesh building functions), and ggez re-exports its own version of nalgebra as a submodule, ggez::nalgebra. This made it possible to separate the two versions very distinctly. I very quickly ended up with my game using nalgebra 0.16 functions and types everywhere, and then when it came time to draw things I converted them to ggez::nalgebra types, and life was grand. It’s an extra copy, but who cares?

Try doing that in C++. :-P (Okay, I confess, I don’t know C++ well enough; maybe it’s possible somehow.)

Finally, using nalgebra… well, nalgebra still inspires both love and hate in me, even after I’ve used it a fair bit. I think the biggest issue is it’s very particular about the semantic meanings of types, and how those meanings get expressed is so heavily based on Rust’s generics that it’s still quite difficult for me to figure out. So one gets easily tripped up by stuff like “hang on, my object’s rotation is just an angle stored as an f32 but ncollide2d needs an Isometry2, which expresses its rotation component as a unit complex number, not an f32… now where the heck is the function to construct a Unit<Complex> out of just an angle? There MUST be one SOMEWHERE…” Part of the difficulty is just ’cause there’s so much stuff in nalgebra that it’s hard to hold it all in one’s head at a time, and I’m not particularly well-versed in math so knowing where to look for things isn’t always obvious. (I’ve heard people who are well-versed in math say it’s all perfectly straightforward and why do you think it’s hard? So, it’s a matter of taste.) If you need conversions between types or functions to compose/decompose them, finding your way looking under the trait impl Similarity<Point2<N>> for UnitComplex<N> takes a bit of work. Maybe a cheat-sheet would help? (Actually maybe there is one now, I need to re-read the docs again; like ncollide2d, nalgebra comes with a very good guide. Unfortunately navigating the reference docs is much more troublesome; I didn’t have this problem much for ncollide2d alone.). Or maybe if I used nalgebra’s Mathy Types (Rotation, Translation, etc. instead of f32, Vector2 and such) everywhere more consistently life would be easier ’cause everything would just line up. I dunno.

specs

specs is an Entity Component System. What that really means is that it’s basically an in-memory database optimized for iteration over sparse rows. Blah blah buzzwords.

…Okay, look. Imagine have something like this:

struct Position {
    pos: Vector2,
    angle: f32,
}

struct Motion {
    velocity: Vector2,
    angular_velocity: f32,
    ...
}

struct World {
   positions: Vec<Option<Position>>,
   motions: Vec<Option<Motion>>,
}

fn motion_system(world: &mut World) {
    for components in world.positions.zip(world.motions) {
        if let (Some(position), Some(motion)) = components {
            // do math here
        }
    }
}

This is the simplest possible ECS. Position and Motion are components. motion_system() is, as the name says, a system. And an entity is just an array index that refers to a set of components. I imagine it like a database table, where each row is an entity and each column is a component. An entity may have zero or one of each type of component, and a system does something to every entity that has a particular set of components. This is a slightly counter-intuitive way to write a program, but ends up being a very nice way to organize games because games tend to have lots of rules that operate on loosely-coupled and loosely-defined objects with associated data. Building things like this lets you iterate through entities quickly in a cache-friendly manner, and also lets you add, remove and refactor components and systems very easily. There, lesson over.

specs gives you exactly the kind of structure written above, just with lots of clever engineering to be super fast. And while back in version 0.8 or whatever when I first used it it was poorly documented and somewhat fiddly, these days specs is much nicer and easier to get going with. If you want to use an ECS (and you do), I highly recommend it. My game has a grand total of 8 component types and less than 20 entities in existence at any given time and I still don’t consider using specs overkill. Like ggez, it solves a lot of the important-but-boring design decisions in your game design.

That said, I still wish specs were a yet little more polished. During the course of the game I upgraded from specs 0.10 to 0.12 for reasons I don’t even remember anymore, because what I mainly remember is that half the names of types and functions changed even though they all worked exactly the same way. Sigh, well, it’s getting there. Nothing really had to change in how the code actually worked, just the names of a few things.

Also, specs-derive: Don’t leave home without it. Why isn’t it just part of specs now? …oh, it is. I guess that’s one of the other areas that needs more polish, since none of the examples or docs show how to use it.

Now for the interesting part: Combining specs and ncollide. It took some work but worked really well once I got it right. Both specs and ncollide have their own type of World object that stores all their state and which you have to manage and pass around to the functions that need it. In both of them, instead of managing the various objects inside the World yourself as objects or references to objects, you get handles that refer to them indirectly. This is very convenient ’cause it basically evades the borrow checker, while still allowing each library the option of checking that handles are valid before trying to do anything with them. Your memory safety becomes checked at runtime instead of compile time, which is generally what you want in this case ’cause your memory objects are all connected to actual things in your game and you have to manage what things exist in your game already… but it’s still Safe because you’re not dealing directly with pointers and such. In function it feels similar to using Rc’s, the implementation is just a little different.

ANYway. Having two different World’s that need to interact with each other was kind of squirrelly, but I solved that by basically making specs in charge of everything. specs’s World has a “resource” feature, which basically lets you shove arbitrary structs into it and retrieve them inside a system, and so I shoved the ncollide2d World into a specs resource. (Upon investigation this wasn’t necessary; I could have stored the ncollide2d World in the same struct holding on to the specs World, but using the resource functionality makes it a little easier to access collision data inside a system.) The important part is, each handle to an ncollide2d CollisionObject was stored in a specs component I called Collider. An ncollide2d CollisionObject also can store a piece of arbitrary data in a field attached to it, so I then stored the specs Entity handle in the respective CollisionObject. Bingo, now specs can figure out what CollisionObject is associated with a particular Entity, and ncollide2d can tell what specs Entitys are part of a collision event. The loop has been closed and life is grand.

Collision detection and handling along with physics in general is implemented as a specs system, albeit a slightly funny-looking one, that moves objects and updates the collider positions for all the objects that have a Collider component. Then it asks the ncollide2d World for all the collisions that are occurring, adjusts the position and motion of objects to resolve them, and that’s that. It ended up looking kind of hairy and rather verbose, but once the basic structure was figured out, actually doing things was very straightforward.

Minor tools

warmy is a crate that does resource loading, caching and hot-reloading. This is one of those boring bits of gamedev that has basically no impact on your actual game design but where doing it badly makes things awful in the long run. warmy was mostly invisible under the ggez game template wrapper code, which is ideal.

Okay, there was one part that caused problems. I’d set up the game template to basically hardcode the path that warmy uses to look for resources, which is not ideal. The problem I was trying to solve is basically that you usually want a game to look for its resources (sound, images, etc.) in one of two places: Either in a directory relative to the .exe file, or in a system-specific directory like /var/games/mygame or C:\Users\my_username\AppData. The first one is much more common and probably more useful these days, so if you have the directory structure mygame/run.exe then your game resources live in mygame/resources/.... The main problem with this is that Rust builds executables in mygame/target/{release,debug,whatever}/run.exe and that’s a pain in the butt; either you have to move the exe to the root directory (and cargo run doesn’t work right) or you move your resources directory into the target directory and things are even less convenient.

This is one of the things that ggez attempts to handle for you, and it generally does okay (finally), but it needs to communicate what it’s doing to warmy so warmy knows where to look for files to watch for changes. This needs to be done differently in development builds (where your exe lives in mygame/target/debug/run.exe and needs to look for resources in ../../resources/ relative to the exe) and in release builds (where your exe lives in mygame/run.exe ’cause you’re zipping it up to send it to someone else, and needs to look for resources in ./resources relative to the exe). Making this happen took less time than writing about it, it’s just… like I said, it’s one of those things that has utterly no impact on your game but doing it badly makes your life as a programmer incrementally more awful. Smoothing this out is on the to-do list.

The failure crate also made life a little easier, as did logging with fern, though for a project this size neither was really necessary.

Game architecture

This is for those who might want to say “how do I write a game in Rust?” I guess. This is basically the structure of the ggez game template. It isn’t really complicated, but took a fair bit of experimentation to get right, so I present it here in the hopes it will be useful to someone. There’s more than one way to do it of course, but games are made of piles of different pieces that all need to interact and Rust’s ownership semantics are not very forgiving getting that sort of thing wrong. Unless you want to sprinkle Rc everywhere of course, which is sometimes the right solution, but I didn’t want to do it for this and it turns out to not be necessary anyway.

The skeleton has only has a few major parts:

struct MainState {
    scenestack: SceneStack<World>,
    // Whatever else you need
}

/// A stack of `Scene`'s that may share some common data.
/// Provided by `ggez-goodies`.
struct SceneStack {
    scenes: Vec<Box<dyn Scene>>,
    world: World,
}

/// Contains all data shared between `Scene`'s.
struct World {
    assets: warmy::Store<ggez::Context>,
    input_state: InputState,
    specs_world: specs::World,
}

/// A single "screen" of the game; the main menu and actual game screen are both
/// separate structs that implement this trait.
///
/// Provided by `ggez-goodies`.
trait Scene {
    fn update(&mut self, gameworld: &mut World, ctx: &mut ggez::Context) -> SceneSwitch;
    fn draw(&mut self, gameworld: &mut World, ctx: &mut ggez::Context) -> ggez::GameResult<()>;
    fn input(&mut self, gameworld: &mut World, ev: InputEvent, started: bool);
}

/// An operation to perform on the scene stack; how a scene signals a transition.
///
/// Provided by `ggez-goodies`.
enum SceneSwitch {
    None,
    Pop,
    Push(Box<dyn Scene>),
    Replace(Box<dyn Scene>),
}

The key part is the SceneStack, which is basically a simple state machine that lets you load and unload scenes and signal transitions between them. The game’s mainloop just calls the update(), draw() and input() methods of the Scene at the top of the stack. Turns out lots of game engines are structured like this, namely Amethyst, and the ones that aren’t structured like this you rapidly wish were (ahem, Unity). It provides a consistent interface to a lot of very nice things: if a Scene can say “draw the next scene down on the stack too” then a scene can be a pause screen, a modal menu or GUI, a fade-in/fade-out transition (with a little more finagling), all sorts of things. The way it’s set up currently, all components are stored in the same specs::World and each Scene runs its own set of systems (by defining its own specs::Dispatcher), but you can change things up if you want. You can even have a Scene contain its own SceneStack and compartmentalize things even more, though I haven’t found a reason to need to do that so far. And creating Scene trait objects gets you dynamic dispatch exactly where you want it, where you have multiple different types of objects that may have drastically different behavior but a common interface.

Conclusion

Well I certainly learned a lot, even if most of it was about ncollide2d! And it was a fun diversion; as I said, sometimes working on ggez feels like it’s 90% subtle bugs and flaws and I might be getting a bit burned out. But actually using ggez reminds me that 0.4.3 is actually really good for the basic use case, and 0.5 is going to be even better!

Also the Rust game dev ecosystem is so much better now than it was a year and a half ago that it’s not even funny. Used to be that specs was hard as hell to figure out, ncollide was experimental, ggez was just a rough wrapper around SDL2, and things like warmy and failure didn’t even exist. Now not only do they all work great, but they work great together without really a lot of effort or friction. Lots of awesome people deserve kudos for working together to solve different problems: there’s been many cases where project A implements something, then someone from project B comes along and says “wait, that design doesn’t work for my use case” and they figure out a better design together. I kind of feel like the Amethyst project deserves a lot of credit for this sort of thing, just ’cause they’re crazy-ambitious enough to actually try to combine all these sub-crates together into a coherent whole, which gets people talking to each other a lot. This is opposed to ggez’s philosophy of pretty much only handling low-level design decisions. I’d love to make a higher-level 2D game engine someday, or perhaps just work on Amethyst, but for now my mission is still to make ggez run on WebAssembly, and I should really get 0.5 out the door and get back to that.

So I hope this was interesting or maybe even useful to people! If you liked this, or want to encourage me to work on Rust gamedev tools more, drop me a buck on Patreon!