Files
playdate-kit/README.md
T
javier c5037dd716 Swift 6.4 migration and exhancements (#1)
This PR contains the work done to update the library and the attached example project to use the Swift 6.4 computer as a minimum supported version and also, to use the latest features introduced in it.

Reviewed-on: #1
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
2026-09-18 13:15:08 +00:00

374 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PlaydateKit
![Playdate logo](.README/Playdate_logo.svg)
Swift bindings to the [Playdate](https://play.date) C API.
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:
| Namespace | Wraps | Highlights |
|---|---|---|
| `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` |
## Requirements
- [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.
Device builds also need:
- 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`.
### One-time SDK setup
`CPlaydate` finds `pd_api.h` through a `playdate` pkg-config module. Create it once per machine:
```sh
make setup
```
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.
## Adding the dependency
```swift
// Package.swift of your game
dependencies: [
.package(
url: "https://github.com/rock-n-code/playdate-kit.git",
from: "0.1.0"
),
],
targets: [
.target(
name: "MyGame",
dependencies: [
.product(
name: "PlaydateKit",
package: "playdate-kit"
)
]
),
]
```
## Getting started
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:
```swift
import CPlaydate
import PlaydateKit
@c(eventHandler)
func eventHandler(
pointer: UnsafeMutableRawPointer,
event: PDSystemEvent,
argument: UInt32
) -> Int32 {
switch SystemEvent(event: event, argument: argument) {
case .initialize:
Playdate.initialize(with: pointer) // must happen before anything else
Game.shared.start()
case .pause:
Game.shared.pause()
default:
break
}
return 0
}
final class Game {
nonisolated(unsafe) static let shared = Game()
var player = Sprite()
func start() {
Display.refreshRate = 50
System.setUpdateCallback {
self.update()
return true // true = redraw the display this frame
}
}
func pause() {
System.log("paused")
}
func update() {
let (_, pushed, _) = System.buttonState
if pushed.contains(.a) {
System.log("A pressed at \(System.currentTimeMilliseconds)ms")
}
Sprite.updateAndDrawAll()
System.drawFPS()
}
}
```
Calling any wrapper before `Playdate.initialize(with:)` crashes.
## Tour of the API
### System: input, time, menu
```swift
// Buttons: held now, pushed this frame, released this frame.
let (current, pushed, released) = System.buttonState
if current.contains([.b, .down]) { /* charge shot */ }
// Crank.
if !System.isCrankDocked {
aim(degrees: System.crankAngle)
spin(by: System.crankChange)
}
// Enable the accelerometer before reading it.
System.setPeripheralsEnabled(.accelerometer)
let (x, y, z) = System.accelerometer
// Menu items stay alive until removed.
System.addCheckmarkMenuItem(title: "music", isChecked: true) { item in
Audio.musicEnabled = item.isChecked
}
System.addOptionsMenuItem(title: "mode", options: ["easy", "hard"]) { item in
Game.shared.difficulty = item.value
}
// Logs go to the Simulator console or the device's serial port.
System.log("spawned \(count) enemies")
System.error("unrecoverable") // stops the game
```
### Graphics: drawing, bitmaps, fonts
Loads (`Bitmap(path:)`, `Font(path:)`, …) throw `PlaydateError` with the OS's message:
```swift
let font = try Graphics.Font(path: "fonts/Asheville-Sans-14-Bold.pft")
Graphics.setFont(font)
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)
// Colors are solid or 8×8 patterns. On macOS 26+, the device, and Linux,
// `rows:` also takes an array literal.
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))
// Draw into a bitmap by pushing it as the drawing context.
let logo = try Graphics.Bitmap(path: "images/logo")
logo.draw(x: 168, y: 88)
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()
```
### Sprites and collisions
```swift
let ball = Sprite()
ball.setImage(try Graphics.Bitmap(path: "images/ball"))
ball.moveTo(x: 200, y: 120)
ball.collideRect = Rect(x: 0, y: 0, width: 16, height: 16)
ball.setCollisionResponseFunction { _, _ in .bounce }
ball.add() // the display list keeps the sprite alive until it is removed
// 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()
}
// Collision and query APIs also have a visitor form that allocates no array:
ball.moveWithCollisions(goalX: goalX, goalY: goalY) { collision in
if collision.other.tag == Tags.brick { collision.other.remove() }
}
```
The binding owns the C userdata slot; store your own per-sprite data in `Sprite.userdata`.
### Sound
```swift
// Stream from disk.
let music = try Sound.FilePlayer(path: "audio/theme")
music.play(repeat: 0) // 0 = loop forever
// Play from memory.
let blip = try Sound.SamplePlayer(path: "audio/blip")
blip.play()
// Synthesis.
let synth = Sound.Synth(waveform: .square)
synth.setAttackTime(0.01)
synth.setReleaseTime(0.2)
synth.playMIDINote(Sound.noteC4, velocity: 0.8, length: 0.5)
// Channels mix sources and effects.
let channel = Sound.Channel()
channel.add()
channel.addSource(synth)
let filter = Sound.TwoPoleFilter(kind: .lowPass)
filter.setFrequency(800)
channel.addEffect(filter)
// Modulator properties accept any SignalValue (LFO, Envelope, …).
let wobble = Sound.LFO(shape: .sine)
wobble.setRate(2)
synth.frequencyModulator = wobble
```
### Files and JSON
```swift
// The open mode decides whether paths resolve in the Data directory or the pdx.
let save = try File.Handle(path: "save.json", mode: .write)
try save.write(JSON.encode(.table([
"level": .int(3),
"name": .string("Röck"),
])))
try save.close()
let loaded = try JSON.decodeFile(path: "save.json")
if case .table(let entries) = loaded, case .int(let level)? = entries["level"] {
Game.shared.level = level
}
try File.listFiles(at: "replays") { name in
System.log("found \(name)")
}
```
### Network
Each server needs the user's permission:
```swift
let reply = Network.HTTPConnection.requestAccess(
server: "example.com", purpose: "Fetching daily puzzles") { allowed in
guard allowed else { return }
Puzzles.fetch()
}
func fetch() {
guard let connection = Network.HTTPConnection(server: "example.com") else { return }
connection.setRequestCompleteCallback { connection in
let body = try? connection.read(length: connection.bytesAvailable)
// Keep `connection` referenced until this callback fires.
}
try? connection.get(path: "/daily.json")
}
```
### Lua interop
Lua callbacks are C function pointers with no context, so they cannot capture:
```swift
let double: Lua.CFunction = { _ in
Lua.push(Lua.intArgument(at: 1) * 2)
return 1 // number of return values pushed
}
try Lua.addFunction(double, name: "mylib.double")
```
## Conventions
- **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.
## Building for the simulator and device
Game logic builds and tests on the host with `swift build` / `swift test`. For a `.pdx`:
- **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.
[`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).
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.
## Make targets
`embedded` and `example*` need the device toolchains; the rest need only Xcode and `make setup`.
| Target | Effect |
|---|---|
| `make setup` | Point the `playdate` pkg-config module at the SDK (once per machine) |
| `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`) |
| `make consumer-test` | Build and run a scratch package depending on playdate-kit |
| `make check` | `build`, `test`, `embedded`, and `consumer-test` |
| `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 |
## Example
[`Examples/HelloPlaydate`](Examples/HelloPlaydate): a bouncing box, crank needle, buttons, and a menu item.
```sh
cd Examples/HelloPlaydate
# Simulator only (SwiftPM, no extra toolchains):
./build.sh
open -a "$HOME/Developer/PlaydateSDK/bin/Playdate Simulator.app" HelloPlaydate.pdx
# Device and Simulator (swift.org toolchain and arm-none-eabi-gcc required):
make
```
Install on a device with Device ▸ Upload Game to Device in the Simulator, or `pdutil`.
## Documentation
DocC: `make docs`, `make docs-preview`, or Xcode's Product ▸ Build Documentation.
## Layout
```
Examples/
swift.mk Device build rules
HelloPlaydate/ Example game
Scripts/
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
Sources/
CPlaydate/ pd_api.h module and log/error shims
PlaydateKit/ Bindings: a folder per subsystem, a type per file, plus the DocC catalog
Tests/
PlaydateKit/ Tests against a mock PlaydateAPI
```
## License
MIT ([LICENSE](LICENSE)). The Playdate SDK is licensed separately by Panic, Inc.