RoboMat Posted April 30, 2014 Posted April 30, 2014 Simple Lua OOPIIIIIIIVVVIVIIVIIII. ForewordI 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 ClassWe'll start with this class skeleton:Character = {};function Character.new() local self = {}; return self;endThis "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 fieldsLet's add a public variable (or field if you will) to our class:Character = {};function Character.new() local self = {}; self.name = "Frodo"; return self;endNow, 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;endThis 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 methodNow 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;endOr 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;endWe 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. InheritanceOne, 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;endNote 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 beardObjects 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;endVI. Hiding from the worldAll 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;endThat'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() endendIf 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;endPersonally 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. PerformanceSince 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. harakka, Dr_Cox1911 and turbotutone 3
turbotutone Posted May 4, 2014 Posted May 4, 2014 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); endendwhich, 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 . 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); endendYou 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); endendwhich looks up ZombRand globally only once uppon creation and uses local reference on function calls. Dr_Cox1911 1
RoboMat Posted January 6, 2015 Author Posted January 6, 2015 Fixed some typos and added a section about modules.
DanielPowerNL Posted April 29, 2015 Posted April 29, 2015 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 selfendAnd here is my child objectBody = {}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 selfendAny help would be greatly appreciated. Thank you again for the tutorial!
RoboMat Posted April 29, 2015 Author Posted April 29, 2015 Hehe I see you are working with LÖVE - it's a great framework 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.
DanielPowerNL Posted April 29, 2015 Posted April 29, 2015 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
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now