I'm lead dev for Bitfighter, a game primarily written in C++, but using Lua to script robot players. We're using Lunar (a variant of Luna) to glue the bits together.
I'm now wrestling with how our Lua scripts can know that an object they have a reference to has been deleted by the C++ code.
Here is some sample robot code (in Lua):
if needTarget then -- needTarget => global(?) boolean
ship = findClosest(findItems(ShipType)) -- ship => global lightUserData obj
end
if ship ~= nil then
bot:setAngleToPoint(ship:getLoc())
bot:fire()
end
Notice that ship is only set when needTarget is true, otherwise the value from a previous iteration is used. It is quite possible (likely, even, if the bot has been doing it's job :-) that the ship will have been killed (and its object deleted by C++) since the variable was last set. If so, C++ will have a fit when we call ship:getLoc(), and will usually crash.
So the question is how to most elegantly handle the situation and limit the damage if (when) a programmer makes a mistake.
I have some ideas. First, we could create some sort of Lua function that the C++ code can call when a ship or other item dies:
function itemDied(deaditem)
if deaditem == ship then
ship = nil
needTarget = true
end
end
Second, we could implement some sort of reference counting smart pointer to "magically" fix the problem. But I would have no idea where to start with this.
Third, we can have some sort of deadness detector (not sure how that would work) that bots could call like so:
if !isAlive(ship) then
needTarget = true
ship = nil -- superfluous, but here for clarity in this example
end
if needTarget then -- needTarget => global(?) boolean
ship = findClosest(findItems(ShipType)) -- ship => global lightUserData obj
end
<...as before...>
Fourth, I could retain only the ID of the ship, rather than a reference, and use that to acquire the ship object each cycle, like this:
local ship = getShip(shipID) -- shipID => global ID
if ship == nil then
needTarget = true
end
if needTarget then -- needTarget => global(?) boolean
ship = findClosest(findItems(ShipType)) -- ship => global lightUserData obj
shipID = ship:getID()
end
<...as before...>
My ideal situation would also throw errors intelligently. If I ran the getLoc() method on a dead ship, I'd like to trigger error handling code to either give the bot a chance to recover, or at least allow the system to kill the robot and log the problem, hopefully cuing me to be more careful in how I code my bot.
Those are my ideas. I'm leaning towards #1, but it feels clunky (and might involve lots of back and forth because we've got lots of short-lifecycle objects like bullets to contend with, most of which we won't be tracking). It might be easy to forget to implement the itemDied() function. #2 is appealing, because I like magic, but have no idea how it would work. #3 & #4 are very easy to understand, and I could limit my deadness detection only to the few objects that are interesting over the span of several game cycles (most likely a single ship).
This has to be a common problem. What do you think of these ideas, and are there any better ones out there?
Thanks!
Here's my current best solution:
In C++, my ship object is called Ship, whose lifecycle is controlled by C++. For each Ship, I create a proxy object, called a LuaShip, which contains a pointer to the Ship, and Ship contains a pointer to the LuaShip. In the Ship's destructor, I set the LuaShip's Ship pointer to NULL, which I use as an indicator that the ship has been destroyed.
My Lua code only has a reference to the LuaShip, and so (theoretically, at least, as this part is still not working properly) Lua will control the lifecycle of the LuaShip once the corresponding Ship object is gone. So Lua will always have a valid handle, even after the Ship object is gone, and I can write proxy methods for the Ship methods that check for Ship being NULL.
So now my task is to better understand how Luna/Lunar manages the lifecycle of pointers, and make sure that my LuaShips do not get deleted when their partner Ships get deleted if there is still some Lua code pointing at them. That should be very doable.
Actually, it turned out not to be doable (at least not by me). What did seem to work was to decouple the Ship and the LuaShip objects a little. Now, when the Lua script requests a LuaShip object, I create a new one and hand it off to Lua, and let Lua delete it when it's done with it. The LuaShip uses a smart pointer to refer to the Ship, so when the Ship dies, that pointer gets set to NULL, which the LuaShip object can detect.
It is up to the Lua coder to check that the Ship is still valid before using it. If they do not, I can trap the sitation and throw out an stern error message, rather than having the whole game crash (as was happening before).
Now Lua has total control over the lifecyle of the LuaShip, C++ can delete Ships without causing problems, and everything seems to work smoothly. The only drawback is that I'm potentially creating a lot of LuaShip objects, but it's really not that bad.
If you are interested in this topic, please see the mailing list thread I posted about a related concept, that ends in some suggestions for refining the above: