Idea: implement bot behaviors with Lua coroutines
One of the main problems that the bot behavior tree DSL solves is how to yield execution for some frames while a BotAction function executes, without losing the current location of control flow. Another way to solve this is using coroutines. Suppose we have the raw action function BotActionFoo imported from C++, which may return STATUS_FAILED, STATUS_SUCCESS, or STATUS_RUNNING. While it returns RUNNING, we want to wait until the next frame and call it again. So we can use yield:
func ActionFooWrapper()
while true
local status = BotActionFoo()
if status != STATUS_RUNNING
return status
end
coroutine.yield()
end
end
Selector nodes can be implemented by basic control flow:
if <something> == STATUS_SUCCESS
return STATUS_SUCCESS
elif <something else> == STATUS_SUCCESS
return STATUS_SUCCESS
elif <third thing> == STATUS SUCCESS
return STATUS_SUCCESS
end
return STATUS_FAILURE
Likewise sequence nodes:
if <something> == STATUS_FAILURE
return STATUS_FAILURE
elif <something else> == STATUS_FAILURE
return STATUS_FAILURE
elif <third thing> == STATUS FAILURE
return STATUS_FAILURE
end
return STATUS_SUCCESS
Even concurrent nodes could be implemented, by creating a new coroutine for each of the subtrees.
Suppose the root node function for the behavior tree is called Root. Then the C++ -> Lua entry point for the behavior tree, which would be called once each frame, could look like this:
function BehaviorMain()
# Repeatedly execute the root node
if co == nil
co = coroutine.create(Root)
end
ok = coroutine.resume(co)
# TODO handle !ok error
if coroutine.status(co) == "dead"
co = nil
end
end
One detail I forgot about when originally writing this is the constant re-evalution of condition nodes. Any conditions which are ancestors of the current node are re-evaluated each frame; if one fails, then its whole subtree fails out.
The condition feature could be implemented in Lua by having a function WhileCondition that takes two arguments: a function that evaluates the condition, and a function that runs the behavior. WhileCondition would create a coroutine with the behavior function and then call the condition function and the behavior coroutine and yield each frame until one of them returns false.
Since I'm likely the main user of BTs, one feature I would need is a simulator. This would save a lot of time, and I've thought about writing one for current system more than once. If people would agree on a subset of the parser, I would likely write a grammar for it, to simulate behaviors on command-line. This idea more or less left my mind because I'm starting to be used to just spy at bots, but I don't really see the point for any tech change that does not offers such kind of tool: a meaningful debugger.
Does that mean we would use lua for the complete BT? That would be awesome. Its easier to use and there could also be synergies for lua scripting in maps.
I'm not sure it would be that awesome.
What would look like a complete BT for example? Say, what would look like camper.bt in lua, to take a simple example for which there is already BT code?
OK here's the camper.bt example. I'm assuming we map STATUS_SUCCESS/STATUS_FAILURE to true/false. Note that some conditions were able to be converted to basic language conditions rather than WhileCondition, because either the condition never changes or the child action always finishes immediately.
function HumanWantHeal()
return Bot.percentHealth(Bot.E_SELF) < 1 or not Bot.haveUpgrade(Bot.UP_MEDKIT)
end
function AlienWantHeal()
return Bot.percentHealth() < 1
end
function BehaviorCamper()
if BehaviorUnstick() then return true end
if WhileCondition(Bot.alertedToEnemy, BotAction.Fight) then return true end
if BotFunc.team() == Bot.TEAM_HUMANS then
return BotAction.Equip() or
WhileCondition(WantMedistat, BotAction.Heal) or
BotAction.RoamInRadius(Bot.E_H_REACTOR, 500)
else
return WhileCondition(AlienWantHeal, BotAction.Heal) or
(Bot.aliveTime() > 1500 and BotAction.Evolve()) or
BotAction.RoamInRadius(Bot.E_A_OVERMIND, 500)
end
end
Very interesting, so we can reuse the current BotActions but also create our own lua functions for it.
If i understand right BehaviorCamper is run every frame until there is a WhileCondition, then this is run every frame until its condition function returns false, and then the BehaviorCamper continues after the WhileCondition ?
How would the function of timers from the current BT be used? They run independent of currently running actions and can let bots start other actions.
How are lua variables handled? Are they reset every frame or stay? Are they per bot or for all available and would be both possible?
If i understand right BehaviorCamper is run every frame until there is a WhileCondition, then this is run every frame until its condition function returns false, and then the BehaviorCamper continues after the WhileCondition ?
There are other actions that can last more than one frame without WhileCondition, such as RoamInRadius. The BotAction functions are envisioned as wrappers around the C++ implementations which call coroutine.yield while it is in STATUS_RUNNING.
How would the function of timers from the current BT be used? They run independent of currently running actions and can let bots start other actions.
Store the time of the last failure in a per-bot variable, and check if the last failure was less than N milliseconds ago before performing the action. Something like
function Timer(action, period, label)
if (botdata._timers[label] or -9e99) + period >Bot.matchTime() then return false end
return action()
It could be annoying to have to come up with a unique label for each timer, so maybe a better API could be found.
How are lua variables handled? Are they reset every frame or stay? Are they per bot or for all available and would be both possible?
We could consider running each bot in a separate Lua VM for isolation purposes, if it doesn't use too much memory. On the other hand maybe some people would want to share state to make bots coordinate. I'm thinking we should have a global variable (botdata in the timer example) to hold per-bot persistent state. If they run in the same VM then this variable will be switched out each time a different bot is running.
Local variables would, as usual, persist as long as the function is running, which may be across frames if the coroutine is suspended. Global variables persist indefinitely.
For the reccord I find the lua snippet above less expressive than the BT (the line compression does not help, but I assume it' s so that it can stick in screens for the sake of demonstration), especially the need to write functions for any block including an action which can return STATUS_RUNNING, plus I'm not sure it would work as expected, for example, equip can return STATUS_RUNNING, since this action implies both moving toward armory and, when done, buy equipment which makes the call finally returning STATUS_SUCCESS.
This is ok for a small BT like camper.bt, but for the real ones like my defaultHuman.bt (which is still very simple, no doubt it will become more and more complex), I have at least 13 leaves which can return STATUS_RUNNING and yes, each time the conditions are different. The defaultAlien.bt tree is much simpler, as I only count 6.
especially the need to write functions for any block including an action which can return STATUS_RUNNING, plus I'm not sure it would work as expected, for example, equip can return STATUS_RUNNING
I think you have missed the idea of the coroutines. You would normally never deal with that status in user BT code, only true or false. Execution is resumed, and then suspended via coroutine.yield() inside BotAction.Equip each frame, until it succeeds or fails.
I think I was wrong about the semantics of selector nodes. Actually they go and retry every action from the beginning every frame. So the nth node of the selector is only continued as long as the 1st through (n-1)th nodes immediately fail every frame.
I think I was wrong about the semantics of selector nodes.
It happens. I have been trying to understand the whole semantics for some time now, and parts are still mysterious to me.
I have been trying to understand the whole semantics for some time now
At least there's some documentation on the wiki now. When I spent time on that, there was only code to read, and it was much less commented than it is now.
and parts are still mysterious to me.
Which ones? They're good candidates for improved documentation, after all.
Which ones?
For example: why do we have fallback?
I can't help on this one, it was added recently by @necessarily-equal
Turns out this is documented here: https://wiki.unvanquished.net/wiki/Formats/Behavior_tree
I would never have found that on my own.
Selector nodes can be implemented by basic control flow:
if <something> == STATUS_SUCCESS
return STATUS_SUCCESS
elif <something else> == STATUS_SUCCESS
return STATUS_SUCCESS
elif <third thing> == STATUS SUCCESS
return STATUS_SUCCESS
end
return STATUS_FAILURE
Or like this:
selector
{
some_function,
second_function,
third_function
}
Selector nodes can be implemented by basic control flow:
if <something> == STATUS_SUCCESS return STATUS_SUCCESS elif <something else> == STATUS_SUCCESS return STATUS_SUCCESS elif <third thing> == STATUS SUCCESS return STATUS_SUCCESS end return STATUS_FAILURE
I believe this is actually the semantics of what is now known as a fallback node.
Or like this:
selector { some_function, second_function, third_function }
I don't think that's valid Lua syntax
I don't think that's valid Lua syntax
It is. Try:
function selector (tab)
for _, f in pairs(tab) do
if f() then return end
end
end
function f1 () print("f1"); return false end
function f2 () print("f2"); return true end
selector { f1, f2 }