Orkn's Pulp Devlog

Performance Optimisations

Performance has been a necessary consideration from the start in developing Patterns of the Wind. In Pulp there's not much overhead available before the frame rate starts dropping from the engine's target 20 fps, especially when drawing a lot to the screen as the game unavoidably does (and especially when running the game on early "Rev A" consoles such as my own). One significant optimisation as already covered is using label with font characters in place of drawing or embedding tiles wherever possible. Without this approach the game's perspective would be a non-starter, but fortunately I had already learned that trick from Daedalus Versus Minotaur. Scroll back a few posts and you can see the current font table with its repurposed characters!

Another lesson from a previous project was that calling multiple events is expensive, something you can read about me discovering with Lolife in an earlier devlog. With Patterns of the Wind I applied that knowledge from the start with most code living inside the game's loop event and the player's draw event instead of being factored out into smaller events and different scripts. That makes the code a little harder to maintain and reason about, but in this case it's a necessity, and it's not too big of a deal considering I'm the only person who has to actually understand the code I've written! At last count the player script is 1188 lines and the game script is 2112 lines, the majority of each taken up by those two events, which makes them pretty hefty but I can still find my way around them.

Where I've had to innovate in Patterns of the Wind from a performance perspective is in the calculation of the new view whenever the player moves. There are 19 tiles visible in the side-on perspective view that is drawn with labels, with 5 tiles in the foreground, 5 in the middle distance and 9 on the horizon. In order to calculate those labels each of those 19 tiles must be queried in some fashion, and the approach I take is to call a view event on each tile. For example the water tile defines its view event like this:

on view do
  n = "a"
  m = "b"
  f = "c"
end

Where the n, m and f variables represent the near, mid and far font characters to use in the perspective labels.

The simplest and most naive approach is to calculate the view every frame. This performs awfully, but it's pretty obvious that an easy optimisation is to instead only calculate the view whenever the player moves and the view changes. With 19 tiles to query however that isn't enough for good performance!

The next trick is to realise that whenever the player moves, most of the tiles in view are the same tiles that were previously in view, just shifted. If the player moves east into the screen the middle five tiles on the horizon become the five tiles in the middle distance, and the five tiles that were previously in the middle distance become the five tiles in the foreground. This means that only nine new tiles need to be queried. Similar logic applies to the player moving in any direction, although it becomes apparent that it's necessary to keep a record of nine tiles at each distance. Only five tiles might be visible in the foreground and at the middle distance, but if the player moves west these will need to be shifted backwards and so a full row of nine tiles are needed. Overall there are now 27 tiles being tracked in variables as opposed to 19, but this is still quicker because only a maximum of 9 tiles are being queried on move. Shuffling around variables is much, much faster than calling events on tiles in the room!

Calling nine tile events in a single frame is however still not ideal. What I do instead is distribute that view calculation across multiple frames in advance of the player moving. Thankfully this can be done because the player is not in full control of when they move but instead has to wait for the wind to build up enough to blow the balloon, so that gives a window in which the new view can be pre-calculated.

The immediate problem with pre-calculation is that the direction the player is going to move in is uncertain. Instead of querying just nine tiles to the east when moving east or three tiles to the north when moving north it's necessary to query nine tiles to the east and three tiles to the north, as well as another nine tiles to the west and three tiles to the south. We're back to querying 24 tiles! Crucially though there are more than 24 frames between the player moving and possibly moving again, so these queries can be distributed across 24 frames at only one tile event call per frame. Calculating the new view on move now only requires shuffling variables around, and there's no uneven dip in performance due to multiple tile events being called in a single frame. That requirement specifically has fallen from an original 19 event calls, through 9 event calls, and is now only 1 event call per frame. That's a big performance win!

Even with all these optimisations Patterns of the Wind does not run at a solid 20 fps on my Rev A console. It actually runs much slower, currently dropping as low as 12 fps, but thankfully this isn't very noticeable or impactful on the player. In my previous post I described how the crank input is both delayed and averaged out to better emulate the non-immediate response of a hot air balloon to its burner. This game-feel helps disguise the low fps very effectively. What's more important is that the fps is stable so there is no noticeable hitching or juddering and thankfully this is the case because of the distributed view calculation described above.

Overall then I'm happy with the performance of the game! If you have a Rev B console, as the majority of Playdate owners probably do, I'm told the game runs at near enough full speed. The only difference then is that the game runs a bit slower on Rev A giving the player longer to react. My solution to that is to make sure I test both on device on my Rev A but also in the simulator where the game does run at full speed, as well as ensuring I get plenty of feedback on balance from playtesters playing on Rev B. That neatly leads me on to my next post which will be all about playtesting!