유니티 따라다니는 적 - yuniti ttaladanineun jeog

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using UnityEngine.AI;

public class EnemyMovement : MonoBehaviour

{

private Transform target;

private NavMeshAgent navAgent;

public void Awake()

{

target = GameObject.FindGameObjectWithTag("Player").transform;

navAgent = GetComponent<NavMeshAgent>();

}

public void Update()

{

navAgent.SetDestination(target.position);

}

}

cs

이번 포스팅에서는Enemy(적) 오브젝트는 맵을 이리저리 배회하다가 Player(플레이어)가 자신의 일정 범위에 닿으면 Player를 쫒아가도록 하겠습니다.

우리가 흔히 아는 스타크래프트, 워크래프트, 리그오브레전드의 오브젝트들이 일정범위가 되면 플레이어를 공격하며 쫒아오는 비슷한 효과를 줄 수 있겠습니다.

포스팅은 유니티 2D 게임 개발(게임 개발 프로그래밍)에 나온 예제로 진행합니다.

#1 유니티2D 개발 시작하기

안녕하세요 첫 포스팅입니다. 유니티2D 관련 포스팅을 진행하겠습니다. 게임개발에 필요한 지식을 전달하고 함께 학습하겠습니다. 먼저 유니티2D 개발 블로그 포스팅에 앞서 참고한 책을 소개합

my-develop-note.tistory.com

유니티 따라다니는 적 - yuniti ttaladanineun jeog

Enemy(적) 설정

유니티 따라다니는 적 - yuniti ttaladanineun jeog

1. EnemyObject 프리팹을 Hierarchy창으로 드래그 앤 드롭하고 컴포넌트를 설정합니다.

2. Circle Collider 2D 컴포넌트를 EnemyObject 프리팹에 추가합니다.

3. Circle Collider 2D 컴포넌트의 Is Trigger 속성을 체크합니다.

4. Circle Collider 2D 컴포넌트의 Radius(반지름) 속성을 1로 설정합니다.

Circle Collider 2D 컴포넌트의 반지름 속성이 Enemy(적)의 시야를 담당합니다.

Circle Collider 2D 반지름 속성의 범위에 플레이어가 겹치면 적이 플레이어를 볼 수 있도록 할 것입니다.

Is Trigger 속성을 체크하여 트리거 콜라이더로 만들었으며 다른 오브젝트를 통과할 수 있습니다.

속성을 설정하였으면 Overrides 하고 모두적용하여 프리팹에 적용하고 Hierarchy창에서 EnemyObject를 삭제합니다.

그리고 EnemyObject의 애니메이션을 미리 설정하도록 하겠습니다.

유니티 따라다니는 적 - yuniti ttaladanineun jeog

EnemyController 애니메이터를 열고 전환을 사진과 같이 설정하였습니다.

유니티 따라다니는 적 - yuniti ttaladanineun jeog

애니메이터의 Prameters를 설정하겠습니다. +버튼을 눌러 bool을 선택합니다

bool형식의 파라미터가 생성이 되었으면 그 파라미터의 이름을 isWalking으로 변경합니다.

전환의 세부설정을 하겠습니다.

유니티 따라다니는 적 - yuniti ttaladanineun jeog

1. enemy-idle > enemy-walk 전환을 사진과 같이 설정합니다.

Has Exit Time(종료 시간 있음) : 체크해제

Fixed Duration(고정 지속 시간) : 체크해제

Transition Duration(%)(전환 지속 시간) : 0

Transition Offset(전환 오프셋) : 0

Interruption Source(중단 소스) : Current State Then Next State 로 설정합니다.

2. 하단의 Conditions의 + 버튼을 눌러 조건을 추가합니다.

isWalking 조건을 true로 변경합니다.

유니티 따라다니는 적 - yuniti ttaladanineun jeog

1.enemy-walk > enemy-idle 전환을 사진과 같이 설정합니다.

Has Exit Time(종료 시간 있음) : 체크해제

Fixed Duration(고정 지속 시간) : 체크해제

Transition Duration(%)(전환 지속 시간) : 0

Transition Offset(전환 오프셋) : 0

Interruption Source(중단 소스) : Current State Then Next State 로 설정합니다.

2. 하단의 Conditions의 + 버튼을 눌러 조건을 추가합니다.

isWalking 조건을 false로 변경합니다.

EnemyObject가 이리저리 돌아다니는 배회 스크립트를 만들도록 하겠습니다.

유니티 따라다니는 적 - yuniti ttaladanineun jeog

Wander스크립트(C#)의 전체코드입니다.

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

[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(CircleCollider2D))]
[RequireComponent(typeof(Animator))]
public class Wander : MonoBehaviour
{
    public float pursuitSpeed;
    public float wanderSpeed;
    public float currentSpeed;

    public float directionChangeInterval;
    public bool followPlayer;

    Coroutine moveCoroutine;
    CircleCollider2D CircleCollider2D;
    Rigidbody2D rb2d;
    Animator animator;

    Transform targetTransform = null;
    Vector3 endPosition;
    float currenAngle = 0;

    void Start()
    {
        animator = GetComponent<Animator>();
        rb2d = GetComponent<Rigidbody2D>();
        CircleCollider2D = GetComponent<CircleCollider2D>();
        currentSpeed = wanderSpeed;
        StartCoroutine(WanderRoutine());
    }

    void Update()
    {
        Debug.DrawLine(rb2d.position, endPosition, Color.red);
    }

    public IEnumerator WanderRoutine()
    {
        while (true)
        {
            ChooseNewEndPoint();

            if(moveCoroutine != null)
            {
                StopCoroutine(moveCoroutine);

            }
            moveCoroutine = StartCoroutine(Move(rb2d, currentSpeed));

            yield return new WaitForSeconds(directionChangeInterval);
        }
    }


    void ChooseNewEndPoint()
    {
        currenAngle += Random.Range(0, 360);
        currenAngle = Mathf.Repeat(currenAngle, 360);
        endPosition += Vector3FromAngle(currenAngle);
    }

    Vector3 Vector3FromAngle(float inputAngleDegrees)
    {
        float inputAngleRadians = inputAngleDegrees * Mathf.Deg2Rad;

        return new Vector3(Mathf.Cos(inputAngleRadians), Mathf.Sin(inputAngleRadians), 0);
    }

    public IEnumerator Move(Rigidbody2D rigidBodyToMove, float speed)
    {
        float remainingDistance = (transform.position - endPosition).sqrMagnitude;

        while(remainingDistance > float.Epsilon)
        {
            if(targetTransform != null)
            {
                endPosition = targetTransform.position;
            
            }

            if(rigidBodyToMove != null)
            {
                animator.SetBool("isWalking", true);
                 Vector3 newPosition = Vector3.MoveTowards(rigidBodyToMove.position, endPosition, speed * Time.deltaTime);

                rb2d.MovePosition(newPosition);
                remainingDistance = (transform.position - endPosition).sqrMagnitude;
            }
            yield return new WaitForFixedUpdate();
        }
        animator.SetBool("isWalking", false);
    }

    void OnTriggerEnter2D(Collider2D collision)
    {
        if(collision.gameObject.CompareTag("Player") && followPlayer)
        {
            currentSpeed = pursuitSpeed;
            targetTransform = collision.gameObject.transform;
            if(moveCoroutine != null)
            {
                StopCoroutine(moveCoroutine);
            }

            moveCoroutine = StartCoroutine(Move(rb2d, currentSpeed));
        }
    }

    void OnTriggerExit2D(Collider2D collision)
    {
        if (collision.gameObject.CompareTag("Player"))
        {
            animator.SetBool("isWalking", false);
            currentSpeed = wanderSpeed;

            if(moveCoroutine != null)
            {
                StopCoroutine(moveCoroutine);
            }
            targetTransform = null;
        }
    }

    void OnDrawGizmos()
    {
        if(CircleCollider2D != null)
        {
            Gizmos.DrawWireSphere(transform.position, CircleCollider2D.radius);
        }
    }

}

전체 코드가 굉장히 길기 때문에 잘 확인하고 저장하시기 바랍니다.

코드를 확인해보도록 하겠습니다.

[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(CircleCollider2D))]
[RequireComponent(typeof(Animator))]

RequrieComponent는 이 스크립트를 추가한 게임 오브젝트에 필요한 컴포넌트가 없으면 그 해당 컴포넌트를 자동으로 추가하게 합니다. Rigidbody2D, CircleCollider2D, Animator 컴포넌트가 없으면 자동으로 추가합니다.

변수 선언된 부분을 확인해보도록 하겠습니다.

public float pursuitSpeed;
public float wanderSpeed;
public float currentSpeed;

public float directionChangeInterval;
public bool followPlayer;

Coroutine moveCoroutine;
CircleCollider2D CircleCollider2D;
Rigidbody2D rb2d;
Animator animator;

Transform targetTransform = null;
Vector3 endPosition;
float currenAngle = 0;

pursuitSpeed는 적이 플레이어를 추적하는 속도, wanderSpeed는 평상시의 적의 속도, currentSpeed는 앞의 둘 중에서 선택할 현재 속도를 설정합니다.

directionChangeInterval은 배회할 방향의 전환 빈도를 에디터에서 설정하겠습니다.

bool형식의 followPlayer은 플레이어를 추적하는 기능을 켜거나 끌 수 있습니다. 다른 캐릭터를 추적하지 않고 배회만 하는 캐릭터를 만들 수도 있습니다.

moveCoroutine 변수에 현재 실행중인 이동 코루틴의 참조를 저장합니다. 이 코루틴은 적을 목적지로 옮기는 역할을 합니다.

CircleCollider2D, rb2d, animator 변수는 해당 컴포넌트를 사용하기 위하여 추가하였습니다.

targetTransform은 적이 플레이러르 추적할 때 사용합니다.  PlayerObject의 transform을 얻어서 targetTransform에 대입합니다.

endPosition은 배회하는 캐릭터의 목적지입니다.

currentAngle는 배회할 방향을 바꿀땐 기존 각도에 새로운 각도를 더합니다.

Start()메서드를 확인하겠습니다.

void Start()
    {
        animator = GetComponent<Animator>();
        rb2d = GetComponent<Rigidbody2D>();
        CircleCollider2D = GetComponent<CircleCollider2D>();
        currentSpeed = wanderSpeed;
        StartCoroutine(WanderRoutine());
    }

animator, rb2d, CircleCollider2D에 해당 컴포넌트를 얻어서 저장합니다.

currentSpeed에 wanderSpeed를 현재속력으로 설정합니다. 처음에는 배회하는 속도로 적이 천천히 움직입니다.

WanderRoutine()메서드를 시작합니다.

WanderRoutine() 메서드를 확인해보도록 하겠습니다.

public IEnumerator WanderRoutine()
    {
        while (true)
        {
            ChooseNewEndPoint();

            if(moveCoroutine != null)
            {
                StopCoroutine(moveCoroutine);

            }
            moveCoroutine = StartCoroutine(Move(rb2d, currentSpeed));

            yield return new WaitForSeconds(directionChangeInterval);
        }
    }

IEnumerator 형식으로 여러 프레임에 걸쳐 실행해야 하는 코루틴입니다.

while문을 살펴보겠습니다. 적이 계속해서 배회해야 하기 때문에 while문안의 과정을 계속 반복합니다.

ChooseNewEndPoint()메서드는 새로운 목적지를 선택하는 역할입니다. 아래에서 이야기하겠습니다.

if문을 확인해보면 moveCoroutine이 null인지 아닌지 확인하여 적이 이동 중인지 확인합니다. null이 아니라면 적이 이동중이라는 의미이므로 새로운 뱡향으로 이동하기 전에 실행중인 이동 코루틴을 중지합니다.

MoveCoroutine에 Move()코루틴을 시작하고 시작한 코루틴의 참조를 저장합니다.

directionChangeInterval에 설정한 값만큼 로루틴의 실행을 양보한 뒤에 다시 루프합니다.

ChooseNewEndPoint() 메서드를 확인해보겠습니다.

    void ChooseNewEndPoint()
    {
        currenAngle += Random.Range(0, 360);
        currenAngle = Mathf.Repeat(currenAngle, 360);
        endPosition += Vector3FromAngle(currenAngle);
    }

currnetAngle에 0~360 사이의 값을 무작위로 선택하고 currnentAngle 값과 무작위 값을 더합니다. 새로운 방향을 나타내는 각도입니다.

Mathf.Repeat 메서드는 주어진값이 지정한 값의 범위 안에 들 때까지 반복합니다. currentAngle, 360이므로 0보다 작거거나 360보다 클수가 없고 0~360의 각도를 currentAngle에 대입합니다.

Vector3FromAngle 메서드를 호출한 결과 값을 endPosition값에 더하고 대입합니다.

Vector3FromAngle()메서드를 확인해보겠습니다.

Vector3 Vector3FromAngle(float inputAngleDegrees)
    {
        float inputAngleRadians = inputAngleDegrees * Mathf.Deg2Rad;

        return new Vector3(Mathf.Cos(inputAngleRadians), Mathf.Sin(inputAngleRadians), 0);
    }

Vector3 형식을 반환하는 메서드입니다. 입력받은 inputAngleDegrees 값에 Mathf.Deg2Rad 상수를 곱하여 호도로 변환합니다.

변환한 호도를 사용하여 적의 방향으로 사용할 방향 Vector를 만듭니다

Move() 메서드를 확인해도록 하겠습니다.

public IEnumerator Move(Rigidbody2D rigidBodyToMove, float speed)
    {
        float remainingDistance = (transform.position - endPosition).sqrMagnitude;

        while(remainingDistance > float.Epsilon)
        {
            if(targetTransform != null)
            {
                endPosition = targetTransform.position;
            
            }

            if(rigidBodyToMove != null)
            {
                animator.SetBool("isWalking", true);
                 Vector3 newPosition = Vector3.MoveTowards(rigidBodyToMove.position, endPosition, speed * Time.deltaTime);

                rb2d.MovePosition(newPosition);
                remainingDistance = (transform.position - endPosition).sqrMagnitude;
            }
            yield return new WaitForFixedUpdate();
        }
        animator.SetBool("isWalking", false);
    }

Move() 코루틴은 정해직 속력으로 Rigidbody2D를 현재 위치에서 endPosition 변수의 위치로 옮기는 역할을 합니다.

transform.position - endPosition의 결과는 Vecto3 값입니다.

Vector3의 sqrMagnitude 속성을 사용하여 적의 현재 위치와 목적지 사이의 대략적인 거리를 구하고remainingDistance에 대입합니다.

유니티에서 제공하는 sqrMagnitude속성은 벡터의 크기를 빠르게 계산할 수 있습니다.

while문을 확인해보도록 하겠습니다.

현재 위치와 endPosition사이에 남은 거리가 0보다 큰지 확인합니다. 맞다면 루프합니다.

if문을 확인해보겠습니다.

targetTransform이 null이 아닌지 확인합니다.

적이 플레이어를 추적 중이라면 targetTransform은 null이 아닌 플레이어의 transform입니다. endPositon의 값에 targetTransform의 position을 덮어씁니다.

다음 if문을 확인해보도록 하겠습니다.

Rigidbody2D 가 null이 아닌지 확인합니다.

Move() 메서드는 Rigidbody2D를 이용하여 적을 움직이므로 Rigidbody2D가 필요합니다. 

null이 아니라면, 위에서 설정하였던 애니메이터 파라미터인 isWalking을 true로 설정합니다. 적이 걷는 애니메이션을 재생합니다. 

Vector3.Movetowards 메서드는 Rigidbody2D의 움직임을 계산할 때 사용합니다.

이 메서드는 현재위치, 최종위치, 프레임 안에 이동할 거리, 세개의 변수를 받습니다. 

MovePosition()메서드를 사용하여 위의 계산한 Rigidbody2D의 newPosition 값으로 이동합니다.

sqrMagnitude속성을 사용하여 남은 거리를 수정하고 remainingDistance에 대입합니다.

yield return new WaitForFixedUpdate()로 다음 고정 프레임 업데이트까지 실행을 양보합니다.

마지막으로 적이 endPosition에 도착하여 새로운 방향의 선택을 기다리며 걷는 애니메이션에서 대기 애니메이션으로 변경합니다.

지금까지는 적이 이리저리 돌아다니는 배회 알고리즘에 대하여 이야기하였습니다.

뒤에서 이야기할 OnTriggerEnter2D, OnTriggerExit2D 메서드는 해당 메서드를 사용하여 Circle Collider 2D 범위에 플레이어가 닿으면

즉 적이 플레이어를 발견하면 플레이러를 추적하는 기능에 대하여 이야기하고 그렇지 않다고 플레이어를 추적하지 않는 내용을 진행하겠습니다.

OnTriggerEnter2D() 메서드입니다.

void OnTriggerEnter2D(Collider2D collision)
    {
        if(collision.gameObject.CompareTag("Player") && followPlayer)
        {
            currentSpeed = pursuitSpeed;
            targetTransform = collision.gameObject.transform;
            if(moveCoroutine != null)
            {
                StopCoroutine(moveCoroutine);
            }

            moveCoroutine = StartCoroutine(Move(rb2d, currentSpeed));
        }
    }

충돌한 오브젝트의 태그가 "Player"인지, followePlayer 속성이 true인지 확인합니다. 두 조건이 참이라면 if문 안의 내용을 실행합니다.

충돌한 오브젝트가 Player이기 때문에 현재 속도(CurrentSpeed)를 추적속도(pursuitSpeed)로 변경합니다.

targerTransform에 충돌한 오브젝트인 플레이어의 transform을 대입합니다.

if문을 확인해보겠습니다.

moveCoroutine이 null이 아니면 적이 움직이는 중이라는 의미입니다. 다시 움직이기 시작하기 전에 멈춰야 하기 때문에 StopCoroutine을 사용하여 코루틴을 멈춥니다.

충돌한 오브젝트인 Player의 tranform을 endPosition에 설정했으므로 Move()를 호출하여 적을 플레이어 쪽으로 움직입니다.

OnTrrigerExit2D() 메서드입니다.

 void OnTriggerExit2D(Collider2D collision)
    {
        if (collision.gameObject.CompareTag("Player"))
        {
            animator.SetBool("isWalking", false);
            currentSpeed = wanderSpeed;

            if(moveCoroutine != null)
            {
                StopCoroutine(moveCoroutine);
            }
            targetTransform = null;
        }
    }

OnTrrigerExit2D()메서드의 내용은 위에서 설정한 OnTrrigerEnter2D()메서드와 유사합니다. 플레이어가 적의 시야에서 벗어나면 애니메이션을 대기 상태로 바꾸고 현재속도를 배회속도로 변경합니다. 

적의 추적을 멈추어야하기 때문에 MoveCoroutine을 중지시키고 targetTransform에 null을 대입하여 플레이를 추적하지 않게 합니다.

OnDrawGizmos()메서드입니다.

void OnDrawGizmos()
    {
        if(CircleCollider2D != null)
        {
            Gizmos.DrawWireSphere(transform.position, CircleCollider2D.radius);
        }
    }

이 메서드는 시각적으로 배회알고리즘을 도와줄 기즈모입니다.

1. 플레이어가 적의 시야에 들어왔는지 감지하는 CircleCollider 2D의 외곽선을 보여주는 기즈모

2. 적의 위치와 적의 목적지를 잇는 선을 보여주는 기즈모

2가지의 기즈모가 필요합니다. OnDrawGizmos()메서드는 1번 기즈모를 보여주는 기능입니다.

코드를 확인하겠습니다.

if문을 확인하여 CircleCollider2D의 참조가 null이 아닌지 확인합니다.

null이 아니라면 Gizmos.DrawWireSphere() 호출하고 구(sphere)를 그릴때 필요한 위치와 반지름을 전달합니다.

이때 필요한 위치는 transform.position인 오브젝트의 위치, 반지름은 CircleCollider2D.radius인 써클 콜라이더 2D 컴포넌트의 반지름 값입니다.

2번 기즈모는 Update() 메서드에 설정하였습니다.

void Update()
    {
        Debug.DrawLine(rb2d.position, endPosition, Color.red);
    }

Debug.DrawLine은 기즈모를 활성화해야 보입니다. 

해당 메서드의 매개변수로 현재위치, 최종위치, 선의 색을 받습니다.

EnemyObject 프리팹에 설정하도록 하겠습니다.

유니티 따라다니는 적 - yuniti ttaladanineun jeog

EnemyObject 프리팹에 Wander 스크립트(C#) 컴포넌트를 추가합니다.

pursuit Speed : 1.4

Wander Speed : 0.8

Current Speed : 0

Direction Change Interval : 3

Follow Player : 체크 

설정하였습니다.

재생버튼을 눌러 게임을 진행합니다. 적 오브젝트의 시야범위와 기즈모 선이 보이는 지 확인하고 플레이어로 적 오브젝트에 다가갔을때 속도를 변경하여 따라오는지 확인합니다.

이해를 돕기위해 책에서 제공한 배회 알고리즘 입니다.

유니티 따라다니는 적 - yuniti ttaladanineun jeog

마지막으로 Ctrl + S를 눌러 Scene을 저장합니다!


감사합니다! :)