Jump to content

Simple Lua OOP (without metatables)


RoboMat

Recommended Posts

Simple Lua OOP




I
II
III
IV
V
VI
VII
VIII


I. Foreword

I haven't written a tutorial in a while and since I had some free time I wrote a quick one :) Today's tutorial isn't specifically about PZ-Modding but about a simple approach to OOP in Lua. I'm not going to talk a lot about the technical details (if you understand those you probably don't need this tutorial anyway).

We'll take a look at the closure–approach. That's the main approach I use when I write my own game projects. The good thing about it is that we don't need any of that crazy metatable stuff every serious lua programmer is constantly talking about.



II. Basic Class

We'll start with this class skeleton:
Character = {};function Character.new()	local self = {};		return self;end
This "Character" class serves as a public entry point which allows us to call the contained new() function from anywhere in our program (I'll call it class even if some people out there might be offended by that).

The new() function can be compared to constructors that other OOP languages have. It creates a new object of type Character and returns it. While coming from the same class all those character objects will be totally independent from each other, which is awesome.

The actual object is created with "local self = {}". This creates a table to hold all of the public information our object will have. I will explain this a bit more detailed in a moment. For now just note that "local self = {}" creates an (empty) object which is returned to the outside world at the end of the new() function via "return self".

At this point we can already create a new character with this line:
local hobbit = Character.new();
Obviously our object is just an empty table and therefore pretty boring. So let's change that.



III. Adding fields

Let's add a public variable (or field if you will) to our class:
Character = {};function Character.new()    local self = {};    self.name = "Frodo";    return self;end
Now, every object we create will have the name "Frodo", which we can access like this:
local hobbit = Character.new();print(hobbit.name); -- prints "Frodo"
This is still pretty straight forward. If you look at the hobbit object from the above example it doesn't really know anything about the Character class or even the new() function. All it knows is it"self". Therefore by storing name (or any variable) in the "self" table the object can access it.

This leads us to our next example which displays nicely why you'd need objects in the first place and how awesome they are. Lets say we want to add a dwarf character then the following will happen:
local hobbit = Character.new();local dwarf = Character.new();print(hobbit.name); -- prints "Frodo"print(dwarf.name); -- prints "Frodo"
Dwarfs never are called Frodo so we need to change that. But how do we do that? If we change the "self.name" in Character.new() to something different then now the hobbit will have the wrong name. There are two ways to solve this (there might be more but we'll ignore them for now).

We could use a parameter to determine the name of the the object upon it's creation:
Character = {};function Character.new(name)    local self = {};    self.name = name;    return self;end
This would work like this:
local hobbit = Character.new("Frodo");local dwarf = Character.new("Gimli");print(hobbit.name); -- prints "Frodo"print(dwarf.name); -- prints "Gimli"
The other (sligtly easier) way would be to change the name directly like this:
local hobbit = Character.new();local dwarf = Character.new();-- We can change the variables directly since they are public.hobbit.name = "Frodo";dwarf.name = "Gimli";print(hobbit.name); -- prints "Frodo"print(dwarf.name); -- prints "Gimli"
If you are new to coding or OOP, this might confuse you, since we changed "self.name" which both objects obviously have (or else we'd get a nil reference). BUT as you can see, they are independent from each other. Basically EVERY object we create from the Character class will have its own name and we could set it to whatever we want without affecting the other objects.

This is also still pretty easily explainable. Just remember that the Character.new() function creates a new table each time it is called, so if we add a variable to that table it of course only exists in there.



IV. Adding a public method

Now our object's already have cool names, but we them to do cool stuff to so we'll need functions. Since functions in lua are basically code hiding behind a variable name we can add them to the class like a public variable:
 
Character = {};function Character.new(name)    local self = {};    self.name = name;    self.speak = function(words)            print(words);    end    return self;end
Or by using lua's syntactic sugar (which I highly recommend, since the syntactic sugar does a little more under the hood than just making it nicer to look at and saves you from a few pain-in-the-ass problems later on down the road):
Character = {};function Character.new(name)    local self = {};    self.name = name;    function self.name(words)            print(words);    end    return self;end
We now can make the hobbit speak like this:
local hobbit = Character.new("Frodo");local dwarf = Character.new("Gimli");hobbit.speak("Damn it Gandalf!"); -- Will print "Damn it Gandalf"dwarf.speak("I hate elves!"); -- Will print "I hate elves"
Now you basically already now enough to create a lot of different objects with all sorts of crazy behaviour. There still are a few more cool things I'd like to show you.



V. Inheritance

One, if not the most important aspect of OOP is Inheritance. Simply said, it just allows one object to inherit certain things from a parent (think about how parents pass on some characterstics like hair color etc. to their children).

This example might show what that means in practise:
-- Character class.Character = {};function Character.new(name)    local self = {};    self.name = name;    function self.speak(words)        print(words);    end    return self;end-- Hobbit class.Hobbit = {};function Hobbit.new(name)    local self = Character.new(name);    self.hasRing = true;    return self;end-- Dwarf class.Dwarf = {};function Dwarf.new(name)    local self = Character.new(name);    self.hasBeard = true;    return self;end
Note that we now have two new classes for hobbits and dwarfs. If you look closely at their new() functions they don't create a new table via "local self = {}". Instead they call the constructor of their parent class "Character", get the new table from that class and use it, to implement their own custom characteristics (hobbit's can have rings / dwarfs can have beards).
 
local hobbit = Hobbit.new("Frodo");local dwarf = Dwarf.new("Gimli");print(dwarf.name) -- Gimliprint(dwarf.speak("Throw me over there!"));print(hobbit.hasRing) -- trueprint(hobbit.hasBeard) -- Causes error since the hobbit object doesn't know how have a beard
Objects can even overwrite inherited behaviour from their parents like this:
-- Hobbit class.Hobbit = {};function Hobbit.new(name)    local self = Character.new(name);    self.hasRing = true;        function self.speak()         print("I overwrite my parent's behaviour!");    end    return self;end

VI. Hiding from the world

All the above examples had ome major flaw. All of their informations and implementations were public and therefore easily changeable from the outside. The explanation for why private variables are private isn't easy, but the implementation is:
Character = {};function Character.new(name)	local self = {};		local name = name; -- local / private variable		function self.getName()		if name then			return name;		else 			return "No Name";		end	end		return self;end
That's actually all it takes to hide a variable from the outside world.
 
local hobbit = Character.new("Frodo")print(hobbit.name) -- can't access name directly -> nilprint(hobbit.getName()) -- prints "Frodo"
Now you might ask yourself, why the hell this would be a good idea. Our example already shows one pro. We can make sure that the object has a name (for example if the programmer forgets to pass one along during the function call) or return "No Name" instead. Of course this is a highly artificial example but I hope it gets the point across ;)

The next question you should ask is how our objects can have seperate names now if they aren't stored in the "self" table anymore. Here comes the part which is a bit more complicated and I'll try to explain it as simple and as short as I can (if you want a more in-depth explanation take a look at this: http://www.lua.org/pil/6.1.html).

The important word here is "closures". Basically what it comes down to is that a function can always "peek" outside of its own body and look at local variables of the function in which it is enclosed.
 
local function enclosingFunction()	local foo = 20;		local function enclosedFunction()		print(foo); -- can see foo from the enclosingFunction()	endend
If you look back at our example from above, you can clearly see that Character.new() is the enclosingFunction() while self.getName() is the enclosedFunction() (of course there can be many more enclosedFunctions with access to name in our object). This means that as long as our object exists, the local variable "name" will live on and on and on ...
Character = {};function Character.new(name)	local self = {};		local name = name; -- local / private variable		function self.getName()		if name then			return name;		else 			return "No Name";		end	end		return self;end
Personally I rarely use public variables and instead write getters and setters if they are needed. It just feels cleaner to me, that I have total control over how and when the variable of an object changes.

I admit that this is a pretty short and non-technical explanation, but trying to explain it in detail is far beyond the scope of this (easy) tutorial. If you really wanted to understand it you'd need to also know about the stack and how memory is handled and all the other boring (well actually it is really interesting) stuff.



VII. Performance

Since performance apparently is very important in game programming another question might be how fast this approach is compared to all the others. My answer is: I don't know and frankly I don't care :)

This article suggests that it might be faster, some others suggest it is not, but in the end the difference is probably neglectable anyway.


I just use what feels right and is nice to use (so I kind of prefer good code over perfect performance). I'm also convinced that getting stuff done is much more important than premature optimisation (thanks to a finnish guy called Antti who taught me this important lesson ;))



turbotutone posted some nice info about performance here.


VIII. Using Modules

!Note: This won't work in PZ since the require function is broken!

I just want to add another section on how to use the classes as proper modules. Up until now all of our classes were stored in a global table and therefore accessible anywhere in our program.

To do this we have to make the table local and return it at the end of the file:

local Class = {};function Class.new()	local self = {};	-- Code ...
    
    	return self;endreturn Class;
Now if we want to use it in a different file we will have to "require" it first:
-- Load the module.local Foo = require('path/to/Class');-- Use the module as before:local object = Foo.new();
That's all we have to do.
Link to comment
Share on other sites

Very neat and clean article!

 

Since performance apparently is very important in game programming another question might be how fast this approach is compared to all the others. My answer is: I don't know and frankly I don't care :)

 

 

As for the performance, my 2 cents from memory ^^

This approach gets its performance boost from upvalue variables from what ive gathered:

function Test.new()    local self = {};    local testvar = 1;   -- <-- upvalue variable    function self.printvar()        print(testvar);    endend

which, if i recall correctly, are accessed faster then anything else. The only big con's would be:
- More memory usage for a constructed object

- Slower construction of objects

- functions cannot be shared or accessed indepently as sugar syntaxed ones can

 

In most cases neglectable, however, if you are creating loads of objects on the fly, you might want to stick to a tabled approach with sugar syntaxed functions.

Also with inheritance, the local variables (upvalues) within your class are not shared with inherinting structures so subsequently you have to puzzle a bit to keep up the performance gain with inheritance :P.

 

Another thing worth mentioning when it comes to performance perhaps, if you have lots of calls to one or more global functions within your class, for example:

function Test.new()    local self = {};    function self.printvar()        return ZombRand(100) + ZombRand(100) + ZombRand(100);    endend

You can save global table lookups by referencing ZombRand locally like:

function Test.new()    local self = {};    local myRand = ZombRand;    function self.printvar()        return myRand(100) + myRand(100) + myRand(100);    endend

which looks up ZombRand globally only once uppon creation and uses local reference on function calls.

Link to comment
Share on other sites

  • 8 months later...
  • 3 months later...

First of all, I'd like to thank you so much for this well written and easy to understand tutorial. After following this I've managed to get a great deal of work done on my first roguelike project in Lua. However, I have one problem I haven't been able to get around, and I'd like to ask your help.

 

I'm trying to setup a constructor for my objects, so that on creation, objects will pass along a message to my debug window. The issue is that if I run self.create in a parent object, it will print the parent's name. However if I run self.create in the parent as well as the child object, I get duplicate messages. I've been unable to get a constructor to run only once, using the data in the class that I am trying to create an object of.

 

Here is my parent object:

Object = {} -- Master Class of all objectsfunction Object.new()    local self = {}    local id = zokEngine.ObjectListAdd(self)    local name = "Object"    self.getID = function()        return id    end    self.getName = function()        return name    end    self.create = function()        zokEngine.print("Object '" .. name .. "' created with id '" .. id .. "'")    end    if self.step == nil then        function self.step(dt)        end    end    if self.draw == nil then        function self.draw()        end    end    return selfend

And here is my child object

Body = {}function Body.new(x, y)    local self = Object.new()    local name = "Body"    self.x = x    self.y = y    self.draw = function()        love.graphics.setColor(255, 255, 255, 255)        love.graphics.rectangle("fill", player.x, player.y, 32, 32)    end    return selfend

Any help would be greatly appreciated. Thank you again for the tutorial!

Link to comment
Share on other sites

Hehe I see you are working with LÖVE - it's a great framework :D

 

This should work:

http://hastebin.com/usihuxomik.lua

 

Your main issue was that your parent class (which needed the "name" variable) only saw its own variable with the value of 'Object' instead of the one you set in the child.

 

For some it's the main drawback, for me it's the main benefit of the Closure Based Approach: If you hide something in the parent / child it really is hidden ^^

 

So you'll have to make sure to communicate the necessary stuff between relatives. The simplest way would be to store everything in "public" variables aka in self.

Link to comment
Share on other sites

Thanks for the quick reply! I see now, I wasn't passing the 'name' along to the parent object. I've only been using Lua for a few days now, but I'm absolutely loving it.

 

Until now the only engine I've gotten anywhere with is Game Maker. As a Linux user, I really wanted to find an easy to use alternative that I could develop on Linux. I'm quite glad I found Love2D.

 

I'm planning on spending more time with Lua/Love2D to become more familiar with game logic before I start delving into C++/SFML, which I think will be my next adventure.

 

I put my current code base up on GitHub (this is my first time using GitHub as well, so I hope I'm doing everything correctly with it).

 

https://github.com/DanielPower/zokEngine

 

PS: Thanks for showing me Hastebin. When I opened it, for a second I questioned if it had somehow read my Atom config. Because it looks almost identical to my setup. Looks like a much nicer alternative to pastebin.

 

http://lookpic.com/O/i2/968/C0ZP7En.png

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...