Unity/IGDC 입문 프로젝트

6. 몬스터 스폰 및 이동

말하는 닭 2023. 9. 1. 20:07

IGDC 입문 프로젝트입니다. 

 

 

몬스터를 스폰시키고 타워까지 이동시켜보겠습니다. 

 

스폰포인트를 먼저 만듭시다.

타워의 반대편에 Resources > monster 폴더에 있는 석관 에셋을 배치해줍시다. 높이(y)는 0.2로 맞춰주세요.

배치된 석관. 방향은 상관없습니다.

 

그 다음 EnemySpawn이라는 스크립트를 하나 생성합니다. 

그리고 다음과 같이 작성해봅시다. 

using UnityEngine;

public class EnemySpawn : MonoBehaviour
{
    public GameObject monster;

    private void Start() {
        InvokeRepeating(nameof(Spawn), 1f, 2f);
    }

    void Spawn() {
        Instantiate(monster, transform.position, Quaternion.identity);
    }
}

 

public GameObject monster;

스폰시킬 게임 오브젝트를 선언합니다. public이므로, 에디터 상에서 끌어다 놓는 식으로 받아올 수 있습니다. 

 

private void Start() {
    InvokeRepeating(nameof(Spawn), 1f, 2f);
}

Start 함수입니다. 시작 시 한 번 실행시킵니다.

InvokeRepeating은 일정 간격으로 함수를 실행시켜주는 함수입니다. 

매개변수로 실행시킬 함수 이름, 최초 실행 지연 시간, 실행 간격을 넣었습니다. Spawn함수를 2초마다 실행시키는 함수가 되는 것이죠.

InvokeRepeating 함수는 반드시 Start 함수 안에 있어야 합니다. 만약 Update 함수 안에 있다가는 매 프레임마다 InvokeRepeating 함수를 호출하므로... 엄청난 수의 몬스터가 소환될 것입니다.

 

void Spawn() {
    Instantiate(monster, transform.position, Quaternion.identity);
}

2초마다 실행될 Spawn 함수입니다. 

Instantiate는 오브젝트를 생성(복제)하는 함수입니다. 위 코드는 monster를 transform.position (이 스크립트의 position)에 Quaternion.identity (회전 없음. 오브젝트의 회전 값을 그대로 적용시킵니다)로 생성시킵니다.

 

 

 

몬스터를 Prefab으로 만들기

 

몬스터 에셋을 Prefab(이하 프리팹)으로 바꿔봅시다. 프로젝트 창에 Prefabs 폴더를 만들어 프리팹을 보관할 곳을 만듭니다.

하이어리키 창에 몬스터를 올려놓고 이것을 다시 Prefabs 폴더로 옮겨줍니다.

 

Prefab을 쓰는 이유

Unity의 프리팹 시스템을 이용하면 게임 오브젝트를 생성, 설정 및 저장할 수 있으며, 해당 게임 오브젝트의 모든 컴포넌트, 프로퍼티 값, 자식 게임 오브젝트를 재사용 가능한 에셋으로 만들 수 있습니다. 프리팹 에셋은 씬에 새로운 프리팹 인스턴스를 만들기 위한 템플릿 역할을 합니다.
특별한 방식으로 설정된 게임 오브젝트(예: 논플레이어 캐릭터(NPC), 장면의 소품 또는 일부)를 씬의 여러 장소 또는 프로젝트의 여러 씬에서 재사용하고 싶은 경우 해당 게임 오브젝트를 프리팹으로 변환해야 합니다. 이렇게 하면 프리팹 시스템을 통해 모든 복사본을 자동으로 동기화할 수 있기 때문에 게임 오브젝트를 단순히 복사해서 붙여넣는 것보다 더 효율적입니다.

- 유니티 공식 문서 중 프리팹 문서(https://docs.unity3d.com/kr/2018.4/Manual/Prefabs.html)에서 발췌

 

EnemySpawn 스크립트를 아까 맵에 배치한 석관에 붙여줍니다. 그 후, 아까 프리팹으로 만든(이름을 Ghost로 변경했습니다) 오브젝트를 Monster에 끌어놓습니다.

아래와 같이 보이면 적용이 된 것입니다.

 

그 후, 재생을 해보면, 석관에 유령 오브젝트가 생성이 됩니다. 지금은 아무것도 없는 유령을 붙인지라 움직이지도, 유령끼리 충돌하지도 않습니다. 그래서 겹쳐서 하나로 보이지만, 하이어리키 창에서 계속 'Ghost(Clone)'이 2초마다 하나씩 늘어나는 것을 볼 수 있습니다. 

 

 

몬스터를 움직여보게 하겠습니다. 

Enemy 스크립트를 새로 만들어줍시다. 

그 후, 다음과 같이 작성해줍시다. 

using System.Collections.Generic;
using UnityEngine;

public class Enemy : MonoBehaviour
{
    private Vector3 target;
    private int index;
    private List<Transform> stopOvers;

    public float speed;
    
    void Start() {
        stopOvers = GameObject.Find("GameDatas").GetComponent<GameDatas>().stopOvers;
        index = 0;
        Turn();
    }

    void Update() {
        var step = speed * Time.deltaTime;
        transform.position = Vector3.MoveTowards(transform.position, target, step);

        if (Vector3.Distance(transform.position, target) < 0.01f && index < stopOvers.Count - 1) {
            index++;
            Turn();
        }
    }

    void Turn() {
        target = stopOvers[index].position;
        transform.LookAt(target);
    }
}

자, 코드가 조금 길어졌습니다. 

이번 코드의 핵심은 Vector3.MoveTowards 함수입니다. 이 함수는 오브젝트를 지정해주면, 그 오브젝트를 향해 움직이는 함수입니다. 이 함수를 이용해 타워까지 몬스터들을 이동하게 할 것입니다.

 

 

중간 경유지 설정하기

 

하지만 타워로 목적지를 지정하면, 석관으로부터 타워까지 직선이동을 할테니, 중간 경유지를 지정해줍시다. 커브 지점마다 중간 경유지를 배치 (하이어리키창 우클릭 ->  Create Empty) 합니다.

 

커브마다 빈 오브젝트를 배치했습니다. (타워 앞에도 하나가 있습니다)

아이콘이 살짝 빗겨 있는데, 카메라가 사선이라 그렇습니다. 실제로는 중앙이에요

빈 오브젝트는 아무것도 없기 때문에 씬에서 클릭하지 않는 이상, 아무것도 보이지 않습니다.
이럴 때에는 Icon을 사용할 수 있습니다.
Icon을 설정했기에, 위의 사진처럼 빈 오브젝트의 위치를 선택하지 않더라도 씬에서 볼 수 있습니다.  

 

GameDatas 스크립트를 생성합니다. 그리고 다음 멤버 변수를 추가합니다. 

public List<Transform> stopOvers;
빈 오브젝트를 하나 추가합니다. 이름을 GameDatas로 지어주고, 위에서 만든 GameDatas 스크립트를 붙여줍시다. 
아래 사진과 같이 석관으로부터 이동할 순서대로 하나씩 넣어줍니다. 

 


 

다시 Enemy 스크립트로 돌아옵니다. 

 

void Start() {
    stopOvers = GameObject.Find("GameDatas").GetComponent<GameDatas>().stopOvers;
    index = 0;
    Turn();
}

stopOvers (구글에서 '경유지'가 영어로 이거라네요) 에 GameDatas에 저장한 오브젝트들을 가져오도록, GameObject.Find("오브젝트 이름") 을 통해 하이어리키 창에서 동일한 이름을 가진 오브젝트를 찾습니다. GameDatas는 오브젝트이지, 스크립트가 아닙니다. 오브젝트만으로는 스크립트의 stopOvers에 접근할 수 없습니다.

.GetComponent<"컴포넌트 이름">() 을 통해 붙인 GameDatas 컴포넌트를 가져옵니다. 비로소 GameDatas의 stopOvers에 접근할 수 있습니다. 가져옵시다.

 

TMI: Transform도 컴포넌트이기 때문에 실제로는 GetComponent<>() 해줘야 하지만, 자주 쓰이기 때문에 유니티에서 생성 때부터 GetComponent를 해놓았습니다. 따라서 Transform은 GetComponent를 통해 가져올 필요가 없습니다.

 

 

index를 0으로 초기화하고, Turn() 함수를 호출합니다.

void Turn() {
    target = stopOvers[index].position;
    transform.LookAt(target);
}

Turn 함수에서는 target을 새로 지정하는 역할을 합니다. Start에서 호출할 때 index는 0이므로, 0번째 경유지를 가져와 target으로 삼습니다. 그리고 몬스터가 경유지를 봐야겠죠. LootAt 함수를 통해 경유지를 바라보도록 합니다.

 

 

void Update() {
    var step = speed * Time.deltaTime;
    transform.position = Vector3.MoveTowards(transform.position, target, step);

    if (Vector3.Distance(transform.position, target) < 0.01f && index < stopOvers.Count - 1) {
        index++;
        Turn();
    }
}

Update 함수에서 MoveTowards 함수가 등장합니다. 

몬스터의 position은 transform.position (현재 위치)로부터 target의 위치 방향으로 step (최대로 이동할 수 있는 거리)만큼 이동합니다. step 값이 크다면 (= speed 값이 크다면) 몬스터의 이동속도도 빨라집니다.

 

경유지에 도착하면 다음 경유지를 설정해줘야 합니다. transform.position == target 으로 처리하고 싶지만, position의 xyz는 float 값이기 때문에 '==' 연산자를 적용할 수 없습니다. 

 

C#에서는 실수형(float, double 등) 뒤에 'f'나 'F'를 붙여줘야 합니다. 대소문자 상관 없습니다.   ex) 0.01f, 1f 등

 

그래서 경유지와의 거리를 계산해야 합니다. 피타고라스의 정리를 쓸 수도 있지만, Vector 클래스에서는 거리를 계산하는 함수를 제공합니다. 바로 Distance 함수죠. transform.position (현재 위치)와 target의 거리가 0.01보다 작다면, index를 1 더하고 Turn 함수를 통해 다음 경유지를 설정합니다.

 

그리고, if문 안에는 index < stopOvers.Count - 1 이 and로 붙어있는데요, Count는 stopOvers(List형)의 길이를 반환합니다.

index가 stopOvers의 길이보다 작도록 하여 Index Out of Range 에러를 예방합니다. 

 

마지막으로 Enemy 스크립트를 몬스터 프리팹에 붙여줍니다.

 

 

 

 

 

자, 그럼 실행시켜봅시다. 

아직도 적들이 움직이지 않나요? Enemy에 Speed가 0이 아닌지 확인해보세요!

'Unity > IGDC 입문 프로젝트' 카테고리의 다른 글

8. 공격 타워 구현  (0) 2023.09.03
7. GameOver 알림  (0) 2023.09.02
5. Vector, Transform  (0) 2023.08.31
4. 맵 만들기  (0) 2023.08.31
3. Script 생성  (0) 2023.08.30