미니 RPG 만들기(5)_몬스터AI

개발조하·2024년 1월 4일
0

Unity

목록 보기
27/30
post-thumbnail

1. BaseController.cs 생성

  • BaseController.cs 생성
    PlayerController와 MonsterController는 중복되는 메서드가 많이 발생한다. 이에 BaseController.cs를 생성하여 공용되는 부분은 상속받아 사용하도록 로직을 구성하자.

  • 애니메이션 상태 정의
    PlayerController.cs에서 enum으로 정의해놓은 PlayerState는 Monster도 사용할 수 있는 것이기 때문에 Define.cs에 State으로 정의해놓으면 훨씬 유용해진다.
    현재 PlayerState로 명명된 코드를 모두 Define.State라고 일괄 변경한다.

ㄴ Monster의 애니메이션 이름도 기존 설정과 같도록 모두 변경 ("WAIT", "RUN", "ATTACK")

  • 코드 정리
  1. PlayerController.cs의 코드 -> BaseController.cs로 이동
    ㄴ State 프로퍼티
    ㄴ Update()
  2. virtual, override 정의
    ㄴ Player와 Monster 모두 UpdateDie(), UpdateMoving(), UpdateIdle(), UpdateSkill()을 사용하지만, 내용은 다를 것이기 때문에 BaseController에서 virtual로 선언해주고, 각자의 스크립트에서 override로 자신의 코드를 실행하도록 한다.

💡 BaseController.cs

public class BaseController : MonoBehaviour
{
    [SerializeField] protected Define.State _state = Define.State.Idle;
    [SerializeField] protected Vector3 _destPos;
    [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.1f);
                    break;
                case Define.State.Moving:
                    anim.CrossFade("RUN", 0.1f);
                    break;
                case Define.State.Skill:
                    anim.CrossFade("ATTACK", 0.1f, -1, 0);
                    break;

            }
        }
    }

    private void Start()
    {
        Init();
    }
    
    public abstract void Init();
    
    void Update()
    {
        //애니메이션 실행
        switch (State)
        {
            case Define.State.Die:
                UpdateDie();
                break;
            case Define.State.Moving:
                UpdateMoving();
                break;
            case Define.State.Idle:
                UpdateIdle();
                break;
            case Define.State.Skill:
                UpdateSkill();
                break;
        }
    }

    protected virtual void UpdateDie() { }
    protected virtual void UpdateMoving() { }
    protected virtual void UpdateIdle() { }
    protected virtual void UpdateSkill() { }
}

💡 PlayerController.cs

public class PlayerController : BaseController
{
    int _mask = (1 << (int)Define.Layer.Ground) | (1 << (int)Define.Layer.Monster);
    PlayerStat _stat;
    bool _stopSkill = false;

    publicoverride void Init()
    {
        _stat = gameObject.GetComponent<PlayerStat>();
        Managers.Input.MouseAction -= OnMouseEvent;
        Managers.Input.MouseAction += OnMouseEvent;
        //HPBar 부착
        Managers.UI.MakeWorldSpaceUI<UI_HPBar>(transform);
    }

    protected override void UpdateMoving() //이동 및 회전
    {
        //적이 player의 사정거리보다 가까우면 공격
        if(_lockTarget != null) 
        {
            _destPos = _lockTarget.transform.position;
            float distance = (_destPos - transform.position).magnitude;
            if(distance <= 1)
            {
                State = Define.State.Skill;
                return;
            }
        }

        Vector3 dir = _destPos - transform.position;
        //player가 destination에 도착하면
        if (dir.magnitude < 0.1f) 
        {
            State = Define.State.Idle;
        }
        else
        {
            //AI Navigation에 따라 목적지로 이동
            NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
            float moveDist = Mathf.Clamp(_stat.MoveSpeed * Time.deltaTime, 0, dir.magnitude);
            nma.Move(dir.normalized * moveDist);
            //목적지로 이동
            //transform.position += dir.normalized * moveDist;

            //장애물 판별 Raycast
            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;
            }

            //목적지를 주시하며 이동 (자연스러운 회전)
            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>();
            PlayerStat myStat = GetComponent<PlayerStat>();

            int damage = Mathf.Max(0,myStat.Attack - targetStat.Defense);
            Debug.Log(damage);
            targetStat.Hp -= damage;
        }

        if (_stopSkill)
        {
            State = Define.State.Idle;
        }
        else
        {
            State = Define.State.Skill;
        }
    }
    
    void OnMouseEvent(Define.MouseEvent evt)
    {
        switch (State)
        {
            case Define.State.Die:
                break;
            case Define.State.Idle:
                OnMouseEvent_IdleRun(evt);
                break;
            case Define.State.Moving:
                OnMouseEvent_IdleRun(evt);
                break;
            case Define.State.Skill:
                {
                    if(evt == Define.MouseEvent.PointerUp)
                        _stopSkill = true;
                }
                break;
        }
    }

    void OnMouseEvent_IdleRun(Define.MouseEvent evt)
    {
        RaycastHit hit;
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        bool raycastHit = Physics.Raycast(ray, out hit, 100.0f, _mask);
        //Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f);

        switch (evt)
        {
            case Define.MouseEvent.PointerDown:
                {
                    if (raycastHit)
                    {
                        _destPos = hit.point;
                        State = Define.State.Moving;
                        _stopSkill = false;

                        if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
                        {
                            _lockTarget = hit.collider.gameObject;
                        }
                        else
                        {
                            _lockTarget = null;
                        }
                    }
                }
                break;
            case Define.MouseEvent.Press:
                {
                    if (_lockTarget == null && raycastHit)
                    {
                        _destPos = hit.point;
                    }
                }
                break;
            case Define.MouseEvent.PointerUp:
                _stopSkill = true;
                break;
        }
    }
}

2. MonsterController.cs

2.1 기본 틀

public class MonsterController : BaseController
{
    Stat _stat;
    public override void Init()
    {
        _stat = gameObject.GetComponent<Stat>();

        //HPBar 부착
        if(gameObject.GetComponent<UI_HPBar>() == null)
            Managers.UI.MakeWorldSpaceUI<UI_HPBar>(transform);
    }

    protected override void UpdateDie()
    {
        Debug.Log("UpdateDie");
    }

    protected override void UpdateIdle()
    {
        Debug.Log("UpdateIdle");
    }

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

    protected override void UpdateSkill()
    {
        Debug.Log("UpdateSkill");
    }

    void OnHitEvent()
    {
        Debug.Log("Monster OnHitEvent");
    }
}

2.2 전체 코드

public class MonsterController : BaseController
{
    Stat _stat;
    [SerializeField] float _scanRange = 10;
    [SerializeField] float _attackRange = 2;

    public override void Init()
    {
        _stat = gameObject.GetComponent<Stat>();

        //HPBar 부착
        if(gameObject.GetComponent<UI_HPBar>() == null)
            Managers.UI.MakeWorldSpaceUI<UI_HPBar>(transform);
    }

    protected override void UpdateDie()
    {
        Debug.Log("UpdateDie");
    }

    protected override void UpdateIdle()
    {
        Debug.Log("UpdateIdle");

        //사정거리 내 player 감지
        GameObject player = GameObject.FindGameObjectWithTag("Player");
        if (player == null)
            return;

        float distance = (player.transform.position - gameObject.transform.position).magnitude;
        if (distance < _scanRange) 
        {
            //감지 시 추적모드로 전환
            _lockTarget = player;
            State = Define.State.Moving;
            return;
        }
    }

    protected override void UpdateMoving()
    {
        NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();

        if (_lockTarget != null)
        {
            _destPos = _lockTarget.transform.position;
            float distance = (_destPos - transform.position).magnitude;
            if (distance <= _attackRange)
            {
                nma.SetDestination(transform.position);
                State = Define.State.Skill;
                return;
            }
        }

        Vector3 dir = _destPos - transform.position;
        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 = 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 (distance < _attackRange)
                    State = Define.State.Skill;
                else
                    State = Define.State.Moving;
            }
            else
            {
                State = Define.State.Idle;
            }
        }
        else
        {
            State = Define.State.Idle;
        }
    }
}

2.3 오류 수정

지금은 Player가 Monster를 밀면서 지나가고 있다. 이는 Player가 Nev Mesh Agent에 의해 움직이고 있기 때문이다. 사실상 Player는 Raycast를 통해 장애물 판별을 해주고 있기 때문에 Nev Mesh에 따른 이동이 불필요하다. 이 부분을 수정해보자.
1. UnityChan에 붙어 있는 Nev Mesh Agent 컴포넌트 제거
2. playerController.cs의 UpdateMoving() 수정

📄참고자료
[인프런] c#과 유니티로 만드는 MMORPG 게임 개발 시리즈_3. 유니티 엔진

profile
Unity 개발자 취준생의 개발로그, Slow and steady wins the race !

0개의 댓글