Improving Performance in Plight of the Wizard
I wrote most of the core code for Plight of the Wizard during a four-day game jam at Recurse Center. The game was perfectly performant at that time given the limited scope. However, I have made a lot of changes to the game since my last blog post, and a lot of my initial code did not scale very well at all. The biggest change I made was implementing a camera to allow the player to explore a larger map size beyond the Playdate's 400x240 screen.
This change was really exciting to implement because it meant that I could now do a lot more with my game. However, it revealed all the assumptions I had made about the gameplay being confined to the screen size no longer held up. First, the gameplay was immediately less fun, which meant that numerous gameplay elements needed to be added or tweaked. Second, the performance took a massive hit. Constantly moving around the screen means that everything has to be redrawn, which has massive performance implications.
UI
The first problem was that the score in the upper left had been hardcoded and was in the physical space. For the game jam, I quickly gave it a collision box to avoid enemies from walking over it. With a camera system, the score was physically in the upper left corner of the map. This was easily fixed by setting the text to ignore the camera offset and applying a z-index to always place it on the top of screen. I went ahead and also added a black rectangle across the top of the screen that the score sits on to prevent sprites from overlapping with the text, which would make it hard to read.
Redrawing sprites
The next issue I faced was related to my grass sprites. My initial approach was creating an algorithm to randomly place grass sprites around the map. This worked fine when the game was bound within the screen, but I learned the hard way that redrawing the individual sprites is extremely CPU-intensive on the Playdate. After a large amount of enemies had spawned, the fps would drop from a stable 30 down to an excruciatingly slow 5 fps when the player moved around the map.
I noticed that the performance dip only occurred when the player moved around the center of the map. When the player hits the edges of the map, the camera locks into place and stops following the player. Combining this revelation with the observation that fps was stable while the player wasn't moving, I discovered that the issue was due to redrawing all of the individual sprites. While I couldn't avoid redrawing the sprites for the player and enemies that moved around and could spawn and despawn, grass was purely a background element.
I thought my initial approach would work fine because I could see on the screen that the black background was not being redrawn as I moved. However, once I converted the grass into a tilemap, the fps issues stopped, despite the Playdate simulator now showing the entire background was being redrawn. It seems that the Playdate SDK optimizes for tilemaps when drawing the background. Switching to this approach led to major performance gains.
Entity spawning
The next issue I faced with my new camera system was my logic for spawning entities such as enemies or items to pick up. This broke down into two separate issues stemming from the same root problem, which was determining where to spawn entities. There are a dozen ways to solve this problem, but I chose to stick with what I had done the first time around, which was to find a solution that works well enough to solve the problem so I can focus on other gameplay elements I still need to implement. My new system is significantly simplified and involves two elements: collision detection and spawning entities off-screen.
Collision detection
The first thing I tackled was a low-hanging fruit I had long been aware needed to be improved.
My system for spawning objects used a repeat...until loop, which chose random x and y coordinates on the map, checked if those coordinates were occupied, and selected new random coordinates if so.
Each time an entity was successfully spawned, I added its ID to a hash map with a reference to its live coordinates.
Whenever I wanted to spawn a new entity, I would iterate through the entire map and check each of the occupied positions.
This logic could run forever in theory since it kept repeating execution until it found an available space.
With a larger map to explore, more entities needed to be spawned to fill up the space, and this system become far too slow.
My new method simply chooses random coordinates, checks for collisions in that position and its surrounding area, and abandons the spawn if there is a collision. This uses the built-in collision detection in the SDK, which I'm making the naive assumption is fairly optimized. So far this seems to work really well and is very easy to read in my code. However, I didn't want to spawn enemies on the screen, and I realized if I spawn them off of the screen, when the enemies naturally move towards the player, the off-screen positions free up.
Spawning entities off-screen
By moving spawning off-screen, this reduced the frequency of spawn collisions, which made spawning much easier to perform. This also had the added benefit of leading to better gameplay. Now my primary concern is if the enemy will spawn on environmental items such as trees, and I no longer have to worry about spawning on top of the player or any items on screen that can be picked up.
However, I ran into an issue, which was how do I determine the location of the screen as I move around the map? My first approach was to use the camera's current bounds. This involved calling for the camera's current left, right, top, and bottom positions whenever an entity needed to be spawned. This worked fine at first but led to some major performance issues once the game filled up with enemies spawning. The amount of calculations for determining the camera position and randomly choosing coordinates when spawning enemies every half-second adds up. But if I weren't moving around, the camera position would be unchanged, so why bother constantly recalculating its position?
Caching available spawn coordinates
A eureka moment regarding available spawn points came to me while I was in the shower. I could cache the camera's bounds. That way if the player isn't moving, the function for choosing random coordinates based on the camera bounds would have one less step to perform. Taking it one step further, I realized that I would only have to update the camera's position in the cache if the camera has moved by the distance that the enemy spawns away from the screen. So if the enemy spawns 32 pixels to the left or right of the camera, then I only need to update the camera's left and right positions when the player has moved 32 pixels horizontally. Next, I realized that this meant the top and bottom camera positions didn't need to change while the player was only moving horizontally. I can cache the horizontal and vertical positions separately and only updated them as-needed when the player moves far enough in a specific direction.
Results
Implementing the new spawning technique alongside the cache improved mid-game performance from previously having occasional dips to 15 fps to now maintaining a stable 30 fps. My game is essentially the worst-case type of game for the Playdate and its screen since it requires constantly redrawing elements everywhere. But that doesn't mean that I can't solve various issues! All I need is a bit of targeted logic and the attitude to learn as I go while being fine with constantly refactoring my code to implement what I've learned.
Other updates
I've made a few gameplay changes since my last blog post that I want to highlight while I'm writing this post.
Camera lerping
I implemented lerping using the smoothstep algorithm. This kicks in when the player first kills the tutorial zombie to provide a sort of "aha" moment and slowly reveal that there is a larger map to explore beyond the small screen. I plan to also use this to snap the camera back on bosses that spawn and constrict the player's movement to the screen again until the boss has been defeated.
Lightning spell
Only having one spell was growing to be boring, so I added a new lightning spell that works similarly to a shotgun. It fires 3 lightning bolts in a spread. As the cast spell travels further, the bolts inflict less damage and have a lessened knockback effect on enemies. This is best used up close with riskier tactics.
Item despawning
Finally, it felt wrong that items remained on the map forever, so I updated them to despawn after a certain amount of time. This helps add a sense of urgency for the player to pick them up while also potentially helping the player if the item spawns again in a more favorable location when the player is pinned on one side of the map.