-
Notifications
You must be signed in to change notification settings - Fork 0
Home
soomnik edited this page Jun 2, 2025
·
4 revisions
- 적용할 대상 (e.g. 공격형 agent)에 컴포넌트로 스크립트
- 적용할 대상의 Update 함수에서 시작(매 프레임)되어서 루트 노드 부터 DFS 형태로 순회
- Node 클래스 (abstract)
- CompositeNode (abstract, Node 상속)
- 구현
- SelectorNode
- SequenceNode
- 구현
- DecoratorNode (abstract, Node 상속)
- LeafNode (abstract, Node 상속)
- ActionNode (abstract, LeafNode 상속)
- ConditionNode (abstract, LeafNode 상속)
- 그냥 예시라서 아래 예시에서는 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코드 예시- 타겟과 거리가 멀면 접근(시퀀스라서 멀지 않으면 실패해서 진행 X) 한다음에 공격 3번
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 ConstructTree()
{
SelectorNode selector = new SelectorNode(); // 빈 셀렉터 노드
SequenceNode sequence = new SequenceNode(); // 빈 시퀀스 노드
sequence.AddChild(new IsTargetTooFarNode(this, blackboard, followDistance)); // 시퀀스 노드에 컨디션 노드(타겟이 충분히 거리가 먼가?) 추가
sequence.AddChild(new MoveToTargetNode(this, blackboard, moveSpeed, followDistance * 0.8f)); // 시퀀스 노드에 액션 노드(타겟에 접근) 추가
RepeaterNode repeat1 = new RepeaterNode(3); // 반복 데코레이터: 3번 반복함
repeat1.SetChild(new AttackPlayerNode(this, blackboard)); // 데코레이터의 자식으로 액션 노드(공격) 등록(자식은 하나만)
sequence.AddChild(repeat1); // 시퀀스에 데코레이터 노드 추가
selector.AddChild(sequence); // 셀렉터의 자식으로 시퀀스 노드 추가
SetRootNode(selector); // 셀렉터 노드를 루트 노드로
}
}-
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;
}
}- 해야할 일
- 그냥 True False를 Success, Failure에 매핑 하면됨
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;
}
}public override NodeState Evaluate()
{
for (int i = currentChildIndex; i < children.Count; i++)
{
var childState = children[i].Evaluate();
switch (childState)
{
case NodeState.Running:
currentChildIndex = i;
state = NodeState.Running;
return state;
case NodeState.Success:
ResetAllChildren();
currentChildIndex = 0;
state = NodeState.Success;
return state;
case NodeState.Failure:
children[i].Reset();
continue; // 다음 자식 노드 시도
}
}
// 모든 자식 노드 실패
ResetAllChildren();
currentChildIndex = 0;
state = NodeState.Failure;
return state;
}public override NodeState Evaluate()
{
for (int i = currentChildIndex; i < children.Count; i++)
{
NodeState childState = children[i].Evaluate();
switch (childState)
{
case NodeState.Running:
currentChildIndex = i;
state = NodeState.Running;
return state;
case NodeState.Failure:
ResetAllChildren();
currentChildIndex = 0;
state = NodeState.Failure;
return state;
case NodeState.Success:
continue;
}
}
// 모든 자식 노드 성공한 경우
ResetAllChildren();
currentChildIndex = 0;
state = NodeState.Success;
return state;
}- 구현
- RepeaterNode - 반복
- InverterNode - 실패 성공 바꾸기
- RetryNode - 실패해도 재시도
- ForceSuccessNode - 강제로 성공으로 처리
- ForceFailureNode - 강제로 실패로 처리
- TimeoutNode - 타임아웃
- DelayNode - 딜레이
- UntilSuccessNode - 성공할 때 까지
- UntilFailureNode - 실패할 때 까지