Can someone explain/help me understand how classes work in the SDK? (failed code included)

I am trying to understand how classes work in order to use them in a sample game I am building. First, I am trying to create an extremely basic class to better understand. My goal is for the instances of this class to have one value and two methods:

instance.color = "red"
instance.alpha() --> prints "orange"
instance.beta() --> prints "blue"

Based on this example from SquidGodDev, I wrote the following:

import 'CoreLibs/sprites.lua'
import 'CoreLibs/graphics.lua'
import 'CoreLibs/object.lua'

class("TestClass").extends(playdate.graphics.sprite)

function TestClass:init()
	self.color = "red"
	function self.alpha()
		print("orange")
	end
end

function TestClass.beta()
	print("blue")
end

test = TestClass:new()

print(test.color)
test.alpha()
test.beta()

function playdate.update()
	
end

My expected output is:

blue
orange
red

Instead, I receive the error:
main.lua:21: field 'alpha' is not callable (a nil value)

I'm guessing I'm doing something very obviously wrong or using classes wrong. Can someone help me understand, or point me in the right direction? I am programming on OSX in Nova.

Making TestClass into a table instead of a class makes everything work, but that's not the goal of this exercise.
import 'CoreLibs/sprites.lua'
import 'CoreLibs/graphics.lua'
import 'CoreLibs/object.lua'

--class('TestClass').extends(playdate.graphics.sprite) 
--above line causes sim to crash with no error

TestClass = {}
--above line produces expected output

function TestClass:new()
	self.color = "red"
	function self.alpha()
		print("orange")
	end
	return self
end

function TestClass.beta()
	print("blue")
end

test = TestClass:new()

print(test.color)
test.alpha()
test.beta()

function playdate.update()
	
end
I doubt my other attempted solutions are relevant, but they are:

I tried changing "init()" to "new()", and after some fiddling I was able to print blue, but the methods produced the same error above.

I tried including TestClass.super.new(self) and similar in various places, without any luck.

I tried redefining "update()" instead of the methods described above; this appears to have resulted in the test variable inheriting the sprite update function, as it would not produce the error above, but would also not do anything.

I tried reading the CoreLibs directly, but the "object" and "sprite" libraries use programming that is beyond my knowledge regarding metatables (i.e. __index and __call).

I think the main thing here is you'll want to instantiate TestClass (or other user-defined classes that use the SDK class system with an init() method) with test = TestClass() rather than TestClass:new(). That makes your example work, but past that since this is defined as a sprite subclass you'll also want to call TestClass.super.init(self) somewhere in the init() method so the sprite class is properly initialized as well.

In general the Object-oriented programming in Lua and Calling functions sections of Inside Playdate are good starting points to keep in mind as you add more methods and classes, if you haven't come across them yet, and in particular the difference between calling a function with . vs. : is crucial to understand.

It can take some time to fully grasp all the ramifications, especially when callbacks and closures come into the mix, but hope this at least helps a bit!

Thank you! That is a huge help.

I wish there were more information about the playdate's OOP libraries though. I read the OOP in Lua section before posting this, but it's very vague. My understanding is that Lua doesn't natively support OOP, so this part of the SDK is basically...undocumented?

For example, this line:

Classes are provided with an init function.

It's not clear to me what "to be provided with" means here, or when this function is automatically called (if ever). I'm assuming from examples like balls.lua that the init function is called whenever an instance of the class is created with instance = Class()?

(Balls.lua for reference)

import 'CoreLibs/sprites.lua'
import 'CoreLibs/graphics.lua'

local gfx = playdate.graphics

playdate.startAccelerometer()
local gravityx, gravityy = 0, 0

class("ball").extends(gfx.sprite)

local gravity = 0.4
local wallbounce = 0.8
local ballCount = 50

function ball:init()
	ball.super.init(self)
	self.radius = 5
	self:setSize(2*self.radius+1, 2*self.radius+1)
	self:setCollideRect(0, 0, 2*self.radius+1, 2*self.radius+1)
	self:moveTo(math.random(400), math.random(240))
	self:setVelocity(math.random(-5,5), math.random(-5,5))
	self:add()
end


function ball:draw()
	
	gfx.setColor(gfx.kColorBlack)
	
	gfx.setLineWidth(0)
	
	if self.collided then
		gfx.drawCircleAtPoint(self.radius, self.radius, self.radius)
	else
		gfx.drawCircleAtPoint(self.radius, self.radius, self.radius)
	end
	
	self.collided = false
end


function ball:setVelocity(dx, dy)
	self.dx = dx
	self.dy = dy
end


local function hypot(x,y)
	return math.sqrt(x*x+y*y)
end


function ball:collide(c)

	local sx = self.x
	local sy = self.y
	local cx = c.x
	local cy = c.y

	local xd = sx - cx
	local yd = sy - cy
	local d = hypot(xd, yd)

	if d == 0 or d >= 2 * self.radius then return end

	c.collided = true

	local nx = xd / d;
	local ny = yd / d;
	
	local p = (self.dx - c.dx) * nx + (self.dy - c.dy) * ny

	self:setVelocity(self.dx - p * nx, self.dy - p * ny)
	self:moveTo((sx + cx) / 2 + nx * self.radius, (sy + cy) / 2 + ny * self.radius)

	c:setVelocity(c.dx + p * nx, c.dy + p * ny)
	c:moveTo((cx + sx) / 2 - nx * self.radius, (cy + sy) / 2 - ny * self.radius)

end


local function checkCollisions()

	local collisions = gfx.sprite.allOverlappingSprites()	

	for i = 1, #collisions do		
		local collisionPair = collisions[i]
		local sprite1 = collisionPair[1]
		local sprite2 = collisionPair[2]
		sprite1:collide(sprite2)
	end	
end


function ball:update()
	
	if gravity > 0 then
		self:setVelocity(self.dx + gravityx / 4, self.dy + gravityy / 4)
	end
	
	-- bounce off the walls
	
	local left = self.radius
	local right = 400 - self.radius
	
	local newx = self.x + self.dx
	local newy = self.y + self.dy
	
	if newx < left and self.dx < 0
	then
		newx = left
		self:setVelocity(-self.dx * wallbounce, self.dy)
		self.collided = true
	elseif newx > right and self.dx > 0
	then 
		newx = right
		self:setVelocity(-self.dx * wallbounce, self.dy)
		self.collided = true
	end

	local top = self.radius
	local bottom = 240 - self.radius
	
	if newy < top and self.dy < 0
	then
		newy = top
		self:setVelocity(self.dx, -self.dy * wallbounce)
		self.collided = true
	elseif newy > bottom and self.dy > 0
	then
		newy = bottom
		self:setVelocity(self.dx, -self.dy * wallbounce)
		self.collided = true
	end
	
	self:moveTo(newx, newy)
end

-- create the ball sprites
for i = 1, ballCount do
	ball()
end


-- in the case of lots of small sprites spread across the full screen, it's
--  often faster to skip dirty rect checking and redraw the entire screen

local alwaysredraw = false

function playdate.AButtonDown()
	alwaysredraw = not alwaysredraw
	print("always redraw: "..tostring(alwaysredraw))
	gfx.sprite.setAlwaysRedraw(alwaysredraw)
end


function playdate.update()
	gravityx, gravityy = playdate.readAccelerometer()
	checkCollisions()
	gfx.sprite.update()
	playdate.drawFPS(0,0)
end

I think most of my confusion comes from the use of instance = sprite:new() for sprite sub-classes. In the example files (such as "vectorsprite.lua" and "asteroid.lua" in the Asheteroids example), they use instance = subclass:new(). However, in that example, each subclass manually inherits the parent class's :new() method.

I.e. the vectorsprite subclass starts with self=gfx.sprite:new()
function VectorSprite:new(verts)
	local self = gfx.sprite:new()
	local points = {}
	
	self.polygon = geom.polygon.new(table.unpack(verts))
	self.drawnpolygon = polygon

	self._x = 0
	self._y = 0
	self.dx = 0
	self.dy = 0
	self.angle = 0
	self.da = 0
	self.scale = 1
	self.xscale = 1
	self.yscale = 1
	self.strokeWidth = 1
	self:setCollideRect(0,0,1,1)	

It's unclear to me what exactly :new() does with the sprite Class, or whether it is inherited by sub-classes that extend gfx.sprite.

1 Like
For posterity, this is the code that implements wildbat's solution, and achieves the desired behavior:
import 'CoreLibs/sprites.lua'
import 'CoreLibs/graphics.lua'
import 'CoreLibs/object.lua'

local gfx = playdate.graphics

class('TestClass').extends(gfx.sprite) 

function TestClass:init()
	TestClass.super.init(self)
	self.color = "red"
	function self.alpha()
		print("orange")
	end
	
end

function TestClass.beta()
	print("blue")
end

test = TestClass()

print(test.color)
test.alpha()
test.beta()

function playdate.update()
	local a = 1
end

It's not clear to me what "to be provided with" means here, or when this function is automatically called (if ever). I'm assuming from examples like balls.lua that the init function is called whenever an instance of the class is created with instance = Class()?

That's correct, and it can all be a bit confusing because as you say Lua doesn't natively support OOP, so the SDK wrappers are emulating that via the mechanisms that Lua does provide (e.g., tables and metatables), but it's not immediately obvious what's going on under the hood. What "provided" here means is that any "class" defined via the class function (as in class("TestClass")) automatically has an init() function (that does nothing by default, and you can redefine for your own classes) by virtue of inheriting from/extending the Object class.

Fortunately it's not critical to follow all the intricate details, but I wrote a lot more below if you're still curious, otherwise feel free to skip ahead :sweat_smile: If you look at CoreLibs/object.lua in the SDK files you can see how the Object "class" is implemented as a table and defines an empty init() function, with an invitation to override (in subclasses):

Object = {}
Object.__index = Object
Object.class = Object
Object.className = 'Object'

--override to initialize 
function Object:init(...) end

Further down in the file there's a definition for the class function, again the same one that gets invoked when your code says class("TestClass"). Note that the properties argument, also a table, gets assigned to __NewClass.properties.

local __NewClass = {}

function class(ClassName, properties, namespace)
    __NewClass.className = ClassName
    __NewClass.properties = properties
    __NewClass.namespace = namespace    
	return __NewClass
end

Then, further down still there's extends, which, among many other things (skipped in the paste below), grabs __NewClass.properties and calls it Child, then defines a metatable with a function that calls Child.init():

function __NewClass.extends(Parent)

[...]

	local Child = __NewClass.properties or {}

[...]

	local mt = {
		__index = Parent,
		__call = function(self, ...)
			local instance = Child.baseObject()
			setmetatable(instance, Child)
--			instance.__index = instance
-- 			instance.class = instance
			instance.super = Child
			Child.init(instance, ...)
			return instance
		end
		}

	setmetatable(Child, mt)

And this is the mechanism that allows test = TestClass() to call TestClass:init(), or if that function doesn't exist, then playdate.graphics.sprite:init() as TestClass extends that, or if that didn't exist then Object:init(), all the way up the inheritance chain. Same for other functions defined in classes that use this mechanism.

In the example files (such as "vectorsprite.lua" and "asteroid.lua" in the Asheteroids example), they use instance = subclass:new(). However, in that example, each subclass manually inherits the parent class's :new() method.

So the confusion I think comes from the fact that sprites and other parts of the SDK can be used both with the OOP-like scaffolding and without. Looking at those example files, there isn't an invocation of class(), instead Asteroid and VectorSprite are defined as empty tables, with a custom new() function that takes the place of what would be an init() function in a class-based setup, instantiates a sprite with sprite.new() (the non-class-based method to create a new sprite), and call it self. The Asteroid and Vectorsprite new() then defines other functions and sets other properties on the sprite, and then returns it.

But playdate.graphics.sprite also has a init() function for cases in which it's being used with the OOP-like object.lua system, such as when TestClass inherits from/extends playdate.graphics.sprite (you can find more about it in CoreLibs/sprites.lua). Both methods work, but for the OOP Object inheritance to come into play we need to set things up first with class(...).extends(...).

I know you had already marked this thread solved but I hope this helps clear up a few more things, or if not at least give enough useful pointers to move forward from :+1:

1 Like

Thank you! That is a great additional explanation, and is enough to work with. It seems like a deeper understanding will require me to read more about how __index and __call work in Lua metatables (i.e. things like this). If you don't mind though, I have one follow-up question on that topic:

In many relevant parts of the core library, and in examples I've seen, the following line is common:

variable.__index = variable

Sometimes I'll even see this:

variable = {}
variable.__index = variable

Other times I'll see __index defined that way a few lines before being redefined.

What is the purpose of this? In the second example above, is there a difference between that and the following?

variable.__index = {}

Thank you again for the previous explanations. I think they will be a great reference for future beginners like me.

1 Like

Other times I'll see __index defined that way a few lines before being redefined.

What is the purpose of this?

The short answer here is I'm not entirely sure why examples like Asheteroids, which don't fully use the object.lua model, define __index this way. My guess is it's still to allow tables like Asteroid and VectorsSprite to be the topmost parent in a chain of inheritance of sorts. I'll type a longer answer below. I couldn't find an example where __index gets redefined among those files so not sure what's happening there either.

In the second example above, is there a difference between that and the following?

variable.__index = {}

__index in a metatable for a value (variable in your examples) defines what happens when indexing into the value (e.g., variable[key]) with a key that doesn't exist, or when the value is not a table to begin with. __index can be a function or even another table that gets indexed as a fallback. Normally you'd set that metatable on variable with setmetatable() (reference).

My understanding is that in the SDK object.lua file this is used to put in place the inheritance chain by setting a Child metatable for any class you instantiate (that has gone through the class(NewClass).extends(Parent) treatment we established) with an __index metavalue that points to the Parent class. The result is "simply" that when you index into the NewClass the key in question is looked up in the Parentclass if it's not present in the NewClass table, and so on up the chain all the way to Object that is the topmost parent.

Object fits the example you were asking about:

Object = {}
Object.__index = Object

and I think that's done just to terminate the chain, as the Object table is set as the metatable for all its subclasses, with __index meaning if you don't find this key in the subclass, look it up in Object, but there's no further metatable that says what to do if that fails.

Finally going back to variable.__index = variable, this should work similarly, and it's likely that by the time __index comes into play at runtime, variable will have more keys that can be indexed in a valid way. With variable.__index = {}, on the other hand, you would always index into an empty table if variable doesn't have the key that you're looking for, so effectively this should be the same as not defining __index I think, except perhaps with more table lookups? This is probably as far as my understanding goes. Here be dragons :dragon::dragon::dragon:

1 Like