Okarin
Okarin

Reputation: 974

Trying to implement object-oriented programming in Lua, but it's not quite working

Ok, so I'm trying to follow the instructions found here: https://www.lua.org/pil/16.1.html to do something resembling OO programming in Lua (and the LOVE game framework), but it's not working. Here's the core of my code. I have a generic Object class:

local Object = {}

function Object:load(arg)
end

function Object:update(dt)
end

function Object:draw()
end

function Object:new(arg)
    o = {}
    setmetatable(o, self)
    self.__index = self
    o:load(arg)
    return o
end

return Object

and a Ship class that inherits from it:

Object = require('objects.object')

local Ship = Object:new()

Ship.sprite = love.graphics.newImage('assets/sprites/ship.png')
Ship.sprite:setFilter('nearest', 'nearest', 0)
Ship.px = Ship.sprite:getWidth()/2
Ship.py = Ship.sprite:getHeight()/2

function Ship:load(arg)
    self.x = arg.x or 0
    self.y = arg.y or 0
    self.sx = arg.sx or arg.s or 1
    self.sy = arg.sy or arg.s or 1
    self.rot = arg.rot or 0
    self.tint = arg.tint or {255, 255, 255}
end

function Ship:draw()
    love.graphics.setColor(self.tint)
    love.graphics.draw(self.sprite, self.x, self.y, self.rot, 
                       self.sx, self.sy, self.px, self.py)
    love.graphics.setColor({255, 255, 255})
end

return Ship

Now the problem is, I create two of these Ships as members of another object with this code:

self.ship1 = Ship:new({x=50, y=self.y1, s=2, tint={0, 0.5, 1}})
self.ship2 = Ship:new({x=750, y=self.y2, s=-2, tint={1, 0.5, 0}})

But when I draw them, I only see one - the second. As it turns out, it's like the code above doesn't assign the new instances of Ship to ship1 and ship2, but directly to self, for reasons that I can't understand. Did I do something wrong or is this a weird bug of the interpreter?

Upvotes: 1

Views: 943

Answers (2)

Dimitry
Dimitry

Reputation: 2358

It is not bug of the interpreter, it is designed behavior of the language. o={} creates global variable, which is not expected by the programmer here. "Why it is so?" is a frequent question to the language creator. There are many efforts to take control over that behavior simpler .

o = {} without local creates global variable global variable is accessible and shared between all the functions in the program, unless you use fancy environment scoping techniques. Using global variable inside a function opens up doors for various side effects, you should be careful with side effects.

I've removed some of the syntactic sugar and added code above can be equivalently written as follows:

Object.new = function(table_before_colon,arg)
    highlander = {} --global variable, there can be only one
    setmetatable(highlander,table_before_colon);
    table_before_colon.__index = table_before_colon;
    highlander:load(arg) -- table_before_colon.__index.load(highlander,arg)
    return highlander
end

local Ship = Object:new()
--global highlander == Ship
--Ship.new points to Object.new

function Ship:load(arg)--equivalent to: Ship.load=function(self,arg)
    --code that sets fields of the `self` object and is called from within new
    self.x = arg.x or 0 -- equivalently highlander.x=arg.x or 0
end

Now, the presence of the global variable would not matter, if nothing happened to it in the period from the start of the new till the new returns. But, apparently, your other code is similar to this:

local OtherObject = Object:new() 
--otherObject ==  highlander

OtherObject.load = function(new_other_obj,arg)
    --highlander == new_other_obj
    new_other_obj.ship1 = Ship:new({x=50, y=self.y1, s=2, tint={0, 0.5, 1}})
    --highlander == new_other_obj.ship1
    new_other_obj.ship2 = Ship:new({x=750, y=self.y2, s=-2, tint={1, 0.5, 0}})
    --highlander == new_other_obj.ship2 
end

So, OtherObject.load calls other functions, and those functions also access and modify the same global variable.

local some_object = OtherObject:new()

returns the global variable as it is at the end of the call, which is last set to the ship2 inside the call to Ship:new inside the call to OtherObject.load inside call to OtherObject:new.

Upvotes: 1

Okarin
Okarin

Reputation: 974

Solved! Apparently, what was needed was this little snippet:

function Object:new(arg)
    local o = {}
    setmetatable(o, self)
    self.__index = self
    o:load(arg)
    return o
end

Making o local in all my new methods solved everything. I don't know how it worked exactly but I assume having it in the global variable space broke something when the meta tables are set.

Upvotes: 1

Related Questions