Behavior Trees: Advanced AI Decision-Making in Java

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

  1. Tree Depth: Deep trees can impact performance
  2. Memory Usage: Blackboard size affects memory footprint
  3. Tick Frequency: Higher frequency requires more optimization
  4. 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

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper