我是Bitfighter的首席开发者,这是一款主要用C ++编写的游戏,但是使用Lua来编写机器人玩家的脚本。我们使用Lunar(Luna的变体)将这些位粘合在一起。
我现在正在努力解决我们的Lua脚本如何知道他们引用的对象已被C ++代码删除。
以下是一些示例机器人代码(在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
请注意,仅在needTarget为true时设置ship,否则使用上一次迭代的值。很可能(甚至,如果机器人一直在做它的工作:-),因为变量是最后一次设置,所以该船将被杀死(并且其对象被C ++删除)。如果是这样,当我们调用ship:getLoc()时,C ++就会适合,并且通常会崩溃。
所以问题是如果程序员犯了错误,如何最优雅地处理这种情况并限制损害。
我有一些想法。首先,我们可以创建某种Lua函数,当船舶或其他项目死亡时,C ++代码可以调用它:
function itemDied(deaditem)
if deaditem == ship then
ship = nil
needTarget = true
end
end
其次,我们可以实现某种引用计数智能指针来“神奇地”修复问题。但我不知道从哪里开始。
第三,我们可以使用某种类型的死机检测器(不确定它是如何工作的)机器人可以这样调用:
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...>
第四,我只能保留船舶的ID,而不是参考,并使用它来获取每个周期的船舶对象,如下所示:
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...>
我理想的情况也会智能地抛出错误。如果我在死船上运行getLoc()方法,我想触发错误处理代码,让机器人有机会恢复,或者至少让系统杀死机器人并记录问题,希望能引导我在我对机器人进行编码时要更加小心。
这些是我的想法。我倾向于#1,但它感觉很笨(并且可能涉及很多来回,因为我们有很多短生命周期对象,比如子弹来应对,其中大多数我们都不会跟踪)。忘记实现itemDied()函数可能很容易。 #2很有吸引力,因为我喜欢魔法,但不知道它是如何工作的。 #3&amp; #4非常容易理解,我可以将我的死亡检测限制在几个游戏周期(很可能只有一艘船)的几个有趣的对象上。
这必须是一个常见问题。您如何看待这些想法,那里有更好的想法吗?
谢谢!
这是我目前最好的解决方案:
在C ++中,我的船舶对象名为Ship,其生命周期由C ++控制。对于每个Ship,我创建一个名为LuaShip的代理对象,其中包含指向Ship的指针,Ship包含指向LuaShip的指针。在Ship的析构函数中,我将LuaShip的Ship指针设置为NULL,我将其用作船舶已被销毁的指示器。
我的Lua代码只引用了LuaShip,所以(理论上,至少,因为这部分仍然无法正常工作)一旦相应的Ship对象消失,Lua将控制LuaShip的生命周期。因此Lua将始终拥有一个有效的句柄,即使在Ship对象消失之后,我也可以为Ship方法编写代理方法,以检查Ship是否为NULL。
所以现在我的任务是更好地了解Luna / Lunar如何管理指针的生命周期,并确保当他们的合作伙伴Ships被删除时,如果仍有一些Lua代码指向它们,我的LuaShips不会被删除。这应该是非常可行的。
实际上,结果证明不可行(至少不是我)。似乎工作的是将Ship和LuaShip对象分离一点。现在,当Lua脚本请求LuaShip对象时,我创建一个新对象并将其交给Lua,让Lua在完成后删除它。 LuaShip使用智能指针来引用Ship,因此当Ship死亡时,该指针被设置为NULL,LuaShip对象可以检测到该指针。
在使用之前,由Lua编码员检查船舶是否仍然有效。如果他们不这样做,我可以捕获引用并抛出严厉的错误消息,而不是让整个游戏崩溃(就像之前发生的那样)。
现在,Lua可以完全控制LuaShip的生命周期,C ++可以在不引起问题的情况下删除Ships,而且一切似乎都能顺利运行。唯一的缺点是我可能会创建很多LuaShip对象,但实际上并没有那么糟糕。
如果您对此主题感兴趣,请参阅我发布的有关相关概念的邮件列表主题,最后提出一些改进上述内容的建议:
答案 0 :(得分:5)
我认为你的Lua方面没有问题,你不应该在那里解决它。
您的C ++代码正在删除仍在引用的对象。无论它们如何被引用,这都是不好的。
简单的解决方案可能是让月球清理所有物体。它已经知道哪些对象必须保持活动,因为脚本正在使用它们,并且让它为随机C ++对象执行GC似乎是可行的(假设C ++端的智能指针,当然 - 每个智能指针都添加到Lunars引用计数)
答案 1 :(得分:4)
我们公司选择了第四个解决方案,它对我们很有用。我推荐它。但是,为了完整性:
1号是稳固的。让船的析构函数调用一些月球代码(或标记它应该被调用,无论如何),然后抱怨如果你找不到它。以这种方式做事意味着你必须非常小心,如果你想在单独的线程中运行游戏引擎和机器人,可能会破坏Lua运行时。
2号并不像你想象的那么难:在C ++端写入或借用引用计数指针,如果你的Lua / C ++粘合剂习惯于处理C ++指针,它可能无需进一步干预就可以工作,除非您通过在运行时检查符号表或其他东西来生成绑定。麻烦的是,它会迫使你的设计发生相当大的变化;如果你使用引用计数指针来指代船只,你必须在任何地方使用它们 - 引用混合使用裸指针和智能指针的船舶所固有的风险应该是显而易见的。所以我不会走那条路,不像你想象的那样在项目的后期。
3号很棘手。您需要一种方法来确定给定的船舶对象是否存活或死亡,即使已经释放了代表它的内存。我能为这个问题想到的所有解决方案基本上都变成了第4个:你可以让死船留下某种被复制到Lua对象中的令牌,可以用来检测死亡(你可以在std中保留死对象) ::设置或类似的东西),但为什么不只是用他们的代币来引用船只?
通常,您无法检测特定C ++指针是否指向已被删除的对象,因此没有简单的神奇方法来解决您的问题。只有在析构函数中执行特殊操作时,才可能捕获在已删除的船上调用ship:getLoc()
的错误。这个问题没有完美的解决方案,祝你好运。
答案 2 :(得分:2)
这是一个老问题,但正确的解决方案IMO应lua_newuserdata()
/ boost::shared_ptr
shared_ptr
创建weak_ptr
或C++11
或weak_ptr
的{{3}} / boost::weak_ptr
。从那里,您可以在需要时创建引用,如果shared_ptr
无法获得std::shared_ptr
shared_ptr
,则会失败。例如(在这个例子中使用Boost的C++11
,因为这是一个老问题,你可能还没有shared_ptr
支持,但对于新项目,我建议使用C ++ 11 {{ 1}}):
using MyObjectPtr = boost::shared_ptr<MyObject>;
using MyObjectWeakPtr = boost::weak_ptr<MyObject>;
auto mySharedPtr = boost::make_shared<MyObject>();
auto userdata = static_cast<MyObjectWeakPtr*>(lua_newuserdata(L, sizeof(MyObjectWeakPtr)));
new(userdata) MyObjectWeakPtr(mySharedPtr);
然后当你需要获得一个C ++对象时:
auto weakObj = *static_cast<MyObjectWeakPtr*>(
luaL_checkudata(L, 1, "MyObject.Metatable"));
luaL_argcheck(L, weakObj != nullptr, 1, "'MyObjectWeakPtr' expected");
// If you're using a weak_ptr, this is required!!!! If your userdata is a
// shared_ptr, you can just act on the shared_ptr after luaL_argcheck()
if (auto obj = weakObj.lock()) {
// You have a valid shared_ptr, the C++ object is alive and you can
// dereference like a normal shared_ptr.
} else {
// The C++ object went away, you can safely garbage collect userdata
}
至关重要的是,您不要忘记在您的lua weak_ptr
元方法中取消分配__gc
:
static int
myobject_lua__gc(lua_State* L) {
auto weakObj = *static_cast<MyObjectWeakPtr*>(
luaL_checkudata(L, 1, "MyObject.Metatable"));
luaL_argcheck(L, weakObj != nullptr, 1, "'MyObjectWeakPtr' expected");
weakObj.~MyObjectWeakPtr();
}
不要忘记使用宏或模板元编程来避免大部分代码重复:static_cast<>
,luaL_argcheck()
等。
只要lua对象也存在,当需要保持C ++对象存活时,请使用shared_ptr
。当C ++可以收获对象时使用weak_ptr
,它可以从lua的脚下消失。当对象的生命周期未知且需要通过引用计数自动管理时,总是使用shared_ptr
或weak_ptr
。
提示:让您的C ++类继承自std::weak_ptr
或lock()
,因为它可以使用boost::enable_shared_from_this
。
答案 3 :(得分:0)
我同意MSalters,我真的不认为你应该从C ++方面释放内存。 Lua userdata支持___gc元方法,让您有机会清理。如果gc不够激进,你可以稍微调整一下,或者以较小的步长手动运行它。 lua gc不是确定性的,所以如果你需要释放资源,那么你需要有一个函数可以调用来释放那些资源(也可以通过__gc调用,并进行适当的检查)。
您可能还希望使用weak tables作为船只参考,这样您就不必将所有引用分配给nil以释放它。有一个强有力的参考(比如,在所有活跃船舶的清单中),然后所有其他都是弱参考。当船舶被摧毁时,在船上设置一个标记它的标志,然后在活动船舶表中将参考设置为nil。然后,当另一艘船想要互动时,你的逻辑是相同的,除非你检查:
if ship==nil or ship.destroyed then
ship = findClosest(findItems(ShipType))
end