2026-07-25 12:01:16 +02:00
# PlaydateKit
2026-07-24 10:33:45 +02:00
2026-07-25 23:30:51 +02:00

2026-07-24 10:57:45 +02:00
2026-07-24 10:33:45 +02:00
Swift bindings to the [Playdate ](https://play.date ) C API.
2026-09-18 12:45:39 +02:00
Namespaces per subsystem, wrapper types that own their C objects, closures instead of function-pointer/userdata pairs, `OptionSet` s and `enum` s instead of raw constants, and typed `throws` . All ten C subsystems are covered:
2026-07-24 10:33:45 +02:00
| Namespace | Wraps | Highlights |
|---|---|---|
2026-07-24 11:10:25 +02:00
| `System` | `playdate->system` | input, time, menu items, logging |
| `Display` | `playdate->display` | refresh rate, scale, mosaic, flip |
| `Graphics` | `playdate->graphics` | drawing, `Bitmap` , `Font` , `TileMap` , video |
| `Sprite` | `playdate->sprite` | display list, collisions, custom draw |
| `Sound` | `playdate->sound` | players, synths, sequences, effects |
| `File` | `playdate->file` | `Handle` , directory operations |
| `JSON` | `playdate->json` | `Value` tree decode/encode |
| `Lua` | `playdate->lua` | C functions, classes, stack access |
| `Scoreboards` | `playdate->scoreboards` | online leaderboards |
| `Network` | `playdate->network` | wifi, `HTTPConnection` , `TCPConnection` |
2026-07-24 10:33:45 +02:00
## Requirements
2026-09-18 12:45:39 +02:00
- [Playdate SDK ](https://play.date/dev/ ) 3.1.1+ (not bundled).
- Swift 6.4 tools.
- Any macOS. `InlineArray` overloads are `@available(macOS 26, *)` in Simulator builds; unrestricted on device and Linux.
2026-07-24 10:33:45 +02:00
2026-09-18 12:45:39 +02:00
Device builds also need:
2026-07-25 10:57:17 +02:00
2026-09-18 12:45:39 +02:00
- A [swift.org toolchain ](https://www.swift.org/install/macos/ ) (6.4 release or a snapshot); Xcode's lacks the Embedded Swift stdlib. With [Swiftly ](https://www.swift.org/swiftly/ ): `swiftly install main-snapshot` .
- The [Arm GNU toolchain ](https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads ) (`arm-none-eabi-gcc` ) on your `PATH` .
2026-07-25 10:57:17 +02:00
2026-07-24 10:33:45 +02:00
### One-time SDK setup
2026-09-18 12:45:39 +02:00
`CPlaydate` finds `pd_api.h` through a `playdate` pkg-config module. Create it once per machine:
2026-07-24 10:33:45 +02:00
```sh
2026-07-25 13:05:57 +02:00
make setup
2026-07-24 10:33:45 +02:00
```
2026-09-18 12:45:39 +02:00
It reads `PLAYDATE_SDK_PATH` (default `~/Developer/PlaydateSDK` ) and writes `/usr/local/lib/pkgconfig/playdate.pc` ; pass another directory with `Scripts/install-pkgconfig.sh <directory>` . If Xcode had the package open, run File ▸ Packages ▸ Reset Package Caches.
2026-07-24 10:33:45 +02:00
## Adding the dependency
```swift
// Package.swift of your game
dependencies : [
2026-07-25 23:30:51 +02:00
. package (
url : "https://github.com/rock-n-code/playdate-kit.git" ,
from : "0.1.0"
),
2026-07-24 10:33:45 +02:00
],
targets : [
. target (
name : "MyGame" ,
2026-07-25 23:30:51 +02:00
dependencies : [
. product (
name : "PlaydateKit" ,
package : "playdate-kit"
)
]
2026-07-24 10:33:45 +02:00
),
]
```
## Getting started
2026-09-18 12:45:39 +02:00
A Playdate game has one C entry point, `eventHandler` . Export it with `@c` , call `Playdate.initialize(with:)` on the first event, and install an update callback:
2026-07-24 10:33:45 +02:00
```swift
import CPlaydate
2026-07-25 12:01:16 +02:00
import PlaydateKit
2026-07-24 10:33:45 +02:00
2026-09-18 11:53:09 +02:00
@ c ( eventHandler )
2026-07-24 11:29:13 +02:00
func eventHandler (
pointer : UnsafeMutableRawPointer ,
event : PDSystemEvent ,
argument : UInt32
) -> Int32 {
2026-07-24 11:10:25 +02:00
switch SystemEvent ( event : event , argument : argument ) {
2026-07-24 10:33:45 +02:00
case . initialize :
Playdate . initialize ( with : pointer ) // must happen before anything else
Game . shared . start ()
case . pause :
Game . shared . pause ()
default :
break
}
2026-07-24 11:29:13 +02:00
2026-07-24 10:33:45 +02:00
return 0
}
final class Game {
nonisolated ( unsafe ) static let shared = Game ()
2026-07-24 11:10:25 +02:00
var player = Sprite ()
2026-07-24 10:33:45 +02:00
func start () {
2026-07-26 00:20:19 +02:00
Display . refreshRate = 50
2026-07-24 10:33:45 +02:00
2026-07-24 11:10:25 +02:00
System . setUpdateCallback {
2026-07-24 10:33:45 +02:00
self . update ()
return true // true = redraw the display this frame
}
}
2026-09-18 12:45:39 +02:00
func pause () {
System . log ( "paused" )
}
2026-07-24 10:33:45 +02:00
func update () {
2026-07-24 11:10:25 +02:00
let ( _ , pushed , _ ) = System . buttonState
2026-07-24 10:33:45 +02:00
if pushed . contains (. a ) {
2026-07-24 11:10:25 +02:00
System . log ( "A pressed at \( System . currentTimeMilliseconds ) ms" )
2026-07-24 10:33:45 +02:00
}
2026-07-24 11:10:25 +02:00
Sprite . updateAndDrawAll ()
System . drawFPS ()
2026-07-24 10:33:45 +02:00
}
}
```
2026-09-18 12:45:39 +02:00
Calling any wrapper before `Playdate.initialize(with:)` crashes.
2026-07-24 10:33:45 +02:00
## Tour of the API
### System: input, time, menu
```swift
2026-09-18 12:45:39 +02:00
// Buttons: held now, pushed this frame, released this frame.
2026-07-24 11:10:25 +02:00
let ( current , pushed , released ) = System . buttonState
2026-07-24 10:33:45 +02:00
if current . contains ([. b , . down ]) { /* charge shot */ }
// Crank.
2026-07-24 11:10:25 +02:00
if ! System . isCrankDocked {
aim ( degrees : System . crankAngle )
spin ( by : System . crankChange )
2026-07-24 10:33:45 +02:00
}
2026-09-18 12:45:39 +02:00
// Enable the accelerometer before reading it.
2026-07-24 11:10:25 +02:00
System . setPeripheralsEnabled (. accelerometer )
let ( x , y , z ) = System . accelerometer
2026-07-24 10:33:45 +02:00
2026-09-18 12:45:39 +02:00
// Menu items stay alive until removed.
2026-07-24 11:10:25 +02:00
System . addCheckmarkMenuItem ( title : "music" , isChecked : true ) { item in
2026-07-24 10:33:45 +02:00
Audio . musicEnabled = item . isChecked
}
2026-07-24 11:10:25 +02:00
System . addOptionsMenuItem ( title : "mode" , options : [ "easy" , "hard" ]) { item in
2026-07-24 10:33:45 +02:00
Game . shared . difficulty = item . value
}
2026-09-18 12:45:39 +02:00
// Logs go to the Simulator console or the device's serial port.
2026-07-24 11:10:25 +02:00
System . log ( "spawned \( count ) enemies" )
2026-09-18 12:45:39 +02:00
System . error ( "unrecoverable" ) // stops the game
2026-07-24 10:33:45 +02:00
```
### Graphics: drawing, bitmaps, fonts
2026-09-18 12:45:39 +02:00
Loads (`Bitmap(path:)` , `Font(path:)` , …) throw `PlaydateError` with the OS's message:
2026-07-24 10:33:45 +02:00
```swift
2026-07-24 11:10:25 +02:00
let font = try Graphics . Font ( path : "fonts/Asheville-Sans-14-Bold.pft" )
Graphics . setFont ( font )
2026-07-24 10:33:45 +02:00
2026-07-24 11:10:25 +02:00
Graphics . clear ( color : . white )
Graphics . fillRect ( x : 0 , y : 0 , width : 400 , height : 32 , color : . black )
Graphics . drawText ( "Hëllo, Playdate" , x : 8 , y : 8 )
2026-07-24 10:33:45 +02:00
2026-09-18 12:45:39 +02:00
// Colors are solid or 8× 8 patterns. On macOS 26+, the device, and Linux,
// `rows:` also takes an array literal.
2026-07-24 11:10:25 +02:00
let checker = Graphics . Pattern ( rows : ( 0xAA , 0x55 , 0xAA , 0x55 ,
0xAA , 0x55 , 0xAA , 0x55 ))
Graphics . fillEllipse ( x : 100 , y : 100 , width : 64 , height : 64 ,
color : . pattern ( checker ))
2026-07-24 10:33:45 +02:00
2026-09-18 12:45:39 +02:00
// Draw into a bitmap by pushing it as the drawing context.
2026-07-24 11:10:25 +02:00
let logo = try Graphics . Bitmap ( path : "images/logo" )
2026-07-24 10:33:45 +02:00
logo . draw ( x : 168 , y : 88 )
2026-07-24 11:10:25 +02:00
let canvas = Graphics . Bitmap ( width : 64 , height : 64 )
Graphics . pushContext ( canvas )
Graphics . drawLine ( x1 : 0 , y1 : 0 , x2 : 63 , y2 : 63 , width : 2 , color : . black )
Graphics . popContext ()
2026-07-24 10:33:45 +02:00
```
### Sprites and collisions
```swift
2026-07-24 11:10:25 +02:00
let ball = Sprite ()
ball . setImage ( try Graphics . Bitmap ( path : "images/ball" ))
2026-07-24 10:33:45 +02:00
ball . moveTo ( x : 200 , y : 120 )
2026-07-24 11:10:25 +02:00
ball . collideRect = Rect ( x : 0 , y : 0 , width : 16 , height : 16 )
2026-07-24 10:33:45 +02:00
ball . setCollisionResponseFunction { _ , _ in . bounce }
2026-09-18 12:45:39 +02:00
ball . add () // the display list keeps the sprite alive until it is removed
2026-07-24 10:33:45 +02:00
// In the update callback:
let ( actual , collisions ) = ball . moveWithCollisions ( goalX : goalX , goalY : goalY )
for collision in collisions where collision . other . tag == Tags . brick {
collision . other . remove ()
}
2026-07-25 10:57:17 +02:00
2026-09-18 12:45:39 +02:00
// Collision and query APIs also have a visitor form that allocates no array:
2026-07-25 10:57:17 +02:00
ball . moveWithCollisions ( goalX : goalX , goalY : goalY ) { collision in
if collision . other . tag == Tags . brick { collision . other . remove () }
}
2026-07-24 10:33:45 +02:00
```
2026-09-18 12:45:39 +02:00
The binding owns the C userdata slot; store your own per-sprite data in `Sprite.userdata` .
2026-07-24 10:33:45 +02:00
### Sound
```swift
2026-09-18 12:45:39 +02:00
// Stream from disk.
2026-07-24 11:10:25 +02:00
let music = try Sound . FilePlayer ( path : "audio/theme" )
2026-07-24 10:33:45 +02:00
music . play ( repeat : 0 ) // 0 = loop forever
2026-09-18 12:45:39 +02:00
// Play from memory.
2026-07-24 11:10:25 +02:00
let blip = try Sound . SamplePlayer ( path : "audio/blip" )
2026-07-24 10:33:45 +02:00
blip . play ()
// Synthesis.
2026-07-24 11:10:25 +02:00
let synth = Sound . Synth ( waveform : . square )
2026-07-24 10:33:45 +02:00
synth . setAttackTime ( 0.01 )
synth . setReleaseTime ( 0.2 )
2026-07-24 11:10:25 +02:00
synth . playMIDINote ( Sound . noteC4 , velocity : 0.8 , length : 0.5 )
2026-07-24 10:33:45 +02:00
// Channels mix sources and effects.
2026-07-24 11:10:25 +02:00
let channel = Sound . Channel ()
2026-07-24 10:33:45 +02:00
channel . add ()
channel . addSource ( synth )
2026-07-24 11:10:25 +02:00
let filter = Sound . TwoPoleFilter ( kind : . lowPass )
2026-07-24 10:33:45 +02:00
filter . setFrequency ( 800 )
channel . addEffect ( filter )
2026-09-18 12:45:39 +02:00
// Modulator properties accept any SignalValue (LFO, Envelope, …).
2026-07-24 11:10:25 +02:00
let wobble = Sound . LFO ( shape : . sine )
2026-07-24 10:33:45 +02:00
wobble . setRate ( 2 )
synth . frequencyModulator = wobble
```
### Files and JSON
```swift
2026-09-18 12:45:39 +02:00
// The open mode decides whether paths resolve in the Data directory or the pdx.
2026-07-24 11:10:25 +02:00
let save = try File . Handle ( path : "save.json" , mode : . write )
try save . write ( JSON . encode (. table ([
2026-07-24 10:33:45 +02:00
"level" : . int ( 3 ),
"name" : . string ( "Röck" ),
])))
try save . close ()
2026-07-26 00:25:43 +02:00
let loaded = try JSON . decodeFile ( path : "save.json" )
2026-07-24 10:33:45 +02:00
if case . table ( let entries ) = loaded , case . int ( let level )? = entries [ "level" ] {
Game . shared . level = level
}
2026-07-24 11:10:25 +02:00
try File . listFiles ( at : "replays" ) { name in
System . log ( "found \( name ) " )
2026-07-24 10:33:45 +02:00
}
```
### Network
2026-09-18 12:45:39 +02:00
Each server needs the user's permission:
2026-07-24 10:33:45 +02:00
```swift
2026-07-24 11:10:25 +02:00
let reply = Network . HTTPConnection . requestAccess (
2026-07-24 10:33:45 +02:00
server : "example.com" , purpose : "Fetching daily puzzles" ) { allowed in
guard allowed else { return }
Puzzles . fetch ()
}
func fetch () {
2026-07-24 11:10:25 +02:00
guard let connection = Network . HTTPConnection ( server : "example.com" ) else { return }
2026-07-24 10:33:45 +02:00
connection . setRequestCompleteCallback { connection in
let body = try ? connection . read ( length : connection . bytesAvailable )
2026-09-18 12:45:39 +02:00
// Keep `connection` referenced until this callback fires.
2026-07-24 10:33:45 +02:00
}
try ? connection . get ( path : "/daily.json" )
}
```
### Lua interop
2026-09-18 12:45:39 +02:00
Lua callbacks are C function pointers with no context, so they cannot capture:
2026-07-24 10:33:45 +02:00
```swift
2026-07-24 11:10:25 +02:00
let double : Lua . CFunction = { _ in
Lua . push ( Lua . intArgument ( at : 1 ) * 2 )
2026-07-24 10:33:45 +02:00
return 1 // number of return values pushed
}
2026-07-24 11:10:25 +02:00
try Lua . addFunction ( double , name : "mylib.double" )
2026-07-24 10:33:45 +02:00
```
## Conventions
2026-09-18 12:45:39 +02:00
- **Namespaces.** Subsystems are top-level; only the bootstrap lives in `Playdate` . Qualify on a clash: `PlaydateKit.System` .
- **Properties vs. methods.** Readable state is a property; `set…` methods are write-only or take extra arguments. Callbacks: `set…Callback` / `set…Function` .
- **Paths.** `path:` for files, `at:` for directories; `File.stat` /`mkdir` /`unlink` are unlabeled like C.
- **Errors.** Typed throws: `PlaydateError` , or `Network.NetError` for network I/O.
- **Buffers.** `Span` /`MutableSpan` , valid only during the call. Mono audio gets an empty `right` .
- **Ownership.** Wrappers free what they create on `deinit` and retain what their C object references. Objects vended by the OS (a table's `Bitmap` , a `Sequence` track) need their owner alive. `File.Handle` is non-copyable and closes at end of scope or on `close()` .
- **Callbacks.** C callbacks without a userdata slot (serial, headphones, scoreboards, `getServerTime` ) keep one closure at a time.
- **Threading.** Single-threaded except audio callbacks; don't call the API from other threads.
2026-07-24 10:33:45 +02:00
## Building for the simulator and device
2026-09-18 12:45:39 +02:00
Game logic builds and tests on the host with `swift build` / `swift test` . For a `.pdx` :
2026-07-24 10:33:45 +02:00
2026-09-18 12:45:39 +02:00
- **Simulator:** a host dylib inside the pdx; see `Examples/HelloPlaydate/build.sh` .
- **Device:** Embedded Swift for `armv7em-none-none-eabi` (`-fshort-enums` ), linked by the SDK's make rules.
2026-07-24 10:33:45 +02:00
2026-09-18 12:45:39 +02:00
[`Examples/swift.mk` ](Examples/swift.mk ) runs the device pipeline; a game adds a short Makefile like [`Examples/HelloPlaydate/Makefile` ](Examples/HelloPlaydate/Makefile ), and `make` builds a `.pdx` with both binaries. Swift compiles `-Osize` (~15% smaller than `-O` ); override with `make SWIFT_OPT=-O` . Adapted from Apple's [swift-playdate-examples ](https://github.com/apple/swift-playdate-examples ).
2026-07-24 10:33:45 +02:00
2026-09-18 12:45:39 +02:00
The library stays within Embedded Swift (no Foundation, reflection, or untyped throws). `make embedded` checks this by compiling the module for the device; it uses `~/Library/Developer/Toolchains/swift-latest.xctoolchain` unless `SWIFT_BIN=<path to swift>` is set.
2026-07-25 13:05:57 +02:00
2026-07-25 11:48:09 +02:00
## Make targets
2026-09-18 12:45:39 +02:00
`embedded` and `example*` need the device toolchains; the rest need only Xcode and `make setup` .
2026-07-25 11:48:09 +02:00
| Target | Effect |
|---|---|
2026-09-18 12:45:39 +02:00
| `make setup` | Point the `playdate` pkg-config module at the SDK (once per machine) |
2026-07-25 11:48:09 +02:00
| `make build` / `make test` | Build the bindings for the host / run the unit tests |
| `make outdated` / `make upgrade` | Show / apply updates to the SwiftPM dependencies |
| `make embedded` | Compile-only device check (Embedded Swift, `armv7em-none-none-eabi` ) |
2026-07-25 12:01:16 +02:00
| `make consumer-test` | Build and run a scratch package depending on playdate-kit |
2026-09-18 12:45:39 +02:00
| `make check` | `build` , `test` , `embedded` , and `consumer-test` |
2026-07-25 11:48:09 +02:00
| `make docs` / `make docs-preview` | Generate / preview the DocC documentation |
| `make example` / `make example-run` | Build the HelloPlaydate example / open it in the Playdate Simulator |
| `make clean` | Remove build products of the package and the example |
2026-07-24 11:29:13 +02:00
## Example
2026-09-18 12:45:39 +02:00
[`Examples/HelloPlaydate` ](Examples/HelloPlaydate ): a bouncing box, crank needle, buttons, and a menu item.
2026-07-24 11:29:13 +02:00
```sh
cd Examples/HelloPlaydate
2026-07-25 10:57:17 +02:00
2026-09-18 12:45:39 +02:00
# Simulator only (SwiftPM, no extra toolchains):
2026-07-24 11:29:13 +02:00
./build.sh
open -a " $HOME /Developer/PlaydateSDK/bin/Playdate Simulator.app" HelloPlaydate.pdx
2026-07-25 10:57:17 +02:00
2026-09-18 12:45:39 +02:00
# Device and Simulator (swift.org toolchain and arm-none-eabi-gcc required):
2026-07-25 10:57:17 +02:00
make
2026-07-24 11:29:13 +02:00
```
2026-09-18 12:45:39 +02:00
Install on a device with Device ▸ Upload Game to Device in the Simulator, or `pdutil` .
2026-07-25 10:57:17 +02:00
2026-07-24 11:29:13 +02:00
## Documentation
2026-09-18 12:45:39 +02:00
DocC: `make docs` , `make docs-preview` , or Xcode's Product ▸ Build Documentation.
2026-07-24 10:33:45 +02:00
## Layout
```
2026-07-24 11:29:13 +02:00
Examples/
2026-09-18 12:45:39 +02:00
swift.mk Device build rules
HelloPlaydate/ Example game
2026-07-24 10:33:45 +02:00
Scripts/
2026-09-18 12:45:39 +02:00
install-pkgconfig.sh Writes the playdate pkg-config module
build-embedded.sh Device compile check
consumer-test.sh Builds a package that depends on playdate-kit
2026-07-24 10:33:45 +02:00
Sources/
2026-09-18 12:45:39 +02:00
CPlaydate/ pd_api.h module and log/error shims
PlaydateKit/ Bindings: a folder per subsystem, a type per file, plus the DocC catalog
2026-07-24 10:33:45 +02:00
Tests/
2026-09-18 12:45:39 +02:00
PlaydateKit/ Tests against a mock PlaydateAPI
2026-07-24 10:33:45 +02:00
```
## License
2026-09-18 12:45:39 +02:00
MIT ([LICENSE ](LICENSE )). The Playdate SDK is licensed separately by Panic, Inc.