-
Notifications
You must be signed in to change notification settings - Fork 0
Home
soomnik edited this page Jun 3, 2025
·
4 revisions
2025.06.03 구조 변경으로 인한 예제 코드파일 수정
- 현재 예제 코드는
behavior-tree-example-1브랜치Scripts/AttackerAI/에 있습니다. - 적용할 대상 (e.g. 공격형 agent)에 컴포넌트로 스크립트
- 적용할 대상의 Update 함수에서 시작(매 프레임)되어서 루트 노드 부터 DFS 형태로 순회
- Node 클래스 (abstract)
- CompositeNode (abstract, Node 상속)
- 구현
- SelectorNode
- SequenceNode
- 구현
- DecoratorNode (abstract, Node 상속)
- 구현
- RepeaterNode - 반복
- InverterNode - 실패 성공 바꾸기
- RetryNode - 실패해도 재시도
- ForceSuccessNode - 강제로 성공으로 처리
- ForceFailureNode - 강제로 실패로 처리
- TimeoutNode - 타임아웃
- DelayNode - 딜레이
- UntilSuccessNode - 성공할 때 까지
- UntilFailureNode - 실패할 때 까지
- 구현
- LeafNode (abstract, Node 상속)
- ActionNode (abstract, LeafNode 상속)
- ConditionNode (abstract, LeafNode 상속)
- 루트 Evaluate
- 루트의 자식들 Evaluate (루트 노드에 추가된 순서대로, 왼쪽 -> 오른쪽)
- 재귀적으로 탐색하면서 leaf node(action, condition, etc)에 도착하면 leaf node 실행
- 실행 결과에 따라서 셀렉터, 시퀀스에서 다음으로 넘어갈지 여기서 끝낼지 등등 판단
- 실행결과가 Running이면 작업이 안끝났기 때문에 다음 프레임(Update)에서 다시 Evaluate
- 그냥 예시라서 아래 예시에서는 AttackerAI 부분을 바꾸면 됩니다.
- BehaviorTree 폴더는 Scripts/BehaviorTree 에 있는 것 사용
- AttackerAI 를 수정
.
├── AttackerAI
│ ├── Actions
│ │ ├── AttackPlayerNode.cs
│ │ ├── MoveToTargetNode.cs
│ ├── AttackerAI.cs
│ └── Conditions
│ └── IsTargetTooFarNode.cs
└── BehaviorTree
├── BehaviorTree.cs
├── DecoratorNode.cs
├── Node.cs
├── Selector.cs
└── Sequence.cs
-
/AttackerAI/Actions/: 액션 노드들 -
/AttackerAI/Conditions/: 컨디션 노드들 -
AttackerAI.cs: BT 매니저 (이걸 플레이어 컴포넌트로 등록) -
AttackerAI코드 예시
using UnityEngine;
using BehaviorTree;
public class AttackerAI : BehaviorTree.BehaviorTree
{
[SerializeField] private Transform target;
[SerializeField] private float followDistance = 3f; // 이 거리 이상 떨어지면 따라감
[SerializeField] private float moveSpeed = 4f;
[SerializeField] private float rotationSpeed = 8f;
private Blackboard blackboard;
void Awake()
{
blackboard = new Blackboard();
InitializeBlackboard();
}
void InitializeBlackboard()
{
blackboard.SetValue("self", this);
blackboard.SetValue("moveSpeed", moveSpeed);
blackboard.SetValue("rotationSpeed", rotationSpeed);
blackboard.SetValue("followDistance", followDistance);
blackboard.SetValue("target", target);
}
protected override void OnUpdate()
{
blackboard.SetValue("target", target);
}
protected override void ConstructTree()
{
// 시각적으로 생각하면 부모의 자식으로 등록된 순서대로 왼쪽 -> 오른쪽
SelectorNode rootSelector = new SelectorNode(); // 최상위에 있는 셀렉터 노드
SequenceNode attackSequence = new SequenceNode(); // 공격 관련 시퀀스 노드
Inverter isTargetClose = new Inverter(); // 결과를 뒤집는 (success->fail / fail->success) 데코레이터
isTargetClose.SetChild(new IsTargetTooFarNode(this, blackboard, followDistance)); // IsTargetTooFarNode 컨디션 노드를 뒤집어서 사용해서 isTargetClose 의미로
attackSequence.AddChild(isTargetClose); // 공격 시퀀스에 `거리가 가까운가?` 컨디션 노드 추가
Repeat repeatAttack = new Repeat(3); // 3번 반복한다는 데코레이터
repeatAttack.SetChild(new AttackPlayerNode(this, blackboard)); // 이 데코레이터에 어택 액션 노드 추가
attackSequence.AddChild(repeatAttack); // 공격 시퀀스에 3번 반복 공격 액션 노드 추가
SequenceNode moveSequence = new SequenceNode(); // 이동 관련 시퀀스
moveSequence.AddChild(new IsTargetTooFarNode(this, blackboard, followDistance)); // 이동 관련 시퀀스에 `거리가 먼가?` 컨디션 노드 추가
moveSequence.AddChild(new MoveToTargetNode(this, blackboard, moveSpeed, followDistance * 0.8f)); // 이동 관련 시퀀스에 타겟으로 이동하는 액션 노드 추가
// 노드가 실행되는 순서는 자식으로 추가된 순서대로. 여기서는 attackSequence -> moveSequence
rootSelector.AddChild(attackSequence); // 루트 셀렉터에 등록
rootSelector.AddChild(moveSequence);
SetRootNode(rootSelector); // 루트 노드로 지정해서 여기서 부터 시작함
}
}-
Blackboard
- blackboard.SetValue(key, 값) -> 값 등록
- blackboard.GetValue<타입>(key) -> 키에 해당하는 값
- blackboard.HasKey(key) -> 키를 가지고 있는가?
-
InitializeBlackboard: 초기 블랙보드 설정
-
ConstructTree: 트리 구조 설정
-
트리 그림 그려놓고 구성하면 어렵지 않음
public enum NodeState
{
Running, // 실행 중
Success, // 성공
Failure // 실패
}- 해야할 일
- ActionNode 클래스를 상속
- 생성자에
: base(owner, blackboard)추가해서 ActionNode (부모) 생성자 처리 -
public override NodeState Evaluate()함수 구현 - 처리 상태에 따라
NodeState타입을 반환해야함- 실패 ->
Failure - 진행 중 ->
Running: 다음 프레임에도 끝날 때 까지 계속 계속 - 성공 ->
Success
- 실패 ->
using UnityEngine;
using BehaviorTree;
public class AttackPlayerNode : ActionNode
{
private float attackCooldown = 2f;
private float lastAttackTime = 0f;
private Animator animator;
public AttackPlayerNode(MonoBehaviour owner, Blackboard blackboard) : base(owner, blackboard)
{
animator = owner.GetComponentInChildren<Animator>();
}
public override NodeState Evaluate()
{
if (!blackboard.HasKey("target"))
{
state = NodeState.Failure;
return state;
}
Transform target = blackboard.GetValue<Transform>("target");
if (Time.time - lastAttackTime < attackCooldown)
{
state = NodeState.Running;
return state;
}
Vector3 lookDirection = (target.transform.position - owner.transform.position).normalized;
owner.transform.rotation = Quaternion.LookRotation(lookDirection);
if (animator != null)
{
animator.SetTrigger("onAttack");
}
lastAttackTime = Time.time;
state = NodeState.Success;
return state;
}
}using UnityEngine;
using BehaviorTree;
public class AttackPlayerNode : ActionNode
{
private float attackCooldown = 2f;
private float lastAttackTime = 0f;
private Animator animator;
public AttackPlayerNode(MonoBehaviour owner, Blackboard blackboard) : base(owner, blackboard)
{
animator = owner.GetComponentInChildren<Animator>();
}
public override NodeState Evaluate()
{
if (!blackboard.HasKey("target"))
{
state = NodeState.Failure;
return state;
}
Transform target = blackboard.GetValue<Transform>("target");
if (Time.time - lastAttackTime < attackCooldown)
{
state = NodeState.Running;
return state;
}
Vector3 lookDirection = (target.transform.position - owner.transform.position).normalized;
owner.transform.rotation = Quaternion.LookRotation(lookDirection);
if (animator != null)
{
animator.SetTrigger("onAttack");
}
lastAttackTime = Time.time;
state = NodeState.Success;
return state;
}
}- 해야할 일
- 그냥 True False를 Success, Failure에 매핑 하면됨
using UnityEngine;
using BehaviorTree;
public class IsTargetTooFarNode : ConditionNode
{
private float maxDistance;
public IsTargetTooFarNode(MonoBehaviour owner, Blackboard blackboard, float maxDistance = 5f)
: base(owner, blackboard)
{
this.maxDistance = maxDistance;
}
public override NodeState Evaluate()
{
Transform target = blackboard.GetValue<Transform>("target");
MonoBehaviour self = blackboard.GetValue<MonoBehaviour>("self");
if (self.transform == null || target == null)
{
state = NodeState.Failure;
return state;
}
float distance = Vector3.Distance(self.transform.position, target.position);
if (distance > maxDistance)
{
state = NodeState.Success;
}
else
{
state = NodeState.Failure;
}
return state;
}
}using UnityEngine;
using System.Collections.Generic;
namespace BehaviorTree
{
public class SequenceNode : CompositeNode
{
public SequenceNode() : base() { }
public SequenceNode(List<Node> children) : base(children) { }
public override NodeState Evaluate()
{
foreach (Node child in children)
{
switch (child.Evaluate())
{
case NodeState.Running:
state = NodeState.Running;
return state;
case NodeState.Failure:
state = NodeState.Failure;
return state;
case NodeState.Success:
continue;
}
}
Reset();
state = NodeState.Success;
return state;
}
public override void Reset()
{
base.Reset();
}
}
}using System.Collections.Generic;
namespace BehaviorTree
{
public class SequenceNode : CompositeNode
{
public SequenceNode() : base() { }
public SequenceNode(List<Node> children) : base(children) { }
public override NodeState Evaluate()
{
foreach (Node child in children)
{
switch (child.Evaluate())
{
case NodeState.Running:
state = NodeState.Running;
return state;
case NodeState.Failure:
state = NodeState.Failure;
return state;
case NodeState.Success:
continue;
}
}
Reset();
state = NodeState.Success;
return state;
}
public override void Reset()
{
base.Reset();
}
}
}