Skip to content

Best Practices

// Good - flat structure
.selector('Main')
.sequence('Combat')
.sequence('Patrol')
.sequence('Idle')
.end()
// Avoid - deep nesting
.selector('Main')
.selector('Level1')
.selector('Level2')
.selector('Level3')
// ...

Break complex behaviors into reusable subtrees:

// Define reusable behaviors
const combatBehavior = createCombatTree();
const patrolBehavior = createPatrolTree();
// Compose main AI
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
.selector('Main')
.subtree(combatBehavior)
.subtree(patrolBehavior)
.end()
.build();
// Good
.defineBlackboardVariable('targetEntity', null)
.defineBlackboardVariable('lastKnownPosition', null)
.defineBlackboardVariable('alertLevel', 0)
// Avoid
.defineBlackboardVariable('t', null)
.defineBlackboardVariable('pos', null)
.defineBlackboardVariable('a', 0)
// Combat-related
combatTarget: Entity
combatRange: number
attackCooldown: number
// Movement-related
moveTarget: Vector2
moveSpeed: number
pathNodes: Vector2[]
// Good - focused actions
class MoveToTarget implements INodeExecutor { }
class AttackTarget implements INodeExecutor { }
class PlayAnimation implements INodeExecutor { }
// Avoid - do-everything actions
class CombatAction implements INodeExecutor {
// Moves, attacks, plays animation, etc.
}
// Good - use context for state
class WaitAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const elapsed = context.runtime.getNodeState(context.node.id, 'elapsed') ?? 0;
// ...
}
}
// Avoid - instance state
class WaitAction implements INodeExecutor {
private elapsed = 0; // Don't do this!
}
.sequence('AttackSequence')
.log('Starting attack sequence', 'Debug')
.action('findTarget')
.log('Target found', 'Debug')
.action('attack')
.log('Attack complete', 'Debug')
.end()
// Good
.sequence('ApproachAndAttackEnemy')
.condition('IsEnemyInRange')
.action('PerformMeleeAttack')
// Avoid
.sequence('Seq1')
.condition('Cond1')
.action('Action1')
  1. Reduce tick rate for distant entities
  2. Use conditions early to fail fast
  3. Cache expensive calculations in blackboard
  4. Limit subtree depth to reduce traversal cost
  5. Profile your trees in real gameplay
.sequence('GuardedAction')
.condition('canPerformAction') // Guard condition
.action('performAction') // Actual action
.end()
.sequence('CooldownAttack')
.condition('isCooldownReady')
.action('attack')
.action('startCooldown')
.end()
.selector('RememberAndAct')
.sequence('UseMemory')
.condition('hasLastKnownPosition')
.action('moveToLastKnownPosition')
.end()
.action('search')
.end()