Cargo Space - devlog #1
I've been working on Cargo Space, my space game prototype, for a couple of weeks now, and I wanted to share the progress I've made so far... and some yak's I've shaved along the way.
Making the idea more concrete
Before I started, I wanted to have some rough idea of what I was making. I like drawing stuff in order to make my ideas more concrete. So I made a very simple mockup of what a "ship" would be in my game.
The idea is that ship modules are boxes/containers on a grid with 8 sockets on them. Sockets can either be "welded" together to make a bigger ship, or they can be holes (to make doors), have thrusters on them, or have turrets, solar panels or other gadgets.
Additional modules and gadgets can be found around in space and attached to the ship.
When the player moves into the ship, gravity turns on and platformer movement is enabled.
The player can interact with various gadgets by pressing a button. For instance: when standing next to a thruster, they can turn it on, making the ship move.
Yak 1: Making a noise library for Bevy
So one of the first things I needed, was some environment for the player to be in and something to collide with. I thought I'd do some procedural asteroids by distorting a circle with some simple gradient noise. Only, I couldn't find any simple gradient noise that I was happy with...
I wanted something that:
- Has gradient (simplex or perlin) noise
- Is fast... -ish. I will generate a lot of stuff
- Works well with glam types, such as
- Ideally has higher level abstractions (fbm, ridged/hybrid multifractal, turbulence)
- Makes it possible to implement identical wgsl version for use in shader effects
- Doesn't require expensive initialization
That didn't exist... So I made a library for it,
noisy_bevy has the same noise implementation on the CPU and GPU, which means the values you get from the same coordinates are very similar on the GPU- and CPU-side. The asteroid you see in the example above is computed both on the CPU (to place the tiles for collision/mining) and on the GPU (to make the background and atmosphere/glow).
noisy_bevy turned out quite nice, and I spent quite some time on it, so I really hope I will find other uses for it as well, so that that time will be justified.
After having an asteroid system in place, I needed a way to tweak the noise parameters. Changing, recompiling, and restarting the game just to see how different values look is a really horrible workflow that I'm way too proud to accept, so I made sure my library worked with
bevy-inspector-egui. Which makes it quite easy to see which values work, and which don't.
I also just randomly spawned some tiny, completely white sprites to have some sense of stars going on.
Basic jet pack controls
After having just some kind of environment, the first thing I did was to add the character mockup as a sprite and make it possible to fly around using the arrow keys.
I based the configuration for the jet pack controls off of a similar one I've used for a platformer previously.
First, there is a max speed, which is the maximum number of units a character can move per second.
Then there are three different time values:
- How much time it takes to go from zero to max speed
- How much time it takes to stop if you actively press the other way
- How much time it takes to stop of you just let go of the button
Each of these three are converted into accelerations before they are used to change the player's velocity. It might seem a bit roundabout to work on time, however I find it way more intuitive to work with as it's easier to imagine the feel of the time it takes than it is to understand what acceleration values mean.
For instance, it's easy to see that if it takes 2 seconds to stop, it will feel quite floaty, which is intentional; we're in space! I still put the active stop time very low, though, because I don't want it to feel unresponsive either, I just want to give the feel of "drifting" without making it hard to control.
In order to apply the acceleration, I copied a simplified version of a physics engine from an earlier project of mine,
bevy_xpbd. I've also written an entire tutorial series/devlog about it if you're interested.
Adding tilemaps and collision
Once I had a player moving around in space, I needed some way to collide with the asteroids.
Up until this point I'd just spawned each asteroid tile as a separate sprite. Which is probably fine for small asteroids, however I wanted to be able to really quickly tell if there was a tile at a certain position.
For this, the existing community crate,
bevy_ecs_tilemap, was exactly what I needed. So I refactored my asteroid code to spawn tilemaps instead, one per asteroid.
Now that I could easily query positions for the existence of tiles, I "just" had to write a circle-to-tilemap (the player is a circle) collision checker which returned collision normals and depth, and I was good to go.
I'm saying "just", because this was a lot of work, since I didn't really do it in a smart way first time around. Fortunately, I wrote unit tests along the way, so refactoring it with a good approach was much easier than it could have been, and I'm now much more confident in my solution. I didn't turn this into an open-source crate yet, but I might in the future.
Making the player slightly more alive
Now that I had a player moving around colliding with stuff. The next thing to do something about was that the player felt really dull and dead...
First, I did some changes to the player sprite, from the mockup
When I asked my wife about it, though (who is a real artist), she preferred the old sprite, saying the new one looked like it was wearing pajamas... And I kind of agree. So I'll probably need to rework this again later.
The second thing I did at this point, was add in some particles to the jet pack, using
bevy_particle_systems, which as far as I know, is the only community particle system supporting 2D sprite-based graphics in wasm (web browsers).
Unfortunately, there doesn't seem to be a way to enable additive blending, though (a limitation in
bevy_sprite). Blend modes are currently being added for Bevy 3D, so hopefully that approach can be adopted for 2D as well, so there might be a hope for Bevy 0.10. In any case, I think it turned out pretty ok anyway. Adding particles definitely made it a little bit more satisfying to move around.
Then I just tweaked the colors of the asteroids slightly, and it was looking much better already.
Adding ship modules, level editor integration
Now that I had some outside environment and jet pack controls that felt decent enough, I wanted to start prototyping 2D platformer movement inside ship modules.
I cleaned up the mockup somewhat and split the walls into a separate layer. I also spent a great deal of time setting up a workflow I was happy with. I discovered clone layers in Krita, which are just fantastic when working on tilesets. I now have a workflow that I'm pretty happy with, so I might do another post just about that.
This is how my tileset looks at the moment:
Still pretty rough, but it lines up with the axes at least, and I've now established that modules are 16 tiles high, where a wall is exactly 1 tile wide.
Since I'd already made a tilemap collision system, it only made sense to re-use these systems for ship modules as well. I only needed to make some small adjustments to make it work for different scales (ship tiles are much smaller than asteroid tiles)
In order to quickly assemble and edit the ship module, I added
bevy_ecs_ldtk to the project, which can load LDtk levels as bevy_ecs_tilemaps.
This means I now have a really easy way to try out different ship module layouts, place platforms and gadgets etc. for the player to interact with.
Adding it was pretty straightforward, but I had some subtle bugs due to
GlobalTransforms for levels being incorrect the first frame, messing up tilemap collision and sending the player flying. I was able to patch
bevy_ecs_ldtk to fix this, but my attempt has some drawbacks... It'll be interesting to see whether this can be fixed upstream or whether I have to maintain my own patches on top of
bevy_ecs_ldtk or find some other workaround.
Eventually, I'll probably want to replace most of the ldtk levels with procedurally generated modules, anyway, but for now, before I have all those systems in place, it's perfect to test other aspects of the prototype.
Yak 2: Enabling editors for everything
Since I'd gotten so spoiled using
bevy-inspector-egui for editing my own stuff, I got disappointed when I couldn't use it for other community crates. So I went ahead and added support to
The fixes were rather trivial, just adding
#[derive(Reflect)] to some components and registering them with the type registry, but hunting down the components, making the PRs, following up etc. took some time. I feel like I made valuable contributions to these projects though.
Now, I can inspect and edit particle systems and tilemaps, which is quite handy when debugging:
Implementing platformer controls
Once I had collision for the ship modules in place, implementing basic platformer controls was quite easy. I just ported something from an old project of mine. The logic is extremely simple, if the player is inside the ship, gravity is on, if gravity is on platformer controls are enabled, otherwise, the jetpack is on.
The parameters are similar to that of the jetpack:
This makes it easy to tweak and find good values while playing.
Yak 3: Adding rotation to physics and
So after I implemented platformer controls, I got kind of tempted to also try out rotating the player. So I added preliminary support for angles, torque (rotation force) to the physics engine, and I was going to add buttons for applying roll to the left or right...
I like to think of player rotation as a kind of 1-axis input, so that you could only rotate in one direction at a time (receive a number from -1 to 1 from the input systems), so I assumed it would be easy to add it in the same way I'd added support for movement (2-axis input). However, it turns out there was no 1-axis equivalent for
VirtualDPad which I'd used for movement, so I wrote a PR to fix that to Leafwing Input Manager, which was promptly reviewed, then accepted (it's always a pleasure to contribute to alice-i-cecile's projects).
So once I had input working and toyed around with rotation for a little while, I decided that it was indeed too early to start thinking about it, it would be a lot of work and wasn't really a required feature. I will revisit if the rest of the prototype is a success. But right now it is too early.
So this is probably my most severe case of yak shaving this time.
- I ended up needlessly complicating my physics engine
- I patched a 3rd party crate to make a proper implementation when I could have just hacked around it using existing regular button input
- I knew up front that I didn't really need it for the prototype and
- I ended up not using any of it (so far at least)
I might have use for rotation later, but so far it's only made my code less maintainable.
I still think my contribution to Leafwing Input Manager will be useful for others, though.
Yak 4: Bloom
I saw so many cool screenshots of games using the new bloom effect in Bevy 0.9, that I decided to try it myself.
It turned out that to enable bloom, you had to enable
hdr (high definition color format) on the camera, and this caused a panic in
bevy_ecs_tilemap. Rob Parrett (rparrett) had a fix for this only hours after I reported the issue, which is quite impressive. Thanks!
Once that was done, it turned out enabling hdr also caused a panic in my own SDF shape drawing library,
bevy_smud, which I'm currently using to draw the atmosphere and background around asteroids. Looking at Rob's patch for
bevy_ecs_tilemap, though, it was quite easy to make a similar fix for
And while at it, I also added a bloom example to
So I spent way more time than I had thought on "just enabling bloom", but I think having bloom at hand will be nice to take the graphics up a notch.
Also, I have hopefully saved someone else the trouble of running into this bug both in
bevy_ecs_tilemap and in
Before adding bloom:
After adding bloom:
Obviously, this needs some tweaking to make sure only the right things are glowing (and not glowing too much). For instance, I feel like the helmet of the player should have had a glow more similar to that of the stars (which probably have too much).
Current status and plan going forward
This is where I'm at at the moment: The player can run around and jump inside the ship, run out and fly around using a jetpack and collide with stuff. It looks ok for a prototype, but doesn't feel very juicy. All animations are missing for the platforming part.
To recap the plan I made in the last post, The goals for the prototype are:
- Implement simple physics without rotation or contact friction
- Make the prototype look good
- Basic 2D platforming that feels good, even on a moving ship
- 2-player p2p networking
I've made a lot of progress on #1. Tilemap collision is working great except for some very tiny snags that I think should be relatively straightforward to fix.
I think the prototype looks a lot better than my prototypes normally do, so I feel I'm mostly on track here. But don't get me wrong, I know there is a lot of room for improvement here, and I am also missing artwork for most of the mechanics. To get up to speed I need:
- Idle standing pose
- Jumping pose
- Falling pose
- Run animation
- Control board/terminal/button for activating thrusters
Now that I have a basic framework for the player moving around, I really want to make sure platforming on moving ships works ok. So next up on the programming side is adding some way for players to move the ship by interacting with the thrusters. I'll do this in a really simple stupid way at first, then iterate later as needed.
Once the ship can move, to actually test how it feels to run around it while someone else controls it, I need to either:
- Implement local multiplayer
- Hack direct ship controls for the ship using special keyboard/gamepad keys
- Implement actual networked 2-player
Option 2 is definitely the least amount of work to verify that platforming on moving ships work... However, it might also be a good alternative to go straight for online 2-player. It's basically the next item on the list of features anyway. And it's probably a good idea to not wait too long before adding networking to the mix.
Adding networking means I'll have to make sure my simulation is completely deterministic, and that it doesn't use features that can't be rolled back. Unfortunately, changes in version 0.9 of Bevy has made the approach used by
bevy_ggrs to roll back resources to no longer work. This means I either need to make do without resources (using only entities), or co-operate with gschup to find some way to fix it properly.
EDIT: since writing this, vrixyz made some progress on Bevy GGRS
Resource rollback in Bevy 0.9. Check out the PR.
In any case, I expect this to take up quite some time without much outwardly visible progress.
For more updates, join the discord server or follow me on Mastodon