
저번 글에선 팩토리패턴을 이용하여 특정 위치에 도달하면 몬스터를 소환하는데 까지 성공했습니다.
이번엔 몬스터가 플레이어를 추적하게 만드려고합니다!


(몬스터가 지나갈 바닥은 이미 Navigation Bake 해주었습니다)
몬스터를 나타내는 Lizard 클래스에 몬스터 AI를 구현하기위해 AI로직을 별도의 컴포넌트로 분리해 재사용성을 높여봤습니다.ㅇ
using UnityEngine;
using UnityEngine.AI;
public class MonsterAI : MonoBehaviour
{
public Transform target; //추적할 타겟
private NavmeshAgent agent; //NavmeshAgent
private void Start()
{
agent = GetComponent<NavmeshAgent>(); //NavmeshAgent 초기화
}
}


using UnityEngine;
using UnityEngine.AI;
public class MonsterAI : MonoBehaviour
{
public Transform target;
private NavMeshAgent agent;
void Start()
{
agent = GetComponent<NavMeshAgent>();
}
void Update()
{
//NullReference오류 발생 방지, 컴포넌트target이 비어있다면
if (target == null)
{
//플레이어 찾아서 넣어줍니다.
GameObject player = GameObject.FindGameObjectWithTag("Player");
//플레이어가 있다면 위치를 얻게됩니다.
if (player != null)
target = player.transform;
}
if (target != null) //목표 값이 있다면
{
agent.SetDestination(target.position); //목표를 향해 추적
Vector3 lookDirection = target.position - transform.position; //쳐다볼 방향
lookDirection.y = 0f; //y값은 필요없으므로 0
Quaternion rotation = Quaternion.LookRotation(lookDirection); //회전
transform.rotation = Quaternion.Slerp(transform.rotation, rotation, Time.deltaTime * 5f); //부드럽게 회전;
}
}
}
타겟이 비어있으므로 null이라면 Player의 태그를 찾아 안에 넣어줄수있게 하였습니다. 수동으로 연결안해도 자동으로 추적하도록 말이죠. 그다음 player가 값이 있다면 추적할수있도록 스크립트를 작성했습니다.
그다음 리자드 스크립트에서도 초기화 해주면 플레이어를 추적합니다.
public class Lizard : MonoBehaviour
{
private MonsterAI monsterAI;
private void Awake()
{
monsterAI = GetComponent<MonsterAI>();
}
}

추적할때 움직이는 애니메이션도 주고싶으므로 BlendTree에서 idle, walk를 추가했습니다.
public class Lizard : MonoBehaviour
{
private MonserAI monsterAI;
private Animator animator;
private NavmeshAgent navMeshAgent;
private void Awake()
{
monsterAI = GetComponent<MonsterAI>();
animator = GetComponentInChildren<Animator>();
navMeshAgent = GetComponent<NavMeshAgent>();
}
private void Update()
{
MonsterMovement();
}
private void MonsterMovement()
{
float speed = navMeshAgent.velocity.magnitude;
animator.SetFloat("speed", speed);
}
}

이러면 플레이어를 추적할때 움직이며 추적하게 될 것입니다. 이제 플레이어 가까이 왔을때 몬스터가 플레이어를 공격하게 하려고합니다. 창을 들고있어서 공격 거리는 2정도로 설정했습니다.
public class Lizard : MonoBehaviour
{
//~생략~
private Transform target;
private Const float ATTACK_RANGE = 2f;
}
//~
//~
private void Update()
{
//생략
CheckForAttack();
}
private void CheckForAttack()
{
float distanceToPlayer = Vector3.Distance(transform.position, target.position);
if (distance <= ATTACK_RANGE)
{
animator.SetTrigger("isAttackging);
}
}


리자드가 플레이어와의 거리 2 이하로 들어오자 찌르기를 시전하고있습니다. 그러나 2이하로 들어온 순간부터 즉시, 무한적으로 찌르기 공격을 하고있습니다. 공격의 딜레이를 주려고합니다. 코루틴을 사용하겠습니다
private float attackDelay = 1.5f; //공격 지연 시간
private float reAttackTime = 2f; //공격 재사용 시간
private float checkInterval = 0.2f //프레임마다 호출 방지
private bool isReadyToAttack = true
private Const float ATTACK_RANGE = 2f;
private void Start
{
StartCoroutine(CheckPlayerDistance());
}
private IEnumerator CheckPlayerDistance()
{
while (true)
{
yield return new WaitForSeconds(checkInterval); //매 프레임마다 호출하는 것을 방지하려고 넣음
float distanceToPlayer = Vector3.Distance(player.position, target.position) //거리
if (distanceToPlayer <= 2f && isReadyToAttack) //플레이어와의 거리가 2이하이며 ture시
{
isReadyToAttack = false;
StartCoroutine(AttackPlayer()); //코루틴 실행
}
}
private IEnumerator AttackPlayer()
{
yield return new WaitForSeconds(attackDelay); //공격 범위가 되었다면 1.5초 뒤에 공격
animator.SetTrigger("isAttacking"); //애니메이션
yield return new WaitForSeconds(reAttackTime); //공격후 다시 공격하는데 까지 걸리는 시간
isReadyToAttack = true;
}
이제 몬스터가 플레이어를 추적하고, 플레이어와 몬스터가 거리가 2이하가 되었다면 코루틴을 실행하고 설정한 시간과 조건에 따라 공격을 시작합니다. 그런데 문제가 2개 남아있습니다.
현재 몬스터와의 거리를 하드코딩하고있으므로, 이 상수 부분도 바꿔주려고합니다.
private const float ATTACK_RANGE = 2f;
이제 몬스터가 플레이어 2이하로 왔을때는 공격을 해야하기때문에 스피드를 0으로 만들어야합니다. 지금은 공격 대기를 하면서 발은 계속 움직이고있습니다. 이떄 움직임을 멈춰주겠습니다.
private void MonsterMovement()
{
float distanceToPlayer = Vector3.Distance(transform.position, target.position);
if (distanceToPlayer <= ATTACK_RANGE && !isReadyToAttack)
{
animator.SetFloat("speed", 0f);
}
else
{
float speed = navMeshAgent.velocity.magnitude;
animatorr.SetFloat("speed", speed);
}
}
범위 체크를 할떄 2f로 적던걸 상수로 바꿔주고, 범위안에 들어왔을때 속도를 0으로 만들었습니다.
그런데, float distanceToPlayer = Vector3.Ditsance(transform.position, target.position)을 또 사용하고있습니다. 반복 코드는 싫으니까 클래스의 멤버 변수로 선언해 중복 계산을 피해보려고합니다.
private float distanceToPlayer; // 멤버 변수 선언
private void Update()
{
distanceToPlayer = Vector3.Distance(transform.position, target.position);
//
}
private IEnumerator CheckPlayerDistance()
{
//
if (distanceToPlayer < ATTACK_RANGE && isReadyToAttack)
{
isReadyToAttack = false;
StartCoroutine("AttackPlayer");
}
}
//
private void MonsterMovement()
{
if(distanceToPlayer <= attackRange && !isReadyToAttack)
{
//speed가 0이 되었을때 velocity 속도가 남아있어서 미끄러지므로 Vector3.zero 해줬다.
navMeshAgent.velocity = Vector3.zero;
navMeshAgent.isStopped = true;
animator.SetFloat("speed", 0f);
}
else
{
navMeshAgent.isStopped = false;
float speed = navMeshAgent.velocity.magnitude;
animator.SetFloat("speed", speed);
}
}
이제 CheckPlayerDistance()와 MonsterMovement()에서 distanceToPlayer를 계산하는 부분이 중복되지 않게 되었습니다.\

이제 제가 원하는 수치만큼 Nav Mesh Agent에서 스피드와 몬스터 겹침을 설정해준다면 끝입니다.
저는 기본적으로 제공하는 속도 3.5를 유지했고, 몬스터와 플레이어와의 최소 거리는 1.5로 설정했습니다.
이제 플레이을 해보죠!

이제 적들은 플레이어를 추적하고, 플레이어와의 거리가 2이하 라면 공격 애니메이션을 코루틴으로 실행합니다.
'Unity Engine > Unity 3D.' 카테고리의 다른 글
| [유니티 3D] 메기도 구현 (무한히 튕기는 광선) (1) | 2023.10.21 |
|---|---|
| 일정한 간격으로 오브젝트 생성 (0) | 2023.10.20 |
| 유니티 3D 잔상효과 (1) | 2023.10.20 |
| 콤보어택 정리 전 (0) | 2023.10.19 |
| [유니티 3D] 팩토리 패턴으로 몬스터와 보스 소환 해보기 (1) | 2023.10.08 |