Porting 'You Cannot go Back' from Pebble to Playdate

If people do not remember Pebble anymore, it was a Kickstarter funded smartwatch which gained quite the cult following. Sadly pebble went out of business at the end of 2016, but the community has since maintained parallel infrastructure such as a copy of the app store Pebble Appstore

Native pebble development was in C, and as Playdate publishes a C API in addition to the Lua one, I have explored porting one of my old Pebble games over to Playdate. Here is 'You Cannot Go Back' - a short memory/puzzle/adventure game running on the Pebble Time smartwatch emulator.

And here it is (work-in-progress) on the Playdate simulator


I managed to get a first version working on the simulator from just one weekend of work, since then I have been adding more features, more quality of life and some more polish.

At 400x240 with 1-bit colour the playdate is close to (but annoyingly not quite twice) the resolution of the Pebble Time's 144x168 "e-paper" display with 8-bit colour (64 colours).

In regular landscape mode I can centre the game window on screen and add some large borders left and right. For portrait mode I can render to a bitmap which then gets rotated 90 degrees and drawn at setScale(2). Vertically there are another 64 pixels to play with, but horizontally 288 is sadly 48 pixels larger than 240 so I have experimented with a horizontal scroll which cuts off either the left or right dungeon walls. It's a bit annoying but shouldn't really affect the gameplay for this particular game, and is a lot less work than re-sizing the dungeon.

Lines, stroke or fill of circles or rectangles - all easy to port. Bitmap text rendering was easy too. It looks like text centring has to be done manually with the C API by querying getTextWidth(), and this only then works for single-line strings, but this is true for the majority of the strings in the game anyway.

Pebble had a rich API for window, layer and custom menu management. Thankfully in this game I was barely using it at all! Stripped this all out.

One change is which caused some trouble is that in Pebble I could load my sprite sheet as a single image and then call gbitmap_create_as_sub_bitmap(m_spriteMap, GRect(x, y, w, h)) to get GBitmap* which reference a region of the parent image - I used this heavily as while the majority of my sprites were 16x16, some were larger, and some were 8x8. As Playdate's compiler chunks the sprite sheet at compile time, I have had to split everything into 8x8 tiles and added a wrapper such that every call to draw a regular 16x16 tile now needs to make 4 separate calls to drawBitmap. So that's up from 48 to 192 draw calls now just to draw the floor. I could split up my sheet into multiple sheets based on size, it remains to be seen if this will be needed...

Audio / Haptic Feedback:
Pebble had haptic feedback, but Playdate doesn't - it is easily removed. The Pebble did not have the ability to make any sounds, not even a buzzer. So all music and sound effects are being added from scratch.

User Input
In Pebble I would subscribe to button presses by supplying a call-back e.g window_single_click_subscribe(BUTTON_ID_UP, gameClickConfigHandler); and subscribe to the accelerometer with accel_data_service_subscribe(1, dataHandler); accel_service_set_sampling_rate(ACCEL_SAMPLING_25HZ);

For playdate I am currently polling buttons and the accelerometer (which I am using for auto-rotation to portrait or landscape mode), though (as also noted on eventHandler not called for buttons - #7 by dave ) there is also a PDSystemEvent kEventKeyPressed defined - it would be a bit nicer if input could be handled this way rather than directly inside the game loop.

Game Loop
On the game loop, I have separate update and render functions. In Pebble I would us a timed callback to keep my update function being called, to request redraw I would layer_mark_dirty(s_dungeonLayer) as I had previously registered layer_set_update_proc(s_dungeonLayer, dungeonUpdateProc);

With playdate I can get the system to call my update function with playdate->system->setUpdateCallback(gameLoop, NULL); from which I can call dungeonUpdateProc when I need to re-render.

One issue is that returning 0 from the update function to signify that the screen does not need to be redrawn looks to be broken (reported on C API: setUpdateCallback function documentation misleads about the meaning of a return value of 0 from the callback )

Improvements to the playdate version
I had to stop adding features to the Pebble game when I hit the 64 kb RAM ceiling of apps on the watch! Playdate's 16 Mb is practically infinite in comparison. I am already working on adding three more room types, new sprites, and all the audio. The drawRotatedBitmap call also means that the buzz saws in the corridor of blades can actually rotate in this version!

To be seen... I don't have a physical device - nor have I managed to compile for ARM yet. I am planning soon on asking if the community can give some feedback on the hardware performance. But going from a Pebble watch's STM32_F2 to Playdate's STM32_F7 should hopefully mean that I don't have to worry here!

Source: GitHub - timboe/YouCannotGoBack: Knightmare inspired dungeon exploration game for pebble watches


This is awesome! I love deep dives like this.

You're already managing dirty areas but you could use the Sprite system to do that. You could also use a sprite for the floor, this would reduce your 192 calls to a one off set of calls on init, or something like that. But it would mean more refactoring so I'd only consider it if you're struggling for performance which I doubt will be the case given the game's heritage.

Please keep posting updates of your progress.

So the release yesterday went well, thanks in part to the members of this forum. I have not had any reports so far of any issues in sideloading the pdx bundle via an itch-served ZIP file and have not needed to scramble to release any patch builds.

Neither did my performance concerns above look to be needed, reports are that the game maintains a steady 20 FPS without me having to go through and refactor the tile-based rendering code.

I just finished a longer writeup of the experience of dusting off this game and bringing to the Playdate, you can find this here:

1 Like

Can you say more about this? Is this referenced somewhere in the documentation? I'm curious to learn more about this. Thanks!

Hi @Rascal here's where you want to look

I have spritesheet-table-8-8.png which gets compiled to spritesheet.pdt and smask-table-1.png through smask-table-48.png which all get compiled down to smask.pdt. I then load both of these as image tables and pull out the individual bitmaps via an index.

All other single-image files get compiled to .pdi files.

Just wanted to say that I am still working my way through the dungeon but this game is addictive and perfect for short sessions... That incentives you do more of them! Clearly a gem!
Sharing your experience is also very interesting, thank you for this!
Edit: Finally found how to open the last chest :smiley: Tricky haha

1 Like

Good job! And yes - the final room of Level 3 is especially devious ^-^

1 Like