Platformer game on Embedded Swift with Playdate Kit

2025-02-16

The Playdate running a version of the Platformer game

When Swift announced that it was supporting embedded systems and supporting the Playdate I wanted to try it out myself. I already had a Playdate and I had tried Pulp before.
First I built some simple games with the Apple Swift-Playdate examples , then i discovered PlaydateKit which is a fantastic , easy to use library.

Platformer game experiment

Platformer game in SpriteKit, player picking up a pig and dropping it

Before my first child was born I built a little platformer game experiment in Swift with SpriteKit trying to replicate the Mario games, I was heavily influenced by Super Mario War by Florian Hufsky as well as the jumping from the original Super Mario Bros assembly code. It wasn’t a “game” with levels and goals etc. more a learning experience project that a game could be built on later.

The game had the following features:

And probably some others I have forgotten.

Five years later...

Fast forward five years and I’m looking through some of my old games with my five year old son over the holiday break and I come across the Platformer game, he immediately dives in and starts making levels for the game! I think, wouldn’t it be great to play those levels on the Playdate and show the rest of the family, so while he is building levels I start looking at building the game on the Playdate.

I find out that I had already separated the code into the Platformer system and the game play / UI, this makes everything much easier because the SpriteKit part is can be replaced with another library for rendering.

Currently, to build Embedded Swift you need to build with a recent nightly Swift toolchain. Since Core Graphics isn’t included in Swift Embedded, I start replacing CGPoint with a simple Point struct and replace CGFloat with Double, these kind of changes will come up a lot in this project.

I soon discoverd the library PlaydateKit which is really easy to use for different game projects where Apple’s example is more of an example of what can be done. But the first issue I found was that I couldn’t import the Platformer System into the Play Date kit project because it isn’t an OSSA module and also the SPM setup of Platdate Kit is for the build system, adding dependencies to the Package.swift file doesn’t add them to your game project, it’s adding them to the Platdate build system for Swift.I tried to just use a symlink but the folder it showed up as a “chain” icon and i couldn’t see the files. The structure of Swift Package Manager projects doesn’t allow inclusion of folders ouside the Source folder so I couldn’t add a local package from higher up the folder hierarchy. I ended up just copying the files into the Playdate project to get started.

Next I removed all instances of import Foundation, luckily PlaydateKit has a lot of functions found inFoundation functions such as sin, cos, pow and sqrt. I removed String(format:) and uses of NotificationCenter and fixed some Swift 6 concurrency issues. I made some of the classes final that had generic functions and kept going through the errors. Sometimes errors show up but go away if you fix another error.

Cannot do dynamic casting in embedded Swift on...

The next issue was that Embedded Swift can’t do dynamic casting to protocols so I changed the Actor which is all things that move in the game from a type alias protocol collection to a regular class that other actors inherit from. This is a bit more like regular object oriented game programming. I also moved some functions out of extensions and they became overrides of Actor functions.

I turned off the actors and hooked up the tile rendering to Playdate Kit. I drew new sprites in black and white for the Playdate and named the sprite sheet blocks-table-16-16.png so it loads sprites that are 16x16 pixels. This was a problem because I had hard coded the Platformer system to be 32x32 pixels, so on the Playdate side I halved all the sizes and positions, this is something that needs to be fixed by setting the tile size at launch.

Sprite sheet for Playdate.

I had some helper variables that give the position of tiles as integers as well as floats, these were two separate variables, looking back, I’m not sure why I did this, and now it was causing problems with the layout of the tiles. I changed this so that there is a computed value for the integers from the float value.

Platformer game running on Playdate with the blocks only.

Some Actors conform to Droppable, meaning they have function they can call if the object is dropped by another Actor, e.g. a Box that was picked up by the Player then dropped. A problem was when checking if an dropped object is Droppable before called drop() e.g. if let dropableAttachable = attachable as? Droppable gives the Cannot do dynamic casting in embedded Swift error. Fixed this by a more object oriented approach by moving the Drop function to be part of Actor and Actors that can’t be picked up have empty drop functions.

This was the same with merging two dictionaries of type [UUID: Actor], instead I just replaced the merge with a loop to add them together.

Replacing Notification Center

In the original implementation, when something changes the level a Notification Center notification is sent out from anywhere in the code to trigger the map to update. Notification Center is obviously not available in Embedded Swift so I has to come up with a different solution.

I made an Observer class, how it works is the main Game class sets up an update closure via the setupUpdate function from the Observer, this update closure updates the map when triggered. The setupUpdate function sets a sendUpdate closure that can be called from anywhere else in the codebase. So somewhere else in the game, e.g. the character hits a block and destroys it, then the Observer’s sendUpdate is called which triggers the update close in the main Game class.

// Pseudocode:

Game.init {
  Observer.setupUpdate {
    // Update closure
    self.updateMap() 
  }
}

Collision.blockDestroyed { Observer.sendUpdate } 

To update the tiles the setupUpdate closure needs to reference self. Normally I would use the [weak self] in pattern to avoid capturing self but the weak attribute isn’t available on Embedded Swift. I tried to use a combination of withUnsafePointer with unsafeBitCast when calling the closure and passing the reference to self.

withUnsafePointer(to: from) { unsafeFrom in
  self.update = { package in
    let fromCast = unsafeBitCast(unsafeFrom, to: T.self)
    update(package, fromCast)
  }
}

This was not the right combination as updating the map from here would cause a crash, and looking at the addresses to self and what the unsafe pointer is pointing to they are different even when using unsafeFrom.pointee.

orig: 105553151664832
unsafe pointer: 4294967297

I tried just using unsafeBitCast

let unsafeSelf = unsafeBitCast(from, to: Int.self)
self.update = { package in
  let fromCast = unsafeBitCast(unsafeSelf, to: T.self)
  update(package, fromCast)
}

Doesn’t crash, and the pointers are the same too:

orig: 105553147994432
unsafe pointer: 105553147994432

I’m not sure if the lifetime of unsafeSelf is guaranteed, it’s created on the init of Game (with an instance of Game) and cast back to Game in the closure when an update arrives. So I’m hoping in this case it wont be invalid until the game quits. Now the call to setupUpdate (inside the main Game class) looks something like this:

Observer.shared.setupUpdate(from: self) { package, aSelf in
  switch package.message {
    case Constants.kNotificationMapChange:
      aSelf.background.mapChange(point: package.point, 
                                 tileType: package.tileType)
    default: 
      break
  }
}

With the observer hooked up, I added the controls back in and player can now jump around and break bricks.

Platformer with player jumping and hitting blocks.

The game started to crash when I hooked up the camera, this turned out to be a classic divide by zero error that was in the old platformer code.

Platformer with the camera moving along with the player.

Now the game is playable on the Playdate! I can build and run it on the Platdate simulator or take the zipped .pdx file and side load it to a physical device.

Porting back to Sprite Kit

After getting it to run on the Playdate I wanted to get it running again with Sprite Kit, this involved hooking up the new Observer class on the Sprite Kit side and since I had trouble linking the Platformer system to the Playdate project I just made a script to copy the code form the Platformer system folder to the Playdate project, which is not ideal but works. The PlaydateKit imports are wrapped in a #if canImport(PlaydateKit) macro otherwise it uses Foundation.

After getting it to run on the Playdate I had to get it to run again with Sprite Kit, this involved hooking up the new Observer class and since I had trouble linking the Platformer system to the Playdate project I just made a script to copy the code form the Platformer system folder to the Playdate project, which is not ideal but works. The PlaydateKit imports are wrapped in a #if canImport(PlaydateKit) macro otherwise it uses Foundation.

While I was getting this running on the Playdate my son was working on levels using the level editor that was built into the older macOS version of the game.

Platformer with built in level editor on macOS.

I loaded up the new level on and side loaded the game on the play date. We had to remove some of the items because it was slowing the Playdate down, this could probably be fixed with some “chunking” of the levels.

So now it’s running on the Playdate, it run a bit slow, and there is no “world map” to change levels. Over all it was a great challenge to work within the constraints of Embedded Swift and very rewarding to work with my son on a game I started just before he was born, we are both excited to add more levels and more things for the player to do!

If you want to check out the game on Platdate or the original Sprite kit version it’s available in the platformer game repo on GitHub

Created in Swift with Ignite