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) }