Anubis: Adding Proxy Nodes and Expanding Selection Logic
Overview
In this tutorial, we’ll explore the advanced concept of proxy nodes and how they help structure and organise complex behaviours. Proxy nodes give you greater control over behaviour trees by enabling modular and reusable components. Additionally, we’ll dive deeper into node selection logic and how Anubis processes and selects actions.
By the end of this tutorial, you will:
- Understand what proxy nodes are and how to use them
- Learn advanced selection logic techniques
- Create a more complex behaviour with modular, reusable nodes
Part One: What Are Proxy Nodes?
A Proxy Node acts as a placeholder or connector in a behaviour tree. You can use proxy nodes to link to other behaviour trees or nodes, enabling you to create modular behaviours. This approach makes behaviour trees more manageable and encourages code reuse.
Think of proxy nodes like “subroutines” in traditional programming, where a proxy node represents another set of states that can be executed within the current tree.
Why Use Proxy Nodes?
- Modularity: Break down complex behaviours into smaller, manageable components.
- Reusability: Share behaviours across multiple characters or scenarios without duplicating code.
- Flexibility: Dynamically switch between different behaviours based on game conditions.
Part Two: Adding Proxy Nodes to Your Behaviour Tree
Creating a Separate GuardWander Script
We will continue working with the script from the Modifying Simple Behaviours with Interruptions and Selectors tutorial.
Here’s the final version of the script from that tutorial:
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 }
Extracting the Wander Action into a Separate Script
Let’s assume that the Wander action in the script above is so well-written that other scripters want to reuse it. We can achieve this by extracting the code into a separate Anubis script.
Create a new script and call it GuardWander.ann
. Add the following code:
game.states.GuardWander = State { function () params.anchor = {type = EParamType.String, help=The trigger around which the character will wander.} local anchor = Entity(params.anchor) nodes.GuardWander = Action { function () DebugText(me, "Wandering around") Wander(4.0, 6.0, MovementSpeed.Stroll, anchor) end, } end
Understanding the GuardWander Script
- Trigger Parameter:
params.anchor = {type = EParamType.String, help=[[The trigger around which the character will wander.]]}
defines a new parameter for the script. When other scripts reference this one, they must provide a trigger, formatted as a string (NAME_UUID
, whereNAME
is optional, but theUUID
is required). - Local Variable: After receiving a trigger as a string, it’s converted to a local variable of type
Entity
:local anchor = Entity(params.anchor)
. - Wander Action:
Wander(4.0, 6.0, MovementSpeed.Stroll, anchor)
instructs the engine to make the character wander around the specified anchor trigger, with a distance of 4 meters, a duration of 6 seconds, and a "Stroll" movement speed.
Using a Proxy Node to Reference the Script
Now, instead of defining the Wander action directly in our main script, we’ll reference the separate GuardWander
behaviour using a Proxy node. Replace the existing Wander node with the following:
-- Wander action nodes.Wander = Proxy { game.states.GuardWander, params = { anchor = [[S_WanderArea_a1017cd3-cbde-44f1-b2d9-00572a2dc9c3]] }, CanEnter = function () return not tired -- Wander only if not tired end, Valid = function() return nearbyPlayer == nil end, -- When the character finishes wandering around, we mark the character as tired -- OnFinished IS NOT called when the current action is interrupted (e.g. the player attacks the guard) OnFinished = function() tired = true end, -- OnLeave will be called when the current action is interrupted (e.g. the player attacks the guard) OnLeave = function() tired = true end, }
Understanding the Changes Related to the Wander Proxy Node
- Proxy Node: The line
nodes.Wander = Proxy
tells Anubis that we’re using a proxy node to reference another script. - Reference to the Other Script:
game.states.GuardWander
tells Anubis which specific script to reuse. - Parameters of a Proxy Node: Since the
GuardWander
script requires a trigger parameter (anchor
), we provide it here in theparams
section. - OnFinished function: Previously, we set the
tired
variable directly in the Wander node, but now we can no longer do that, so instead we modify it after theGuardWander
action finishes (which achieves the same result). - OnLeave function: Does the same as OnFinished, but they are triggered under different circumstances. OnFinished is triggered when the guard actually finishes the wandering action, while OnLeave is called if the wandering action was interrupted (e.g. the player attacked the guard).
Part Three: Creating a More Complex Selector
Choosing an Action Based on Probability
Currently, the guard switches between wandering and guarding every time an action finishes. Let’s modify this behaviour to make the guard wander 70% of the time and rest (stand guard) 30% of the time.
Here’s the updated script:
game.states.Guard = State { function () -- 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 } nodes.GuardPeacefulAction = Selector { function(nodes) return FindWeightedRandomSelectable(nodes, { [nodes.Wander] = 7, [nodes.Stand] = 3 }) end } -- Wander action nodes.GuardPeacefulAction.Wander = Proxy { game.states.GuardWander, params = { anchor = S_WanderArea_a1017cd3-cbde-44f1-b2d9-00572a2dc9c3 } } -- Stand and guard action nodes.GuardPeacefulAction.Stand = Action { function () DebugText(me, "Tired, standing still and guarding something") Sleep(6.0) 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 }
Understanding the Changes
- Local Variable
tired
: We removed this variable since we now choose the next action based on chance. - New Selector
GuardPeacefulAction
: Both Wander and Stand are now nodes within a selector. TheCanEnter
andValid
checks were removed because they’re no longer needed. - FindWeightedRandomSelectable Selection function: We use the
FindWeightedRandomSelectable
function to pick nodes based on their weights. Total weight is 10 (7 + 3) and Wander has a 7/10 chance of being selected, while Stand has a 3/10 chance.
Part Four: Testing the Script
- Launch the Editor: If it’s not already open, launch the Editor. Otherwise, reload the Anubis framework.
- Assign the config: Ensure the
Guard
config is assigned to the character you’re testing. - Enter Game Mode: Observe the guard switching between the two behaviours based on the defined probabilities.
Conclusion
In this tutorial, you learned how to use proxy nodes to modularise and reuse behaviours across different scenarios. You also explored advanced selection logic, allowing your characters to make more dynamic decisions based on probabilities. These techniques allow you to build complex, responsive behaviours in a manageable way.
Next, you can explore more complex logic with events, exceptions, and deeper customisation of behaviours in Custom Combat Scripting.