Chapter 13-8. 미니 RPG : 몬스터 AI

Date:     Updated:

카테고리:

태그:

인프런에 있는 Rookiss님의 [C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part3: 유니티 엔진 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click

Chapter 13. 미니 RPG 만들기

🚀 몬스터 AI

  • 📜BaseController 👉 플레이어와 몬스터의 공통적인 속성과 기능 모음
    • 📜PlayerController
    • 📜MonsterController

📜MonsterController와 📜PlayerController의 공통적인 함수 및 멤버들은 📜BaseController로 옮겨주었음. 몬스터 애니메이션 컨트롤러의 애니메이션 클립의 이름들도 플레이어 애니메이션 컨트롤러와 동일하게.

📜MonsterController

몬스터 오브젝트에 붙여준다.

📜PlayerController 와 상당수 비슷하다. 여기에 없는건 📜BaseController로부터 상속 받음.

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.MakeWorldSpace<UI_HPBar>(transform);
    }
  • 게임이 시작되면 UI_HPBar를 몬스터에게 붙인다.
    • 📜BaseController로부터 이 Init을 실행시키는 Start 를 상속 받음
    protected override void UpdateIdle()
    {
        Debug.Log("Monster 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;
        }
    }
  • UpdateIdle 👉 몬스터가 Idle 상태일 때 매프레임 실행할 일
    • “Player”태그를 가진 오브젝트를 찾아 player에 할당. 플레이어 오브젝트 찾기.
      • 플레이어가 사정거리내에 존재하면 _lockTarget에 플레이어 오브젝트 할당하고 이제 플레이어 쫓아가야 하니까 상태를 Moving으로 변경
    protected override void UpdateMoving()
    {
        Debug.Log("Monster 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;
        }
        else
        {
            NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
            nma.SetDestination(_destPos);
            nma.speed = _stat.MoveSpeed;

            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 20 * Time.deltaTime);
        }
    }
  • UpdateMoving 👉 몬스터가 Moving 상태일 때 매프레임 실행할 일
    • ‘시야’ 사정거리보다 가까우면 UpdateIdle 를 통해 _lockTarget에 플레이어 들어있는 상태
      • 플레이어를 향해 _destPos 업뎃하고
      • 플레이어가 ‘공격’ 사정거리보다 가까우면 공격. 그리고 길 찾을 필요 없으니 return
    • 길 찾기
      • 도착했다면 Idle 상태로 돌아가기
      • 아니라면 플레이어 향해 바라보며 쫓아가야 함..
    protected override void UpdateSkill()
    {
        Debug.Log("Monster 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);
        }
    }
  • UpdateSkill 👉 몬스터가 Skill 상태일 때 매프레임 실행할 일
    • 공격 중에 플레이어 바라보고 공격하게끔
    void OnHitEvent()
    {
        Debug.Log("Monster OnHitEvent");

        if (_lockTarget != null) // 플레이어 타겟팅 중
        {
            // 체력
            Stat targetStat = _lockTarget.GetComponent<Stat>();
            int damage = Mathf.Max(0, _stat.Attack - targetStat.Defense);
            targetStat.Hp -= damage;

            if (targetStat.Hp > 0)
            {
                float distance = (_lockTarget.transform.position - transform.position).magnitude;
                if (_attackRange >= distance)
                    State = Define.State.Skill;
                else
                    State = Define.State.Moving;
            }
            else
            {
                State = Define.State.Idle;
            }
        }
        else  // 플레이어 타겟팅 중이 아닐 땐
        {
            State = Define.State.Idle;
        }
    }
}
  • OnHitEvent 👉 몬스터의 공격 애니메이션 중 발생하는 이벤트
    • 플레이어의 체력 깎기
      • 플레이어가 아직 안 죽었다면
        • 공격 사정거리 이내라면 다시 공격
        • 아니라면 다시 쫓기
      • 플레이어가 죽었다면
        • 정지


✈ 이동시 밀리는 현상

📜MonsterController : 몬스터가 이동시 플레이어를 미는 현상

    protected override void UpdateMoving()
    {
        Debug.Log("Monster UpdateMoving");

        // 플레이어가 내 사정거리보다 가까우면 공격
        if (_lockTarget != null)
        {
            _destPos = _lockTarget.transform.position;
            float distance = (_destPos - transform.position).magnitude;
            if (distance <= _attackRange)
            {
                NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
                nma.SetDestination(transform.position);
                State = Define.State.Skill;
                return;
            }
        }
            if (distance <= _attackRange)
            {
                NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
                nma.SetDestination(transform.position);

공격할 때도 계속 짧은 새 마다 플레이어를 쫓지 않도록(밀지 않도록), 공격 사정 거리 내에 있으면 그냥 제자리에 있도록 nma.SetDestination(transform.position)


📜PlayerController : 플레이어가 이동시 몬스터를 미는 현상

protected override void UpdateMoving()
{
		// 이동
		Vector3 dir = _destPos - transform.position;
		if (dir.magnitude < 0.1f)
		{
			State = Define.State.Idle;
		}
		else
		{
			Debug.DrawRay(transform.position + Vector3.up * 0.5f, dir.normalized, Color.green);
			if (Physics.Raycast(transform.position + Vector3.up * 0.5f, dir, 1.0f, LayerMask.GetMask("Block")))
			{
				if (Input.GetMouseButton(0) == false) //
					State = Define.State.Idle;
				return;
			}

			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), 20 * Time.deltaTime);
		}
	}

NavMeshAgentnma.Move 함수로 이동하지 않고 직접 플레이어의 위치를 업뎃시켜 해결하였다. transform.position += dir.normalized * moveDist;

NavMeshAgent를 붙여서 이동하는 방식은 기본적으로 Agent들은 서로 피해가도록 되어 있어 너무 인접하게 붙으면 의도치 않게 상대를 밀치기도 한다. Obstacle Avoidance 속성 때문이다. 이를 해결하는 방법 중 하나는 NavMeshAgent 를 사용하지 않고 레이저를 쏴서 이동 가능한지를 확인 한 후 일반적인 플레이어 위치 세팅으로 이동을 하는 것이다. -출처 : Rookiss님 답변-

https://stackoverflow.com/questions/23451983/how-to-avoid-two-navmeshagent-push-away-each-other-in-unity



🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

Unity Lesson 2 카테고리 내 다른 글 보러가기

댓글 남기기