Unvanquished icon indicating copy to clipboard operation
Unvanquished copied to clipboard

Idea: implement bot behaviors with Lua coroutines

Open slipher opened this issue 4 years ago • 18 comments

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

slipher avatar Jul 01 '21 08:07 slipher

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.

slipher avatar Jul 20 '21 01:07 slipher

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.

ghost avatar Jul 20 '21 01:07 ghost

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.

Gireen avatar Jul 20 '21 11:07 Gireen

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?

ghost avatar Jul 21 '21 09:07 ghost

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

slipher avatar Jul 21 '21 18:07 slipher

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?

Gireen avatar Jul 24 '21 14:07 Gireen

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.

slipher avatar Jul 24 '21 18:07 slipher

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.

ghost avatar Jul 25 '21 13:07 ghost

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.

slipher avatar Jul 25 '21 15:07 slipher

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.

slipher avatar Dec 06 '21 01:12 slipher

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.

sweet235 avatar Feb 26 '23 07:02 sweet235

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.

ghost avatar Feb 26 '23 08:02 ghost

Which ones?

For example: why do we have fallback?

sweet235 avatar Feb 26 '23 10:02 sweet235

I can't help on this one, it was added recently by @necessarily-equal

ghost avatar Feb 26 '23 10:02 ghost

Turns out this is documented here: https://wiki.unvanquished.net/wiki/Formats/Behavior_tree

I would never have found that on my own.

sweet235 avatar Feb 26 '23 10:02 sweet235

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
}

sweet235 avatar Feb 26 '23 10:02 sweet235

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

slipher avatar Feb 26 '23 23:02 slipher

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 }

sweet235 avatar Jul 02 '24 11:07 sweet235