I’m working on a game engine in C++ using Lua for NPC behaviour. I ran into some problems during the design.
For everything that needs more than one frame for execution I wanted to use a linked list of processes (which are C++ classes). So this:
goto(point_a)
say("Oh dear, this lawn looks really scruffy!")
mowLawn()
would create a GotoProcess object, which would have a pointer to a SayProcess object, which would have a pointer to a MowLawnProcess object. These objects would be created instantly when the NPC is spawned, no further scripting needed.
The first of these objects will be updated each frame. When it’s finished, it will be deleted and the next one will be used for updating.
I extended this model by a ParallelProcess which would contain multiple processes that are updated simultaneously.
I found some serious problems. Look at this example: I want a character to walk to point_a and then go berserk and just attack anybody who comes near. The script would look like that:
goto(point_a)
while true do
character = getNearestCharacterId()
attack(character)
end
That wouldn’t work at all with my design. First of all, the character variable would be set at the beginning, when the character hasn’t even started walking to point_a. Then, then script would continue adding AttackProcesses forever due to the while loop.
I could implement a WhileProcess for the loop and evaluate the script line by line. I doubt this would increase readability of the code though.
Is there another common approach I didn’t think of to tackle this problem?
I think the approach you give loses a lot of the advantages of using a scripting language. It will break with conditionals as well as loops.
With coroutines all you really need to do is:
goto, say and mowLawn return immediately but initiate the action in C++. Once C++ completes those actions it calls coroutine.resume(npc_behaviour)
To avoid all the yields you can hide them inside the goto etc. functions, or do what I do which is have a waitFor function like:
activeEvents is just a Lua table which keeps track of all the things which are currently in progress – so a goto will add an ID to the table when it starts, and remove it when it finishes, and then every time an action finishes, all coroutines are activated to check if the action they’re waiting for is finished.