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

2025.06.03 구조 변경으로 인한 예제 코드파일 수정

Overview

  • 현재 예제 코드는 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 상속)

대략의 실행 흐름

  1. 루트 Evaluate
  2. 루트의 자식들 Evaluate (루트 노드에 추가된 순서대로, 왼쪽 -> 오른쪽)
  3. 재귀적으로 탐색하면서 leaf node(action, condition, etc)에 도착하면 leaf node 실행
  4. 실행 결과에 따라서 셀렉터, 시퀀스에서 다음으로 넘어갈지 여기서 끝낼지 등등 판단
  5. 실행결과가 Running이면 작업이 안끝났기 때문에 다음 프레임(Update)에서 다시 Evaluate

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 코드 예시

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: 트리 구조 설정

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

새로운 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;
    }
}

예시코드

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에 매핑 하면됨
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;
    }
}

Selector

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

Sequence

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

Clone this wiki locally