지금까지 공격과 공격 시 체력바 변화를 구현했으니, 이제는 몬스터의 AI를 구현할 차례이다.
PlayerController로 플레이어를 관리 했던것처럼 몬스터 또한 컨트롤러 스크립트로 관리할 것이다.
그 전에 PlayerController와 MonsterController는 겹치는 기능이 많기 때문에 구현해놓은 PlayerController의 일부를 BaseController라는 클래스에 옮겨서 PlayerController와 MonsterController가 BaseController를 상속받도록 하겠다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class baseController : MonoBehaviour
{
[SerializeField]
protected Vector3 _destPos;
[SerializeField]
protected Define.State _state = Define.State.Idle;
[SerializeField]
protected GameObject _lockTarget;
public virtual Define.State State
{
get { return _state; }
set
{
_state = value;
Animator anim = GetComponent<Animator>();
switch (_state)
{
case Define.State.Die:
break;
case Define.State.Idle:
anim.CrossFade("WAIT", 0.2f);
break;
case Define.State.Moving:
anim.CrossFade("RUN", 0.2f);
break;
case Define.State.Skill:
anim.CrossFade("ATTACK", 0.2f, -1, 0);
break;
}
}
}
private void Start()
{
Init();
}
protected virtual void Init()
{
}
void Update()
{
switch (State)
{
case Define.State.Idle:
UpdateIdle();
break;
case Define.State.Moving:
UpdateMoving();
break;
case Define.State.Die:
UpdateDie();
break;
case Define.State.Skill:
UpdateSkill();
break;
}
}
public abstract void Init();
protected virtual void UpdateIdle() { }
protected virtual void UpdateMoving() { }
protected virtual void UpdateDie() { }
protected virtual void UpdateSkill() { }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class PlayerController : BaseController
{
int _mask = (1 << (int)Define.Layer.Ground) | (1 << (int)Define.Layer.Monster);
PlayerStat _stat;
bool _stopSkill = false;
protected override void Init()
{
...
}
protected override void UpdateMoving()
{
...
}
protected override void UpdateSkill()
{
...
}
void OnHitEvent()
{
...
}
void OnMouseEvent(Define.MouseEvent evt)
{
...
}
void OnMouseEvent_IdleRun(Define.MouseEvent evt)
{
...
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class MonsterController : BaseController
{
Stat _stat;
[SerializeField]
float _scanRange = 10;
[SerializeField]
float _attackRange = 2;
public override void Init()
{
_stat = gameObject.GetComponent<Stat>();
if (gameObject.GetComponentInChildren<UI_HPBar>() == null)
Managers.UI.MakeWorldSpaceUI<UI_HPBar>(transform);
}
protected override void UpdateIdle()
{
GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player == null)
return;
float distance = (player.transform.position - transform.position).magnitude;
if (distance <= _scanRange)
{
_lockTarget = player;
State = Define.State.Moving;
return;
}
}
protected override void UpdateMoving()
{
// 플레이어가 내 사정거리보다 가까우면 공격
if (_lockTarget != null)
{
_destPos = _lockTarget.transform.position;
float distance = (_destPos - transform.position).magnitude;
if (distance <= _attackRange)
{
State = Define.State.Skill;
return;
}
}
// 이동
Vector3 dir = _destPos - transform.position;
if (dir.magnitude < 0.1f)
{
State = Define.State.Idle;
} // 목적지까지의 거리가 매우 작다면(도착했다면) 이동중이라는 상태를 false로 만든다.
else
{
NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
nma.SetDestination(_destPos);
nma.speed = _stat.MoveSpeed;
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 10 * Time.deltaTime);
}
}
protected override void UpdateSkill()
{
if (_lockTarget != null)
{
Vector3 dir = _lockTarget.transform.position - transform.position;
Quaternion quat = Quaternion.LookRotation(dir);
transform.rotation = Quaternion.Lerp(transform.rotation, quat, 20 * Time.deltaTime);
}
}
void OnHitEvent()
{
if (_lockTarget != null)
{
Stat targetStat = _lockTarget.GetComponent<Stat>();
Stat myStat = gameObject.GetComponent<Stat>();
int damage = Mathf.Max(0, myStat.Attack = targetStat.Defense);
targetStat.Hp -= damage;
if (targetStat.Hp > 0)
{
float distance = (_lockTarget.transform.position - transform.position).magnitude;
if (distance <= _attackRange)
State = Define.State.Skill;
else
State = Define.State.Moving;
}
else
{
State = Define.State.Idle;
}
}
else
{
State = Define.State.Idle;
}
}
}
적 입장에서 일정 거리 안에 플레이어가 들어오면 플레이어에게 이동하고, 공격 가능 거리 안까지 접근한다면 공격을 하기 시작한다.
이후에 게임을 실행해보니 원하는 대로 작동하지만, 적이 플레이어의 정확한 좌표로 이동을 하려다보니 플레이어가 적에게 밀려서 위치가 변하는 오류가 발생한다.
protected override void UpdateMoving()
{
// 플레이어가 내 사정거리보다 가까우면 공격
if (_lockTarget != null)
{
...
if (distance <= _attackRange)
{
NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
nma.SetDestination(transform.position);
...
}
}
UpdateMoving() 함수에 플레이어가 공격 거리 안으로 들어오면 이동 좌표를 현재 자신의 위치로 변화하는 코드를 추가했다.
이후에 오류가 해결되는 것을 확인하였다.
마지막으로, 전에 이동 관련 함수를 작성했을 때 플레이어의 앞으로 레이캐스팅을 해서 갈 수 있을지 없을지를 판단했다. 따라서 NavMesh 관련된 기능이 플레이어에겐 필요없어 졌으므로 플레이어의 NavMesh 관련된 기능을 모두 삭제했다.
protected override void UpdateMoving()
{
...
Vector3 dir = _destPos - transform.position;
if (dir.magnitude < 0.1f)
{
State = Define.State.Idle;
} // 목적지까지의 거리가 매우 작다면(도착했다면) 이동중이라는 상태를 false로 만든다.
else
{
if (Physics.Raycast(transform.position + Vector3.up * 0.5f, dir, 1.0f, LayerMask.GetMask("Block")))
{
...
}
float moveDist = Mathf.Clamp(_stat.MoveSpeed * Time.deltaTime, 0, dir.magnitude);
transform.position += dir.normalized * moveDist;
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 10 * Time.deltaTime);
}
}
이동 관련 함수 또한 NavMesh를 사용하지 않는 형태로 바꾸었다.