The action portion of this game was originally inspired by Vampire Survivors. In order to meet that standard, I had to maximize the number of instances that could be shown on the Playdate's screen. The sprite API and built-in collision system couldn't meet these needs, so I structured all instance management as parallel arrays, where an index acts as an object ID in a series of arrays.
The Playdate doesn't have very much operating power, but it has a lot of memory. And Lua is significantly faster with local variables, so setting up data on the initial load will already help the game run faster. Creating an instance is simply setting all the array values at the last index.
var i = 1
while i <= activeBullets then
moveBullet(i)
drawBullet(i)
deleteBullet(i)
i += 1
end
The update call for these instance managers loops through every index and updates the appropriate array values. These are used to draw the image directly to the screen, check if the instance should be removed, and other interactions. Drawing directly to the screen works here since there's already a specific draw order. We only operate on active instances, which helps manage processing quantity for other types of instances as well.
The goal is to simplify as much of the drawing process as possible. All instances are drawn in a specific order, one on top of the other. This does mean that a pixel might be drawn over multiple times, but there is so much moving on the screen at any point that this inefficiency is okay. This is also a very limited draw order, but it works with the perspective the art style has so far. This is why it's possible to iterate over and draw all groups of instances at once.
Deleting an instance is done by overwriting its data with the that of the last instance, and reducing the active instance count. The order of an instance doesn't matter for this game, so we can take advantage of that. And this is pretty cheap too! Since the index was replaced, we can continue through the loop without needing to restart it.
local MOVE_BULLET = [
function(i, type, pos, target) --bullet type 1
...
function(i, type, pos, target) --bullet type 2
...
function(i, type, pos, target) --bullet type 3
...
]
Lua has this cool feature of creating functions in tables! We can have an array of instance types, and order the calculations by type. For a single instance, this is slightly faster than a series of if-statements, but it saves a significant amount of time per frame for larger mixed quantities. This method can also be used for selecting image tables!
Playdate's collision detection is tied its sprite system, which is not accessible when drawing directly to the screen. Fortunately, their system is based off of bump.lua, so I was able to import that library. For every bullet, I can check if a cell has any colliders in it, and if there's anything in that bullet's movement path, perform the collision and delete the bullet. This helps reduce unnecessary operations.
I continued to make micro-optimizations by timing sections of code and finding the best averages while playing on the device. This is how I knew that progress was being made, but a major mistake on my part was not recording my findings as I made changes. This is a lesson I'll carry into future projects.
local resetTime = pd.resetElapsedTime
local getTime = pd.getElapsedTime
local function addTotalTime()
local function printAndClearTotalTime()