Anubis: Modifying Simple Behaviours with Interruptions and Selectors

From Baldur's Gate 3 Modding

Overview

In this tutorial, we will expand on the basic guard behaviour created in the Getting Started tutorial by introducing the concepts of interruptions and node selectors. This is where Anubis scripting starts to become more powerful and flexible, allowing you to control when behaviours get interrupted and how the game decides which actions to take. We will modify our script to make the guard either Flee or Attack the player when they see the player approaching them.

By the end of this tutorial, you will:

  1. Implement interrupts that dynamically change behaviour mid-execution
  2. Understand the selection process between different nodes in a behaviour tree

Part One: What Are Interruptions and Node Selection?

Interruptions

Interrupts allow a character’s current behaviour to stop and switch to a higher-priority action. For example, if a character is wandering but a player suddenly enters the area, the wander action can be interrupted, and the greet action can take over.

Node Selectors

Node selection is how Anubis decides which action to execute in a behaviour tree. We use Selectors to define this process. Anubis evaluates each child node and chooses the first one that is valid based on its conditions.

Part Two: Expanding the Behaviour Tree

Let’s modify the guard that we created in the Getting Started tutorial.

Add a New Action: Cower

In this expanded version of the behaviour tree, we will add a new action called Cower that makes the guard cower in fear if they see a player character (because our guard is a coward, that’s why!). This action will interrupt the wandering or guarding actions.

Open Guard.ann and update it to this version:

game.states.Guard = State {
    function ()
        -- local variable that tells us whether the guard is tired or not
        local tired = false
        -- local variable for a trigger upon entering which the player will trigger Cower reaction from our guard
        -- you will need to replace S_EventTrigger_0687d319-0436-4091-8389-15f28536a8e8 with your trigger
        -- P.S. An event trigger is used here for tutorial purposes only. In real scripting you should use
        -- Box Triggers
        local playerApproachTrigger = Entity("S_EventTrigger_0687d319-0436-4091-8389-15f28536a8e8")
        -- local variable that tells us whether there is a player nearby (we store the player in this variable)
        local nearbyPlayer = nil

        -- Cower action
        nodes.Cower = Action {
            function ()
                DebugText(me, "I'm cowering!")
                Sleep(6.0)
            end,
            Valid = function()
                return nearbyPlayer ~= nil
            end
        }

        -- Wander action
        nodes.Wander = Action {
            function ()
                DebugText(me, "Not tired, wandering around")
                Sleep(6.0)
                tired = true -- after walking around needs to rest
            end,
            CanEnter = function ()
                return not tired -- Wander only if not tired
            end,
            Valid = function()
                return nearbyPlayer == null
            end
        }

        -- Stand and guard action
        nodes.Stand = Action {
            function ()
                DebugText(me, "Tired, standing still and guarding something")
                Sleep(6.0)
                tired = false
            end,
            CanEnter = function ()
                return tired -- Stand still only if tired
            end,
            Valid = function()
                return nearbyPlayer == null
            end
        }

        events.EnteredTrigger = function(e)
            if e.Trigger == playerApproachTrigger and e.Target.Character.IsPlayer then
                nearbyPlayer = e.Target
            end
        end

        events.LeftTrigger = function(e)
            if e.Trigger == playerApproachTrigger and e.Target == nearbyPlayer then
                nearbyPlayer = nil
            end
        end
    end
}

What Changed?

Variable playerApproachTrigger: This is an Event Trigger that we use to determine if the player is near the guard. Normally you would use a Box Trigger, which only sends the events EnteredTrigger and LeftTrigger if it is first registered in Osiris. For simplicity's sake in this tutorial, we use an Event Trigger, which sends events for all characters without any registration.

Variable playerIsNearby: We’ve added a new local variable that is set to true when a player character approaches the guard.

Cower Action: This action triggers if a player character approaches the guard. The guard can only enter the action and stay in it as long as the variable nearbyPlayer is set (not nil). To simulate the guard fleeing, we display a debug message every 6 seconds.

Valid function for Wander and Stand nodes: We’ve added the Valid function to the existing Wander and Stand nodes. Both of these nodes are only valid as long as the variable playerIsNearby is set to false. As soon as as it is set to true, the Wander/Stand action is interrupted and the Anubis framework re-evaluates all the nodes, trying to find the one it can enter (which, in our case, will be the Cower node).

event EnteredTrigger: This event is triggered when the player enters a trigger. We check that (a) it’s our trigger and (b) the character that entered the trigger is a player. Then we store the entered trigger player in a variable.

event LeftTrigger: This event is triggered when the player leaves our Event Trigger. We check that (a) it’s our trigger and (b) the character that left the trigger is the same player that we stored in the nearbyPlayer variable before. Then we reset the nearbyPlayer variable back to nil, which forces the guard to exit the Cower action and trigger either the Wander or the Guard action.

Testing Interrupts

To test your changes, you will need to:

  1. Reload the Anubis framework
  2. Enter Game Mode, approach the guard, and enter the Event Trigger you placed earlier

Part Three: Adding a Selector

Let’s imagine that while our guard is a coward, there is a 50% chance that, instead of cowering, he will attack the player.

So let's add a new Action called Attack and make sure that when the player is nearby, the guard randomly chooses between Cower and Attack. This can be achieved via Selectors – a special mechanism that allows us to choose a node from a group of nodes using some custom rules.

Add Attack Action and Guard Action Selector

game.states.Guard = State {
    function ()
        -- local variable that tells us whether the guard is tired or not
        local tired = false
        -- local variable for a trigger upon entering which the player will trigger Cower reaction from our guard
        local playerApproachTrigger = Entity("S_EventTrigger_0687d319-0436-4091-8389-15f28536a8e8")
        -- local variable that tells us whether there is a player nearby (we store the player in this variable)
        local nearbyPlayer = nil

        nodes.GuardAction = Selector {
            function(nodes)
                return FindRandomSelectable(nodes)
            end,
            Valid = function()
                return nearbyPlayer ~= nil
            end
        }

        -- Cower action
        nodes.GuardAction.Cower = Action {
            function ()
                DebugText(me, "I'm cowering!")
                Sleep(6.0)
            end
        }

        -- Attack action
        nodes.GuardAction.Attack = Action {
            function ()
                DebugText(me, "I'm attacking!")
                Sleep(6.0)
            end
        }

        -- Wander action
        nodes.Wander = Action {
            function ()
                DebugText(me, "Not tired, wandering around")
                Sleep(6.0)
                tired = true -- after walking around needs to rest
            end,
            CanEnter = function ()
                return not tired -- Wander only if not tired
            end,
            Valid = function()
                return nearbyPlayer == nil
            end
        }

        -- Stand and guard action
        nodes.Stand = Action {
            function ()
                DebugText(me, "Tired, standing still and guarding something")
                Sleep(6.0)
                tired = false
            end,
            CanEnter = function ()
                return tired -- Stand still only if tired
            end,
            Valid = function()
                return nearbyPlayer == nil
            end
        }

        events.EnteredTrigger = function(e)
            if e.Trigger == playerApproachTrigger and e.Target.Character.IsPlayer then
                nearbyPlayer = e.Target
            end
        end

        events.LeftTrigger = function(e)
            if e.Trigger == playerApproachTrigger and e.Target == nearbyPlayer then
                nearbyPlayer = nil
            end
        end
    end
}

What Changed?

  • GuardActionSelector Selector:
    • This is a special type of node that contains several others nodes within it and can choose one of them as the main behaviour of the NPC following some rules.
    • FindRandomSelectable functionreturn FindRandomSelectable(nodes) – returns a random node of the Selector. To make a node a part of the selector, it should have a name in the format {SelectorName}.{ActionName}, for example GuardAction.Cower.
    • Valid function: Selectors can use CanEnter and Valid, just like other nodes.
  • Cower action: This is now named GuardAction.Cower, which allows the Anubis framework to treat it as a node that belongs to the GuardAction Selector. We also removed the Valid function from the Cower action, since we now check the Selector for validity instead.
  • Attack action: A new action that is implemented similarly to the Cower action.

Testing the Selector

Enter Game Mode and enter/leave the trigger near the guard several times. You will see that “I’m cowering!”/“I’m attacking!” appears above the head of the guard randomly every time you enter the trigger.

Conclusion

In this tutorial, you expanded your understanding of Anubis by learning how to implement interruptions and selection logic in behaviour trees. You now have a character that can dynamically change its actions based on player actions. In Adding Proxy Nodes and Expanding Selection Logic, we’ll dive into more advanced topics like using proxy nodes.