行为树Skill behavior-trees

行为树技能是用于游戏AI系统的行为树设计与实现,支持Unity、Unreal Engine和Godot等多个游戏引擎。它包括树结构设计、自定义节点、黑板系统以及与游戏引擎的集成,是游戏NPC和敌对AI系统的核心技能。

人工智能 0 次安装 2 次浏览 更新于 2/25/2026

行为树技能

游戏AI的行为树设计与实现技能,支持多个引擎和框架。

概述

这项技能提供了设计和实现游戏AI行为树的能力。它涵盖了树结构的创建、自定义节点、黑板系统以及与Unity、Unreal Engine和Godot行为树实现的集成。

能力

树设计

  • 根据规格设计行为树结构
  • 创建层次化AI行为
  • 在反应性和目标导向行为之间取得平衡
  • 优化树执行以提高性能

节点类型

  • 复合节点:序列、选择器、并行、随机
  • 装饰器节点:反转器、重复器、冷却、条件
  • 叶节点:动作、条件、服务

黑板系统

  • 设计黑板模式
  • 实现黑板观察者
  • 管理共享AI状态
  • 处理黑板键类型

引擎集成

  • Unity:NodeCanvas、Behavior Designer、自定义实现
  • Unreal:行为树编辑器、自定义任务和服务
  • Godot:Beehave、LimboAI、自定义实现

调试

  • 树可视化
  • 节点状态跟踪
  • 执行日志记录
  • 性能分析

先决条件

Unity (Node Canvas)

# 通过包管理器或资产商店安装
# Node Canvas、Behavior Designer或类似

Unreal Engine (内置)

// 在Build.cs中启用AI模块
PublicDependencyModuleNames.AddRange(new string[] {
    "AIModule",
    "GameplayTasks"
});

Godot (Beehave)

# 通过资产库安装
Beehave或LimboAI

使用模式

基本行为树结构

根
└── 选择器(尝试行为直到一个成功)
    ├── 序列(如果可能则攻击)
    │   ├── 条件:HasTarget
    │   ├── 条件:InAttackRange
    │   └── 动作:Attack
    ├── 序列(追逐目标)
    │   ├── 条件:HasTarget
    │   ├── 装饰器:Cooldown(0.5s)
    │   │   └── 动作:MoveToTarget
    │   └── 服务:UpdateTargetLocation
    └── 序列(巡逻)
        ├── 动作:MoveToPatrolPoint
        └── 动作:Wait(2s)

Unity实现(自定义)

// BehaviorTree.cs
public class BehaviorTree : MonoBehaviour
{
    private BTNode _root;
    private Blackboard _blackboard;

    private void Start()
    {
        _blackboard = new Blackboard();
        _root = BuildTree();
    }

    private void Update()
    {
        _root?.Execute(_blackboard);
    }

    private BTNode BuildTree()
    {
        return new Selector(
            new Sequence(
                new HasTargetCondition(),
                new InRangeCondition(attackRange: 2f),
                new AttackAction()
            ),
            new Sequence(
                new HasTargetCondition(),
                new Cooldown(0.5f,
                    new MoveToTargetAction()
                )
            ),
            new Sequence(
                new PatrolAction(),
                new WaitAction(2f)
            )
        );
    }
}

// BTNode.cs
public abstract class BTNode
{
    public enum NodeState { 运行中, 成功, 失败 }
    public NodeState State { get; protected set; }
    public abstract NodeState Execute(Blackboard blackboard);
}

// Selector.cs
public class Selector : BTNode
{
    private readonly BTNode[] _children;

    public Selector(params BTNode[] children)
    {
        _children = children;
    }

    public override NodeState Execute(Blackboard blackboard)
    {
        foreach (var child in _children)
        {
            var state = child.Execute(blackboard);
            if (state != NodeState.失败)
            {
                State = state;
                return State;
            }
        }
        State = NodeState.失败;
        return State;
    }
}

// Sequence.cs
public class Sequence : BTNode
{
    private readonly BTNode[] _children;
    private int _currentIndex;

    public Sequence(params BTNode[] children)
    {
        _children = children;
    }

    public override NodeState Execute(Blackboard blackboard)
    {
        while (_currentIndex < _children.Length)
        {
            var state = _children[_currentIndex].Execute(blackboard);
            if (state == NodeState.失败)
            {
                _currentIndex = 0;
                State = NodeState.失败;
                return State;
            }
            if (state == NodeState.运行中)
            {
                State = NodeState.运行中;
                return State;
            }
            _currentIndex++;
        }
        _currentIndex = 0;
        State = NodeState.成功;
        return State;
    }
}

// Blackboard.cs
public class Blackboard
{
    private readonly Dictionary<string, object> _data = new();

    public void Set<T>(string key, T value) => _data[key] = value;
    public T Get<T>(string key) => _data.TryGetValue(key, out var value) ? (T)value : default;
    public bool Has(string key) => _data.ContainsKey(key);
    public void Remove(string key) => _data.Remove(key);
}

Unreal Engine实现(C++)

// BTTask_AttackTarget.h
#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_AttackTarget.generated.h"

UCLASS()
class MYGAME_API UBTTask_AttackTarget : public UBTTaskNode
{
    GENERATED_BODY()

public:
    UBTTask_AttackTarget();

    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;

protected:
    UPROPERTY(EditAnywhere, Category = "Attack")
    float AttackDamage = 10.0f;

    UPROPERTY(EditAnywhere, Category = "Attack")
    float AttackDuration = 1.0f;

    UPROPERTY(EditAnywhere, Category = "Blackboard")
    FBlackboardKeySelector TargetKey;
};

// BTTask_AttackTarget.cpp
#include "BTTask_AttackTarget.h"
#include "AIController.h"
#include "BehaviorTree/BlackboardComponent.h"

UBTTask_AttackTarget::UBTTask_AttackTarget()
{
    NodeName = "Attack Target";
    bNotifyTick = true;
}

EBTNodeResult::Type UBTTask_AttackTarget::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
    AAIController* AIController = OwnerComp.GetAIOwner();
    if (!AIController)
    {
        return EBTNodeResult::Failed;
    }

    UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();
    AActor* TargetActor = Cast<AActor>(BlackboardComp->GetValueAsObject(TargetKey.SelectedKeyName));

    if (!TargetActor)
    {
        return EBTNodeResult::Failed;
    }

    // 执行攻击逻辑
    // ...

    return EBTNodeResult::Succeeded;
}

// BTService_UpdateTargetLocation.h
#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTService.h"
#include "BTService_UpdateTargetLocation.generated.h"

UCLASS()
class MYGAME_API UBTService_UpdateTargetLocation : public UBTService
{
    GENERATED_BODY()

public:
    UBTService_UpdateTargetLocation();

protected:
    virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;

    UPROPERTY(EditAnywhere, Category = "Blackboard")
    FBlackboardKeySelector TargetKey;

    UPROPERTY(EditAnywhere, Category = "Blackboard")
    FBlackboardKeySelector TargetLocationKey;
};

// BTDecorator_InRange.h
#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTDecorator.h"
#include "BTDecorator_InRange.generated.h"

UCLASS()
class MYGAME_API UBTDecorator_InRange : public UBTDecorator
{
    GENERATED_BODY()

public:
    UBTDecorator_InRange();

protected:
    virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override;

    UPROPERTY(EditAnywhere, Category = "Range")
    float AcceptableRadius = 200.0f;

    UPROPERTY(EditAnywhere, Category = "Blackboard")
    FBlackboardKeySelector TargetKey;
};

Godot实现(GDScript与Beehave)

# enemy_ai.gd
extends CharacterBody2D

@onready var behavior_tree: BeehaveTree = $BeehaveTree
@onready var blackboard: Blackboard = $Blackboard

func _ready() -> void:
    blackboard.set_value("patrol_points", $PatrolPoints.get_children())
    blackboard.set_value("current_patrol_index", 0)

# has_target_condition.gd
extends ConditionLeaf
class_name HasTargetCondition

func tick(actor: Node, blackboard: Blackboard) -> int:
    var target = blackboard.get_value("target")
    if target != null and is_instance_valid(target):
        return SUCCESS
    return FAILURE

# in_attack_range_condition.gd
extends ConditionLeaf
class_name InAttackRangeCondition

@export var attack_range: float = 50.0

func tick(actor: Node, blackboard: Blackboard) -> int:
    var target = blackboard.get_value("target")
    if target == null:
        return FAILURE

    var distance = actor.global_position.distance_to(target.global_position)
    if distance <= attack_range:
        return SUCCESS
    return FAILURE

# attack_action.gd
extends ActionLeaf
class_name AttackAction

@export var damage: int = 10
@export var attack_duration: float = 0.5

var _attack_timer: float = 0.0
var _is_attacking: bool = false

func tick(actor: Node, blackboard: Blackboard) -> int:
    if not _is_attacking:
        _start_attack(actor, blackboard)
        return RUNNING

    _attack_timer -= get_process_delta_time()
    if _attack_timer <= 0:
        _finish_attack(actor, blackboard)
        return SUCCESS

    return RUNNING

func _start_attack(actor: Node, blackboard: Blackboard) -> void:
    _is_attacking = true
    _attack_timer = attack_duration
    # 播放攻击动画等

func _finish_attack(actor: Node, blackboard: Blackboard) -> void:
    _is_attacking = false
    var target = blackboard.get_value("target")
    if target and target.has_method("take_damage"):
        target.take_damage(damage)

# move_to_target_action.gd
extends ActionLeaf
class_name MoveToTargetAction

@export var move_speed: float = 100.0
@export var arrival_distance: float = 10.0

func tick(actor: Node, blackboard: Blackboard) -> int:
    var target = blackboard.get_value("target")
    if target == null:
        return FAILURE

    var target_pos = target.global_position
    var distance = actor.global_position.distance_to(target_pos)

    if distance <= arrival_distance:
        return SUCCESS

    var direction = (target_pos - actor.global_position).normalized()
    actor.velocity = direction * move_speed
    actor.move_and_slide()

    return RUNNING

与Babysitter SDK集成

任务定义示例

const behaviorTreeTask = defineTask({
  name: 'behavior-tree-design',
  description: 'Design and implement behavior tree for AI',

  inputs: {
    engine: { type: 'string', required: true }, // unity, unreal, godot
    aiType: { type: 'string', required: true }, // enemy, npc, companion
    behaviors: { type: 'array', required: true },
    outputPath: { type: 'string', required: true }
  },

  outputs: {
    treePath: { type: 'string' },
    nodeFiles: { type: 'array' },
    success: { type: 'boolean' }
  },

  async run(inputs, taskCtx) {
    return {
      kind: 'skill',
      title: `Design behavior tree for ${inputs.aiType}`,
      skill: {
        name: 'behavior-trees',
        context: {
          operation: 'design_tree',
          engine: inputs.engine,
          aiType: inputs.aiType,
          behaviors: inputs.behaviors,
          outputPath: inputs.outputPath
        }
      },
      io: {
        inputJsonPath: `tasks/${taskCtx.effectId}/input.json`,
        outputJsonPath: `tasks/${taskCtx.effectId}/result.json`
      }
    };
  }
});

常见行为模式

战斗AI

选择器
├── 序列 [低生命值时逃跑]
│   ├── 条件:HealthBelowThreshold(20%)
│   └── 动作:FleeFromTarget
├── 序列 [范围内攻击]
│   ├── 条件:HasTarget
│   ├── 条件:InAttackRange
│   └── 动作:Attack
├── 序列 [接近目标]
│   ├── 条件:HasTarget
│   └── 动作:MoveToTarget
└── 动作:SearchForTarget

巡逻AI

选择器
├── 序列 [调查干扰]
│   ├── 条件:HeardNoise
│   ├── 动作:MoveToNoiseLocation
│   └── 动作:LookAround
├── 序列 [巡逻]
│   ├── 动作:MoveToNextPatrolPoint
│   ├── 动作:Wait(2s)
│   └── 动作:AdvancePatrolIndex
└── 动作:Idle

伴侣AI

选择器
├── 序列 [帮助玩家战斗]
│   ├── 条件:PlayerInCombat
│   ├── 条件:HasTarget
│   └── 动作:AttackPlayerTarget
├── 序列 [治疗玩家]
│   ├── 条件:PlayerHealthLow
│   ├── 条件:HasHealAbility
│   └── 动作:HealPlayer
├── 序列 [跟随玩家]
│   ├── 条件:TooFarFromPlayer
│   └── 动作:MoveToPlayer
└── 动作:IdleNearPlayer

最佳实践

  1. 保持树浅:深树更难调试和维护
  2. 使用服务:在服务中更新黑板值,而不是条件
  3. 快速失败:将便宜的条件放在昂贵的条件之前
  4. 黑板键:使用类型化的键并在设计时验证
  5. 模块化节点:创建可重用、单一目的的节点
  6. 调试可视化:始终实现树可视化以便于调试

性能考虑

优化 描述
条件中止 当条件变化时停止低优先级分支
服务间隔 如果不需要,不要每帧更新
黑板观察者 响应变化而不是轮询
节点池 为动态树重用节点实例

参考资料