每日算法 — 使用java实现行为树:游戏AI层级决策与状态切换策略

行为树(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. 第1-25帧:玩家距离约99,超出攻击范围(15)但在视野内(50)?不,超出视野。行为树进入巡逻分支,敌人向第一个路点(0,0)移动,随后依次遍历四个路点。

  2. 第26帧:将玩家移至敌人附近(修改测试代码),玩家距离敌人约14,进入攻击范围。

  3. 行为树重新评估:选择器优先检查攻击分支,条件节点IsTargetInAttackRange返回Success,顺序节点继续执行AttackTarget,敌人对玩家造成伤害。

  4. 攻击冷却期间:AttackTarget返回Running,行为树保持在该节点,直到冷却结束。

十、复杂度与扩展

指标 复杂度 说明
时间复杂度 O(h) h为树高,每帧从根遍历至叶
空间复杂度 O(n) n为节点数,存储树结构
扩展性 极高 新增行为只需添加/重组节点

扩展方向:
– 添加黑板(Blackboard)实现节点间数据共享
– 实现行为树可视化编辑器,支持运行时调试
– 引入优先级选择器(Priority Selector),按优先级而非顺序评估子节点
– 结合GOAP(目标导向行动计划)处理复杂多目标决策

行为树通过树状结构将复杂的AI决策拆解为可复用的原子节点,是现代游戏引擎(如Unity的Behavior Designer、Unreal的Behavior Tree)的核心基础。理解其原理后,可根据项目需求灵活定制。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注