npm install @bt-studio/core
# or
yarn add @bt-studio/core
# or
pnpm add @bt-studio/coreLet's build a simple patrol-and-attack AI. The entity patrols by default, but attacks when an enemy is in range.
import {
BehaviourTree,
Fallback,
Sequence,
ConditionNode,
Action,
NodeResult,
} from '@bt-studio/core';
const entity = { enemyInRange: false, position: 0 };
// A condition checks a boolean -- Succeeded if true, Failed if false
const enemyInRange = ConditionNode.from('Enemy in range?', () => entity.enemyInRange);
// An action performs work and returns a NodeResult
const attack = Action.from('Attack', () => {
console.log('Attacking!');
return NodeResult.Succeeded;
});
const patrol = Action.from('Patrol', () => {
entity.position += 1;
return NodeResult.Running; // Patrol is ongoing
});
// Fallback (OR): tries attack sequence first, falls back to patrol
const root = Fallback.from('AI', [
Sequence.from('Attack Flow', [enemyInRange, attack]),
patrol,
]);
const tree = new BehaviourTree(root);The same tree using the builder API, which supports inline decorator props:
import { BehaviourTree, NodeResult } from '@bt-studio/core';
import { fallback, sequence, condition, action } from '@bt-studio/core/builder';
const entity = { enemyInRange: false, position: 0 };
const root = fallback({ name: 'AI' }, [
sequence({ name: 'Attack Flow' }, [
condition({ name: 'Enemy in range?', eval: () => entity.enemyInRange }),
action({ name: 'Attack', execute: () => {
console.log('Attacking!');
return NodeResult.Succeeded;
}}),
]),
action({ name: 'Patrol', execute: () => {
entity.position += 1;
return NodeResult.Running;
}}),
]);
const tree = new BehaviourTree(root);The same tree in TSX (see tsx.md for setup):
import { BehaviourTree, NodeResult } from '@bt-studio/core';
import { BT } from '@bt-studio/core/tsx';
const entity = { enemyInRange: false, position: 0 };
const root = (
<fallback name="AI">
<sequence name="Attack Flow">
<condition name="Enemy in range?" eval={() => entity.enemyInRange} />
<action name="Attack" execute={() => {
console.log('Attacking!');
return NodeResult.Succeeded;
}} />
</sequence>
<action name="Patrol" execute={() => {
entity.position += 1;
return NodeResult.Running;
}} />
</fallback>
);
const tree = new BehaviourTree(root);A behaviour tree executes by ticking -- calling tree.tick() repeatedly, typically once per game loop iteration:
// Simple game loop
setInterval(() => {
const result = tree.tick({ now: Date.now() });
// result is a TickRecord: { tickId, timestamp, events }
}, 100); // 10 ticks per secondEach tick traverses the tree from the root, executing nodes according to their logic. The three possible outcomes are:
| Result | Meaning |
|---|---|
Succeeded |
The node's objective was achieved |
Failed |
The node's objective cannot be achieved |
Running |
The node needs more ticks to complete |
The now parameter supplies the current time value for the tick. Timing decorators like Timeout, Cooldown, and Delay compute elapsed time as differences between now values. It defaults to Date.now() (milliseconds) but can be any monotonically increasing number — game ticks, frame counts, or a custom clock. Duration parameters passed to timing decorators must use the same unit.
- Core Concepts -- Understand the tick lifecycle, hooks, and TickContext
- Leaf Nodes -- Action and ConditionNode in depth
- Composite Nodes -- Sequence, Fallback, Parallel, and more
- Decorators -- All 30+ decorators
- Construction APIs -- Comparison of all three APIs