Skip to content
Engine

State and Logic

BehaviorTree

Build AI out of small reusable nodes. One compiled tree can drive any number of NPCs, each with its own progress.

PackageShared

Getting it

local BehaviorTree = Engine:GetPackage("BehaviorTree")

Overview

A behavior tree describes what an agent (usually an NPC) should do as a tree of nodes. Every tick the tree is evaluated from the root, and each node reports one of three statuses:

StatusMeaning
"Success"The node finished and did what it should.
"Failure"The node could not do it.
"Running"The node needs more ticks. Next tick picks up where it left off.

You describe the tree once with node functions, then compile it. A compiled tree keeps progress per agent, so the same tree can run hundreds of NPCs. Progress is stored with weak keys, so an agent that is garbage collected takes its state with it.

A guard that chases players it can see, and patrols otherwise
local BT = Engine:GetPackage("BehaviorTree")

local guardTree = BT.Compile(BT.Selector({
	BT.Sequence({
		BT.Condition(function(npc, board)
			board.target = findVisiblePlayer(npc)
			return board.target ~= nil
		end),
		BT.Task({
			run = function(npc, board, dt)
				if not board.target.Parent then
					return BT.Failure
				end
				npc.Humanoid:MoveTo(board.target.HumanoidRootPart.Position)
				return BT.Running
			end,
		}),
	}),
	BT.Sequence({
		BT.Task({ run = moveToNextWaypoint }),
		BT.Wait(2),
	}),
}))

local boards = {}
RunService.Heartbeat:Connect(function(dt)
	for _, npc in guards do
		boards[npc] = boards[npc] or {}
		guardTree:Tick(npc, boards[npc], dt)
	end
end)

The blackboard is any table you pass in on every tick. Nodes use it to share data, such as the current target.

Tree

Compile

BehaviorTree.Compile(def: NodeDef): Tree

Turns a node description into a runnable tree.

MethodDescription
tree:Tick(agent, blackboard, dt): StatusEvaluates the tree once for this agent.
tree:Reset(agent, blackboard)Throws away the agent's progress. Running tasks get their onExit with reason "Aborted".
tree:GetLastStatus(agent): Status?The status the last Tick returned for this agent.

BehaviorTree.Success, BehaviorTree.Failure and BehaviorTree.Running hold the status strings.

Composite nodes

Sequence

BehaviorTree.Sequence(nodes: { NodeDef }): NodeDef

Runs children in order until one fails. Succeeds when all succeed. Think "do A, then B, then C".

Selector

BehaviorTree.Selector(nodes: { NodeDef }): NodeDef

Runs children in order until one succeeds. Fails when all fail. Think "try A, else B, else C".

If a higher-priority child starts succeeding or running while a later child was still running, the later child is reset, so selectors react to new situations immediately.

RandomSequence / RandomSelector

BehaviorTree.RandomSequence(nodes: { NodeDef }): NodeDef
BehaviorTree.RandomSelector(nodes: { NodeDef }): NodeDef

Like Sequence and Selector, but in a fresh random order every time they start over.

Parallel

BehaviorTree.Parallel(nodes: { NodeDef }, policy: ParallelPolicy?): NodeDef

Ticks every child each tick. Succeeds once SuccessThreshold children succeeded (default: all) and fails once FailureThreshold children failed (default: 1). Children that are still running when it finishes are reset.

Decorators

NodeBehaviour
Invert(node)Swaps Success and Failure.
Succeeder(node)Always Success once the child finishes.
Failer(node)Always Failure once the child finishes.
Repeat(node, n?, breakOnFailure?)Runs the child n times, or forever without n. Stops with Failure when the child fails, unless breakOnFailure is false.
RepeatUntilFailure(node, maxAttempts?)Repeats until the child fails (then fails). Succeeds after maxAttempts successes.
RepeatUntilSuccess(node, maxAttempts?)Repeats until the child succeeds. Fails after maxAttempts failures.
Cooldown(node, t)After the child finishes, returns Failure for t seconds without running it.
Timeout(node, t)Fails and resets the child if it is still running after t seconds.
Delay(node, t)Returns Running for t seconds, then runs the child.

Leaf nodes

Condition

BehaviorTree.Condition(fn: (agent, blackboard) -> boolean): NodeDef

Success when fn returns true, Failure otherwise.

Task

BehaviorTree.Task(def: TaskDef): NodeDef

Does the actual work.

FieldDescription
run: (agent, blackboard, dt) -> StatusCalled every tick while the task runs. Return a status.
onEnter: (agent, blackboard) -> ()?Called on the first tick of a new run.
onExit: (agent, blackboard, reason) -> ()?Called when the task finishes ("Success" or "Failure") or is interrupted ("Aborted").

Wait / Succeed / Fail

BehaviorTree.Wait(t: number): NodeDef  -- Running for t seconds, then Success
BehaviorTree.Succeed(): NodeDef         -- always Success
BehaviorTree.Fail(): NodeDef            -- always Failure

Types

export type Status = "Success" | "Failure" | "Running"
export type ExitReason = Status | "Aborted"
export type Blackboard = { [any]: any }
export type NodeDef
export type TaskDef
export type ParallelPolicy = { SuccessThreshold: number?, FailureThreshold: number? }
export type Tree