Skip to content
soomnik edited this page Jun 2, 2025 · 4 revisions

Overview

  • 적용할 대상 (e.g. 공격형 agent)에 컴포넌트로 스크립트
  • 적용할 대상의 Update 함수에서 시작(매 프레임)되어서 루트 노드 부터 DFS 형태로 순회
  • Node 클래스 (abstract)
  • CompositeNode (abstract, Node 상속)
    • 구현
      • SelectorNode
      • SequenceNode
  • DecoratorNode (abstract, Node 상속)
  • LeafNode (abstract, Node 상속)
  • ActionNode (abstract, LeafNode 상속)
  • ConditionNode (abstract, LeafNode 상속)

Example

  • 그냥 예시라서 아래 예시에서는 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: 트리 구조 설정

  • 트리 그림 그려놓고 구성하면 어렵지 않음

새로운 Action node, Condition node 개발

public enum NodeState
{
    Running,    // 실행 중
    Success,    // 성공
    Failure     // 실패
}

action node

  • 해야할 일
  1. ActionNode 클래스를 상속
  2. 생성자에 : base(owner, blackboard) 추가해서 ActionNode (부모) 생성자 처리
  3. public override NodeState Evaluate() 함수 구현
  4. 처리 상태에 따라 NodeState 타입을 반환해야함
    1. 실패 -> Failure
    2. 진행 중 -> Running : 다음 프레임에도 끝날 때 까지 계속 계속
    3. 성공 -> 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;
    }
}

condition node

  • 해야할 일
    • 그냥 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;
    }
}

Selector

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;
}

Sequence

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;
}

Decorator

  • 구현
    • RepeaterNode - 반복
    • InverterNode - 실패 성공 바꾸기
    • RetryNode - 실패해도 재시도
    • ForceSuccessNode - 강제로 성공으로 처리
    • ForceFailureNode - 강제로 실패로 처리
    • TimeoutNode - 타임아웃
    • DelayNode - 딜레이
    • UntilSuccessNode - 성공할 때 까지
    • UntilFailureNode - 실패할 때 까지

Clone this wiki locally