[AI] 행동 트리를 이용한 AI 개발
Behaviour Tree의 개념
Behavior Tree
는 AI 구현에서 행동을 구조적으로 설계하고 관리하기 위한 계층적 트리 구조입니다. 복잡한 AI 로직을 단순화하고 확장성을 높이는 데 효과적입니다.
Unity에서는 Behavior Tree를 직접 구현하거나, Bolt, NodeCanvas, Behavior Designer 같은 플러그인을 사용할 수 있습니다.
구성요소
- Root 노드
- 트리의 시작점으로, 하나의 자식 노드만 가질 수 있습니다.
- Composite 노드
- 여러 자식 노드를 관리하며, 조건에 따라 실행 순서를 제어합니다.
- Selector: 자식 노드 중 하나라도 성공하면 성공.
- Sequence: 모든 자식 노드가 성공해야 성공.
- 여러 자식 노드를 관리하며, 조건에 따라 실행 순서를 제어합니다.
- Decorator 노드
- 자식 노드의 실행을 제어하거나 조건을 추가합니다.
(ex. 특정 조건이 참일 때만 자식 노드를 실행)
- 자식 노드의 실행을 제어하거나 조건을 추가합니다.
- Leaf 노드
- 실제 행동(Behavior)을 수행하는 노드입니다.
예: 이동, 공격, 대기 등
- 실제 행동(Behavior)을 수행하는 노드입니다.
장단점
장점
- 유연성: 다양한 AI 로직을 구현할 수 있습니다.
- 가독성: 시각적인 트리 구조로 인해 코드를 쉽게 이해할 수 있습니다.
- 확장성: 새로운 행동이나 조건을 추가하기 쉽습니다.
- 재사용성: 여러 캐릭터에게 동일한 Behavior Tree를 사용할 수 있습니다.
단점
- 복잡한 구현: 직접 구현하는 경우 많은 시간과 노력이 필요할 수 있습니다.
- 성능 오버헤드: 복잡한 트리 구조는 성능에 영향을 줄 수 있습니다.
- 디버깅 어려움: 문제 발생 시 디버깅이 어려울 수 있습니다.
작동 방식
트리 순회 방식으로 작동합니다.
Root 노드에서 시작해 자식 노드를 순차적으로 탐색하며 이때 각 노드는 성공(Success), 실패(Failure), 실행 중(Running) 상태를 반환합니다.
이때 노드가 반환한 상태에 따라 다음 행동을 판단하여 수행하는 방식입니다.
노드 상태
- Success: 작업이 성공적으로 완료됨.
- Failure: 작업이 실패했거나 조건이 충족되지 않음.
- Running: 작업이 아직 진행 중이며 완료되지 않음.
코드
[기본 구조 구현]
using System.Collections.Generic;
using UnityEngine;
public abstract class Node
{
public enum NodeState { Success, Failure, Running }
protected NodeState state;
public NodeState State => state;
public abstract NodeState Evaluate();
}
[Composite 노드 구현]
public class Selector : Node
{
private List<Node> children = new List<Node>();
public Selector(List<Node> children)
{
this.children = children;
}
public override NodeState Evaluate()
{
foreach (var child in children)
{
NodeState result = child.Evaluate();
if (result == NodeState.Success)
{
state = NodeState.Success;
return state;
}
else if (result == NodeState.Running)
{
state = NodeState.Running;
return state;
}
}
state = NodeState.Failure;
return state;
}
}
public class Sequence : Node
{
private List<Node> children = new List<Node>();
public Sequence(List<Node> children)
{
this.children = children;
}
public override NodeState Evaluate()
{
foreach (var child in children)
{
NodeState result = child.Evaluate();
if (result == NodeState.Failure)
{
state = NodeState.Failure;
return state;
}
else if (result == NodeState.Running)
{
state = NodeState.Running;
return state;
}
}
state = NodeState.Success;
return state;
}
}
[Leaf 노드 구현]
public class MoveToTarget : Node
{
private Transform transform;
private Transform target;
private float speed;
public MoveToTarget(Transform transform, Transform target, float speed)
{
this.transform = transform;
this.target = target;
this.speed = speed;
}
public override NodeState Evaluate()
{
if (Vector3.Distance(transform.position, target.position) > 0.1f)
{
transform.position = Vector3.MoveTowards(transform.position, target.position, speed * Time.deltaTime);
state = NodeState.Running;
}
else
{
state = NodeState.Success;
}
return state;
}
}
public class CheckTargetInRange : Node
{
private Transform transform;
private Transform target;
private float range;
public CheckTargetInRange(Transform transform, Transform target, float range)
{
this.transform = transform;
this.target = target;
this.range = range;
}
public override NodeState Evaluate()
{
float distance = Vector3.Distance(transform.position, target.position);
if (distance <= range)
{
state = NodeState.Success;
}
else
{
state = NodeState.Failure;
}
return state;
}
}
[Behavior Tree 구성]
using UnityEngine;
public class AIController : MonoBehaviour
{
public Transform target;
public float moveSpeed = 2.0f;
public float attackRange = 1.5f;
private Node rootNode;
private void Start()
{
// Leaf 노드
Node moveToTarget = new MoveToTarget(transform, target, moveSpeed);
Node checkTargetInRange = new CheckTargetInRange(transform, target, attackRange);
// Composite 노드
Sequence attackSequence = new Sequence(new List<Node> { checkTargetInRange, moveToTarget });
// Root 노드 설정
rootNode = new Selector(new List<Node> { attackSequence });
}
private void Update()
{
rootNode.Evaluate();
}
}