July 18, 2023
Hello there, I’m Josh, sometimes known by my handle superfunc; I’m the designer and programmer behind Gun Trails, a bullet-hell shmup for Playdate.
The game started back in October of 2022. I was living in Tokyo, had just quit my job, and was honestly having the programmer’s equivalent of an Eat-Pray-Love moment. Unsure if I’d continue programming professionally, spending most of my time playing Mushihimesama at the arcades in Akihabara, I was feeling a bit aimless.
Enter Playdate. I had wanted to build a shmup for years, but for various reasons—primarily bad scoping—the efforts always sputtered out. This little yellow device could provide the constraints needed, with the added bonus of a programming challenge to hit consistently high framerates. If anything was going to recapture the magic, this was it. From there, I texted my artist/brother Sam, asking if he’d work on the project. Soon after, I met our composer, Adam. The team was formed, and the work began in earnest.
Inspirations
Gun Trails gameplay takes inspiration from early CAVE games, particularly Mushihimesama and ESP Ra. De. This era was a bridge between earlier, less chaotic shmups and later, even more intense games. Given the screen size, this middle ground felt appropriate for a light bullet hell game.
In terms of scoring, it is inspired by the fantastic, underrated indie shmup, Blue Revolver, focusing on rewarding players in the short term, rather than on level-long or game-long goals for the player.
Technical Challenges
A couple months of tinkering went by; we shared the first footage of the game in action on twitter, and the reception was greater than I had anticipated. The music and artwork were amazing, but the largest response was to the games performance. So, how did we get to a solid 50 frames per second? (the fastest Playdate’s LCD will go for full screen redraws). Let’s examine a few techniques.
Context is King
One simple optimization Gun Trails does is with respect to data layout of entities. With contiguous data, when you remove an element, there is a cascade effect where everything to the right of the removed element must shift to retain its contiguous property.
As you can imagine, as enemies and bullets are constantly flying into and out of frame, there are a lot of removals from this data structure going on, so this operation being fast is important.
Thinking on this, I realized that the order of elements was unimportant for GT–they all store their own position. From this, we could do a “swap removal”: by swapping the to-be-removed element with the last, then removing it, it prevented the need for those unnecessary shifts.
How much faster is this approach? A lot. The takeaway here: consider the context of your problem; it almost always yields a faster approach.
Avoiding Unnecessary Work
Any time you involve the runtime, you give up performance control. In the case of Playdate, this is true whether you’re in C or Lua. So, how do we avoid these performance pits? By managing our own memory, of course. It’s not as bad as it might sound!
We’ll use the example of bullet data. If we create a large buffer representing bullet positions, instead of doing an object creation/allocation when we want to make one, we can simply increment a number (num_bullets
) and initialize some position values. For removal, we use our swap trick, and decrement the size (num_bullets
).
Much like our swap trick, this approach is much faster than creating new objects. The takeaway here is to model your data minimally, and in a way amenable to the CPU.
Bring What You Need
CPUs operate on lines of data; meaning, when you pull some object from memory, the adjacent data—up to 64 or 32 bytes—will come along for the ride. Given this, it is important to avoid object bloat, and to pack data in a way that maximizes the utility of the data brought into the CPU.
One approach for this is called Structure-of-Array, or SOA. Instead of having a set of objects with an x value, a y value, and a health value, we can have separate arrays of x values, y values and health values. While this may seem counterintuitive, consider this: most operations on our data do not require all of the different components. Given this, it behooves us to separate this data, so our operations only bring in the relevant components.
There are middle grounds for this; you can identify the highly used data and split that out, before structuring all data this way. Both a hybrid and full SOA approach will yield significant performance benefits.
Closing Thoughts
We’re finally here: release day. I’d like to thank you all for your patience, thank my friends for encouragement, my collaborators for their incredible work, and my partner Kelsey for keeping me sane throughout this process.
Now, go forth and chase those high scores.
Check out Gun Trails in Catalog