diff --git a/Examples/Pong/.swiftpm/build-and-run.sh b/Examples/Pong/.swiftpm/build-and-run.sh
new file mode 100755
index 00000000..942d29c7
--- /dev/null
+++ b/Examples/Pong/.swiftpm/build-and-run.sh
@@ -0,0 +1,6 @@
+#! /bin/sh
+set -e
+(killall 'Playdate Simulator' || true) 2>/dev/null
+cd ..
+swift package pdc
+~/Developer/PlaydateSDK/bin/Playdate\ Simulator.app/Contents/MacOS/Playdate\ Simulator .build/plugins/PDCPlugin/outputs/$PRODUCT_NAME.pdx
diff --git a/Examples/Pong/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/Examples/Pong/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..919434a6
--- /dev/null
+++ b/Examples/Pong/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/Examples/Pong/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Examples/Pong/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000..18d98100
--- /dev/null
+++ b/Examples/Pong/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/Examples/Pong/.swiftpm/xcode/xcshareddata/xcschemes/Pong.xcscheme b/Examples/Pong/.swiftpm/xcode/xcshareddata/xcschemes/Pong.xcscheme
new file mode 100644
index 00000000..30c25b13
--- /dev/null
+++ b/Examples/Pong/.swiftpm/xcode/xcshareddata/xcschemes/Pong.xcscheme
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Examples/Pong/Package.swift b/Examples/Pong/Package.swift
new file mode 100644
index 00000000..8edd3afe
--- /dev/null
+++ b/Examples/Pong/Package.swift
@@ -0,0 +1,42 @@
+// swift-tools-version: 5.10
+
+import Foundation
+import PackageDescription
+
+let gccIncludePrefix =
+ "/usr/local/playdate/gcc-arm-none-eabi-9-2019-q4-major/lib/gcc/arm-none-eabi/9.2.1"
+
+let playdateSDKPath: String = if let path = Context.environment["PLAYDATE_SDK_PATH"] {
+ path
+} else {
+ "\(Context.environment["HOME"]!)/Developer/PlaydateSDK/"
+}
+
+let package = Package(
+ name: "Pong",
+ platforms: [.macOS(.v14)],
+ products: [.library(name: "Pong", targets: ["Pong"])],
+ dependencies: [
+ .package(path: "../.."),
+ ],
+ targets: [
+ .target(
+ name: "Pong",
+ dependencies: [.product(name: "PlaydateKit", package: "PlaydateKit")],
+ swiftSettings: [
+ .enableExperimentalFeature("Embedded"),
+ .unsafeFlags([
+ "-Xfrontend", "-disable-objc-interop",
+ "-Xfrontend", "-disable-stack-protector",
+ "-Xfrontend", "-function-sections",
+ "-Xfrontend", "-gline-tables-only",
+ "-Xcc", "-DTARGET_EXTENSION",
+ "-Xcc", "-I", "-Xcc", "\(gccIncludePrefix)/include",
+ "-Xcc", "-I", "-Xcc", "\(gccIncludePrefix)/include-fixed",
+ "-Xcc", "-I", "-Xcc", "\(gccIncludePrefix)/../../../../arm-none-eabi/include",
+ "-I", "\(playdateSDKPath)/C_API"
+ ]),
+ ]
+ )
+ ]
+)
diff --git a/Examples/Pong/Sources/Pong/Game.swift b/Examples/Pong/Sources/Pong/Game.swift
new file mode 100644
index 00000000..7a42a4ec
--- /dev/null
+++ b/Examples/Pong/Sources/Pong/Game.swift
@@ -0,0 +1,221 @@
+import PlaydateKit
+
+// MARK: - Game
+
+final class Game: PlaydateGame {
+ // MARK: Lifecycle
+
+ init() {
+ [
+ playerPaddle, computerPaddle,
+ ball,
+ topWall, bottomWall, leftWall, rightWall
+ ].forEach { $0.addToDisplayList() }
+
+ playerPaddle.position = Point(x: 10, y: (Float(Display.height) / 2) - (playerPaddle.bounds.height / 2))
+ computerPaddle.position = Point(
+ x: Float(Display.width - 10) - computerPaddle.bounds.width,
+ y: Float(Display.height / 2) - (computerPaddle.bounds.height / 2)
+ )
+ ball.position = Point(x: Display.width / 2, y: 10)
+ }
+
+ // MARK: Internal
+
+ enum State {
+ case playing
+ case gameOver
+ }
+
+ var state: State = .playing
+ var score: (player: Int, computer: Int) = (0, 0)
+ let winningScore = 11
+ let playerPaddle = PlayerPaddle()
+ let computerPaddle = ComputerPaddle()
+ let ball = Ball()
+
+ let topWall = Wall(bounds: Rect(x: 0, y: -1, width: Display.width, height: 1))
+ let bottomWall = Wall(bounds: Rect(x: 0, y: Display.height, width: Display.width, height: 1))
+ let leftWall = Wall(bounds: Rect(x: -1, y: 0, width: 1, height: Display.height))
+ let rightWall = Wall(bounds: Rect(x: Display.width, y: 0, width: 1, height: Display.height))
+
+ var hasWinner: Bool { score.player >= winningScore || score.computer >= winningScore }
+
+ func update() -> Bool {
+ switch state {
+ case .playing:
+ Sprite.updateAndDrawDisplayListSprites()
+ case .gameOver:
+ if System.buttonState.current.contains(.a) {
+ score = (0, 0)
+ state = .playing
+ }
+
+ // TODO: - Center properly
+ Graphics.drawText(
+ "Game Over",
+ at: Point(x: (Display.width / 2) - 40, y: (Display.height / 2) - 20)
+ )
+ Graphics.drawText(
+ "Press Ⓐ to play again",
+ at: Point(x: (Display.width / 2) - 80, y: Display.height / 2)
+ )
+ }
+
+ Graphics.drawText("\(score.player)", at: Point(x: (Display.width / 2) - 80, y: 10))
+ Graphics.drawText("\(score.computer)", at: Point(x: (Display.width / 2) + 80, y: 10))
+ Graphics.drawLine(
+ Line(
+ start: Point(x: Display.width / 2, y: 0),
+ end: Point(x: Display.width / 2, y: Display.height)
+ ),
+ lineWidth: 1,
+ color: .pattern((0x0, 0x0, 0xFF, 0xFF, 0x0, 0x0, 0xFF, 0xFF))
+ )
+
+ return true
+ }
+}
+
+// MARK: - Wall
+
+class Wall: Sprite.Sprite {
+ init(bounds: Rect) {
+ super.init()
+ self.bounds = bounds
+ collideRect = Rect(origin: .zero, width: bounds.width, height: bounds.height)
+ }
+}
+
+// MARK: - Ball
+
+typealias Vector = Point
+
+// MARK: - Ball
+
+class Ball: Sprite.Sprite {
+ // MARK: Lifecycle
+
+ override init() {
+ super.init()
+ bounds = .init(x: 0, y: 0, width: 8, height: 8)
+ collideRect = bounds
+ }
+
+ // MARK: Internal
+
+ var velocity = Vector(x: 4, y: 5)
+
+ func reset() {
+ position = Point(x: Display.width / 2, y: 10)
+ velocity.x *= Bool.random() ? 1 : -1
+ velocity.y = abs(velocity.y)
+ }
+
+ override func update() {
+ let collisionInfo = moveWithCollisions(
+ goal: position + velocity
+ )
+ for collision in collisionInfo.collisions {
+ if collision.other == game.leftWall {
+ game.score.computer += 1
+ game.ball.reset()
+ if game.hasWinner { game.state = .gameOver }
+ } else if collision.other == game.rightWall {
+ game.score.player += 1
+ game.ball.reset()
+ if game.hasWinner { game.state = .gameOver }
+ } else {
+ synth.playNote(frequency: 220.0, volume: 0.7, length: 0.1)
+ if collision.normal.x != 0 {
+ velocity.x *= -1
+ }
+ if collision.normal.y != 0 {
+ velocity.y *= -1
+ }
+ }
+ }
+ }
+
+ /// Setting to `.slide` prevents the ball from getting stuck between the paddle and top/bottom.
+ override func collisionResponse(other _: Sprite.Sprite) -> Sprite.CollisionResponseType {
+ .slide
+ }
+
+ override func draw(bounds: Rect, drawRect _: Rect) {
+ Graphics.fillEllipse(in: bounds)
+ }
+
+ // MARK: Private
+
+ private let synth: Sound.Synth = {
+ let synth = Sound.Synth()
+ synth.setWaveform(.square)
+ synth.setAttackTime(0.001)
+ synth.setDecayTime(0.05)
+ synth.setSustainLevel(0.0)
+ synth.setReleaseTime(0.05)
+ return synth
+ }()
+}
+
+// MARK: - ComputerPaddle
+
+class ComputerPaddle: Paddle {
+ override func update() {
+ let ball = game.ball
+ let paddleCenter = position.y + bounds.height / 2
+ let ballCenter = ball.position.y + ball.bounds.height / 2
+
+ if ballCenter < paddleCenter - 5 {
+ // Ball is above the paddle center
+ moveWithCollisions(
+ goal: position - Vector(x: 0, y: speed)
+ )
+ } else if ballCenter > paddleCenter + 5 {
+ // Ball is below the paddle center
+ moveWithCollisions(
+ goal: position + Vector(x: 0, y: speed)
+ )
+ }
+ // If the ball is within 5 pixels of the paddle center, don't move
+ }
+}
+
+// MARK: - PlayerPaddle
+
+class PlayerPaddle: Paddle {
+ override func update() {
+ if System.buttonState.current.contains(.down) {
+ moveWithCollisions(
+ goal: position + Vector(x: 0, y: speed)
+ )
+ }
+
+ if System.buttonState.current.contains(.up) {
+ moveWithCollisions(
+ goal: position - Vector(x: 0, y: speed)
+ )
+ }
+ }
+}
+
+// MARK: - Paddle
+
+class Paddle: Sprite.Sprite {
+ // MARK: Lifecycle
+
+ override init() {
+ super.init()
+ bounds = .init(x: 0, y: 0, width: 8, height: 48)
+ collideRect = bounds
+ }
+
+ // MARK: Internal
+
+ let speed: Float = 4.5
+
+ override func draw(bounds: Rect, drawRect _: Rect) {
+ Graphics.fillRect(bounds)
+ }
+}
diff --git a/Examples/Pong/Sources/Pong/Resources/pdxinfo b/Examples/Pong/Sources/Pong/Resources/pdxinfo
new file mode 100644
index 00000000..6e6f189d
--- /dev/null
+++ b/Examples/Pong/Sources/Pong/Resources/pdxinfo
@@ -0,0 +1,5 @@
+name=Pong
+author=Finn Voorhees
+description=Pong built using PlaydateKit
+bundleID=com.finnvoorhees.Pong
+imagePath=
diff --git a/Examples/Pong/Sources/Pong/entry.swift b/Examples/Pong/Sources/Pong/entry.swift
new file mode 100644
index 00000000..ecadbd27
--- /dev/null
+++ b/Examples/Pong/Sources/Pong/entry.swift
@@ -0,0 +1,18 @@
+import PlaydateKit
+
+/// Boilerplate entry code
+nonisolated(unsafe) var game: Game!
+@_cdecl("eventHandler") func eventHandler(
+ pointer: UnsafeMutableRawPointer!,
+ event: System.Event,
+ _: CUnsignedInt
+) -> CInt {
+ switch event {
+ case .initialize:
+ Playdate.initialize(with: pointer)
+ game = Game()
+ System.updateCallback = game.update
+ default: game.handle(event)
+ }
+ return 0
+}
diff --git a/README.md b/README.md
index 5e2c072e..bd04b1d2 100644
--- a/README.md
+++ b/README.md
@@ -72,7 +72,7 @@ final class Game: PlaydateGame {
## Contributing
I'm happy to accept contributions on this project, whether it's bug fixes, implementing missing features, or opening an issue. Please try to follow the existing conventions/style in the project.
-If you create a game using PlaydateKit and would like it listed here, please open an issue or pull request! I am also hoping to add some simple games to an Examples/ directory in this repo if anyone wants to demake a retro game or create a new one that demos PlaydateKit.
+If you create a game using PlaydateKit and would like it featured here, please open an issue or pull request! If you would like to demake a retro game or create a new one that demonstrates PlaydateKit's capabilities, feel free to add an example game in the `Examples/` directory.
## Acknowledgements
diff --git a/Sources/PlaydateKit/Core/Sprite.swift b/Sources/PlaydateKit/Core/Sprite.swift
index caf525b4..1fd720fd 100644
--- a/Sources/PlaydateKit/Core/Sprite.swift
+++ b/Sources/PlaydateKit/Core/Sprite.swift
@@ -80,6 +80,8 @@ public enum Sprite {
// MARK: Public
+ public let pointer: OpaquePointer
+
/// The sprite's stencil bitmap, if set.
public var stencil: Graphics.Bitmap? { _stencil }
@@ -101,9 +103,14 @@ public enum Sprite {
/// Gets the current position of sprite.
public var position: Point {
- var x: Float = 0, y: Float = 0
- sprite.getPosition.unsafelyUnwrapped(pointer, &x, &y)
- return Point(x: x, y: y)
+ get {
+ var x: Float = 0, y: Float = 0
+ sprite.getPosition.unsafelyUnwrapped(pointer, &x, &y)
+ return Point(x: x, y: y)
+ } set {
+ bounds.x = newValue.x
+ bounds.y = newValue.y
+ }
}
/// Gets/sets the bounds of the sprite.
@@ -316,8 +323,6 @@ public enum Sprite {
// MARK: Internal
- let pointer: OpaquePointer
-
/// Gets/sets the sprite’s userdata, an arbitrary pointer used for associating the sprite with other data.
var userdata: UnsafeMutableRawPointer? {
get { sprite.getUserdata.unsafelyUnwrapped(pointer) }