오늘 배운 내용
- 몬스터 Ai 구현 - 최종
몬스터 Ai 구현 - 최종
팀 프로젝트에서 내가 구현하게 된 부분은 몬스터를 생성하고 몬스터의 AI를 만드는 부분이 였다.
처음 생각한 것은 전략 패턴을 사용해서 몬스터들의 패턴인 전략을 여러가지 만들고, 하나의 Controller를 이용해서
추가한 모든 전략들 중에서 조건에 맞는 전략을 실행하도록 만드는 것이였다.
각각의 전략들은 IBehaviour 인터페이스를 구현하고, 추상클래스 EnemyBehaviour를 상속받아서 만들어 주었다.
public enum StrategyState
{
Rest, // 대기 상태 ( 행동에 대한 조건을 체크)
Ready,// 조건을 만족한 상태 현재 오브젝트의 상태를 확인하고 실행할 수 있는지 확인.
Action,// 대기열에 들어간 상태
CoolTime, // 쿨타임
}
public enum StratgeyType
{
Move,
Hurt,
Attack,
Skill,
Dead,
EndPoint,
}
public interface IBehaviour
{
StrategyState State { get; set; }
StratgeyType Type { get;}
void OnRest();
void OnAction();
void OffAction();
void OnCoolTime();
}
public abstract class EnemyBehaviour : MonoBehaviour
{
protected GameObject Target; // ÀÓ½Ã
protected EnemyAnimationController animationController;
protected EnemyBehaviourController behaviourController;
protected CharacterStats stats;
public Vector2 Direction { get { return (Target.transform.position - transform.position).normalized; } }
public float Distance { get { return Vector3.Distance(transform.position, Target.transform.position); } }
protected virtual void Awake()
{
animationController = GetComponent<EnemyAnimationController>();
behaviourController = GetComponent<EnemyBehaviourController>();
}
protected virtual void Start()
{
Target = GameManager.Instance.PlayerInActive;
stats = GetComponent<CharacterStatsHandler>().CurrentStats;
}
public void StartAction(IBehaviour behaviour)
{
IBehaviour prevBehaviour = behaviourController.CurrentBehaviour;
if (prevBehaviour != null)
{
EndAction(prevBehaviour);
}
behaviour.State = StrategyState.Action;
behaviourController.CurrentBehaviour = behaviour;
}
public void EndAction(IBehaviour behaviour)
{
behaviour.OffAction();
behaviourController.CurrentBehaviour = null;
behaviour.State = StrategyState.CoolTime;
}
protected StratgeyType? CurrentBehaviourType()
{
if (behaviourController.CurrentBehaviour != null)
{
return behaviourController.CurrentBehaviour.Type;
}
return null;
}
}
각각의 기능들은 StrategyState(동작의 상태), StratgeyType(동작의 타입 - Enemy가 취하게될 상태)를 가지고
현재 상태에 따라서 IBehaviour를 통해서 구현한 OnRest(), OnAction(), OnCoolTime(), OffAction()등의 메소드를
실행한다.
그리고 EnemyBehavior에 정의된 StartAction(), EndAction(), CurrentBehaviourType()를 통해서
EnemyBehaviourController와 상호작용하여 IBehaviour들을 관리한다.
기능을 구현한 예시
using UnityEngine;
public class Move : EnemyBehaviour, IBehaviour
{
private Rigidbody2D _rb2D;
[SerializeField] float followRange;
[SerializeField] float attackRange;
private StrategyState _state = StrategyState.Rest;
private StratgeyType _type = StratgeyType.Move;
public StrategyState State { get => _state; set => _state = value; }
public StratgeyType Type { get => _type;}
protected override void Awake()
{
base.Awake();
_rb2D = GetComponent<Rigidbody2D>();
}
public void OnRest()
{
if (!CheckCondition()) return;
switch (CurrentBehaviourType())
{
case null:
case StratgeyType.Move:
StartAction(this);
break;
default:
break;
}
}
public void OnAction()
{
Vector2 direction = Direction;
_rb2D.velocity = Quaternion.Euler(0, 0, Random.Range(-15f, 15f)) * direction * stats.speed;
animationController.Move(direction);
if (!CheckCondition())
{
EndAction(this);
}
}
public void OnCoolTime()
{
State = StrategyState.Rest;
}
public void OffAction()
{
_rb2D.velocity = Vector2.zero;
animationController.Move(Vector2.zero);
}
private bool CheckCondition()
{
float distance = Distance;
return attackRange < distance && distance < followRange ? true : false;
}
}
public class Rush : EnemyBehaviour, IBehaviour
{
private SpriteRenderer _spriteRenderer;
private Rigidbody2D _rb2D;
private Collider2D _collider2D;
enum RushStep
{
Rest,
Charge,
Complete,
Rush,
}
protected override void Awake()
{
base.Awake();
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
_rb2D = GetComponent<Rigidbody2D>();
_collider2D = GetComponent<Collider2D>();
remainTime = UnityEngine.Random.Range(1f, coolTime);
}
private StrategyState _state = StrategyState.CoolTime;
private StratgeyType _type = StratgeyType.Skill;
public StrategyState State { get => _state; set => _state = value; }
public StratgeyType Type { get => _type;}
public void OnRest()
{
if (!(Distance < range)) return;
switch (CurrentBehaviourType())
{
case null:
case StratgeyType.Move:
case StratgeyType.Attack:
StartAction(this);
break;
default:
break;
}
}
public void OnAction()
{
switch (rushState)
{
case RushStep.Rest:
Init();
break;
case RushStep.Charge:
OnCharging();
break;
case RushStep.Complete:
ReadyRush();
break;
case RushStep.Rush:
OnRush();
break;
}
}
public void OnCoolTime()
{
remainTime -= Time.deltaTime;
if (remainTime < 0f)
{
State = StrategyState.Rest;
}
}
public void OffAction()
{
_rb2D.velocity = Vector2.zero;
remainTime = coolTime;
rushState = RushStep.Rest;
}
#region OnAction()
private void Init()
{
_rb2D.velocity = Vector2.zero;
animationController.Move(Vector2.zero);
rushState = RushStep.Charge;
remainchargingTime = chargingTime;
startPos = transform.position;
}
private void OnCharging()
{
remainchargingTime -= Time.deltaTime;
if (remainchargingTime < waitTime)
{
rushState = RushStep.Complete;
return;
}
// 타겟이 확정된 타이밍의 방향을 알기위해서
direction = Direction;
float rotZ = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
_spriteRenderer.flipX = Mathf.Abs(rotZ) > 90f;
rushRange.transform.rotation = Quaternion.Euler(0, 0, rotZ);
rushRange.SetActive(true);
}
private void ReadyRush()
{
remainchargingTime -= Time.deltaTime;
if (remainchargingTime < 0)
{
rushRange.SetActive(false);
rushState = RushStep.Rush;
}
}
private void OnRush()
{
_rb2D.velocity = direction * stats.speed * speedCoefficient;
animationController.Move(direction);
// 이동한 거리와 시작지점에서 목표지점까지의 거리를 비교
if (Vector2.Distance(startPos, (Vector2)transform.position) > rushDistance)
{
EndAction(this);
}
}
#endregion
private void OnCollisionEnter2D(Collision2D collision)
{
if (State == StrategyState.Action && rushState == RushStep.Rush)
{
GameObject go = collision.gameObject;
if (go == null) return;
if (go.CompareTag("Player"))
{
HealthController hc = go.GetComponent<HealthController>();
if (hc == null) return;
hc.ChangeHealth(-(int)stats.attackSO.power * damageCoefficient);
}
}
}
[SerializeField] private GameObject rushRange;
[SerializeField][Range(0f, 100f)] float range;
[SerializeField][Range(0f, 100f)] float coolTime;
[SerializeField][Range(0f, 100f)] float chargingTime;
[SerializeField][Range(0f, 100f)] float waitTime;
[SerializeField][Range(0f, 100f)] float rushDistance;
[SerializeField][Range(0f, 100f)] float remainchargingTime;
[SerializeField][Range(0f, 100f)] float speedCoefficient;
[SerializeField][Range(0, 20)] int damageCoefficient;
Vector2 direction = Vector2.zero;
Vector2 startPos = Vector2.zero;
private float remainTime;
RushStep rushState = RushStep.Rest;
}
EnemyBehaviourController
public class EnemyBehaviourController : MonoBehaviour
{
public IBehaviour CurrentBehaviour { get; set; }
[SerializeField] public List<IBehaviour> BehavioursList = new List<IBehaviour>();
private void Start()
{
foreach (IBehaviour behaviour in GetComponents<IBehaviour>())
{
BehavioursList.Add(behaviour);
}
}
private void Update()
{
BehaviourUpdate();
CurrentBehaviour?.OnAction();
}
// Action
private void BehaviourUpdate()
{
List<IBehaviour> temp = new List<IBehaviour>();
while(BehavioursList.Count > 0)
{
IBehaviour behaviour = BehavioursList[0];
BehavioursList.RemoveAt(0);
switch (behaviour.State)
{
case StrategyState.Rest:
behaviour.OnRest();
break;
case StrategyState.Ready:
break;
case StrategyState.Action:
break;
case StrategyState.CoolTime:
behaviour.OnCoolTime();
break;
default:
break;
}
temp.Add(behaviour);
}
BehavioursList = temp;
}
}
EnemyBehaviourController에서는 List로 IBehaviour들을 관리하고
Update()문에서 List에 있는 IBehaviour들의 상태에 따라서 메서드를 실행하고 상태를 변경한 후
최종적으로 선택된 하나의 IBehaviour의 실행부분을 CurrentBehaviour?.OnAction()로 실행한다.