thefnox/btree

A behavior tree library

BTree

A behavior tree library for Roblox, written in Luau.

Features

  • Fully tested
  • Flat array-based runtime — no nested table allocations during ticks
  • Full Luau strict-mode types
  • Built-in parameter validation for task modules
  • Subtree inlining — subtrees are merged into the parent flat array at build time
  • Pause/resume support
  • Debug snapshots with per-node status, compatible with the BTree Studio plugin

Installation

Install via pesde:

pesde add thefnox/btree

Then require it in your code:

local BT = require("@Packages/btree")

Usage

Defining a tree

Trees are built from a nested definition using builder functions. The definition is a plain Luau table — it can be stored in a ModuleScript and required like any other module.

-- ServerStorage/AI/PatrolTree.lua
local BT = require(game.ReplicatedStorage.BehaviorTree)
local MoveToPoint = require(script.Parent.Tasks.MoveToPoint)
local IsAlerted   = require(script.Parent.Tasks.IsAlerted)
local ChaseTarget = require(script.Parent.Tasks.ChaseTarget)

return BT.selector({
    BT.sequence({
        BT.condition(function(bb) return bb.alerted end),
        BT.task(ChaseTarget),
    }),
    BT.task(MoveToPoint, { speed = 8 }),
})

Creating and running a tree

local BT         = require(game.ReplicatedStorage.BehaviorTree)
local definition = require(game.ServerStorage.AI.PatrolTree)

local blackboard = { alerted = false, target = nil }
local tree = BT.new(definition, blackboard)

-- In a RunService loop or heartbeat:
RunService.Heartbeat:Connect(function()
    tree:update()
end)

update() ticks the tree once and returns the root status:

local status = tree:update()
if status == BT.SUCCESS then ... end
if status == BT.FAILURE then ... end
if status == BT.RUNNING  then ... end

Writing a task module

A task is a ModuleScript that returns a BTreeTask table. All callbacks are optional.

-- Tasks/MoveToPoint.lua
local BT = require(game.ReplicatedStorage.BehaviorTree)

return {
    params = {
        speed = "number",
    },

    onStart = function(bb, params)
        bb.agent:MoveTo(bb.targetPosition)
    end,

    run = function(bb, params): BT.Status?
        if bb.agent.MoveToFinished:Wait() then
            return BT.SUCCESS
        end
        return BT.FAILURE
    end,

    onStop = function(bb, params)
        bb.agent:MoveTo(bb.agent.HumanoidRootPart.Position) -- cancel
    end,
} :: BT.Task
CallbackWhen it fires
onEnterFirst tick this node is reached after not being reached last tick
onLeaveFirst tick this node is not reached after being reached last tick
onStartWhen the task first becomes active
onStopWhen the task exits (any status) or is interrupted
runEvery tick while active — return SUCCESS/FAILURE or nothing to keep RUNNING

Subtrees

Use BT.subtree to compose a definition from another ModuleScript. Subtree nodes are inlined into the parent's flat array at build time — there is no runtime indirection.

local PatrolLoop = require(script.Parent.PatrolLoop)

return BT.sequence({
    BT.subtree(PatrolLoop),
    BT.task(ReturnToBase),
})

Node reference

Composites

FunctionBehaviour
BT.sequence(children, meta?)Ticks children left to right. Fails on first FAILURE. Succeeds when all succeed.
BT.selector(children, meta?)Ticks children left to right. Succeeds on first SUCCESS. Fails when all fail.
BT.parallel(children, successPolicy?, failurePolicy?, meta?)Ticks all children every frame. Policies are "requireAll" (default) or "requireOne".
BT.randomSelector(children, weights?, meta?)Picks one child at random each activation. weights must be the same length as children if provided.

Decorators

FunctionBehaviour
BT.invert(child, meta?)Flips SUCCESSFAILURE. RUNNING passes through.
BT.alwaysSucceed(child?, meta?)Returns SUCCESS regardless of child result.
BT.alwaysFail(child?, meta?)Returns FAILURE regardless of child result.
BT.repeatNode(child, times, meta?)Repeats child on SUCCESS. Pass -1 for infinite.
BT.retryNode(child, times, meta?)Retries child on FAILURE. Pass -1 for infinite.

Leaves

FunctionBehaviour
BT.task(module, params?, meta?)Runs a task module.
BT.condition(check, meta?)Calls check(blackboard) — returns SUCCESS if true, FAILURE if false.
BT.subtree(module, meta?)Inlines another definition tree.

Tree API

tree:update()           -- tick once, returns (Status, DebugSnapshot?)
tree:stop()             -- immediately interrupt all active tasks
tree:pause()            -- suspend ticking
tree:resume()           -- resume ticking
tree:isPaused()         -- returns boolean

Debug mode

Pass a third argument to BT.new to enable debug mode. This makes update() return a DebugSnapshot as a second value and fires a BTDebugSnapshot BindableEvent that the BTree Studio plugin listens to.

-- Pass true to enable (definition path defaults to "")
local tree = BT.new(definition, blackboard, true)

-- Pass the script path so the plugin can locate the definition source
local tree = BT.new(definition, blackboard, script:GetFullName())

The DebugSnapshot contains:

type DebugSnapshot = {
    tick: number,               -- monotonically increasing tick counter
    paused: boolean,
    nodeStates: { [number]: Status }, -- keyed by 1-based DFS index
}

Debug mode adds no overhead when disabled (false / nil).

Parameter validation

When a task module declares a params schema, BT.task() validates the values passed at definition time. Supported type strings:

  • Lua primitives: "number", "string", "boolean"
  • Roblox types: "Vector2", "Vector3", any Roblox Instance class name
BT.task(MyTask, { speed = 14, target = workspace.Boss })

NodeMeta

Every builder function accepts an optional meta table as its last argument. This is used by the visual editor and has no effect at runtime.

type NodeMeta = {
    label: string?,
    size: Vector2?,
    position: Vector2?,
}

License

MIT