Behavior Trees
Behavior Trees are mathematical models for planning the execution of a complex task composed of many simple tasks. Its ease of human understanding makes them much less error prone compared to hand written state switches and they are the preferred way to define complex logic in our framework.
You can build them using our online editor and then export the trees as JSON files.
Types of Nodes
Behavior Trees are built using four types of nodes:
- Root, which defines the entry point of the tree.
- Composites, which have multiple children and handle the scheduling of these children.
- Decorators, which have a single child that they transform its behavior of.
- Action, which are the leaf nodes that actually define tasks.

Composites
- Select nodes, represented by the
?
symbol, execute each child in order until one of them succeeds. - Sequence nodes, represented by the
->
symbol, execute each child in order until one of them fails. - Parallel nodes, represented by the
#
symbol, execute each child in parallel until all of them succeed. - Random nodes, represented by the
*
symbol, pick a random child on each pulse and propagates the result as is.
For each of these primitives there's additionally a Mem*
version that remembers the last child executed in the event that the child indicates it is still running. They are represented by the same symbol as the non Mem
equivalent but are instead colored in red.
Generally speaking, you should use the Mem*
variants, however in certain cases such as when you want to enforce a constraint, non-memory variants are useful.
For instance consider the behavior tree below.

By using the MemSequence
node instead of Sequence
we introduced a bug here where the visibility is checked only once and we continue to aimbot towards the target even if it's not currently visible. Swapping this with a Sequence
node will make sure that the aimAt
task is halted once the target is no longer visible.
Tasks
Behavior tree task primitives are implemented as either:
- Stateless functions that only take the current context as its argument returning a
BT.Result
. - Or, any object with the appropriate pulse and reset functions.
type FailResult = false | string;
type SuccessResult = true | void | undefined | null;
type Result = SuccessResult | FailResult;
type Status = typeof RUNNING | Result;
interface ITask<T> {
pulse(this: any, ctx: T): Status;
reset(this: any): void;
}
type SimpleTask<T> = (this: void, ctx: T) => Result;
type Task<T> = SimpleTask<T> | ITask<T>;
-- BT status codes.
BTStatus BT.SUCCESS -- Indicates success.
BTStatus BT.FAIL -- Indicates failure.
BTStatus BT.RUNNING -- Indicates async progress, halts the tree.
-- Checks if the given BTStatus is equivalent to SUCCESS / FAIL.
bool BT.IsSuccess(BTStatus status)
bool BT.IsFail(BTStatus status)
-- Converts a BTStatus to a string.
string BT.ToString(BTStatus status)
Here's a sample task that suspends the tree for a single tick on each visit and then succeeds if the context is "hello"
.
bt.bind("myTask", {
pulse: function (ctx) {
if (!this.ranBefore) {
this.ranBefore = true;
print("#1 = ", ctx);
return BT.RUNNING;
} else {
print("#2 = ", ctx);
return ctx == "hello" ? BT.SUCCESS : BT.FAIL;
}
},
reset: function () {
this.ranBefore = false;
},
});
A more elegant way to define tasks are using coroutines with generators. We can rewrite the previous sample as a coroutine like demonstrated below.
bt.bindGen("myTask", function* (ctx) {
print("#1 = ", ctx);
ctx = yield;
print("#2 = ", ctx);
if (ctx != "hello") {
return "fail";
}
});
You can also use an async function that takes a single context argument as a task after wrapping using BT.WrapFn
or binding using bindAsync
.
bt.bindAsync("myTask", async (ctx, cancellationToken) => {
await Event.OnGamePause;
});
Construction and members
Although you can use BT.new()
to create a new empty instance, behavior trees are generally constructed from their JSON definitions using BT.From
or using the file path with BT.Load
.
You can also use variables in all node properties (e.g. timeout duration of <property>
in a waiter node) and then specify a resolver in the BT.From
function to remap it.
-- Creates an empty behavior tree.
BehaviorTree BT.new()
-- Loads a behaviour tree from the given path to it's JSON descriptor.
BehaviorTree BT.Load(string path, (function<string key>|table)? variableResolver)
-- Loads a behaviour tree from the given JSON descriptor.
BehaviorTree BT.From(string json, (function<string key>|table)? variableResolver)
-- Properties as set in the constructing descriptor.
string BehaviorTree.title
string BehaviorTree.description
table BehaviorTree.properties
-- Root node.
BTNode? BehaviorTree.root
-- Names of every task referenced by the tree.
string[] BehaviorTree:getTasks()
-- Names of every undefined task referenced by the tree.
string[] BehaviorTree:getMissingTasks()
-- Whether or not the tree definition is complete.
bool BehaviorTree:isComplete()
-- Clones the behavior tree instance.
BehaviorTree BehaviorTree:clone()
-- Gets a bound task.
any? BehaviorTree:get(string taskName)
-- Binds a task.
void BehaviorTree:bind(string taskName, Task? t)
void BehaviorTree:bindAsync(string taskName, AsyncTask? asyncT)
void BehaviorTree:bindGen(string taskName, GeneratorTask? genT)
-- Pulses the tree and returns the status.
BTStatus BehaviorTree:pulse(any ctx)
-- Resets the tree memory.
void BehaviorTree:reset()
-- Enumerates each node in the tree.
void BehaviorTree:forEach(function<Node n> cb)
import * as BT from "@Core/BT";
let bt = BT.Load("test-tree");
bt.bind("taskOk1", () => {});
bt.bind("taskOk2", () => true);
bt.bind("taskFail1", () => false);
bt.bind("taskFail2", () => "error");
bt.bind("taskFail3", undefined);
// Equivalent to testTree.title
print(bt);
The BT.From
function only allows loading of tree-scoped exports so make sure you use the Tree as JSON
option in the editor. Given an invalid description the function may throw.
Dynamic composites
An aimbot
node might need further children nodes for each module to implement, however since behavior-tree definitions are static JSON files, this wouldn't be possible without a way to dynamically create composite nodes. To solve this issue, you can instantiate all composite nodes manually.
-- Creates the composite nodes with the given children.
--
BTNode BT.NewSelect(Task[]? children)
BTNode BT.NewSequence(Task[]? children)
BTNode BT.NewRandom(Task[]? children)
BTNode BT.NewParallel(Task[]? children)
BTNode BT.NewMemSelect(Task[]? children)
BTNode BT.NewMemSequence(Task[]? children)
BTNode BT.NewMemRandom(Task[]? children)
Nodes have the following members.
-- Properties as set in the constructing descriptor.
string BTNode.title
string BTNode.description
table BTNode.properties
-- Resets the node memory.
void BTNode:reset()
-- If composite: List of children.
BTNode[] BTNode.children
-- If composite: Inserts a child at the end of the list.
BTNode BTNode:push(Task t)
-- If composite: Inserts a child at the beginning of the list.
BTNode BTNode:unshift(Task t)
You can see an example use of this functionality below.
import * as BT from "@Core/BT";
const aimbotTask = BT.NewMemSelect();
export function AddAimbot(x: BT.Task) {
aimbotTask.push(x);
}
export const Tree = BT.Load("my-tree");
Tree.bind("aimbotTask", aimbotTask);
local Logic = require("@Logic")
Logic.AddAimbot(|ctx| do
-- ...
end)
import * as BT from "@Core/BT";
import * as Logic from "@Logic";
const selNode = Logic.Tree.get("aimbot") as BT.Select;
selNode.unshift(() => {
// ...
});
Example
sample-tree.json

import * as BT from "@Core/BT";
// Load the behavior tree.
//
const bt = BT.Load<number>("sample-tree");
print("Loaded behavior tree:", bt); // Prints 'Loaded behavior tree: A behavior tree'
// Enumerate all required tasks.
//
for (const k of bt.tasks) {
print("Task:", k);
// Prints: 'Task: neverFinish2'
// 'Task: neverFinish1'
}
print(bt.isComplete); // Prints 'false'
// Define the tasks.
//
bt.bindGen("neverFinish1", function* (arg: number) {
for (let n = 0; ; ++n) {
print(`=> neverFinish1(${arg} | ${n})`);
arg = yield;
}
});
bt.bind("neverFinish2", {
pulse: (arg) => {
print(`=> neverFinish2(${arg})`);
return BT.RUNNING;
},
reset: () => {},
});
print(bt.isComplete); // Prints 'true'
// Tick.
//
for (let i of $range(1, 9)) {
print(`--- Tick ${i} ---`);
const result = bt.pulse(i);
print(BT.ToString(result));
}
/*
--- Tick 1 ---
=> neverFinish2(1)
Running
--- Tick 2 ---
=> neverFinish2(2)
Running
--- Tick 3 ---
=> neverFinish2(3)
Running
--- Tick 4 ---
=> neverFinish2(4)
Running
--- Tick 5 ---
=> neverFinish2(5)
Running
--- Tick 6 ---
=> neverFinish2(6)
Running
--- Tick 7 ---
=> neverFinish1(7 | 0)
Running
--- Tick 8 ---
=> neverFinish1(8 | 1)
Running
--- Tick 9 ---
Success
*/