people often ask about object orientation (OOP) and inheritance, but can get confusing or incomplete guides. I want to help everyone understand how prototypes work!
lua does support object-oriented programming (but does not require it), based on prototype inheritance — not classes.
metatables are part of this but also often misunderstood. this is a complete explainer that I wrote on discord and keep not putting here in a clean way. so I am putting it here as it is!
so what is a prototype?
goblin={
sp=42,
}
function goblin:new(x,y)
local obj={x=x, y=y}
return setmetatable(obj, {__index=self})
end
function goblin:draw()
spr(self.sp,self.x,self.y)
end
gob1=goblin:new(40,40)
gob2=goblin:new(60,70) |
alright so this is like 6 different things working together.
1) table with methods: goblin is a table with property sp and methods new and draw (a method is a function attached to an object)
calling goblin:new(40,40) is the same as goblin.new(self,40,40); self is «the object we are working on»; defining function goblin:draw() is the same as function goblin.draw(self) and the same as goblin={sp=42, draw=function(self) spr(...) end, somethingelse=true}
there is a difference between the two methods in my example: when we call goblin:new, the self param inside it will be the goblin table itself (the prototype), but when we have gob1:draw() then self inside draw will refer to gob1.
2) metatables. there are special methods that lua uses to implement operators (+, /, .., etc). the method for addition for example is __add (we call these metamethods). if I want to be able to do vector1 + vector2for my custom vector tables for example, I need to define a method named __add but I can’t do it in the vector prototype table, lua will not look it there; it is required to define these metamethods in a table’s metatable. (the metatable is similar to a class in other object-oriented languages: it is the template, or the definition, or the model for many tables.) so that’s what happens in goblin:new: I define a metatable with one metamethod and set it on my goblin object.
3) prototype. these are great! if I have 50 goblins on screen and they have different health and coordinates but the same properties for sprite, width, height, etc, and the same methods for update, attack, draw, etc, I would like to define all the shared things in one place, then have custom things in each goblin table. that’s what a prototype is for! here in goblin example we have sp and draw shared, and in a specific goblin table (it starts as obj on line 6, then it’s gob1 or gob2 in my example) we have specific data like x and y.
much more info on this really useful site: https://gameprogrammingpatterns.com/prototype.html
alright so how do I say «gob1 and gob2 should delegate to goblin prototype for some properties»? I define a metatable with an __index property. it is called by lua when I do gob1.sp and sp is not found inside gob1. it can be a function (can be useful to have a dynamic property that’s computed from other properties) or another table: the prototype!
more: https://www.lua.org/pil/13.4.1.html
now this is the cool thing with prototype-based object-oriented language that you don’t have in such a nice way with class-based OO languages
what if I want 40 goblins on screen and 10 archer goblins? these should have a different sprite and different attack method.
archer=goblin:new() archer.sp=52 function archer:attack(target) --use the bow end arcgob1=archer:new(20,50) arcgob2=archer:new(40,70) |
look at this from bottom to top:
arcgob1.xwill be 20,y50- arcgob1’s and arcgob2’s prototype is
archer, because when we callarcher:new,selfon line 6 of my first code block points toarcher arcgob1.spis 52 (the value comes from the prototype)arcgob1:attack(hero)will use ourarcher:attackmethod.arcgob1:draw()will usegoblin:draw! archer itself is a table that has goblin for prototype
drawis not found in arcgob1, look at its prototype archer, not found, look at its prototype goblin, found! call it withselfpointing to arcgob1.
result:
the confusion is that we often see code like this to explain prototypes (or «class», but lua doesn’t have classes):
goblin={sp=42}
function goblin:new(obj)
setmetatable(obj, self)
self.__index=self
return obj
end |
why do we need to add goblin.__index = goblin?? it’s its own prototype? but it’s a metatable? both?!
the thing is, we don’t need that. a metatable is only strictly required to implement metamethods to define what operators do. when we want a prototype, {__index=proto} is the metatable, and proto the prototype. clarity at last!)
hope this helps! please ask follow-up questions if needed, try to implement your own cart and share it!
this guide is complementary for sequence tables (to have multiple goblins and update/draw all of them easily): https://www.lexaloffle.com/bbs/?tid=44686
Hey Merwok, just wanted to thank you for taking the time to make this post!
I'm a fairly new Pico-8/Lua user and this is exactly what I was looking for regarding class inheritance! I will be applying it!
Really nice guide! But I'm a little confused about the second code where "archer=goblin:new()", it doesn't pass x and y into the new(x,y) function. Did I miss some knowledge about this?
good catch! lua doesn’t raise an error there, it just passes nil for values
so archer is a table with x=nil and y=nil, but it has all the methods we want, including new (through its metatable and prototype)
then when we call arcgob1 = archer:new(20,10), we create a new table with x=20 and y=10, using archer as prototype (and thus a different sprite)
this could be done in other ways, for example with different methods to derive a new prototype (say archer=goblin:subclass(sp=52) / arcgob1=archer:new(x,y)), but with lua we are not required to
it’s good to understand that we are not dealing with classes, subclasses and instances here: it’s all tables and prototypes! there is no difference (for lua) of stature between goblin, gob1, archer and arcgob1.
hope this helps!
So I wanted to use this code to do some experiments to make sure I understand how prototypes and tables work (I understand the basics, but I'm murky on details) and... I can't get any of it to work. Most commonly I get an attempt to index global 'goblin' (a nil value) for one reason or another.
Do you have a cart with a working example that one could mess around with, or any advice on how to get this to run?
@wtfpantera I did some experiments of my own and managed to get a working version. Be careful not to confuse ':' with '.' or else you'll get some strange results.
Edit: Here's a better example.
As you can see, each goblin type has a different head and weapon, but they all share the same goblin prototype body without having to do any extra work. Cool stuff!
I also made the buns to show how object-like objects could be made without metatables (it's all tables after all), but it comes with some caveats. Namely, if you add a new variable to the prototype, then you'll have to update the object-creating functions to take in the new variable.
@Verb Thank you! I'm taking it slow for now with this element, but it still helps to have working examples of code, and I do want to engage with this at some point. Appreciate the effort!
Hey! this code is so usefull to me, but i have a doubt, theres a way to use something like "super()", in that way i could use update method to do something general and then my "childs" could make something more in the same method...
thanks for the people who helped while I was away! I realize I forgot to say in the initial post that your _draw function should call gob1:draw() arcgob1:draw() and so on. (I would have then in a table and draw with a loop! that’s the sort of things explained by the other thread I linked, but it is useful to have a complete example here regardless)
about super: that’s an interesting question! I don’t think there is a built-in way, so you would have to make something custom.
- you could use
rawgetto get the prototype table - or change the
newmethod to storeobj.proto = proto - or invert the question and define
prototype:attack(...)to do something likeif (self.attack_extra) self:attack_extra(...)(the method in the prototype calls a method in the child table if defined)
[Please log in to post a comment]




