얼마전 에셋 스토어 할인으로 Behavior Desinger를 구매하였다. 50%할인을 받았지만.. 그래도 5만원가까운 돈이 나갔다. Behavior Tree가 Ai를 만드는데 그렇게 좋다길래 한번 사보았지만 실효셩에 대해 의문이었다. 이번에 프로젝트를 하면서 적 AI를 BT로 만들기로 하였는데 웬걸 이건 신세계이다. 다음 할인 때 무조건 사세요.
AI 기획은 다음과 같이 단순하였다.
1. 플레이어가 주변에 있다.
2. 따라간다.
2-1. 플레이어를 계속 따라간다.
2-2. 플레이어가 범위를 벗어나면 원래 위치로 돌아간다.
2-3. 원래 위치로 돌아가다가 플레이어가 다시 범위안에 들어오면 따라간다.
3, 공격범위에 들어온다.
4, 공격한다.
이를 Behavior Tree로 구성하면 다음과 같이 구성할 수 있었다.

Repeater로 무한 반복시켜준다.
그 밑에 sequence를 둔다. 왼쪽에는 이동관련 로직을 오른쪽에는 공격관련 로직을 두었다.
노드를 sequence로 둔 이유는 이동관련 로직이 실패하면 애초에 공격관련 로직이 성공하면 안되기 때문에 이동관련(플레이어 감지) 로직이 fail을 뱉으면 공격관련 로직이 실행되지 않도록 하였다.
왼쪽 노드 즉, 플레이어 감지 겸 이동관련 로직을 Selector로 두었는데 이는 2-2를 구현하기 위해서이다. Can Move to Player로 player를 감지하고 Move to Player로 따라간다. 그러다 공격범위 안에 들어오면 Move To Player가 success를 반환하고 그럼 selector가 success를 반환 그럼 오른쪽 sequence가 실행된다.
그러면 바로 Can Attack이 실행되고 Can Attack이 가능하다면 Log가 실행되는 로직이다.
좀 길었는데 어쨌든 2-1에서 3,4번 로직은 이런식으로 실행된다. 그럼 왜 Selector로 두었는가에 대한 질문에 답하자면 Sequencer가 fali을 뱉었을 때 즉, Move to Player에서 너무 멀어지면 Fail을 뱉게 하였다. 그럼 Move To Original Pos가 실행되게 되며 2-2로직이 실행된다. 그리고 can Attack은 fail을 반환하여 로직이 종료된다. 하지만 이런 경우가 있다. 2-2로직 실행중 플레이어가 다시 범위에 들어오면 어떡하지? 그걸 구현하기 위해 Sequence에 조건부 종료 lower Priority를 넣어두었다. 즉 Move To Original Pos가 실행되는 동안 계속 Can Move To player가 재평가 된다. 돌아가다 다시 플레이어를 쫓아갈 수 있다는 이야기이다.
Behavior Tree를 처음 작성해봐서 로직이 좀 난잡하고 더 쉬운 로직이 있을 수도 있다. 양해하고 봐주시길 바란다.
using BehaviorDesigner.Runtime.Tasks;
using BehaviorDesigner.Runtime;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static UnityEngine.GraphicsBuffer;
public class MoveToOriginalPos : Action
{
Vector3 originalPos;
public float speed;
public override void OnAwake() {
originalPos = transform.position;
}
public override TaskStatus OnUpdate() {
if ((transform.position - originalPos).magnitude < 0.01f) return TaskStatus.Success;
transform.position = Vector2.MoveTowards(transform.position,
originalPos, speed * Time.deltaTime);
return TaskStatus.Running;
}
}
using BehaviorDesigner.Runtime.Tasks;
using BehaviorDesigner.Runtime;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CanMoveToPlayer : Conditional
{
public SharedTransform target;
public float distance;
public override void OnAwake() {
target.Value = GameObject.FindGameObjectWithTag("Player").transform;
}
public override TaskStatus OnUpdate() {
if (WithinSight(target.Value)) {
return TaskStatus.Success;
}
return TaskStatus.Failure;
}
public bool WithinSight(Transform targetTransform) {
Vector2 direction = targetTransform.position - transform.position;
float dist = direction.magnitude;
return dist <= distance;
}
}
using UnityEngine;
using BehaviorDesigner.Runtime;
using BehaviorDesigner.Runtime.Tasks;
public class MoveToPlayer : Action {
// 오브젝트의 속도
public float speed = 0;
// 오브젝트가 이동해야할 위치 정보가 담긴 Transform
public SharedTransform target;
public override TaskStatus OnUpdate() {
// target에 도달하면 성공의 태스크 상태를 반환합니다.
float dist = Vector2.SqrMagnitude(transform.position - target.Value.position);
if (dist < 3f) {
return TaskStatus.Success;
}
else if(dist > 10f) {
return TaskStatus.Failure;
}
// 아직 목표에 도달하지 못했으므로 계속 이동합니다.
transform.position = Vector2.MoveTowards(transform.position,
target.Value.position, speed * Time.deltaTime);
return TaskStatus.Running;
}
}
using BehaviorDesigner.Runtime.Tasks;
using BehaviorDesigner.Runtime;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static UnityEngine.GraphicsBuffer;
using Unity.VisualScripting;
public class CanAttack : Conditional
{
public SharedTransform target;
public float distance;
public float fov;
public override TaskStatus OnUpdate() {
if (WithinSight(target.Value, fov)) {
return TaskStatus.Success;
}
return TaskStatus.Failure;
}
public bool WithinSight(Transform targetTransform, float fieldOfViewAngle) {
Vector2 direction = targetTransform.position - transform.position;
float dist = direction.magnitude;
return Vector2.Angle(direction, transform.forward) < fieldOfViewAngle && dist <= distance;
}
}