I am about to try to create simple 2D game using Lua. I have watched a couple of videos and tutorials and also checked the syntax, but I am confused by multiple developers pursuing different approaches to OOP.
Some uses metatables, some consider it too much complex and create object using like functions that return table.
And I am lost... It is not straightforward what I should use.
I want to use real OOP, like for example Java or C# have.
I saw many videos where they are creating 2D games where they don't use metatables because of the difficulty to read and work with metatables.
What shall I choose though?
As I like to quote, "Lua gives you the power, you build the mechanisms". It is no different for OOP in Lua. This is probably what's leaving you confused.
I think this is the first misconception we need to eliminate: Java / C# OOP isn't "real" OOP. It is a particular implementation / variant of OOP called "class-based OOP" which has good language support both in Java and C#. It is as real as Lua's (or JavaScript's, for that matter) prototype-based OOP implementations are, though these tend to leave you more degrees of freedom.
Let's get started. First, we'll look at some ways to build class-based OOP in terms of metatable-based OOP. For that, we first need to understand what a metatable is.
Metatables
Consider the following snippet:
Suppose we want to throw an error if we try to access an absent field. We can't do that, right? Because
.is not a normal function. It's built in to the language. We can't change what it does. We could introduce a function, but writingget(my_math, "pi")ormy_math.get"pi"or similar would be pretty verbose. No, we want to keep the concisemy_math.pinotation, but we want to change what it does.Enter metatables: They let you do just this - change the behavior of
t.name(technically, more generally the behavior oft[k];t.nameis just syntactic sugar fort["name"]).When you do
t[k], Lua actually does the following work behind the scenes:kint. If this is notnil,t[k]evaluates to this value.t[k]evaluates tonil.__indexfield which stores a functionf, callt[k]evaluates tof(t, k). This lets you customize what happens when absent keys are accessed! That's exactly what we want.my_math.taushould do nothing special, it should just give us the value of 2 pi.But if and only if we access an absent field - as is the case for
my_math.pi- the__index"metamethod" of the metatable ofmy_mathwill be called! This lets us throw an error:Now, if you access
my_math.pi, you'll get an error. But__indexis more powerful than that even. You can return another value. The most frequent use for__indexis for "defaults" however, which is why Lua provides language support for this:__indexcan be a tablemt, in which caset[k]evaluates tot[k]ift[k]is notnilandmt[k]otherwise. Consider the following example:__indexis not the only metamethod - there are also metamethods for arithmetic, concatenation, comparisons, table length (#), each with their own rules (see the metatable docs in the 5.1 reference manual) - but it is the only one that's really necessary for implementing metatable-based OOP. Some scripting languages, like JavaScript, only have the equivalent of__index(called__prototype__in JS).Some syntactic sugar
It's also helpful to be aware of the following frequently used syntactic sugar:
function class:method(...) ... endis equivalent tofunction class.method(self, ...) ... end: It adds an implicitselfas a first parameter. (As my naming suggests, this is frequently used for methods.)instance:method(...)is roughly equivalent toinstance:method(instance, ...). This is usually used for method calls, passing the object as the first argument (note how this corresponds to theclass:methodsyntactic sugar in the function definition: theinstanceis passed forself).Your first class
Now we can get back to the core of the issue: How can we implement a class? Well, what is an instance, if not a table where absent keys should be looked up as "method" names - as opposed to fields - in the parent (class) table? Since you're interested in gamedev, let's start work on a rudimentary 2d vector class. I will meticulously separate all tables here to make it clear which is which. There are various variants on this. Some variants conflate metatable & class table. Others conflate class table & method table. All of this works, but is not helpful in understanding it.
First, let's make the tables. They are all closely tied: The methods may need to call constructors or other "static" (using Java terminology) functions, which are stored in the class table. The constructors will need to set the metatable. The metatable needs to have its
__indexfield set to the methodtable, such that accessinginstead.methodgives us the proper method:Let's start with the method table. This will be the table behind the
__indexfield in our metatable. Now we can write the constructors.Let's have a method to add two vectors, and a method to multiply a vector by a scalar.
Et voilà! Your first class! You can now do:
This is really the fundament of metatable-based (prototype-based) OOP. You can now build on this, adding support for:
getmetatable(instance)againstmetatable)methodsto the parent'smethodstable) - I'd urge you to consider "composition over inheritance" though;metatable);Note that you get polymorphism "for free" through the metatable, and field access "for free" through the fact that your object is just a table.
But, this is indeed not the only way to implement OOP. Another popular way is what I call "closure-based OOP", which has its advantages and disadvantages. Many consider it simpler to understand.
Closures
Lua has first-class closures: These functions hold references to the variables surrounding them ("upvalues"), allowing them to read & write these "upvalues". A simple example:
set_foocan writefoo, andget_foocan read it.foois what I call a "shared" upvalue ofset_fooandget_foo: Both could read & write it, and the changes are visible in the respective other closure.Closures - and with them, the (fresh) set of upvalues - get created by
functiondefinitions. Closures are what allows Lua's lexical scoping to work so seamlessly.We can use this to write a vector class using closures. Here's how it looks:
This lets us change the way we call methods; we no longer have to pass the instance, because the methods already know the instance as an upvalue:
Comparison
I'll let you be the judge which of these two approaches you consider simpler. Since you're still new to Lua, I would recommend starting with that one. After that, learn the other one; you should know both.
The metatable-based approach shines in terms of performance: You don't need to create many upvalues or closures for each instantiation. It also enables operator overloading further down the road (you will likely want this for vectors, for example). I'd prefer this the more objects and instantiations you expect there to be (for example I would prefer it for vectors, which may be used very frequently).
Where metatable-based OOP doesn't shine is encapsulation: There are no "private" variables. You can only have "private by convention" table fields. In the closure-based approach, you can use
localvariables as true private fields only accessible to the set of closures (methods).(There are some more nuances, but this is the gist of it.)