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 ofggez
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 forggez
. They tend to be more opinionated and higher-level than whatggez
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
andncollide
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
Entity
s 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!