Skip to content

Commit

Permalink
Merge pull request #1 from mossprescott/spm
Browse files Browse the repository at this point in the history
Add support for building separate apps; SpriteGame example
  • Loading branch information
mossprescott committed Mar 25, 2022
2 parents 7efdab4 + 7e244a8 commit 9b02e5e
Show file tree
Hide file tree
Showing 47 changed files with 1,365 additions and 115 deletions.
6 changes: 4 additions & 2 deletions .gitignore
@@ -1,2 +1,4 @@
HelloWorld.pdx
Source/pdex.bin
/swift-pd/Sources/CPlaydate/pd_api.h
/swift-pd/Sources/CPlaydate/pd_api
.build
*.pdx
24 changes: 24 additions & 0 deletions Examples/HelloWorld/Package.swift
@@ -0,0 +1,24 @@
// swift-tools-version:5.5
import PackageDescription

let package = Package(
name: "HelloWorld",
products: [
.library(
name: "HelloWorld",
type: .dynamic,
targets: ["HelloWorld"]
),
],
dependencies: [
.package(name: "swift-pd", path: "../../swift-pd")
],
targets: [
.target(
name: "HelloWorld",
dependencies: [
.product(name: "Playdate", package: "swift-pd")
]
)
]
)
52 changes: 52 additions & 0 deletions Examples/HelloWorld/Sources/HelloWorld/HelloWorld.swift
@@ -0,0 +1,52 @@
import Playdate

let TEXT_WIDTH = 86
let TEXT_HEIGHT = 16

var x = (400-TEXT_WIDTH)/2
var y = (240-TEXT_HEIGHT)/2
var dx = 1
var dy = 2

var font: Graphics.Font!

class HelloWorld: App {
/// Called once when the app is loaded, before the first call to update().
/// You can put code to do any one-time setup here.
init() {
do {
font = try Graphics.Font(path: "/System/Fonts/Asheville-Sans-14-Bold.pft")
} catch {
Playdate.System.error("\(error)")
}
}

/// Called once per frame to consume input and draw to the screen. If anything was drawn and the
/// screen needs to be updated by the sytem, return true.
func update() -> Bool {
Graphics.clear(.white)

Graphics.setFont(font)
Graphics.drawText("Hello World!", x: x, y: y)

x += dx
y += dy

if x < 0 || x > Int(Display.width) - TEXT_WIDTH {
dx = -dx
}

if y < 0 || y > Int(Display.height) - TEXT_HEIGHT {
dy = -dy
}

Playdate.System.drawFPS(x: 0, y: 0)

return true;
}
}

@_dynamicReplacement(for: makeApp())
func myApp() throws -> App {
HelloWorld()
}
File renamed without changes.
24 changes: 24 additions & 0 deletions Examples/SpriteGame/Package.swift
@@ -0,0 +1,24 @@
// swift-tools-version:5.5
import PackageDescription

let package = Package(
name: "SpriteGame",
products: [
.library(
name: "SpriteGame",
type: .dynamic,
targets: ["SpriteGame"]
),
],
dependencies: [
.package(name: "swift-pd", path: "../../swift-pd")
],
targets: [
.target(
name: "SpriteGame",
dependencies: [
.product(name: "Playdate", package: "swift-pd")
]
)
]
)
38 changes: 38 additions & 0 deletions Examples/SpriteGame/README.md
@@ -0,0 +1,38 @@
# Sprite Game Example

A port of the SpriteGame example from the SDK, which shows off how the Swift types handle
object lifecycles: allocating and freeing Bitmaps and Sprites automatically when they're
no longer referenced.

There are a few changes compared to the SDK's original version:
- Button B produces continuous fire, because it's fun and because more sprites make a more
interesting test
- The score is displayed in the corner of the screen. Did somebody forget to include that,
after adding a font for it to the project?
- A couple of minor bug fixes

The code makes no attempt to preload bitmaps or fonts, instead just loading them as needed. That
makes a more interesting test of memory management, and the speed penalty doesn't matter in the
simulator. If we ever get this running on device, might want to fix that for better
performance/efficiency.

## Notes on the Swift port

It's not exactly clear how best to break up a moderately involved app like this one. I chose to put
each type of sprite into a separate .swift file, because it makes it easier to see how they compare to one another.

Even so, the total number of lines, including blanks and comments, is a little less than the
original single-file C version (about 410 versus 490). It might be interesting to compare that
with a Lua version.

To keep things simple, only the main class [`SpriteGame`](Sources/SpriteGame/SpriteGame.swift)
keeps track of any state. The rest of the files just contain functions to create and update one
type of sprite each. That ended up involving some callbacks, which could be considered overly fancy,
but it also demonstrates how Swift features like closures can be used to help organize even a small
codebase like this.

Using a Swift `enum` for sprite tags seems natural, but involves some manual mapping back and forth.
In fact, this game takes advantage of the untyped nature of those tags by storing either nothing,
an `enum` value, or a frame number, depending on the sprite. So in Swift you have to know
what the tag means based on the sprite you're looking at, and then you can interpret it into a nicer
type if you want.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
92 changes: 92 additions & 0 deletions Examples/SpriteGame/Sources/Resources/fonts/namco.fnt
@@ -0,0 +1,92 @@
space 9
! 9
" 9
# 9
$ 9
% 9
& 9
' 9
( 9
) 9
* 9
+ 9
, 9
- 9
. 9
/ 9
0 9
1 9
2 9
3 9
4 9
5 9
6 9
7 9
8 9
9 9
: 9
; 9
< 9
= 9
> 9
? 9
@ 9
A 9
B 9
C 9
D 9
E 9
F 9
G 9
H 9
I 9
J 9
K 9
L 9
M 9
N 9
O 9
P 9
Q 9
R 9
S 9
T 9
U 9
V 9
W 9
X 9
Y 9
Z 9
[ 9
\ 9
] 9
_ 9
` 9
a 9
b 9
c 9
d 9
e 9
f 9
g 9
h 9
i 9
j 9
k 9
l 9
m 9
n 9
o 9
p 9
q 9
r 9
s 9
t 9
u 9
v 9
w 9
x 9
y 9
z 9
| 9
~ 9
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions Examples/SpriteGame/Sources/Resources/pdxinfo
@@ -0,0 +1,5 @@
name=Sprite Game
author=Moss Prescott
description=It's not much of a game! (originally by Panic, Inc)
bundleID=com.example.sprite
imagePath=
39 changes: 39 additions & 0 deletions Examples/SpriteGame/Sources/SpriteGame/Background.swift
@@ -0,0 +1,39 @@
import Playdate

enum Background {
static var y = 0
static var image: Graphics.Bitmap!
static var sprite: Sprite!

static func setup() throws {
image = try Graphics.Bitmap(path: "images/background")
sprite = Sprite()

sprite.setUpdateFunction(update)
sprite.setDrawFunction { _, _ in draw() }

sprite.bounds = Rect(x: 0, y: 0, width: 400, height: 240)
sprite.zIndex = 0

Sprite.add(sprite)
}

private static func update() {
y += 1
if y > image.height {
y = 0
}

// Note: this sprite moves every frame and draws the entire screen,
// So marking it dirty causes a full redraw every time. That means none
// of the other sprites actually need to call it.
// In fact, if each sprite tries to dirty itself, the wrong parts of the
// the screen get drawn. Hmm.
sprite.markDirty()
}

private static func draw() {
image.draw(x: 0, y: y)
image.draw(x: 0, y: y-image.height)
}
}
40 changes: 40 additions & 0 deletions Examples/SpriteGame/Sources/SpriteGame/BackgroundPlane.swift
@@ -0,0 +1,40 @@
import Playdate

/// Manage the small planes that fly harmlessly by underneath the action. Note, there's no state
/// aside from the actual Sprite, so no instances of BackgroundPlane.
enum BackgroundPlane {
/// "createBackgroundPlane"
static func spawn(departedCallback: @escaping () -> Void) {
let plane = Sprite()

plane.setUpdateFunction { sprite in update(plane: sprite, departedCallback: departedCallback) }

// TODO: preload
guard let planeImage = try? Graphics.Bitmap(path: "images/plane2") else {
fatalError()
}

let w = planeImage.width
let planeHeight = planeImage.height

plane.setImage(planeImage)

plane.moveTo(x: Float.random(in: 0 ..< 400) - Float(w)/2, y: -Float(planeHeight))
plane.zIndex = 100
Sprite.add(plane)
}

/// "updateBackgroundPlane"
private static func update(plane: Sprite, departedCallback: @escaping () -> Void) {
let (x, y) = plane.position
let newY = y + 2

if let h = plane.getImage()?.height, newY > 400 + Float(h) {
Sprite.remove(plane)
departedCallback()
}
else {
plane.moveTo(x: x, y: newY)
}
}
}
60 changes: 60 additions & 0 deletions Examples/SpriteGame/Sources/SpriteGame/Bullet.swift
@@ -0,0 +1,60 @@
import Playdate

/// Manage bullets fired by the player's plane. Note, there's no state aside from the actual
/// Sprite, so no instances of Bullet.
enum Bullet {
/// "playerFire"
static func spawn(x: Float, y: Float, hitEnemyCallback: @escaping () -> Void) {
let bullet = Sprite()

bullet.setUpdateFunction { sprite in update(sprite, hitEnemyCallback) }

// TODO: preload
guard let bulletImage = try? Graphics.Bitmap(path: "images/doubleBullet") else {
fatalError()
}

let w = bulletImage.width
let bulletHeight = bulletImage.height

bullet.setImage(bulletImage)

bullet.collideRect = Rect(x: 0, y: 0, width: Float(w), height: Float(bulletHeight))
bullet.setCollisionResponseFunction { _, _ in .overlap }

bullet.moveTo(x: x, y: y) // Note: the original example has a bug here that makes the bullets misaligned
bullet.zIndex = 999
Sprite.add(bullet)

bullet.tag = SpriteType.bullet.rawValue
}

/// "updatePlayerFire"
private static func update(_ bullet: Sprite, _ hitEnemyCallback: @escaping () -> Void) {
let (x, y) = bullet.position
let newY = y - 20

if let h = bullet.getImage()?.height, newY < Float(-h) { // bullet is offscreen, remove it
Sprite.remove(bullet)
}
else {
let (_, _, collisions) = bullet.moveWithCollisions(goalX: x, goalY: newY)

var hit = false
for c in collisions {
let type = SpriteType(rawValue: c.other.tag)
if type == .enemy {
EnemyPlane.destroy(c.other)
hit = true
hitEnemyCallback()
}
}

if hit {
Sprite.remove(bullet)
}

// Note: no need to free the collision info; Swift takes care of it
}
}
}

0 comments on commit 9b02e5e

Please sign in to comment.