For a platformer, the most important element is how it feels. If the controls aren't responsive or intuitive, nothing else matters. Good controls require solid physics. Good physics require accurate collisions. Accurate collisions require a well-structured architecture. Right now, I'm focused on building that structure from the ground up.
Lua wasn’t taught at my university, so I'm learning it alongside the Playdate SDK. While the SDK is well-made, I'm intentionally minimizing my use of it. This is not a criticism of Panic's work. I want to implement the systems myself to fully understand each part.
Classes in Lua
I’m using to using object-oriented programming, and Lua doesn’t have the idea of classes as part of it’s core structure. I need to abstract classes manually. It makes sense for most things in side the game to be objects. I don’t need to stick strictly to this paradigm, but I think it’s a great place to start.
I initially wanted to just write closures to simulate object, wrapping everything into a function of functions. The code is super clean, there’s no silly colons. It would not matter at all on a modern system, but dealing with only 16mb of ram, I’d rather get rid of this overhead right away.
I used object.lua from the Playdate SDK’s CoreLibs as a reference. It provides a working class system, but includes methods I either didn’t need or didn’t fully understand. I’ve kept the bare minimum and will push code up and down as needed.
Class = {}
Class.__index = Class
function Class:new()
local obj = setmetatable({}, self)
return obj
end
function Class:print()
for k, v in pairs(self) do
print(k, v)
end
end
So this makes a “Class” table in the global space. Using Class:new() will clone the data members of the table (in this case there are none), and give reference to the “print” method so that only gets created once.
Every single class will derive from “class” so every single object will have a obj:print() to dump all the data members and methods if I need to use it for debugging.
Point = {}
Point.__index = Point
setmetatable(Point, { __index = Class })
-- Class Data Members
function Point:new(x, y)
local obj = Class:new()
setmetatable(obj, self)
obj.x = x or 0
obj.y = y or 0
return obj
end
-- Move a point along the x axis
function Point:move_x(x)
self.x = self.x + x
end
-- Move a point along the y axis
function Point:move_y(y)
self.y = self.y + y
end
-- Move along both axes
function Point:move(x, y)
self:move_x(x)
self:move_y(y)
end
-- Draw the point as a single pixel, or define a radius.
function Point:draw(radius)
if radius == nil then
return DrawPixel(self.x, self.y)
end
return FillCircleAtPoint(self.x, self.y, radius)
end
So far:
Class
DeltaTime (For counting time between frames / button presses, etc)
Point
Vector
Shapes
MainCharacter
ButtonState (For a single button)
Controller State (For the entire playdate controller)
Lua’s dynamic typing makes passing functions down super easy. If “Point:move(x, y)” exists, it will be available to any derived class (like Vector, or Shape)
Right now, MainCharacter is a point, (plus some more). I might introduce an “entity” class between the two to have enemies and NPCs, but until I need to pull that out I will keep it where it is.
The “Template” project is free and open source, hosted here:
https://github.com/lodomo/PlaydateResources
This repository will continue to evolve.
Extras
The import script is carefully crafted. SDK CoreLibs are all imported at the top, commented out, and enabled as needed. No need to go hunting for what is available. Then I put some handy constants into the _G table, like PI.
To streamline testing, I created a Makefile. Running “make run” compiles the game, and launches it directly into the Playdate Simulator. I work in the terminal with Neovim, and Tmux so having a terminal-based workflow is the best for me.
Next time: Basic Collision Detection!
-Lodomo
I'm just going to put a word of warning out there. This is not working how I want it to, but you know... if you want an omelette you gotta code some eggs.