Making Dis order - post-mortem and reflections on Bevy
The required limitation for this jam was "Chaos is everything". I decided to pick up and old idea I've had and make a game about avoiding all kinds of patterns and lining up, so kind of being obsessive about not being obsessive.
In more concrete terms, I thought I'd make patterns on walls and boxes, and you'd complete a level when there were no pieces with connecting patterns.
I was also really inspired by paths and flow field renders and wanted to add their chaotic aesthetic to the game somehow.
I didn't start completely from scratch this time, but managed to salvage some old code from another turn-based puzzle game. That meant I saved a lot of time by not implementing input, text rendering, ldtk integration etc., and I could focus on the gameplay and unique aspects of the game.
I finished the core game very loop quickly, within just a couple of hours, which meant I could spend the rest of the time just making the game cooler.
I made a "finished" jam game using Bevy. This is a first for me. In my previous attempts with Bevy, I've gotten lost in the details whenever I want something that don't exist or is hard to do with Bevy.
I made use of compute shaders in a jam game. I've wanted to get more familiar with compute shaders for a long time. And I'm happy that I now know how to modify the render graph to include compute passes.
I made use of a couple of previous projects, it's always satisfying when previous work pays off.
I spent way more time on compute shaders than I'd originally planned, and it was the kind of work where either I'd finish it, or all the work is wasted. So I powered through, but it meant I didn't have enough time to make more levels. Game jam games should be short, but I think this one is too short.
The particle effects
So the particles are made using compute shaders and a customized render graph.
Here is the setup:
- One buffer contains all the particle's positions and velocities.
- The particles' velocities are affected by an acceleration each frame.
- Acceleration is determined by a flow field texture
- A custom render-pass is run to create the flow field
- All sdf shapes are rendered with a custom fill that uses the shader derivative functions,
dpdxto approximate a gradient function for the shape.
- The shapes are then blended based on the distance
- This means particles will tend to gravitate towards the closest shapes.
- I couldn't get textures with negative values working, so I ended up using yellow (0.5, 0.5, 0.0) as zero, and then adding/subtracting it before storing to the texture.
- Each frame all the particles are rendered black into the a texture using a compute shader.
The particle texture is never cleared, but each frame the values are dimmed slightly, which produces the "trail" effect.
I think the result turned out pretty ok, though I'd liked to have a little bit more movement. Ideally, the particles would have respawned more frequently within the black shapes.
Shader derivative functions didn't work on WebGL
dpdx are functions that creates an approximate "derivatives" and partial derivatives of a value within a fragment shader.
For some reason Bevy or one of its dependencies seems to choke on
fwidth and friends when built for web. Since
fwidth is used in bevy_smud, which I use for shape rendering, I made the choice of scrapping the web build this time. This likely means that less people will play the game, but it also meant I didn't have to care about features not supported in browsers, and opened up the possibility of playing around with compute shaders.
Rendering to textures is not yet supported
...but there is a PR for it, so I ended up using that branch instead of bevy 0.6. Normally, I avoid unreleased versions, and certainly PRs, but with jam games you can take certain liberties.
Render layers are not implemented for sprites and text
After writing my new flow field render pass, I found out render layers are only supported by the 3d stack. So I had to make a quick and dirty implementation of render layers for both sprites, text2d and bevy_smud in order to make my setup work as I'd intended.
After the jam, I cleaned this up and submitted a PR to fix it properly in Bevy
Good things about Bevy
- The rendering API in bevy 0.6 is really powerful. Adding a compute pass was much easier than I thought.
- The examples are great, and even being able to look at and take parts of the Bevy engine code (since it's MIT/apache licensed) itself is awesome.
- Builds are really fast compared to Unity. In my setup, automated builds take only 2 minutes per platform, which is amazing compared to Unity which took at least 4 times as long.
- When there are engine bugs, it's very convenient if you want to fix them yourself (like I did with render layers). Cargo's
[patches]feature is awesome, and it's really nice that everything is one language.
- For comparison, we have an issue at work with Unity. Our CI builds are killed because of a hard-coded 30s timeout in the Unity editor. We know the exact line in the Unity source code where this should be fixed, but no-one at work knows how actually build Unity itself (is it even allowed?) and Unity doesn't prioritize our bug report. As a result, our CI hasn't been running on anything but Windows for months. /rant
- Builds are small, at least compared to Unity. The game ended up taking only 7mb, and I didn't even try to optimize for size this time.
bevy_ecs_ldtkmakes it really easy to live-reload level changes.