行为树技能
游戏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
最佳实践
- 保持树浅:深树更难调试和维护
- 使用服务:在服务中更新黑板值,而不是条件
- 快速失败:将便宜的条件放在昂贵的条件之前
- 黑板键:使用类型化的键并在设计时验证
- 模块化节点:创建可重用、单一目的的节点
- 调试可视化:始终实现树可视化以便于调试
性能考虑
| 优化 | 描述 |
|---|---|
| 条件中止 | 当条件变化时停止低优先级分支 |
| 服务间隔 | 如果不需要,不要每帧更新 |
| 黑板观察者 | 响应变化而不是轮询 |
| 节点池 | 为动态树重用节点实例 |