유니티 몬스터 이동 - yuniti monseuteo idong

LambFerret's Blog

사실 게임의 대부분은 이미 다 만들었다. 게임을 잠정적으로 끝마쳤다고 생각했을때는 7월15일쯤이고, 첫 포스팅이 7월 8일인데 유니티 2일차였으니까 열흘정도 투자했다고 볼 수 있다. 당연 시간을 알차게 전부 쓴것은 아니지만 계속 만들면서 유익하게 배워나간 것 같다. 그런이유로 스포일러하자면.. 이번파트가 제일 시간이 오래 걸렸다. 

유니티 몬스터 이동 - yuniti monseuteo idong
겁 나 어 렵 습 니 다

만들어야 하는 상호작용

  1. 이동
  2. 점프
  3. 사다리
  4. 피격 -> 패배
  5. 점수 -> 승리
  6. 적 이동

우선 미리 코드를 보여주겠다. 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyMovement : MonoBehaviour
{
    public float speed;
    Rigidbody2D rb;
    RaycastHit2D rayHit;
    Ray2D ray;


    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
        ray = new Ray2D();
    }


    void Update()
    {
        rb.velocity = new Vector2(speed, rb.velocity.y);
    }


    void FixedUpdate()
    {
        float distance = 4.42f;
        if (speed > 0)
        {
            ray.direction = new Vector3(1, -1, 0);
            rayHit = Physics2D.Raycast(rb.position, ray.direction, distance, ~3);
        }
        else
        {
            ray.direction = new Vector3(-1, -1, 0);
            rayHit = Physics2D.Raycast(rb.position, ray.direction , distance, ~3);

        }
        if (!rayHit)
        {
            speed = -speed;
        }
    }
}

우선은

하고싶은 것 :

 게임 시작과 동시에 단순하게 x축으로 이동한다. 바닥이 끝날때쯤엔 반대쪽 방향으로 똑같이 이동한다. 이를 게임끝날때 까지 반복한다. 

방법 : 

  유니티에 주어진 Ray를 사용한다. ray는 말그대로 광선으로 이 광선이 물체에 닿는지 아닌지를 판단할 수 있다. 몬스터의 중심에서 발밑 앞쪽으로 ( 오른쪽을 보고있다면 5시방향 ) 광선을 발사한다. 이 광선에 platform이 닿지 않으면, speed를 역전시켜 반대방향으로 향하게 한다. 

는 아이디어를 구현해보겠다.

간단히 말하자면 <<발밑 만 보면서 다니다가 아무것도 없으면 뒤를 보겠다.>> 라는 것이다. 

원하는대로 되지 않아서 시간을 잡아먹은 부분이 한두가지가 아니였다. 

1. 2D게임이지만 ray는 3D로도 만들어 질 수 있었다. 

2. 광선이 보이지 않는다. 광선은 Debug.DrawRay(rb.position, ray.direction * distance, Color.red); 로 간접적으로 알아보는 수 밖에 없다. 

3. 광선이 닿았는지 아닌지를 판별하는 Physics2D.Raycast(rb.position, ray.direction, distance, ~3); 의 메소드 오버로딩때문에 파라미터의 순서가 중요하다. 

각각 한가지씩 해결하는게아니라 세개가 꼬여서 해결하려면 한번에 해야했기에 더 힘들었던듯 하다. 

1번 : 2D에서 Debug.DrawRay가 안보일때 해결법

유니티 몬스터 이동 - yuniti monseuteo idong
현재는 해결했지만 전에는 광선이 Z축으로 나와서 2d에서 확인할 수가 없었다.

#Scene 밑에 2d를 눌러보면 3d로 전환이 가능하다 그리고 오른쪽 위의 Gizmos를 켜야한다. 

2번 : Debug.DrawRay , Physics2D.Raycast 의 관계

처음에는 DrawRay는 Raycast와 다르게 distance를 넣는 자리가 없어서 광선의 길이에서는 서로간의 관련성에 신뢰가지 못했다. 따라서 발밑까지의 distance가 얼마인지도 알 수 없었다. 이 거리를 재기 위해서 

유니티 몬스터 이동 - yuniti monseuteo idong

작은 스프라이트 박스를 만든 후 이 서로간의 계산을 위해 

Vector3.Distance(transform.position, other.position)

를 사용했다. 

이 값으로 distance = 4.42f를 얻게 되었다. 

3번 : Raycast의 파라미터

코드에서는 Physics2D.Raycast(rb.position, ray.direction, distance, ~3); 를 사용했다 마지막의 ~3은 '무시'할 레이어를 선택한 것이다. 여기서는 바닥 플랫폼의 레이어인 3번을 제외한 전부 라는 뜻의 ~3 을 적었다. 비트 연산자는 아직 익숙하지않기에 !3 도 사용해보고 ||도 사용해보았지만 결국 그냥 바닥을 제외한 전부를 무시하기로 하였다. 

유니티 몬스터 이동 - yuniti monseuteo idong

완성! 이번엔 별다른 고칠점이 없어보인다. 

너굴맨은 잠정적으로 여기서 끝냈다. 같은 방법을 응용하면 다른 레벨도 만들 수 있고 적도 만들 수 있다. 기본적인 유니티의 움직임과 사용법을 알기 위해서 시작했지만 생각보다 더욱 충실하게 익힐 수 있는 시간이었다. 역시 실전이 좋은 선생인듯 하다. 

Date: 2021.01.13    Updated: 2021.01.13

카테고리: Unity Lesson 2

태그: Unity Game Engine

인프런에 있는 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);
		}
	}

NavMeshAgent의 nma.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 카테고리 내 다른 글 보러가기