When I was in middle and high school, I made a few of my own video games with GameMaker. I hung this up to pursue other interests, but I recently took up game development again to make one for the Playdate, in part to help justify my purchase.

Unlike developing for modern platforms, there's no game engine like Unity or Godot for Playdate -- everything's written in a text editor and tested either by uploading to the console itself or with an emulator. I chose to write a Dr. Mario clone because it's one of my favorite Game Boy games, which the Playdate closely resembles in concept.

Most of my coding experience (professional or otherwise) has been with scripting, which uses a totally different mindset from game development. A script runs commands once and in the order that they're written. But a game can be in one of several "states" that do different things, runs an "update" function dozens of times per second, and handles real-time user input. So my first challenge was to wrap my head around writing a program like this in the first place. I took a class in college on finite state machines, and that concept applied here.

The next challenge was to learn the Lua programming language. Having little experience with Lua prior to this, my perspective has been mostly shaped by comparing it to Python, which is more complex and resource-heavy, yet also tries to have readable syntax. I came to enjoy Lua's simple syntax, and the fact there's only one complex data structure (tables).

Once I had a working prototype of my game, I immediately realized it needed optimization. Every time I had to update a "cell" on the game board, it made the CPU do EIGHT table lookups. This was because I chose to store the game board as two 2D matrices (a matrix is a table of rows, where each entry is itself a table whose entries represent the columns). My "get data from the board" function also returned a table, so whatever used it needed to look up the relevant piece and discard the rest. Each table lookup takes time and might access memory, which affects performance when done so frequently. And wasted data fills up the memory, so Lua's "garbage collector" needs to run more often to clean up the mess.

I decided that, if I wanted to optimize, I might as well start fresh and use the most efficient solution I could think of. Instead of two 2D matrices, I now store the board as a single table of integers. The code uses bit masking and bitwise operations to store and extract data from the individual bits of an integer, according to a scheme I came up with:

black, white, gray = 1, 2, 3 = 01, 10, 11 in binary
is pill, is virus  = 1, 3 = 01, 11 in binary
cell value = 0 => cell is EMPTY

Integers in Lua are 64 bit, but this game only uses the lowest 16 bits to store data:
  bits 1-2  = color
  bits 3-4  = pill or virus
  5-16      = pill ID.
Example int value of a white pill segment with ID of twenty-two:
0000000000000000 0000000000000000 0000000000000000 |000000010110|01|10

function Game:encode(color,type,id)
  local out = 0
  -- use bitmask to isolate part to update; OR with truncated and bit-shifted input val.
  out = (out & ~(0x3))        | (color & 0x3) -- color
  out = (out & ~(0x3 << 2))   | ((type & 0x3) << 2) -- type
  out = (out & ~(0xFFF << 4)) | ((id & 0xFFF) << 4) -- pill id
  return out
end

-- math to extract specific cell properties, given encoded integer
function Game:get_clr(val) return val and (val & 0x3) or -1 end
function Game:get_typ(val) return val and ((val >> 2) & 0x3) or -1 end
function Game:get_iid(val) return val and ((val >> 4) & 0xFFF) or -1 end -- if val is nil, returns -1

Since the board is now a linear table, its "boundaries" are no longer implicit, so I use integer math to enforce them when necessary (such when moving a pill):

-- math to check if a cell is on an edge of the board
function Game:in_lft_col(idx) return idx % self.col_count == 1 end            -- if true, no LEFT neighbor.
function Game:in_ryt_col(idx) return idx % self.col_count == 0 end            -- if true, no RIGHT neighbor.
function Game:in_top_row(idx) return idx <= self.col_count end                -- if true, no ABOVE neighbor.
function Game:in_bot_row(idx) return idx > (#self.board)-self.col_count end   -- if true, no BELOW neighbor.

The payoff to all this is that now, when I want information from the board, it only requires a single table lookup; the rest is bitwise operations and maybe some basic integer math. These are way more efficient than table lookups, because a CPU's job is ultimately to do math (that's what computers were made for -- to "compute" things), so it can do math very quickly.

Doing a project like this, particularly the optimization stage, gave me a better appreciation for older video games and computer software from the days of 8-bit computers and game consoles. This method of development and clever optimization tricks were NECESSARY back then, because there was much less power to work with. For as underpowered as the Playdate is, it's still way more powerful than an NES or Commodore 64, and programs for those were often written in assembly code for performance, instead of a readable high-level language like Lua.

My game, "Pill Push", is available for purchase here.