오늘 배운 내용
- 몬스터 Ai 구현
몬스터 Ai 구현
이번 주차 팀 프로젝트에서 몬스터 Ai를 구현하는 부분을 맡게 되었다.
강의에서는 각 기능을 클래스로 만들고 메서드를 Controller의 이벤트에 연결해서 몬스터 컨트롤러들을 완성해 주었다.
그리고 Controller의 FixedUpdate()에서 붙어있는 기능을 호출해 주었는데
즉, 오브젝트가 가진 기능 클래스들에 따라서 컨트롤러 클래스를 여러 종류 만들게 되었다.
그런데 내가 생각하기엔 이렇게되면 기능은 기능대로 클래스가 필요해지고
기능의 조합에 따라서 또 다른 컨트롤러가 필요해지는게 아닌가? 라는 생각이 들었다.
그래서 나는 각 기능들의 처리는 각각의 기능을 구현한 클래스에서 처리하는게
더 확장성?이 좋지 않을까 라는 생각을 하면서 코드를 작성했다.
using System;
using UnityEngine;
public enum EnemyBehaviourType
{
Look,
Move,
Attack,
Skill,
}
public class EnemyBehaviour : MonoBehaviour
{
public Func<bool> CheckBehaviour;
public Action OnBehaviour;
public int Priority { get; protected set; }
protected EnemyController controller;
protected virtual void Awake()
{
controller = GetComponent<EnemyController>();
}
}
먼저 나는 EnemyBehaviour라는 클래스를 만들어서 각 기능 클래스들이 이 클래스를 상속하도록 만들어 주었다.
EnemyBehaviour는 EnemyController를 들고 있고 Priority를 가진다 Priority는 각 기능의
우선 순위를 정할 필요가 있다고 생각해서 추가했다.
using System.Collections.Generic;
using UnityEngine;
public class EnemyController : MonoBehaviour
{
[SerializeField] public GameObject Target; // 임시
[SerializeField] private List<EnemyBehaviour> enemyBehaviours = new List<EnemyBehaviour>();
[SerializeField][Range(0f, 100f)] public float followRange;
[SerializeField][Range(0f, 100f)] public float speed;
public Rigidbody2D rb2D;
public SpriteRenderer spriteRenderer;
[SerializeField]private List<EnemyBehaviour> _nextBehaviours = new List<EnemyBehaviour>();
public Vector2 Direction { get { return (Target.transform.position - transform.position).normalized; } }
public float Distance { get { return Vector3.Distance(transform.position, Target.transform.position); } }
private void Awake()
{
rb2D = GetComponent<Rigidbody2D>();
spriteRenderer = GetComponent<SpriteRenderer>();
foreach (EnemyBehaviour enemyBehaviour in GetComponents<EnemyBehaviour>())
{
enemyBehaviours.Add(enemyBehaviour);
}
}
private void FixedUpdate()
{
_nextBehaviours.Clear();
rb2D.velocity = Vector3.zero;
foreach (EnemyBehaviour enemyBehaviour in enemyBehaviours)
{
if (!enemyBehaviour.CheckBehaviour())
{
continue;
}
if (enemyBehaviour.Priority == 0)
{
enemyBehaviour.OnBehaviour();
continue;
}
if(_nextBehaviours.Count == 0)
{
_nextBehaviours.Add(enemyBehaviour);
}
else
{
if(enemyBehaviour.Priority > _nextBehaviours[0].Priority)
{
_nextBehaviours.Clear();
_nextBehaviours.Add(enemyBehaviour);
}
else if(enemyBehaviour.Priority == _nextBehaviours[0].Priority)
{
_nextBehaviours.Add(enemyBehaviour);
}
}
}
if (_nextBehaviours.Count != 0)
{
_nextBehaviours[Random.Range(0, _nextBehaviours.Count)].OnBehaviour();
}
}
}
EnemyController에서는 Awake()될때 해당 오브젝트가 가진 모든 EnemyBehaviour컴포넌트를 받아
List로 관리한다. 그리고 FixedUpdate()마다 해당 EnemyBehaviour 들의 조건을 Check해주고
우선순위를 비교한 뒤 우선순위가 가장 높은 EnemyBehaviour 실행하게된다.
만약 같은 우선순위가 여러개라면 그중 하나를 랜덤으로 선택해서 실행한다.
using System;
using Unity.VisualScripting;
using UnityEngine;
public class Chase : EnemyBehaviour
{
private int _priority = 1;
protected override void Awake()
{
base.Awake();
}
protected void Start()
{
Priority = _priority;
CheckBehaviour += CheckChase;
OnBehaviour += OnChase;
}
private void OnChase()
{
controller.rb2D.velocity = controller.speed * controller.Direction;
}
private bool CheckChase()
{
float distance = controller.Distance;
if (distance < controller.followRange)
{
return true;
}
return false;
}
}
그리고 각 EnemyBehaviour를 상속받은 기능들은 EnemyBehaviour에 있는 델리게이트에 On과 Check() 메소드를 정의해서 연결해준다.
사실 델리게이트가 필요한지 안 필요한지 헷갈리는 부분이 있는데.
EnemyBehaviour에서 델리게이트가 아니라 가상함수로 OnBehaviour를 정의하고 Chase에서 OnBehaviour을 오버라이딩 했다면
Chase를 EnemyBehaviour List에 넣었다가 뽑아서 OnBehaviour를 델리게이트가 아닌 그냥 메소드로 실행하게 되면
Chase에서 오버라이딩한 OnBehaviour가 제대로 호출되는지 헷갈려서 일단은 델리게이트를 사용했는데
내일 시간을 내서 한번 더 테스트를 해볼 예정이다.
-> 테스트 결과 오버라이딩을했다면 Chase의 OnBehaviour가 호출되는게 맞았다
이게 헷갈렸던 이유는 전에 했던 프로젝트에서 리플렉션을 사용하며 생겼던 문제와 혼동된 것 같다.
그래서 테스트 후 EnemyBehaviour를 추상클래스로 선언하고 델리게이트가 아니라 추상메서드를 오버라이드 하여
사용하는 방식으로 변경했다.
public abstract class EnemyBehaviour : MonoBehaviour
{
public int Priority { get; protected set; }
protected EnemyController controller;
protected virtual void Awake()
{
controller = GetComponent<EnemyController>();
}
public abstract void OnBehaviour();
public abstract bool CheckBehaviour();
}