When the Playdate was announced, I was immediately charmed by the cute yellow handheld, intrigued by the unique black and white display, and excited about the crank opening up all sorts of creative possibilities.
But there was something technically exciting about Playdate as well. Modern platforms are easier than every to develop for, but they all have the same boring CPU/GPU architecture. Not since the days of Nintendo’s Gameboy and DS has a console asked us to design our games and structure our code around the specific quirks and constraints of their hardware. The idea of writing code for the Playdate was just as exciting to me as the creative aspects.
Vertex Pop created Hyper Meteor for Playdate, as part of its first season of games. Hyper Meteor is an arcade shoot-em-up where you attack waves of meteors and hostile forces by ramming your ship into them. Technically, we wanted to have our cake and eat it too: a silky smooth framerate throughout was critical, but so was throwing lots of enemies on screen at once.
This article serves as both a guide to developing for Playdate, as well as a technical postmortem detailing how we achieved our goals for Hyper Meteor.
Here are the notable hardware specs for the Playdate:
- 180 MHz ARM CPU (with FPU)
- 16 MB RAM
- 400x240 1-bit display
When we started development, I didn’t have a good sense of how powerful the device was. On paper 180 MHz sounded pretty slow, and when was the last time you measured RAM in megabytes? It was hard to understand what the numbers meant tangibly, especially without existing games to reference, so we designed and scoped conservatively.
However, we quickly found that the Playdate was far more powerful than it seemed. With a bit of adjustment to our development process, we were able to achieve our creative goals with room to spare.
Picking a language
One of the first decisions you’ll need to make is whether to write your game’s code in Lua or C. The (excellent) SDK documentation strongly encourages using Lua, but I opted to use C.
Lua is an easy language to get into. It’s flexible and intuitive, and Panic provides a generous collection of game-specific helper classes, including functionality for sprites, collision response, geometry, pathfinding, UI, and much more. Lua is definitely the fastest way to get started on Playdate.
The downside is that it’s a slow language in general, especially since it’s garbage collected. As your game grows in complexity, performance will become an issue. Lua is still a great choice, just expect to spend some time on optimization if your game grows in scope. The documentation has some great tips!
I found Playdate’s C API to be elegant, powerful, and enjoyable to work with. But it’s more “framework” than engine. The API exposes functionality for input, graphics, audio, file I/O, and so on, but you’re on your own when it comes to gameplay code.
If you choose to use C, you’re really writing “to the metal” and with virtually no system overhead from the Playdate OS, the CPU is blazingly fast. Even though Hyper Meteor is an intense game, I never needed to think about optimization.
For some people “writing everything from scratch” is a dealbreaker, which is totally reasonable. But for others it’s part of the fun. I definitely fall into the latter category, happy to code every part of the game, all the way down to the vec2 struct. I also found writing C to be fun and kind of liberating. If you’re familiar with C++/C# you’ll adjust pretty quickly.
GPU? What GPU?
Rendering on Playdate is probably a little different than you’re used to. Playdate doesn’t have a dedicated GPU, or any hardware accelerated graphics at all. Everything is performed on the CPU.
Thankfully, the native drawing APIs are really fast. Hyper Meteor draws dozens of sprites, particles, background layers, UI, and so on, and the Playdate doesn’t break a sweat. However, bitmap transformations (like scale and rotation) are just not performant in real-time.
As luck would have it, among our top requirements for Hyper Meteor were... lots of rotated sprites. To achieve this, we opted to pre-render each sprite at every required angle and simply load them in as bitmaps on launch.
Knowing this was going to be a critical part of making the game work at all, I invested time upfront into building a robust asset pipeline. Here’s how it works:
- All the art is vector-based, created using Sketch, and exported as SVG files
- Then, a script written in Processing loads each SVG file, rotates and rasterizes them (with anti-aliasing disabled), and applies post-processing effects (such as fill patterns and outlines)
- Finally, these single-frame images are compiled into sprite sheets
We end up loading over 3000 bitmaps on launch. While that sounds pretty ridiculous, it’s actually the perfect approach for Playdate. The game loads in just a few seconds and, since bitmaps are 2-bits-per-pixel, 16 MB of RAM is more than plenty.
One caveat is that while Playdate can load images quickly, it’s pretty slow at querying files off the disk. To work around this, you can use BitmapTable s (Playdate’s term for a sprite sheet) to pack multiple bitmaps into a single file and reduce the number of file lookups.
Finally, for games with less demanding needs you can also render and cache the bitmaps at launch using playdate->graphics->transformedBitmap at the expense of some additional loading time.
Advanced Graphics Effects
Now that we’ve talked about bitmaps and pre-rendered graphics, it’s worth discussing Playdate’s more advanced graphics capabilities. While I would recommend doing the majority of your rendering using bitmaps, these techniques are what really make the Playdate aesthetic stand out.
The Geometry drawing functions have incredible raster quality, while also being quite performant. They include support for drawing lines, triangles, rectangles, ellipses and arcs, with parameters for line width, and even end cap styles.
The geometry functions also support Fill Patterns, set by calling playdate- >graphics->setColorToPattern . It sets the fill colour to an 8x8 bitmap pattern, creating dithering effects that look striking on the Playdate’s display.
Blend modes don’t really make sense on a 1-bit display, but Playdate can perform some fun source/destination pixel manipulations. Bitmaps support Draw Modes, ranging from simple masks ( kDrawModeFillWhite ), to inverted colours
( kDrawModeInverted ) to binary operators ( kDrawModeXOR and kDrawModeNXOR ). Geometry can be drawn with the colour set to kColorXOR , which inverts the destination pixel colour.
Finally, every graphics function includes a Stencil parameter, which you can use to create masking effects. Stencils are LCDBitmap s, just like the screen, so you can use all the same drawing functions to create your stencil. White pixels pass, while black and transparent pixels are discarded. Because everything is done on the CPU stencil usage is pretty flexible, you don’t need to worry about optimizing your render passes or anything like that.
I spent a lot of time experimenting with these techniques over the course of Hyper Meteor’s development. While I was tempted to go overboard, we ended up picking a few high impact moments (such as smartbomb activation and player death) to use these effects.
Creating Hyper Meteor was incredibly rewarding from both a technical and creative perspective. There’s just something about the Playdate that invites small scope games and experimental design. I hope this article was helpful, and inspires you to make your own Playdate games. I look forward to playing them!