Delayed execution of behavior functions in Lua/C++

Started by
3 comments, last by Gambini 11 years, 10 months ago
Hello,

i have a game with a mainloop - on each loop i call for every NPC in the game ->ProcessAI() to execute any actions.
This is a server so the call to ProcessAI is not executed on every frame like on a client game! Its also singlethreaded.

Now i wanted to extend the C++ codebase with lua using luabind (maybe, even with boost overhead). So i expose some functions of my NPC class to LUA.

I wanted to create actor scripts for example - boss battles which have more sophisticated behaviour - whenever in my c++ ProcessAI function an event happens - i would delegate this to the corresponding lua script for the specific NPC.

i imagined in my boss.lua script i would have something like this

function OnEngageCombat(NPC)
NPC:say ("Some taunts...")
ScheduleEvent(CastEvilSpell,/*time*/2000,/*numExecutions*/1,)
end

function CastEvilSpell(NPC)
NPC:CastSpell("someSpell")
end

However - i have no idea how to do this - i gather ScheduleEvent should be some C++ function exported to Lua - but what would be the best approach to keep the object reference of the NPC (boss) with this and call a function in that script about 2 seconds later ?

Furthmore along with this delayed execution - i want that NPCs can interact with each other - my current idea is to have an actor behavior script for each special NPC.

Now what i imagined is to initiate a conversation between two NPCs e.g.

function DoGossip(NPC)
// check if NPC1 is close to NPC2
if NPC:DistanceToNpc("SomeGuy") < 10 then
StartConversation1()
end

function StartConversation1(NPC)
NPC:Say("Hello ...")
// wait a moment now trigger NPC to reply
????

Basically - how do i call a function from lua scriptA which exists in lua scriptB which is the behavior script for NPC2.
What would be a good design?

Thanks
Advertisement
This is one heck of a question. I am not familliar with luabind, so I will go over the general ideas, and maybe hit some specifics.

I'm going to make a few assumptions, and some of which might be wrong. I'm assuming that the Lua scripts are on the server, and that the ProcessAI function is called fairly regularly (at least every 100 ms for decent accuracy, up to 1000 ms). I'm also assuming that you are using luabind, rather than rolling your own bindings. I strongly suggest you not do your own bindings unless you really want to learn the Lua C API or have a large timeframe you want to complete the project in.

The ScheduleEvent function is first. Hopefully http://www.rasterbar...g-lua-functions will give you a decent idea as to where to start when calling Lua functions from C++. The interesting part is having timed events. You will need a single container to hold all of the events. It needs to be sequential and have fast random access; linked list seems to fit the bill perfectly.

Overall idea is to have a sorted list, which is done by placing each node in the list to its correct location upon insertion. The next part assumes that you have a running total game time. Each node will have the time at which the event should be fired, which function to call, and any data that needs to be passed to that function. During or before each ProcessAI call, you should go through the list comparing the current time to the time when it should fire. Because it is sorted, you can stop iterating over the list when you find an event that should not be fired. C++ code will look something like this:
//note that this probably won't compile, it is a mix of real C++ and pseudo code
//function is the function to call
//delay is the time in milliseconds from now from when it should be called
//repeat is the number of times to call (bad name?). -1 is a special value for "infinite"
//args is the object to be passed to "function". It can be an actual object or a table of objects.
void ScheduleEvent(luabind::object const& function, int delay, int repeat, luabind::object const& args)
{
if(repeat < 1) return; //if the user wishes to call the function 0 times, then don't call it.
long absolute_delay = current_time + delay; //current time is how long it has been since the program started in milliseconds
ScriptEvent* event = new ScriptEvent(absolute_delay,repeat,delay,function,args);
//there is a list defined elsewhere named EventList as std::list<ScriptEvent*>
//insertion sort
for(itr = iterate over EventList)
{
if(event->delay <= (*itr)->delay)
{ EventList.insert(itr,event); break; }
}
}

struct ScriptEvent
{
long delay;
int repeat;
int repeat_delay; //the original time passed to the function
luabind::object function;
luabind::object args;
ScriptEvent(long delay,int repeat,int repeat_delay, luabind::object const& function, luabind::object const& args) { set the values}
ScriptEvent(const ScriptEvent& se) { copy constructor}
}

Simple enough, it does exactly as I explained above. The next snippet is called every ProcessAI or Update or some regular interval.//I'm going to assume that by now, current_time has been updated
void CheckEvents()
{
//fire all of the events that should have happened since now.
for(itr = iterate through EventList)
{
if((*itr)->delay >= current_time)
{
ScriptEvent* evt = (*itr);
luabind::call_function(evt->function,evt->args);
if(evt->repeat == -1) //special case of inifinite
{
evt->delay += evt->repeat_delay;
AddToEventList(evt);
continue;
}
evt->repeat -= 1; //decrement the number of times to fire the event
if(evt->repeat > 0) //if we should repeat again
{
evt->delay += evt->repeat_delay; //set next time to fire
//insertion to a list while iterating through it does NOT
//invalidate the iterator
AddToEventList(evt);
}
else
{
delete evt;
evt = NULL;
}
//since it has been called, remove it from the list so it won't get
//called again.
itr = EventList.erase(itr);
}
else
break; //it is sorted, so no need to go through every item
}
}

//insertion sort function
void AddToEventList(ScriptEvent* node)
{
for(finditr = iterate through EventList)
{
if(node->delay <= (*finditr)->delay)
{ EventList.insert(finditr,node); return; }
}
}
It looks like a lot, but it is just a whole bunch of integer comparisons and pointer dereferencing, so it should not be too expensive other than luabind::call_function.

To answer your second question: The functions are in the same scope as long as they are loaded (luaL_dofile) with the same lua_State. So you can call those functions any time you want. However, I do suggest that you create tables named after each NPC behavior to hold those functions to avoid name conflicts. Example --Lua code in file Miner.lua or something
--create a table to hold the NPC behavior functions. This NPC is a "Miner"
Miner = Miner or {} --equivelant to if(Miner == nil) then Miner = {} end, so we do not overwrite a table that has already been created

function Miner.StartConversation1(NPC)
NPC:DoStuff()
end


--some other file BigBoss.lua
BigBoss = BigBoss or {}
function BigBoss.BargainWithMiner(NPC)
local miner = BigBoss.FindMiner(NPC) --finds a miner in range of it
Miner.StartConversation1(miner)
end


It is a lot, and I have probably misunderstood a question. If you need more details on a certain point, just say so.
For question 1, you passed the Lua function callback in ScheduleEvent, so just let the C++ hold the CastEvilSpell object and call it when time out? There is "object" in Luabind, maybe you can hold its reference, though I'm not quite familiar with Luabind.

For question 2, if your two NPC scripts are using the same Lua state, they are just same as in the same script, so you can call functions "cross script" freely.
If they are using different Lua state, you can store any information in C++ then fetch it from another Lua state.

https://www.kbasm.com -- My personal website

https://github.com/wqking/eventpp  eventpp -- C++ library for event dispatcher and callback list

https://github.com/cpgf/cpgf  cpgf library -- free C++ open source library for reflection, serialization, script binding, callbacks, and meta data for OpenGL Box2D, SFML and Irrlicht.


This is one heck of a question. I am not familliar with luabind, so I will go over the general ideas, and maybe hit some specifics.


Aye, and thanks alot for taking the time to lay out all that - i checked this post yesterday regularly but there wasnt a reply and in the meantime i got the idea to the solution myself which is basically similar to your pseudo code - however there is a gist - first the basic schedule function was pretty easy - its just a callback which - as you said i check the time in c++ - here it is for completion



void NPC::AddSE(const luabind::objectluabind::object &fn,int millisec) {
if(luabind::type(fn) == LUA_TFUNCTION)
{
ScheduledEvent sEvent;
sEvent.eventFunction = fn;
sEvent.time = GetMilliCount() + millisec;
printf("AddScheduledEvent\n");
m_scheduledEventFunctions.push_back(sEvent);
}
}
void NPC::CheckForScheduledEvents() {
std::vector<ScheduledEvent>::iterator sEvent = m_scheduledEventFunctions.begin();

while (sEvent != m_scheduledEventFunctions.end())
{
if(sEvent->time < GetMilliCount())
{
if(sEvent->eventFunction.is_valid()) {
luabind::call_function<void>(sEvent->eventFunction);
}
// erase returns the new iterator
sEvent = m_scheduledEventFunctions.erase(sEvent);
}
else
{
++sEvent;
}
}
}

Now the hard part was having scheduled movie like events with delays in between for scheduled events e.g.

NPC:Say("Hello")
WaitTime(1000)
NPC:Say("Continue after 1sec")

That without letting the mainloop sleep! I got around this with the unique function of lua = coroutines - basically if WaitTime calls yield(x) the x will be returned to my lua_resume function - and in my code i will schedule the next call to resume after XX amount of time.


To answer your second question: The functions are in the same scope as long as they are loaded (luaL_dofile) with the same lua_State. So you can call those functions any time you want. However, I do suggest that you create tables named after each NPC behavior to hold those functions to avoid name conflicts.


Yes - thats what i want todo - is there a scope for a lua_state ? i dont like to name my functions unqiuely in each lua script - e.g. there should be no collision - how can i export one lua script to a specific scope only valid for one NPC ? Lets say all NPCs have an ID instead of doing

luabind::globals(myLuaState)["npc"] = npc;

i somehow have to put this NPC only in the scope of the loaded script - which brings the next problem if i use Lual_doFile or similar luaL_dostring(myLuaState, "code...") how put this in a scope ? I dont want that the scripts need to make namespaces or something similar - this should be safely done in c++ code. To reach the script of another NPC i would just expose an object to lua e.g. npc->GetScript()->CallFunction(luabind::object)... (I gather best practise is to have one lua_state for all NPCs and not multiple)

Any idea?

And thanks again =)
Sorry to take so long to reply, but I have been looking for an example of some other library having a scripting/behavior system without explicit namespacing, and I have yet to find one. If you could point me to an example (if you know of one), then it shouldn't be too hard to reason around it. I like the idea, and I wouldn't mind using it myself somewhere.

And yes, keep the number of lua_State-s to one. Only break that rule when you figure out why, and even then it is questionable except for having one lua_State per thread. It gets complex quickly when dealing with more than one.

This topic is closed to new replies.

Advertisement