One of the issues that seems to confuse beginners is the bewildering variety of Object-Oriented Programming approaches possible with Lua. Though Lua itself has no native/intrinsic OOP facilities, it does offer all the raw features you need to implement something that acts like a traditional OOP language (to one degree or another).
As far as Lua is concerned, an “object” is merely yet another table containing methods and properties. Once this sinks in, it becomes clear that there are numerous ways you might create and populate such a table, as well as countless minor variations on each of the major strategies — thus the many and varied OOP approaches.
This is intended only to be a quick “survey” of some of the more common OOP strategies in Lua – particularly as it applies to Corona developers. It is not intended to be a “how to” (Google is your friend for that) so little explanation of the implementations will be provided. A thoughtful read and a bit of investigation should easily reveal each strategy’s “trick” though.
Implementations are provided in bare-minimum “do-it-yourself” form, but are completely self-contained and run-able as-is. Complete sample source code here.
Disclaimer: The sample code is meant for illustrative purposes only as material to accompany this “survey” — you are on your own if you insist on treating it as production code despite this disclaimer.
Those looking for a more polished and complete OOP system should look for one of the third-party libraries such as 30log or middleclass or others. Usage of any of these OOP libraries is not covered here. (though each such library likely follows one or more of these ‘patterns’ or variations thereof)
As a sample “case study”, each implementation below will consider a game scenerio, with two game “entities”: a “player” and an “enemy”, each with its own display object for visual representation, as well as some custom properties (fe name) and methods, as well as getters/setters (fe position) to show the approach for interfacing with the display object. the display object will just be a simple colored rectangle to eliminate the need for image files.
I’ll start with what I consider to be the simpler approaches, then move toward the more complex approaches.
1. Decorated Display Objects
Probably the simplest way to add custom behavior to a display object — just create a display object then add your custom properties and methods to it directly. This may tend to produce a lot of duplicated code if your objects are closely related, and doesn’t yet support the creation of multiple instances, but might be suitable if only a few unique “singletons” are needed.
|
local player = require("Player") local enemy = require("Enemy") player:setX(player:getX()+100) player:setY(player:getY()+100) player:speak() enemy:setX(enemy:getX()+100) enemy:setY(enemy:getY()+200) enemy:speak() |
|
local Player = display.newRect(0,0,10,10) Player:setFillColor(0,1,0) Player.name = "player" function Player:getX() return self.x end function Player:getY() return self.y end function Player:setX(x) self.x=x end function Player:setY(y) self.y=y end function Player:speak() print("Hello, I am "..self.name) end return Player |
|
local Enemy = display.newRect(0,0,10,10) Enemy:setFillColor(1,0,0) Enemy.name = "enemy" function Enemy:getX() return self.x end function Enemy:getY() return self.y end function Enemy:setX(x) self.x=x end function Enemy:setY(y) self.y=y end function Enemy:speak() print("Hello, I am "..self.name) end return Enemy |
2. Factory Function with Closures
This strategy typically develops out of the desire to eliminate some of the duplicated code that can occur with 1) above by supporting “instances”. The use of closures also offers an opportunity to support truly private members, though each individual instance still remains a bit “heavy” with its own unique copy of each method.
|
Entity = require("Entity") local player = Entity("player",{0,1,0}) player:setX(player:getX()+100) player:setY(player:getY()+100) player:speak() local enemy = Entity("enemy",{1,0,0}) enemy:setX(enemy:getX()+100) enemy:setY(enemy:getY()+200) enemy:speak() |
|
local function Entity(name,color) local entity = display.newRect(0,0,10,10) --entity.name = name -- not needed, in closure entity:setFillColor(unpack(color)) function entity:getX() return self.x end function entity:getY() return self.y end function entity:setX(x) self.x=x end function entity:setY(y) self.y=y end function entity:speak() print("Hello, I am "..name) end return entity end return Entity |
3. Factory Function with Closures and Statics
This strategy typically develops out of the desire to retain private closure members from 2) above, while reducing the “weight” of each individual instance by referencing a set of “static” function for non-closure members.
|
local Entity = require("Entity") local player = Entity("player",{0,1,0}) player:setX(player:getX()+100) player:setY(player:getY()+100) player:speak() local enemy = Entity("enemy",{1,0,0}) enemy:setX(enemy:getX()+100) enemy:setY(enemy:getY()+200) enemy:speak() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
local function entityGetX(self) return self.x end local function entityGetY(self) return self.y end local function entitySetX(self,x) self.x=x end local function entitySetY(self,y) self.y=y end local function entitySpeak(self) print("Hello, I am "..self:getName()) end local function Entity(name,color) local entity = display.newRect(0,0,10,10) entity:setFillColor(unpack(color)) entity.getX = entityGetX entity.getY = entityGetY entity.setX = entitySetX entity.setY = entitySetY entity.speak = entitySpeak entity.getName = function(self) return name end return entity end return Entity |
4A. Factory Function via Prototype
This strategy typically develops from prior “factory” methods just as a means to simplify populating a newly created instance with its members from a predefined prototype or “pattern”.
|
local Entity = require("Entity") local player = Entity("player",{0,1,0}) player:setX(player:getX()+100) player:setY(player:getY()+100) player:speak() local enemy = Entity("enemy",{1,0,0}) enemy:setX(enemy:getX()+100) enemy:setY(enemy:getY()+200) enemy:speak() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
local EntityPrototype = { name = "entity", getX = function(self) return self.x end, getY = function(self) return self.y end, setX = function(self,x) self.x=x end, setY = function(self,y) self.y=y end, speak = function(self) print("Hello, I am "..self.name) end } local function Entity(name,color) local entity = display.newRect(0,0,10,10) for k,v in pairs(EntityPrototype) do entity[k] = v end entity.name = name entity:setFillColor(unpack(color)) return entity end return Entity |
4B. Factory Function via Prototype (Alternate Syntax)
This strategy is equivalent to 4A above, just worded a bit differently where the prototype is declared. Those wishing to implement a “prototype” approach will often want to pass in some other “class” which serves as the “pattern”, and the syntax used to establish the “pattern” in this variation perhaps better illustrates that approach. (you may also want a “deep” copy, rather than the shallow one here – an implementation detail beyond the scope of this survey, just mentioned in passing)
|
local Entity = require("Entity") local player = Entity("player",{0,1,0}) player:setX(player:getX()+100) player:setY(player:getY()+100) player:speak() local enemy = Entity("enemy",{1,0,0}) enemy:setX(enemy:getX()+100) enemy:setY(enemy:getY()+200) enemy:speak() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
local EntityPrototype = {} EntityPrototype.name = "" function EntityPrototype:getX() return self.x end function EntityPrototype:getY() return self.y end function EntityPrototype:setX(x) self.x=x end function EntityPrototype:setY(y) self.y=y end function EntityPrototype:speak() print("Hello, I am "..self.name) end local function Entity(name,color) local entity = display.newRect(0,0,10,10) for k,v in pairs(EntityPrototype) do entity[k] = v end entity.name = name entity:setFillColor(unpack(color)) return entity end return Entity |
5. Wrapper Class
This strategy typically develops once some need for more-elaborate inheritance is identified. At some point it may become more tedious to continue “decorating” display objects directly (as all previous methods have done), and instead treat the “view” (the display object) as separate from its “model” (the Lua class). Something along these lines is a typical first “baby step” implementation of metatable classes.
|
local Entity = require("Entity") local player = Entity:new("player",{0,1,0}) player:setX(player:getX()+100) player:setY(player:getY()+100) player:speak() local enemy = Entity:new("enemy",{1,0,0}) enemy:setX(enemy:getX()+100) enemy:setY(enemy:getY()+200) enemy:speak() |
|
local Entity = {} local Entity_mt = {__index=Entity} function Entity:new(name,color) local instance = {} instance.name = name instance.sprite = display.newRect(0,0,10,10) instance.sprite:setFillColor(unpack(color)) return setmetatable(instance,Entity_mt) end function Entity:getX() return self.sprite.x end function Entity:getY() return self.sprite.y end function Entity:setX(x) self.sprite.x=x end function Entity:setY(y) self.sprite.y=y end function Entity:speak() print("Hello, I am "..self.name) end return Entity |
6. Metatable Class Inheritance
This strategy typically develops directly from the needs identified in 5) above. Now that a metatable-based class system is in place, it is now possible to consolidate shared functionality in a super-class and derived special-purpose subclasses from it.
|
local Player = require("Player") local Enemy = require("Enemy") local player = Player:new() player:setX(player:getX()+100) player:setY(player:getY()+100) player:speak() local enemy = Enemy:new() enemy:setX(enemy:getX()+100) enemy:setY(enemy:getY()+200) enemy:speak() |
|
local Entity = {} local Entity_mt = {__index=Entity} function Entity:new(name,color) local instance = {} instance.name = name instance.sprite = display.newRect(0,0,10,10) instance.sprite:setFillColor(unpack(color)) return setmetatable(instance,Entity_mt) end function Entity:getX() return self.sprite.x end function Entity:getY() return self.sprite.y end function Entity:setX(x) self.sprite.x=x end function Entity:setY(y) self.sprite.y=y end function Entity:speak() print("Hello, I am "..self.name) end return Entity |
|
local Entity = require("Entity") local Player = setmetatable({},{__index=Entity}) local Player_mt = {__index=Player} function Player:new() local instance = Entity:new("player",{0,1,0}) return setmetatable(instance,Player_mt) end return Player |
|
local Entity = require("Entity") local Enemy = setmetatable({},{__index=Entity}) local Enemy_mt = {__index=Enemy} function Enemy:new() local instance = Entity:new("enemy",{1,0,0}) return setmetatable(instance,Enemy_mt) end return Enemy |
7. Metatable Class Inheritence with Function Helper
This strategy typically develops out of an attempt to better “organize” the functionality of 6) above in preparation for an even more elaborate inheritance hierarchy. Some type of helper functions (as here) or a “base class” with similar functions, is used to create/extend classes instead of inline code. This helps prep for storing class names (for identification) or a reference to the superclass (for instanceOf or super calls) within this class creation “framework”. (if you’re still writing DIY code at this point, then it’s perhaps time to start seriously considering whether one of the third-party libraries mentioned up top might save you some effort)
|
require("Class") local Player = require("Player") local Enemy = require("Enemy") local player = Player:new() player:setX(player:getX()+100) player:setY(player:getY()+100) player:speak() local enemy = Enemy:new() enemy:setX(enemy:getX()+100) enemy:setY(enemy:getY()+200) enemy:speak() |
|
function class() local c = {} c.__index = c return setmetatable(c,{__index=c}) end function extends(c) local s = {} s.__index = s return setmetatable(s,getmetatable(c)) end |
|
local Entity = class() function Entity:new(name,color) local instance = {} instance.name = name instance.sprite = display.newRect(0,0,10,10) instance.sprite:setFillColor(unpack(color)) return setmetatable(instance,self) end function Entity:getX() return self.sprite.x end function Entity:getY() return self.sprite.y end function Entity:setX(x) self.sprite.x=x end function Entity:setY(y) self.sprite.y=y end function Entity:speak() print("Hello, I am "..self.name) end return Entity |
|
local Entity = require("Entity") local Player = extends(Entity) function Player:new() local instance = Entity:new("player",{0,1,0}) return setmetatable(instance,self) end return Player |
|
local Entity = require("Entity") local Enemy = extends(Entity) function Enemy:new() local instance = Entity:new("enemy",{1,0,0}) return setmetatable(instance,self) end return Enemy |
8. Display Object Metatable Chaining
This strategy typically develops out of a need to “monkey patch” something deep within the internals of a Corona object. It is not typically seen in usage for more general-purpose OOP needs where a simpler decorator pattern would likely suffice. As such, the sample used throughout this post is really not an appropriate use for this strategy – it is provided merely to close out the metatable portion of this survey.
|
local Entity = require("Entity") local player = Entity:new(display.newRect(0,0,10,10), "player", {0,1,0}) player:setX(player:getX()+100) player:setY(player:getY()+100) player:speak() local enemy = Entity:new(display.newRect(0,0,10,10), "enemy", {1,0,0}) enemy:setX(enemy:getX()+100) enemy:setY(enemy:getY()+200) enemy:speak() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
local Entity = {} local Entity_cmt = { __index = function(t,k) local ret = Entity[k] if (ret==nil) then ret = t._cmt.__index(t,k) end return ret end, __newindex = function(t,k,v) local ret = t._cmt.__index(t,k) if (ret~=nil) then t._cmt.__newindex(t,k,v) else Entity[k]=v end end } function Entity:new(dispobj,name,color) dispobj._cmt = getmetatable(dispobj) local instance = setmetatable(dispobj, Entity_cmt) instance.name = name instance:setFillColor(unpack(color)) return instance end function Entity:getX() return self.x end function Entity:getY() return self.y end function Entity:setX(x) self.x=x end function Entity:setY(y) self.y=y end function Entity:speak() print("Hello, I am "..self.name) end return Entity |
Conclusion
So, is this a full and complete list of all possible approaches to Lua OOP? Hah! Nope. Not even close. But it’s a pretty good start on some of the more common ‘fundamentals’ used by more elaborate approaches.
Once you grasp these ‘fundamentals’ then it becomes more obvious how you might extend them further to support other OOP concepts. (though again, as previously stated several times, there are existing third-party libraries that you might want to investigate and potentially save some development effort if you can find one that fits your needs)
I’ll suggest that there is no single best approach for all users, under all circumstances, particularly given the vast range of experience levels among Corona developers. So just select an approach that best works for you. Hopefully this survey might help a bit in weeding through those choices.