Ludicrous Software

Simple OOP With Inheritance in Corona: My Approach

(In Jesse Warden’s excellent blog post about his latest weekend with Corona, he commented on the fact that inheritance and calls to the superclass isn’t exactly easy when trying to do OOP in Lua. I fired off an email to him in which I explained, for what it was worth, how I handle inheritance when I’m writing Corona apps, and after a couple emails back and forth with Jesse thought I’d put this approach into a blog post of my own. The short version of this approach is to store a reference to the superclass’s method, and then call it via that reference in the overriding method. If you’d like a much more detailed explanation that also explains my overall approach to OOP in Corona, read on.)

One of the approaches to OOP in Lua is through the use of metatables, but since you can’t set the metatables of display objects in Corona, I avoid that approach to OOP. If you’re committed to metatables, then there’s a workaround I’ve seen posted on the Ansca forums that involves making your display object a property of a plain old Lua table, which you can then subclass, etc. using metatables.

I haven’t tried this. I assume it works, but it bothers me because Corona display objects are effectively Lua tables (if you’re new to Lua/Corona, then keep in mind that a table in Lua is essentially an object in other languages such as ActionScript). So, having to create another table seems redundant.

This is a pretty stripped-down example of how I approach OOP in Corona. There are three files: a BasicBox base “class”, an ExtendedBox class that, as the name implies, extends BasicBox, and a main.lua file for kicking everything off. Here’s a zip file containing the code.

Let’s start with a BasicBox class. This would be a Lua module, with a filename of BasicBox.lua:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module (..., package.seeall)

local function new()

   local box = display.newRect( 0, 0, 30, 30 )

   function box:move()
      self.x = math.random(display.contentWidth)
      self.y = math.random(display.contentHeight)
   end

   function box:setColor()
      self:setFillColor(math.random(255), math.random(255), math.random(255))
   end

   return box

end

This doesn’t do anything terribly exciting: it creates a new Corona display object using the vector drawing API and attaches a couple of methods to it. What those do should be pretty self-evident. Note the reference to self that Jesse talks about in his blog post.

(One thing that I’ll point out is that even though this doesn’t seem to be a convention in Lua, I capitalize the first letter of my file names. This is a carryover from ActionScript, where class names are capitalized. Since I tend to approach modules as class files, this makes sense to me. Your mileage may vary.)

In main.lua, we can now create an instance of our basic box and call the methods attached to it:

1
2
3
4
5
local BasicBox = require("BasicBox")

local box = BasicBox.new()
box:setColor()
box:move()

Nothing earth-shattering here. We’re simply creating an instance of BasicBox by calling the new() function in that module, and then calling the methods attached to it. Once again, you can see what I suspect is non-typical capitalization, in this case of my BasicBox local variable.

Now, let’s say we want to create a different type of box that extends the functionality of the basic box. In addition to being placed at a random position on the screen, this box will also be rotated to some random angle. Also, the box will always be red instead of some random colour. We need another Lua module, this one called (creatively enough) ExtendedBox.lua. Let’s start with this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module (..., package.seeall)

function new()

   local box = require("BasicBox").new()

   function box:move()
      self.rotation = math.random(360)
   end

   function box:setColor()
      self:setFillColor(255, 0, 0)
   end

   return box

end

Next, we need to add some code to main.lua to create an instance of ExtendedBox. Here’s the revised main.lua file:

1
2
3
4
5
6
7
8
9
10
11
local BasicBox = require("BasicBox")
local ExtendedBox = require("ExtendedBox")

local basicBox = BasicBox.new()
local extendedBox = ExtendedBox.new()

basicBox:setColor()
basicBox:move()

extendedBox:setColor()
extendedBox:move()

Again, straightforward. If you test this code, you’ll see that ExtendedBox is correctly overriding the superclass’s setColor() method, so that extendedBox is always red. But there’s a problem: extendedBox isn’t getting positioned properly. It’s getting the random rotation, but it’s always appearing at the 0, 0 position on the screen. This is because the move() method in ExtendedBox needs to call the move() method in the superclass (BasicBox). But they’re both called move(), so how do you do that? We can do that by creating a reference to BasicBox:move() in ExtendedBox, and then calling this in ExtendedBox:move() - this is basically the same as a call to super.foo() in AS3. Here’s how the revised ExtendedBox module looks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module (..., package.seeall)

function new()

   local box = require("BasicBox").new()

   box.superMove = box.move

   function box:move()
      self:superMove()
      self.rotation = math.random(360)
   end

   function box:setColor()
      self:setFillColor(255, 0, 0)
   end

   return box

end

And that’s it. Now, if you test the code again, both boxes on the screen will appear at random positions, which means that ExtendedBox is successfully calling the superclass’s move() method, giving the inheritance we’re looking for.

This is a pretty versatile approach - in addition to using this with my own code, I’ve also used this basic OOP approach to extend the functionality of some of the UI code that Ansca provides. I don’t really need to worry about changes to Ansca’s code (unless they drastically change the API, of course); I can just drop my EnhancedButton class into a project along with their ui module, and it just works.

Ultimately, this akin to OOP using abstract classes. It’s not completely abstract, of course, since BasicBox does actually return a functional display object, but I find the analogy useful, if flawed. For example, the methods in BasicBox could be left empty, so the default behaviour for each method would be to do nothing, and the specifics of implementation would be up to the subclasses. Similarly, if you want to force each of the subclasses to override the abstract methods, you could put an assert() in each abstract method, causing a runtime error. (Yes, I know: you end up with the ability to create a concrete object with abstract methods, when usually it’s an abstract class with potentially concrete methods; like I said, flawed but useful analogy).