Behavior Trees are a powerful AI architecture that have become the standard for game AI and robotics. They provide a modular, reusable, and maintainable way to model complex decision-making processes through a tree structure of tasks.
Understanding Behavior Trees
Core Concepts
Behavior Trees consist of nodes that define the AI's decision process:
- Composite Nodes: Control flow (Sequence, Selector, Parallel)
- Decorator Nodes: Modify child behavior (Inverter, Repeater, Condition)
- Leaf Nodes: Perform actions (Action, Condition)
Node States
- SUCCESS: Task completed successfully
- FAILURE: Task failed
- RUNNING: Task still executing
Basic Behavior Tree Implementation
Example 1: Core Node Architecture
import java.util.ArrayList; import java.util.List; import java.util.function.BooleanSupplier; // Node states enum NodeStatus { SUCCESS, FAILURE, RUNNING } // Base node interface interface BehaviorNode { NodeStatus tick(); void reset(); } // Base class for all nodes abstract class BaseNode implements BehaviorNode { protected String name; public BaseNode(String name) { this.name = name; } @Override public void reset() { // Default reset behavior } public String getName() { return name; } } Example 2: Composite Nodes
// Sequence: Runs children in order, fails if any child fails class SequenceNode extends BaseNode { protected List<BehaviorNode> children; protected int currentChildIndex; public SequenceNode(String name) { super(name); this.children = new ArrayList<>(); this.currentChildIndex = 0; } public void addChild(BehaviorNode child) { children.add(child); } @Override public NodeStatus tick() { if (children.isEmpty()) { return NodeStatus.FAILURE; } while (currentChildIndex < children.size()) { BehaviorNode currentChild = children.get(currentChildIndex); NodeStatus status = currentChild.tick(); if (status == NodeStatus.RUNNING) { return NodeStatus.RUNNING; } else if (status == NodeStatus.FAILURE) { reset(); return NodeStatus.FAILURE; } currentChildIndex++; } reset(); return NodeStatus.SUCCESS; } @Override public void reset() { currentChildIndex = 0; for (BehaviorNode child : children) { child.reset(); } } } // Selector (Fallback): Runs children until one succeeds class SelectorNode extends BaseNode { protected List<BehaviorNode> children; protected int currentChildIndex; public SelectorNode(String name) { super(name); this.children = new ArrayList<>(); this.currentChildIndex = 0; } public void addChild(BehaviorNode child) { children.add(child); } @Override public NodeStatus tick() { if (children.isEmpty()) { return NodeStatus.FAILURE; } while (currentChildIndex < children.size()) { BehaviorNode currentChild = children.get(currentChildIndex); NodeStatus status = currentChild.tick(); if (status == NodeStatus.RUNNING) { return NodeStatus.RUNNING; } else if (status == NodeStatus.SUCCESS) { reset(); return NodeStatus.SUCCESS; } currentChildIndex++; } reset(); return NodeStatus.FAILURE; } @Override public void reset() { currentChildIndex = 0; for (BehaviorNode child : children) { child.reset(); } } } // Parallel: Runs all children simultaneously class ParallelNode extends BaseNode { protected List<BehaviorNode> children; private final int requiredSuccesses; private final int requiredFailures; public ParallelNode(String name, int requiredSuccesses, int requiredFailures) { super(name); this.children = new ArrayList<>(); this.requiredSuccesses = requiredSuccesses; this.requiredFailures = requiredFailures; } public void addChild(BehaviorNode child) { children.add(child); } @Override public NodeStatus tick() { int successCount = 0; int failureCount = 0; for (BehaviorNode child : children) { NodeStatus status = child.tick(); if (status == NodeStatus.SUCCESS) { successCount++; if (successCount >= requiredSuccesses) { return NodeStatus.SUCCESS; } } else if (status == NodeStatus.FAILURE) { failureCount++; if (failureCount >= requiredFailures) { return NodeStatus.FAILURE; } } } return NodeStatus.RUNNING; } } Example 3: Decorator Nodes
// Inverter: Inverts the child's result class InverterNode extends BaseNode { private BehaviorNode child; public InverterNode(String name, BehaviorNode child) { super(name); this.child = child; } @Override public NodeStatus tick() { NodeStatus status = child.tick(); switch (status) { case SUCCESS: return NodeStatus.FAILURE; case FAILURE: return NodeStatus.SUCCESS; case RUNNING: return NodeStatus.RUNNING; default: return NodeStatus.FAILURE; } } @Override public void reset() { child.reset(); } } // Repeater: Repeats child a specified number of times class RepeaterNode extends BaseNode { private BehaviorNode child; private final int repeatCount; private int currentCount; public RepeaterNode(String name, BehaviorNode child, int repeatCount) { super(name); this.child = child; this.repeatCount = repeatCount; this.currentCount = 0; } @Override public NodeStatus tick() { while (currentCount < repeatCount) { NodeStatus status = child.tick(); if (status == NodeStatus.RUNNING) { return NodeStatus.RUNNING; } else if (status == NodeStatus.FAILURE) { reset(); return NodeStatus.FAILURE; } currentCount++; } reset(); return NodeStatus.SUCCESS; } @Override public void reset() { currentCount = 0; child.reset(); } } // UntilFail: Repeats child until it fails class UntilFailNode extends BaseNode { private BehaviorNode child; public UntilFailNode(String name, BehaviorNode child) { super(name); this.child = child; } @Override public NodeStatus tick() { while (true) { NodeStatus status = child.tick(); if (status == NodeStatus.FAILURE) { return NodeStatus.SUCCESS; } else if (status == NodeStatus.RUNNING) { return NodeStatus.RUNNING; } child.reset(); } } @Override public void reset() { child.reset(); } } Example 4: Leaf Nodes
// Condition node: Checks a condition class ConditionNode extends BaseNode { private BooleanSupplier condition; public ConditionNode(String name, BooleanSupplier condition) { super(name); this.condition = condition; } @Override public NodeStatus tick() { boolean result = condition.getAsBoolean(); return result ? NodeStatus.SUCCESS : NodeStatus.FAILURE; } } // Action node: Performs an action class ActionNode extends BaseNode { private Runnable action; public ActionNode(String name, Runnable action) { super(name); this.action = action; } @Override public NodeStatus tick() { try { action.run(); return NodeStatus.SUCCESS; } catch (Exception e) { return NodeStatus.FAILURE; } } } // Async Action node: Long-running action class AsyncActionNode extends BaseNode { private final Runnable action; private boolean isRunning; public AsyncActionNode(String name, Runnable action) { super(name); this.action = action; this.isRunning = false; } @Override public NodeStatus tick() { if (!isRunning) { // Start the action new Thread(() -> { isRunning = true; action.run(); isRunning = false; }).start(); return NodeStatus.RUNNING; } // Check if action is still running return isRunning ? NodeStatus.RUNNING : NodeStatus.SUCCESS; } @Override public void reset() { isRunning = false; } } Advanced Behavior Tree Implementation
Example 5: Game AI Enemy Behavior
// Game context for sharing data between nodes class GameContext { private final java.util.Map<String, Object> blackboard; public GameContext() { this.blackboard = new java.util.HashMap<>(); } public void set(String key, Object value) { blackboard.put(key, value); } @SuppressWarnings("unchecked") public <T> T get(String key) { return (T) blackboard.get(key); } public boolean has(String key) { return blackboard.containsKey(key); } } // Context-aware base node abstract class ContextAwareNode extends BaseNode { protected GameContext context; public ContextAwareNode(String name, GameContext context) { super(name); this.context = context; } } // Enemy AI Behavior Tree class EnemyAI { private BehaviorNode behaviorTree; private GameContext context; public EnemyAI() { this.context = new GameContext(); buildBehaviorTree(); } private void buildBehaviorTree() { // Root is a selector: try different behaviors in order SelectorNode root = new SelectorNode("Root"); // 1. Attack behavior sequence SequenceNode attackSequence = new SequenceNode("Attack Player"); attackSequence.addChild(new ConditionNode("Can See Player", () -> context.get("playerVisible") == Boolean.TRUE)); attackSequence.addChild(new ConditionNode("Has Ammo", () -> { Integer ammo = context.get("ammo"); return ammo != null && ammo > 0; })); attackSequence.addChild(new ActionNode("Shoot", () -> { System.out.println("Enemy shooting at player!"); Integer ammo = context.get("ammo"); context.set("ammo", ammo - 1); })); // 2. Chase behavior sequence SequenceNode chaseSequence = new SequenceNode("Chase Player"); chaseSequence.addChild(new ConditionNode("Player Nearby", () -> { Double distance = context.get("playerDistance"); return distance != null && distance < 50.0; })); chaseSequence.addChild(new ActionNode("Move To Player", () -> System.out.println("Enemy moving toward player"))); // 3. Patrol behavior SequenceNode patrolSequence = new SequenceNode("Patrol"); patrolSequence.addChild(new ActionNode("Move To Waypoint", () -> System.out.println("Enemy moving to waypoint"))); patrolSequence.addChild(new ActionNode("Wait", () -> { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } })); patrolSequence.addChild(new RepeaterNode("Repeat Patrol", patrolSequence, 3)); // 4. Idle behavior (always succeeds) ActionNode idleAction = new ActionNode("Idle", () -> System.out.println("Enemy idling")); root.addChild(attackSequence); root.addChild(chaseSequence); root.addChild(patrolSequence); root.addChild(idleAction); this.behaviorTree = root; } public void update() { // Update context updateSensors(); // Tick behavior tree NodeStatus status = behaviorTree.tick(); System.out.println("Behavior Tree Status: " + status); } private void updateSensors() { // Simulate sensor updates context.set("playerVisible", Math.random() > 0.3); context.set("playerDistance", Math.random() * 100); context.set("ammo", 5); // Always have ammo for demo } public static void main(String[] args) throws InterruptedException { EnemyAI enemy = new EnemyAI(); // Run several updates for (int i = 0; i < 10; i++) { System.out.println("\n--- Update " + (i + 1) + " ---"); enemy.update(); Thread.sleep(1000); } } } Example 6: Robotics Behavior Tree
class RoboticsAI { private BehaviorNode behaviorTree; private GameContext context; public RoboticsAI() { this.context = new GameContext(); buildBehaviorTree(); } private void buildBehaviorTree() { // Emergency stop has highest priority SelectorNode root = new SelectorNode("Robot Root"); // Emergency behavior SequenceNode emergencySequence = new SequenceNode("Emergency"); emergencySequence.addChild(new ConditionNode("Battery Critical", () -> { Double battery = context.get("batteryLevel"); return battery != null && battery < 0.1; })); emergencySequence.addChild(new ActionNode("Return To Base", () -> System.out.println("EMERGENCY: Returning to charging base"))); // Main operation sequence SequenceNode operationSequence = new SequenceNode("Normal Operation"); // Check battery level operationSequence.addChild(new ConditionNode("Battery OK", () -> { Double battery = context.get("batteryLevel"); return battery != null && battery > 0.3; })); // Parallel: Monitor sensors while performing tasks ParallelNode monitorAndWork = new ParallelNode("Monitor and Work", 1, 1); // Monitoring branch SequenceNode monitoring = new SequenceNode("Monitoring"); monitoring.addChild(new ActionNode("Check Temperature", () -> { Double temp = context.get("temperature"); if (temp != null && temp > 80.0) { context.set("overheating", true); } })); monitoring.addChild(new ConditionNode("Temperature Normal", () -> context.get("overheating") != Boolean.TRUE)); // Work branch SelectorNode workSelector = new SelectorNode("Work Selection"); // Cleaning task SequenceNode cleanSequence = new SequenceNode("Clean Area"); cleanSequence.addChild(new ConditionNode("Area Dirty", () -> context.get("areaClean") != Boolean.TRUE)); cleanSequence.addChild(new AsyncActionNode("Clean", () -> { System.out.println("Cleaning in progress..."); try { Thread.sleep(2000); context.set("areaClean", true); System.out.println("Cleaning completed"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } })); // Patrol task SequenceNode patrolSequence = new SequenceNode("Patrol"); patrolSequence.addChild(new ActionNode("Navigate", () -> System.out.println("Patrolling area"))); patrolSequence.addChild(new ActionNode("Report Status", () -> System.out.println("Area secure"))); workSelector.addChild(cleanSequence); workSelector.addChild(patrolSequence); monitorAndWork.addChild(monitoring); monitorAndWork.addChild(workSelector); operationSequence.addChild(monitorAndWork); root.addChild(emergencySequence); root.addChild(operationSequence); this.behaviorTree = root; } public void update() { // Update sensor data updateSensors(); System.out.println("--- Robot Update ---"); System.out.println("Battery: " + context.get("batteryLevel")); System.out.println("Temperature: " + context.get("temperature")); System.out.println("Area Clean: " + context.get("areaClean")); NodeStatus status = behaviorTree.tick(); System.out.println("Behavior Status: " + status); } private void updateSensors() { // Simulate changing sensor readings context.set("batteryLevel", Math.max(0.0, context.get("batteryLevel") != null ? (Double)context.get("batteryLevel") - 0.05 : 1.0)); context.set("temperature", 70.0 + Math.random() * 20.0); // Occasionally make area dirty if (Math.random() > 0.7) { context.set("areaClean", false); } } public static void main(String[] args) throws InterruptedException { RoboticsAI robot = new RoboticsAI(); // Initialize context robot.context.set("batteryLevel", 0.8); robot.context.set("temperature", 75.0); robot.context.set("areaClean", false); robot.context.set("overheating", false); // Run updates until battery depletes for (int i = 0; i < 15; i++) { System.out.println("\n=== Cycle " + (i + 1) + " ==="); robot.update(); Thread.sleep(1000); } } } Example 7: Behavior Tree with Memory
// Blackboard with memory support class Blackboard { private final java.util.Map<String, Object> data; private final java.util.Map<String, Long> timestamps; public Blackboard() { this.data = new java.util.HashMap<>(); this.timestamps = new java.util.HashMap<>(); } public void set(String key, Object value) { data.put(key, value); timestamps.put(key, System.currentTimeMillis()); } @SuppressWarnings("unchecked") public <T> T get(String key) { return (T) data.get(key); } public long getTimestamp(String key) { return timestamps.getOrDefault(key, 0L); } public boolean isRecent(String key, long maxAgeMillis) { Long timestamp = timestamps.get(key); if (timestamp == null) return false; return (System.currentTimeMillis() - timestamp) < maxAgeMillis; } } // Memory condition node class MemoryConditionNode extends BaseNode { private final Blackboard blackboard; private final String key; private final long maxAgeMillis; public MemoryConditionNode(String name, Blackboard blackboard, String key, long maxAgeMillis) { super(name); this.blackboard = blackboard; this.key = key; this.maxAgeMillis = maxAgeMillis; } @Override public NodeStatus tick() { return blackboard.isRecent(key, maxAgeMillis) ? NodeStatus.SUCCESS : NodeStatus.FAILURE; } } // Timer node for time-based behaviors class TimerNode extends BaseNode { private final long durationMillis; private Long startTime; public TimerNode(String name, long durationMillis) { super(name); this.durationMillis = durationMillis; } @Override public NodeStatus tick() { if (startTime == null) { startTime = System.currentTimeMillis(); return NodeStatus.RUNNING; } long elapsed = System.currentTimeMillis() - startTime; if (elapsed >= durationMillis) { reset(); return NodeStatus.SUCCESS; } return NodeStatus.RUNNING; } @Override public void reset() { startTime = null; } } Best Practices and Patterns
1. Tree Design Patterns
class BehaviorTreePatterns { // Utility method patterns public static BehaviorNode createRetry(BehaviorNode child, int maxRetries) { SequenceNode retrySequence = new SequenceNode("Retry"); for (int i = 0; i < maxRetries; i++) { retrySequence.addChild(child); } return retrySequence; } public static BehaviorNode createTimeout(BehaviorNode child, long timeoutMillis) { ParallelNode timeoutParallel = new ParallelNode("Timeout", 1, 1); timeoutParallel.addChild(child); timeoutParallel.addChild(new TimerNode("Timeout Timer", timeoutMillis)); return timeoutParallel; } // Behavior tree factory methods public static BehaviorNode createPatrolBehavior(Blackboard blackboard) { SequenceNode patrol = new SequenceNode("Patrol"); patrol.addChild(new ActionNode("Move to Point A", () -> System.out.println("Moving to A"))); patrol.addChild(new TimerNode("Wait at A", 2000)); patrol.addChild(new ActionNode("Move to Point B", () -> System.out.println("Moving to B"))); patrol.addChild(new TimerNode("Wait at B", 2000)); return new RepeaterNode("Repeat Patrol", patrol, -1); // Infinite repeat } } 2. Testing Behavior Trees
import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class BehaviorTreeTest { @Test void testSequenceSuccess() { SequenceNode sequence = new SequenceNode("Test Sequence"); sequence.addChild(new ActionNode("Success 1", () -> {})); sequence.addChild(new ActionNode("Success 2", () -> {})); NodeStatus result = sequence.tick(); assertEquals(NodeStatus.SUCCESS, result); } @Test void testSelectorFailure() { SelectorNode selector = new SelectorNode("Test Selector"); selector.addChild(new ConditionNode("False Condition", () -> false)); selector.addChild(new ConditionNode("Another False", () -> false)); NodeStatus result = selector.tick(); assertEquals(NodeStatus.FAILURE, result); } @Test void testInverter() { ConditionNode trueCondition = new ConditionNode("True", () -> true); InverterNode inverter = new InverterNode("Invert True", trueCondition); NodeStatus result = inverter.tick(); assertEquals(NodeStatus.FAILURE, result); } } Performance Considerations
- Tree Depth: Deep trees can impact performance
- Memory Usage: Blackboard size affects memory footprint
- Tick Frequency: Higher frequency requires more optimization
- Async Operations: Use async nodes for I/O operations
Conclusion
Behavior Trees provide a robust, maintainable architecture for AI systems with several key advantages:
- Modularity: Reusable nodes and behaviors
- Maintainability: Clear, hierarchical structure
- Flexibility: Easy to modify and extend
- Debugging: Visual representation aids debugging
- Performance: Efficient execution model
They are particularly well-suited for:
- Game AI: Enemy behaviors, NPC routines
- Robotics: Autonomous decision making
- IoT Systems: Smart device behaviors
- Business Processes: Workflow automation
The Java implementation shown provides a solid foundation that can be extended with additional node types, better memory management, and integration with existing systems. For production use, consider adding features like:
- Tree serialization/deserialization
- Visual tree editor
- Performance profiling
- Hot-reloading of behaviors
- Integration with machine learning