| name | playdate-dev |
| description | Playdate game development in Lua with the Playdate SDK. Covers game loop, sprites, graphics, input (crank, buttons, accelerometer), audio, UI, performance, metadata (pdxinfo), and simulator/device workflow. Use when asked to make a Playdate game, implement Playdate-specific mechanics, or apply Playdate design and accessibility guidelines. |
Playdate Dev
Overview
Build Playdate games in Lua using the official Playdate SDK. The Playdate is a small yellow handheld with a 400×240 1-bit display, a physical crank, A/B buttons, and a D-pad. Understanding its constraints and unique input is essential.
Hardware specs:
- Display: 400×240 pixels, 1-bit (black/white only)
- Memory: ~16 MB RAM (aim for <8 MB peak usage)
- CPU: 180 MHz Cortex-M7 (device is slower than simulator — always profile on hardware)
- Input: A button, B button, D-pad (up/down/left/right), crank, menu button
- Audio: 44.1kHz stereo, Synth + SamplePlayer APIs
- Accelerometer: 3-axis, opt-in to save battery
Quick Start Workflow
- Clarify the request scope (gameplay goal, target device vs simulator, SDK version, release vs prototype).
- Choose inputs and accessibility (buttons, crank, accelerometer; provide non-crank alternatives; respect reduce-flashing setting).
- Choose rendering approach (sprites vs immediate draw, image sizes, refresh rate, 1x vs 2x scale).
- Implement the core loop (define
playdate.update(), update game state, call playdate.graphics.sprite.update() and playdate.timer.updateTimers() when used).
- Add metadata and launcher assets (
pdxinfo, buildNumber, launcher card and icon sizes).
- Test in the Simulator and on hardware (screen legibility, crank feel, audio balance, performance).
Starter Project
- Copy
assets/lua-starter into a new project folder.
- Keep
Source/main.lua and Source/pdxinfo in the source root.
- Replace placeholder values in
pdxinfo and extend the update loop.
Build with:
pdc Source GameName.pdx
Core Game Loop
import "CoreLibs/object"
import "CoreLibs/graphics"
import "CoreLibs/sprites"
import "CoreLibs/timer"
local gfx <const> = playdate.graphics
local playerX, playerY = 200, 120
function playdate.update()
if playdate.buttonIsPressed(playdate.kButtonLeft) then
playerX -= 2
elseif playdate.buttonIsPressed(playdate.kButtonRight) then
playerX += 2
end
local crankDelta = playdate.getCrankChange()
playerY += crankDelta * 0.1
gfx.sprite.update()
playdate.timer.updateTimers()
gfx.clear()
gfx.fillCircleAtPoint(playerX, playerY, 10)
end
Input API
Buttons
playdate.buttonIsPressed(playdate.kButtonA)
playdate.buttonIsPressed(playdate.kButtonB)
playdate.buttonIsPressed(playdate.kButtonUp)
playdate.buttonIsPressed(playdate.kButtonDown)
playdate.buttonIsPressed(playdate.kButtonLeft)
playdate.buttonIsPressed(playdate.kButtonRight)
local pressed, released = playdate.getButtonState()
if pressed & playdate.kButtonA ~= 0 then
end
function playdate.AButtonDown()
end
function playdate.AButtonUp()
end
Crank
local angle = playdate.getCrankPosition()
local delta = playdate.getCrankChange()
if playdate.isCrankDocked() then
end
playdate.setCrankSoundsDisabled(true)
Accelerometer
playdate.startAccelerometer()
local x, y, z = playdate.readAccelerometer()
playdate.stopAccelerometer()
Graphics
Drawing Modes
local gfx <const> = playdate.graphics
gfx.setColor(gfx.kColorBlack)
gfx.setImageDrawMode(gfx.kDrawModeFillBlack)
function playdate.update()
gfx.clear(gfx.kColorWhite)
gfx.drawRect(10, 10, 100, 50)
gfx.fillRect(20, 20, 80, 30)
gfx.drawLine(0, 0, 400, 240)
gfx.drawCircleAtPoint(200, 120, 40)
gfx.fillCircleAtPoint(200, 120, 40)
gfx.drawText("Hello Playdate!", 10, 10)
end
Images
local img = gfx.image.new("images/player")
img:draw(x, y)
img:drawCentered(x, y)
img:draw(x, y, gfx.kImageFlippedX)
local table = gfx.imagetable.new("images/walk")
local frame = table:getImage(frameIndex)
Fonts and Text
local font = gfx.font.new("fonts/Roobert-10-Bold")
gfx.setFont(font)
gfx.drawText("Score: " .. score, 10, 10)
gfx.drawTextInRect("Hello!", 0, 100, 400, 30, nil, nil, kTextAlignment.center)
Sprite System
local playerSprite = gfx.sprite.new()
playerSprite:setImage(gfx.image.new("images/player"))
playerSprite:moveTo(200, 120)
playerSprite:setZIndex(10)
playerSprite:add()
class('Player').extends(gfx.sprite)
function Player:init()
Player.super.init(self)
self:setImage(gfx.image.new("images/player"))
self:add()
end
function Player:update()
if playdate.buttonIsPressed(playdate.kButtonRight) then
self:moveBy(2, 0)
end
end
gfx.sprite.update()
Collision Detection (via Sprites)
playerSprite:setCollideRect(0, 0, playerSprite:getSize())
local actualX, actualY, cols, len = playerSprite:moveWithCollisions(newX, newY)
for i = 1, len do
local col = cols[i]
print("Hit:", col.other:getTag())
end
Audio
local sfx = playdate.sound.sampleplayer.new("sounds/jump")
sfx:play()
local music = playdate.sound.fileplayer.new("sounds/bgm")
music:play(0)
music:setVolume(0.7)
local synth = playdate.sound.synth.new(playdate.sound.kWaveformSquare)
synth:setFrequency(440)
synth:setVolume(0.5)
synth:playNote("A4", 0.5, 0.25)
Timers
import "CoreLibs/timer"
playdate.timer.performAfterDelay(2000, function()
print("Two seconds passed!")
end)
local t = playdate.timer.new(500, function()
end)
t.repeats = true
local vt = playdate.timer.new(1000, 0, 100)
playdate.timer.updateTimers()
pdxinfo Metadata
name=My Game
author=Your Name
description=A short game description
bundleID=com.yourname.mygame
version=1.0.0
buildNumber=1
imagePath=images/
launchSoundPath=sounds/launch
contentWarning=Contains flashing lights
Required launcher assets (put in images/ or configured imagePath):
launcher/card.png — 350×155 pixels
launcher/card~highlight.png — 350×155 pixels (highlighted state)
launcher/icon.png — 32×32 pixels
launcher/icon~highlight.png — 32×32 pixels
Performance Tips
- Target 30fps on device (50fps max, but 30fps is standard)
- Limit
gfx.clear() — use gfx.sprite.update() dirty-rect rendering instead
- Prefer sprite system for moving objects; avoids full-screen redraws
- Pool objects — avoid creating new tables/objects every frame
- Use
playdate.display.setRefreshRate(30) if 50fps isn't needed
- Profile on device — Simulator is 2-3x faster than hardware
playdate.display.setRefreshRate(30)
playdate.drawFPS(0, 0)
Accessibility
if playdate.getReduceFlashing() then
end
if playdate.isCrankDocked() then
showCrankHint = false
end
Crank UI Indicators
import "CoreLibs/ui"
local crankIndicator = playdate.ui.crankIndicator
function playdate.update()
if playdate.isCrankDocked() then
crankIndicator:draw()
end
end
Menu Integration
local menu = playdate.getSystemMenu()
local soundItem, err = menu:addCheckmarkMenuItem("Sound", true, function(value)
soundEnabled = value
end)
local diffItem, err = menu:addOptionsMenuItem("Difficulty", {"Easy","Normal","Hard"}, "Normal", function(value)
difficulty = value
end)
Save/Load Data
local data = {score = 1234, level = 5}
playdate.datastore.write(data)
local data = playdate.datastore.read()
if data then
score = data.score or 0
end
playdate.datastore.delete()
Common Patterns
Scene Management
local currentScene = nil
function switchScene(newScene)
if currentScene and currentScene.leave then
currentScene:leave()
end
gfx.sprite.removeAll()
currentScene = newScene
if currentScene.enter then
currentScene:enter()
end
end
local titleScene = {}
function titleScene:enter() ... end
function titleScene:update() ... end
function playdate.update()
if currentScene and currentScene.update then
currentScene:update()
end
gfx.sprite.update()
playdate.timer.updateTimers()
end
Crank-Driven Mechanic
local TICKS_PER_STEP = 12
local crankAccumulator = 0
function playdate.update()
local delta = playdate.getCrankChange()
crankAccumulator += delta
while crankAccumulator >= TICKS_PER_STEP do
crankAccumulator -= TICKS_PER_STEP
moveRight()
end
while crankAccumulator <= -TICKS_PER_STEP do
crankAccumulator += TICKS_PER_STEP
moveLeft()
end
if playdate.buttonJustPressed(playdate.kButtonLeft) then moveLeft() end
if playdate.buttonJustPressed(playdate.kButtonRight) then moveRight() end
end
Resources
references/designing-for-playdate.md — Screen, text, input, audio, UI, launcher guidance
references/inside-playdate-lua.md — Full Lua API names, file layout, workflow details
assets/lua-starter/ — Starter project template
- Official SDK Docs — Authoritative Lua API reference
- SDK Download — Free from Panic
- Playdate Developer Forum — Community Q&A and examples