In my
previous post, I mentioned a flaw with the interaction between Elpis and it's Lua scripts. The issue consisted of an application 'stall' while using sleep/wait functions in Lua. Looking at how Lua scripts are being called from the C++ side of things, it becomes obvious why this was happening.
|
Figure - 0 |
If a script used a wait function, the screen would not update until it was over, because scene drawing was waiting for the script to end.
The quick work-around I had used in "Oh BeeHive!" was to tell Elpis to run a line of script in x amount time and let Elpis take care of the delays each frame. A global script function had to be made available to call from Elpis. A rather poor excuse for a wait function if you ask me.
A Modified Design
A good implementation of sleep or wait might be used something like this:
function Character:Attack(target)
...
while(target:IsAlive()) do
self:FireAt(target);
wait(1000) -- 1 second reload time
end
...
end
My first idea was to have all scripts running in an individual thread, but reading around the Lua manuals, another method was introduced to me.
If a script could be paused mid execution, it would be possible to start a script, let it pause if it wants, and resume it on the next frame update. This would allow the above script to pause the script until 1000 milliseconds has elapsed without freezing the game. Of course, this will not be a high-performance timer, since it relies on frame rate, but this should do fine for the games I will be developing. A plus side to this way over a script thread at this point, is that Elpis' architecture doesn't need many changes. Being a lone dev with few hours to spare... a plus indeed!
Enter Lua Coroutines! These little guys let you pause scripts and resume them later, as described earlier. Here is the method I use now to execute a function with coroutines:
void ExecuteFunction(std::string const& _func, script_argList const& _args)
{
//Create a new thread and pop store a reference to it
lua_State* pState = lua_newthread(m_lua.get_state());
int refKey = luaL_ref(m_lua.get_state(), LUA_REGISTRYINDEX);
//Get the function (pushing it onto the stack)
lua_getglobal(pState, _func.c_str());
PushArgsToState(pState, _args);
//Start/Resume the funciton call
int res = lua_resume(pState, _args.size());
if(res == LUA_YIELD)
{
// Save the coroutine info for the next frame update
AddYieldingCoroutine(pState, refKey, _func, _args);
// Pop the function from the stack
lua_pop(pState, 1);
}
else if(res)
{
Log(m_lua.GetLatestError(pState));
}
else
{
// Function complete, get returned values
script_argList returnResults;
GetReturnResults(pState, returnResults);
EventSystem.FunctionExecutionCompleted.invoke(_func, returnResults);
// Unreference the thread, allowing the GC to get rid of it at it's leisure
luaL_unref(m_lua.get_state(), LUA_REGISTRYINDEX, refKey);
}
}
When a script function is called from Elpis, a new coroutine is created, arguments are pushed onto it's new stack and the function is called using 'lua_resume'. If the Cfunction yield is called from the script, the coroutine is paused and stored in a list to be resumed on the next frame update. Once a function is completed, the return values are collected to the stack and a callback is executed to inform the caller that the function is complete. This callback event carries the return values with it.
The CFunction LuaYield looks like this:
int LuaYield(lua_State* L)
{
return lua_yield(L, 1);
}
In lua, the wait function might look something like this:
function wait(duration)
local totalElapsed = 0;
local elapsedTime = GetFrameElapsedTime();
if(elapsedTime ~= nil) then
while(totalElapsed <= duration) do
elapsedTime = GetFrameElapsedTime();
totalElapsed = totalElapsed + elapsedTime;
LuaYield(); --Call CFunction LuaYield
end
end
end
Issues Encountered
While testing out this new design, lua_newthread crashed intermittently. After a small bout of investigation, I discovered that the issue was being caused by a stack overload. New threads are pushed onto the stack, and in many cases this would be handled by the Lua Garbage Collector (GC), though this did not seem to be sufficient in this implementation. To solve this, I used luaL_ref and luaL_unref at key moments during the thread's life.
lua_State* pState = lua_newthread(m_lua.get_state());
int refKey = luaL_ref(m_lua.get_state(), LUA_REGISTRYINDEX);
luaL_ref pops the top item off the stack (getting rid of it before it gets buried) and returns a unique integer reference key (so we can get rid of it later). Having a reference to the thread keeps the garbage collector from mistakenly destroying it before it's time. I store both the lua_State* and the reference key until it's time to kill the coroutine. At which point, luaL_unref is called to let the GC know it's safe to be rid of this thread.
luaL_unref(m_lua.get_state(), LUA_REGISTRYINDEX, refKey);