行为树(Behavior Tree)是游戏开发中广泛采用的AI决策框架,从《光环》到《孤岛危机》,几乎所有现代3A游戏的NPC智能都依赖它实现。相比有限状态机,行为树具有更好的模块化、可复用性和可视化调试能力。本文用Java实现一个完整的行为树引擎,涵盖顺序节点、选择节点、并行节点、装饰器与条件节点的核心逻辑,并通过一个追击-巡逻-攻击的敌人AI案例展示其在实际游戏场景中的应用。
一、行为树核心概念
行为树由若干节点组成,每个节点执行后返回三种状态之一:
- Success:节点成功完成
- Failure:节点执行失败
- Running:节点正在执行,需要下一帧继续
节点分为四大类:
| 节点类型 | 作用 |
|---|---|
| 组合节点(Composite) | 控制子节点的执行顺序,如顺序节点Sequence、选择节点Selector |
| 动作节点(Action) | 执行具体游戏逻辑,如移动、攻击、等待 |
| 条件节点(Condition) | 判断环境状态,返回Success或Failure |
| 装饰器节点(Decorator) | 修饰子节点行为,如循环、取反、超时 |
二、节点基类设计
package com.gamedev.ai.behavior;
/**
* 行为树节点状态枚举
*/
public enum NodeState {
SUCCESS, // 成功
FAILURE, // 失败
RUNNING // 运行中,需要下一帧继续
}
/**
* 行为树节点抽象基类
* 所有具体节点(动作、条件、组合、装饰器)均继承此类
*/
public abstract class BTNode {
protected String name;
protected NodeState state = NodeState.FAILURE;
public BTNode(String name) {
this.name = name;
}
/**
* 核心执行方法,每帧调用一次
* @param context 游戏上下文,包含NPC、玩家、场景等信息
* @return 节点执行状态
*/
public abstract NodeState tick(BTContext context);
/**
* 节点重置,通常在行为树切换分支时调用
*/
public void reset() {
this.state = NodeState.FAILURE;
}
public String getName() {
return name;
}
}
/**
* 行为树执行上下文
* 封装游戏运行时数据,供各节点读取和修改
*/
public class BTContext {
private final GameEntity owner; // 行为树拥有者(如敌人NPC)
private final GameEntity target; // 目标(如玩家)
private final GameWorld world; // 游戏世界引用
private final Map<String, Object> blackboard = new HashMap<>();
public BTContext(GameEntity owner, GameEntity target, GameWorld world) {
this.owner = owner;
this.target = target;
this.world = world;
}
public GameEntity getOwner() { return owner; }
public GameEntity getTarget() { return target; }
public GameWorld getWorld() { return world; }
public void set(String key, Object value) {
blackboard.put(key, value);
}
@SuppressWarnings("unchecked")
public <T> T get(String key) {
return (T) blackboard.get(key);
}
}
三、组合节点实现
3.1 顺序节点(Sequence)
顺序节点从左到右依次执行子节点,若任一子节点返回Failure则整体返回Failure;若全部返回Success则整体返回Success。
package com.gamedev.ai.behavior.composite;
import com.gamedev.ai.behavior.*;
import java.util.List;
/**
* 顺序节点:子节点全部成功才返回成功,类似逻辑与
*/
public class Sequence extends BTNode {
private final List<BTNode> children;
private int currentIndex = 0;
public Sequence(String name, List<BTNode> children) {
super(name);
this.children = children;
}
@Override
public NodeState tick(BTContext context) {
// 如果之前正在运行,从上次中断的节点继续
while (currentIndex < children.size()) {
BTNode child = children.get(currentIndex);
NodeState childState = child.tick(context);
switch (childState) {
case FAILURE:
// 任一子节点失败,整个顺序节点失败,重置所有子节点
reset();
return NodeState.FAILURE;
case SUCCESS:
// 当前子节点成功,继续下一个
currentIndex++;
break;
case RUNNING:
// 当前子节点正在执行,保持状态等待下一帧
this.state = NodeState.RUNNING;
return NodeState.RUNNING;
}
}
// 所有子节点都成功
reset();
return NodeState.SUCCESS;
}
@Override
public void reset() {
super.reset();
currentIndex = 0;
for (BTNode child : children) {
child.reset();
}
}
}
3.2 选择节点(Selector)
选择节点从左到右依次执行子节点,若任一子节点返回Success则整体返回Success;若全部返回Failure则整体返回Failure。类似逻辑或。
package com.gamedev.ai.behavior.composite;
import com.gamedev.ai.behavior.*;
import java.util.List;
/**
* 选择节点:任一子节点成功即返回成功,类似逻辑或
*/
public class Selector extends BTNode {
private final List<BTNode> children;
private int currentIndex = 0;
public Selector(String name, List<BTNode> children) {
super(name);
this.children = children;
}
@Override
public NodeState tick(BTContext context) {
while (currentIndex < children.size()) {
BTNode child = children.get(currentIndex);
NodeState childState = child.tick(context);
switch (childState) {
case SUCCESS:
// 任一子节点成功,整个选择节点成功
reset();
return NodeState.SUCCESS;
case FAILURE:
// 当前子节点失败,尝试下一个
currentIndex++;
break;
case RUNNING:
// 当前子节点正在执行,保持状态
this.state = NodeState.RUNNING;
return NodeState.RUNNING;
}
}
// 所有子节点都失败
reset();
return NodeState.FAILURE;
}
@Override
public void reset() {
super.reset();
currentIndex = 0;
for (BTNode child : children) {
child.reset();
}
}
}
3.3 并行节点(Parallel)
并行节点同时执行所有子节点,根据策略(全部成功/任一成功)返回结果。
package com.gamedev.ai.behavior.composite;
import com.gamedev.ai.behavior.*;
import java.util.List;
/**
* 并行节点:同时执行所有子节点
* Policy.ALL: 全部成功才算成功
* Policy.ANY: 任一成功就算成功
*/
public class Parallel extends BTNode {
public enum Policy { ALL, ANY }
private final List<BTNode> children;
private final Policy successPolicy;
private final Policy failurePolicy;
public Parallel(String name, List<BTNode> children,
Policy successPolicy, Policy failurePolicy) {
super(name);
this.children = children;
this.successPolicy = successPolicy;
this.failurePolicy = failurePolicy;
}
@Override
public NodeState tick(BTContext context) {
int successCount = 0;
int failureCount = 0;
for (BTNode child : children) {
NodeState childState = child.tick(context);
if (childState == NodeState.SUCCESS) {
successCount++;
} else if (childState == NodeState.FAILURE) {
failureCount++;
}
}
// 检查成功策略
if (successPolicy == Policy.ALL && successCount == children.size()) {
reset();
return NodeState.SUCCESS;
}
if (successPolicy == Policy.ANY && successCount > 0) {
reset();
return NodeState.SUCCESS;
}
// 检查失败策略
if (failurePolicy == Policy.ALL && failureCount == children.size()) {
reset();
return NodeState.FAILURE;
}
if (failurePolicy == Policy.ANY && failureCount > 0) {
reset();
return NodeState.FAILURE;
}
return NodeState.RUNNING;
}
@Override
public void reset() {
super.reset();
for (BTNode child : children) {
child.reset();
}
}
}
四、装饰器节点
package com.gamedev.ai.behavior.decorator;
import com.gamedev.ai.behavior.*;
/**
* 取反装饰器:子节点返回Success则返回Failure,反之亦然
*/
public class Inverter extends BTNode {
private final BTNode child;
public Inverter(String name, BTNode child) {
super(name);
this.child = child;
}
@Override
public NodeState tick(BTContext context) {
NodeState childState = child.tick(context);
if (childState == NodeState.RUNNING) {
return NodeState.RUNNING;
}
return childState == NodeState.SUCCESS ? NodeState.FAILURE : NodeState.SUCCESS;
}
@Override
public void reset() {
super.reset();
child.reset();
}
}
/**
* 循环装饰器:重复执行子节点指定次数
*/
public class Repeater extends BTNode {
private final BTNode child;
private final int maxRepeats;
private int currentRepeat = 0;
public Repeater(String name, BTNode child, int maxRepeats) {
super(name);
this.child = child;
this.maxRepeats = maxRepeats;
}
@Override
public NodeState tick(BTContext context) {
NodeState childState = child.tick(context);
if (childState == NodeState.RUNNING) {
return NodeState.RUNNING;
}
if (childState == NodeState.SUCCESS) {
currentRepeat++;
child.reset();
if (maxRepeats > 0 && currentRepeat >= maxRepeats) {
reset();
return NodeState.SUCCESS;
}
return NodeState.RUNNING; // 继续循环
}
// 子节点失败,循环中断
reset();
return NodeState.FAILURE;
}
@Override
public void reset() {
super.reset();
currentRepeat = 0;
child.reset();
}
}
五、动作与条件节点
package com.gamedev.ai.behavior.leaf;
import com.gamedev.ai.behavior.*;
/**
* 移动至目标动作节点
*/
public class MoveToTarget extends BTNode {
private final float speed;
private final float arrivalThreshold;
public MoveToTarget(String name, float speed, float arrivalThreshold) {
super(name);
this.speed = speed;
this.arrivalThreshold = arrivalThreshold;
}
@Override
public NodeState tick(BTContext context) {
GameEntity owner = context.getOwner();
GameEntity target = context.getTarget();
if (target == null) {
return NodeState.FAILURE;
}
float dx = target.getX() - owner.getX();
float dy = target.getY() - owner.getY();
float distance = (float) Math.sqrt(dx * dx + dy * dy);
if (distance <= arrivalThreshold) {
return NodeState.SUCCESS; // 已到达
}
// 执行移动
float moveX = (dx / distance) * speed;
float moveY = (dy / distance) * speed;
owner.setPosition(owner.getX() + moveX, owner.getY() + moveY);
return NodeState.RUNNING; // 移动中,下一帧继续
}
}
/**
* 攻击动作节点
*/
public class AttackTarget extends BTNode {
private final float attackRange;
private final int damage;
private int cooldown = 0;
private static final int ATTACK_COOLDOWN = 30; // 帧数
public AttackTarget(String name, float attackRange, int damage) {
super(name);
this.attackRange = attackRange;
this.damage = damage;
}
@Override
public NodeState tick(BTContext context) {
if (cooldown > 0) {
cooldown--;
return NodeState.RUNNING;
}
GameEntity owner = context.getOwner();
GameEntity target = context.getTarget();
if (target == null) {
return NodeState.FAILURE;
}
float distance = owner.distanceTo(target);
if (distance > attackRange) {
return NodeState.FAILURE; // 目标超出攻击范围
}
// 执行攻击
target.takeDamage(damage);
cooldown = ATTACK_COOLDOWN;
System.out.println(owner.getName() + " 对 " + target.getName() + " 造成 " + damage + " 点伤害!");
return NodeState.SUCCESS;
}
}
/**
* 条件节点:检测目标是否在视野范围内
*/
public class IsTargetInSight extends BTNode {
private final float sightRange;
public IsTargetInSight(String name, float sightRange) {
super(name);
this.sightRange = sightRange;
}
@Override
public NodeState tick(BTContext context) {
GameEntity owner = context.getOwner();
GameEntity target = context.getTarget();
if (target == null) {
return NodeState.FAILURE;
}
float distance = owner.distanceTo(target);
return distance <= sightRange ? NodeState.SUCCESS : NodeState.FAILURE;
}
}
/**
* 条件节点:检测目标是否在攻击范围内
*/
public class IsTargetInAttackRange extends BTNode {
private final float attackRange;
public IsTargetInAttackRange(String name, float attackRange) {
super(name);
this.attackRange = attackRange;
}
@Override
public NodeState tick(BTContext context) {
GameEntity owner = context.getOwner();
GameEntity target = context.getTarget();
if (target == null) {
return NodeState.FAILURE;
}
float distance = owner.distanceTo(target);
return distance <= attackRange ? NodeState.SUCCESS : NodeState.FAILURE;
}
}
/**
* 巡逻动作节点:在预设路点间循环移动
*/
public class Patrol extends BTNode {
private final float[][] waypoints;
private final float speed;
private int currentWaypoint = 0;
public Patrol(String name, float[][] waypoints, float speed) {
super(name);
this.waypoints = waypoints;
this.speed = speed;
}
@Override
public NodeState tick(BTContext context) {
GameEntity owner = context.getOwner();
float[] target = waypoints[currentWaypoint];
float dx = target[0] - owner.getX();
float dy = target[1] - owner.getY();
float distance = (float) Math.sqrt(dx * dx + dy * dy);
if (distance < 1.0f) {
// 到达当前路点,切换至下一个
currentWaypoint = (currentWaypoint + 1) % waypoints.length;
return NodeState.SUCCESS;
}
float moveX = (dx / distance) * speed;
float moveY = (dy / distance) * speed;
owner.setPosition(owner.getX() + moveX, owner.getY() + moveY);
return NodeState.RUNNING;
}
@Override
public void reset() {
super.reset();
currentWaypoint = 0;
}
}
六、游戏实体辅助类
package com.gamedev.ai.behavior;
/**
* 游戏实体基类
*/
public class GameEntity {
private String name;
private float x, y;
private int health;
public GameEntity(String name, float x, float y, int health) {
this.name = name;
this.x = x;
this.y = y;
this.health = health;
}
public void setPosition(float x, float y) {
this.x = x;
this.y = y;
}
public float distanceTo(GameEntity other) {
float dx = this.x - other.x;
float dy = this.y - other.y;
return (float) Math.sqrt(dx * dx + dy * dy);
}
public void takeDamage(int damage) {
this.health -= damage;
System.out.println(name + " 剩余生命值: " + health);
}
public boolean isAlive() {
return health > 0;
}
// Getters
public String getName() { return name; }
public float getX() { return x; }
public float getY() { return y; }
public int getHealth() { return health; }
}
/**
* 游戏世界(简化版)
*/
public class GameWorld {
// 可扩展:地形、障碍物、其他NPC等
}
七、构建敌人AI行为树
package com.gamedev.ai.behavior;
import com.gamedev.ai.behavior.composite.*;
import com.gamedev.ai.behavior.decorator.*;
import com.gamedev.ai.behavior.leaf.*;
import java.util.Arrays;
/**
* 敌人AI行为树工厂
* 构建一个完整的追击-攻击-巡逻逻辑
*/
public class EnemyAIFactory {
public static BTNode createEnemyAI() {
// 攻击分支:目标在攻击范围内 -> 攻击
BTNode attackBranch = new Sequence("AttackSequence", Arrays.asList(
new IsTargetInAttackRange("CheckAttackRange", 15.0f),
new AttackTarget("Attack", 15.0f, 20)
));
// 追击分支:目标在视野内 -> 移动到目标
BTNode chaseBranch = new Sequence("ChaseSequence", Arrays.asList(
new IsTargetInSight("CheckSight", 50.0f),
new MoveToTarget("Chase", 2.0f, 15.0f)
));
// 巡逻分支:没有目标时巡逻
float[][] waypoints = {
{0, 0}, {100, 0}, {100, 100}, {0, 100}
};
BTNode patrolBranch = new Patrol("Patrol", waypoints, 1.5f);
// 根节点选择器:优先尝试攻击,其次追击,最后巡逻
return new Selector("EnemyAIRoot", Arrays.asList(
attackBranch,
chaseBranch,
patrolBranch
));
}
}
八、主程序与测试
package com.gamedev.ai.behavior;
/**
* 行为树主测试类
* 模拟游戏主循环,每帧tick一次行为树
*/
public class BehaviorTreeDemo {
public static void main(String[] args) {
// 创建游戏实体
GameEntity player = new GameEntity("玩家", 80, 80, 100);
GameEntity enemy = new GameEntity("敌人", 10, 10, 100);
GameWorld world = new GameWorld();
// 创建行为树上下文
BTContext context = new BTContext(enemy, player, world);
// 创建敌人AI行为树
BTNode enemyAI = EnemyAIFactory.createEnemyAI();
System.out.println("=== 行为树AI演示开始 ===\n");
System.out.println("初始位置 - 玩家: (" + player.getX() + "," + player.getY() +
") 敌人: (" + enemy.getX() + "," + enemy.getY() + ")\n");
// 模拟60帧游戏循环
for (int frame = 1; frame <= 60 && player.isAlive() && enemy.isAlive(); frame++) {
System.out.println("--- 第 " + frame + " 帧 ---");
NodeState state = enemyAI.tick(context);
System.out.println("行为树根节点状态: " + state);
// 模拟玩家轻微移动
if (frame % 10 == 0) {
player.setPosition(player.getX() + 2, player.getY() + 2);
}
}
System.out.println("\n=== 演示结束 ===");
}
}
九、执行流程分析
以初始状态(敌人(10,10),玩家(80,80))为例:
-
第1-25帧:玩家距离约99,超出攻击范围(15)但在视野内(50)?不,超出视野。行为树进入巡逻分支,敌人向第一个路点(0,0)移动,随后依次遍历四个路点。
-
第26帧:将玩家移至敌人附近(修改测试代码),玩家距离敌人约14,进入攻击范围。
-
行为树重新评估:选择器优先检查攻击分支,条件节点
IsTargetInAttackRange返回Success,顺序节点继续执行AttackTarget,敌人对玩家造成伤害。 -
攻击冷却期间:
AttackTarget返回Running,行为树保持在该节点,直到冷却结束。
十、复杂度与扩展
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(h) | h为树高,每帧从根遍历至叶 |
| 空间复杂度 | O(n) | n为节点数,存储树结构 |
| 扩展性 | 极高 | 新增行为只需添加/重组节点 |
扩展方向:
– 添加黑板(Blackboard)实现节点间数据共享
– 实现行为树可视化编辑器,支持运行时调试
– 引入优先级选择器(Priority Selector),按优先级而非顺序评估子节点
– 结合GOAP(目标导向行动计划)处理复杂多目标决策
行为树通过树状结构将复杂的AI决策拆解为可复用的原子节点,是现代游戏引擎(如Unity的Behavior Designer、Unreal的Behavior Tree)的核心基础。理解其原理后,可根据项目需求灵活定制。