New year, new adventure!
I'm making this thing a little more official by spinning up a website so people can follow along, so let's talk a bit about what game we're aiming to build.
What are we building?
Broforce x Magicka x Noita - a local multiplayer couch-coop 2D pixel-art platformer-lite, with a sweet magic system that lets you abuse pixel physics to kill baddies and (accidentally) your friends.
(Concept art from your favourite AI; ChatGPT clearly doesn't understand what "2D" means!)
I'm going for fast-paced, chaotic and easy to pick up & play, with plenty of "woah that's sick" and "NOOoo why did you do that!". Target platform is PC & friends (no consoles). Online multiplayer would be amazing but I expect it will also make building take twice as long... so yeah, we'll see about that.
How we got there
- Local multiplayer: I've had a ton of fun playing local multiplayer games over the years, and I'd like to "give back" by sharing some of that joy. I've also played most (all?) popular games on Steam in this genre, so I have a good understanding of conventions and (hopefully) what makes these games fun.
- 2D: Couch coop games are normally (at least mostly) 2D in gameplay anyway, because that works well to keep all played characters in view - but most importantly it limits our art requirements: 3D modeling skills seem to build on drawing skills.. which I don't yet have.
- Pixel art: pixel art might be the most forgiving form of game art, and forgiveness is not optional with the amount of artistic sins I plan to commit.
- Platformer-lite: unlike top-down/isometric gameplay, side-on gameplay means gravity, and gravity leads to funny things. But I definitely don't want to punish folks who can't land jumps properly (or else my wife will hate me), so the platforming should be fairly forgiving.
- Sweet magic system: every game needs a reason to keep playing, and showing off new sick spells you've found feels like a compelling one to me.
- Killing badies: players need a reason to use cool spells. Initially I was thinking of some kind of stealth gameplay to use a wider range of spells, but multiplayer stealth can be super frustrating and doesn't lend itself to "pick up and play". I can bolt on the fun bits of some light stealth gameplay later if it fits.
- Killing your friends: friendly fire is hilarious, when applied in moderation (or in intentional excess).
- Pixel physics: we need a hook to get people interested, and if it worked for Noita, surely it can work for us! Lots of other reasons: we already have pixel art so why not, it provides a fertile ground for spell design, and it should be fun to code up.
So that's the initial plan! I haven't even prototyped most of this yet, so it'll be funny to look back in a year and see just how wrong I was.
What we've got so far
So, what's the most distinctive feature of "a coop 2D platformer with a sick magic system"?
A: The pixel physics!
Terrible jokes aside (have I mentioned I'm a dad? Apparently bad jokes are encouraged now), the pure platforming aspect should be relatively straightforward (if fiddly) to implement, and the magic system design is still noodling around in my head, so I started by hacking away on the most fun part: the falling sand simulation, or pixel physics for short.
I implemented something similar about a decade and a half ago as part of cloning Jetmen Revival; if you watch some gameplay you can see it has flowing water and destructible terrain, with each pixel simulated individually. My clone did a terrible job of implementing bouncing particles as I didn't understand trignometry properly at the time (maybe I still don't - we'll see!), but you could destroy terrain and have water flow through the gaps in it.
So I needed to re-implement that! I am writing this whole shebang in Rust (a topic for another time), and I didn't want to get too caught up in engine choices yet (also another topic), so I picked the most lightweight GPU-based rendering abstraction I could find that still let me easily set individual pixel colors: a library fittingly called Pixels.
With inner brutalist appeased, it was time to implement some "physics".
My first pass at bricks, sand and water technically worked, but looked.. not so great.
Both water and sand biased towards flowing to the right, and water had a not-so-wonderful tendency to pile up rather than disperse; these were both side effects from implementing pixel physics as a deterministic cellular automata, which is a fancy way of saying that the state of each pixel is determined by the state of the immediately surrounding pixels.
Fortunately, there is plenty of prior art around, such as Powder Toy and Sandspiel, so quite a few iterations later I got to something a bit more reasonable.
The implementation is complicated by performance concerns: a naive implementation of simulating even a screen-full of sand (which is much cheaper to simulate than water) will already drop speeds to below 60 frames per second - and any actual game will want a larger amount of pixels than that. So when pixels move, I keep a record of the old and new positions and next frame the pixels near those get updated; it sounds simple, but I tried about 3 other approaches to reduce false-liveness and they all cause weird behavior in pixels.
Still, it's not a particularly great approach! Sometimes large swathes of water get marked inactive, and then only slowly dissipates:
Noita's Petri Purho gave a fantastic talk on how they implemented falling sand at scale based on dividing pixels into chunks, keeping track of live regions within those chunks, and updating chunks in separate threads; I'll have a stab at that soon. Another way might be to move simulation to the GPU and thereby brute force it, but that will make it much harder for me to iterate on gameplay design so I think I will hold off on that for as long as possible.
Anyway, that's the bulk of what we've got so far! Still to do is pixel-pixel interactions, such as ensuring that sand displaces water, and adding support for upwards-moving elements such as gas. It may also be possible to implement water via an actual attempt at approximating fluid dynamics, but that doesn't seem so important for this initial prototyping phase.