본문 바로가기
도전! N잡러!/도전! 게임 개발!

TAP FLOAT 개발 일기 #8

by NAWE 2023. 9. 22.
반응형

안녕하세요. 나위입니다.

STUDIO NAWE의 첫 타이틀. TAP FLOAT의 개발 일기를 찾아주셔서 감사합니다.

이전 포스팅에서 이어지는 내용이오니, 못 보신 분들은 아래 링크로 앞 내용을 먼저 보고 오시는 게 좋겠어요!

 

어서오세요! TAP FLOAT 개발 일기입니다!

 

TAP FLOAT 개발 일기 #0 바로가기 ◀

TAP FLOAT 개발 일기 #1 바로가기 ◀

TAP FLOAT 개발 일기 #2 바로가기 ◀

TAP FLOAT 개발 일기 #3 바로가기 ◀

TAP FLOAT 개발 일기 #4 바로가기 ◀

TAP FLOAT 개발 일기 #5 바로가기 ◀

TAP FLOAT 개발 일기 #6 바로가기 ◀

TAP FLOAT 개발 일기 #7 바로가기 ◀

 

그럼, 시작합니다!


글을 쓰다보니 문득, 이 개발 일기는 유니티3D를 알려주는 것인가? 하는 생각이 들었습니다.

개발 일기니까 개발 과정을 다루는 것이 맞긴 하겠지만...

이보다는 '왜 이렇게 만든 것인지'를 설명하는 것이 더 맞는건가? 하는 의문이 이어지네요.

 

...으음.

고민이 들긴 했지만 별 수 없습니다.

누군가 내게 다가와서 '게임을 왜 이렇게 만들었나요?'라고 물어본다면, 할 말이 정해져 있거든요.

 

 

"제가.. 지금은 이렇게밖에 못만들어서요.."

 

"그나마 제가 할 수 있는 한에서는 재미있게 만들어 본 거에요. 헤헤..."

 

헤...헤헤헤...

 

지금까지 조금은 슬픈 이야기였습니다.

각설하고, 지난 포스팅에 이어서 Enemy를 뿌려댈 친구인 Spawner를 만들어 볼게요!


Spawner.

뭘 하는 놈이냐면, 의미 그대로 '소환하는 녀석'이죠.

이 녀석은 플레이어에게 보이지 않도록 메인 카메라의 상단에 위치하게 될 거에요.

그리고 그 자리에서 '아래로 내려오면서 회전하는 Enemy'를 계속 소환하는 역할을 하게 될 겁니다.

 

우선, 빈 오브젝트를 하나 만들어 줍니다.

Hierarchy에서 마우스 우클릭 → Create Empty를 눌러 빈 오브젝트를 생성해 주세요.

빈 오브젝트를 생성합니다.

그런 뒤, 오브젝트의 이름을 Spawner로 지정하고 위치를 카메라의 상단으로 적당히 잡아줄게요.

 

이렇게요!

Spawner의 위치를 적당히 카메라 상단으로 잡았습니다.

 

이제, 저 Spawner에게 스크립트를 붙여 줄 차례입니다.

스크립트의 내용은 이렇게 될 거에요.

 

"미리 만들어 둔 Enemy 프리팹들을 지정한 몇몇 좌표 중에서 랜덤하게 생성시켜 줘."

 

자, 어떻게 저 내용이 만들어지는지를 같이 볼까요?

스크립트 전문은 아래와 같습니다.

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

public class Spawner : MonoBehaviour
{
    [SerializeField]
    private GameObject[] enemies;
    private float[] arrPosX = { -2f, -1f, 0f, 1f, 2f };
    [SerializeField]
    private float spawnInterval = 2f;
    public int intervalEnemy = 10;
    public float downSpawnInterval = 0.1f;

    void Start()
    {
        StartEnemyRoutine();
    }

    void StartEnemyRoutine()
    {
        StartCoroutine("EnemyRoutien");
    }

    public void StopEnemyRoutine()
    {
        StopCoroutine("EnemyRoutien");
    }

    IEnumerator EnemyRoutien()
    {
        yield return new WaitForSeconds(1f);

        int spawnCount = 0;
        while (true)
        {
            int index = Random.Range(0, enemies.Length);
            int posRandom = Random.Range(0, arrPosX.Length);
            SpawnEnemy(arrPosX[posRandom], index);
            spawnCount++;

            if(spawnCount % intervalEnemy == 0)
            {
                spawnInterval -= downSpawnInterval;
            }

            yield return new WaitForSeconds(spawnInterval);
        }
    }

    void SpawnEnemy(float posX, int index)
    {
        Vector3 spawnPos = new Vector3(posX, transform.position.y, transform.position.z);
        Instantiate(enemies[index], spawnPos, Quaternion.identity);
    }
}

이전까지의 스크립트와는 달리 상당히 복잡해 보이는 모습입니다.

하지만! 하나하나씩 해석해나가보면 그다지 복잡하지 않아요!

 

우선 변수들의 역할부터 알아보시죠!

    [SerializeField]
    private GameObject[] enemies;
    private float[] arrPosX = { -2f, -1f, 0f, 1f, 2f };
    [SerializeField]
    private float spawnInterval = 2f;
    public int intervalEnemy = 10;
    public float downSpawnInterval = 0.1f;

접근제한자로 private를 사용하지만, {SerializeField]를 사용하여 엔진상에서 자유롭게 등록할 수 있도록 한 enemies 변수.

여기에는 int라던지 float, string 등 자료형이 들어가야 할 자리에 자료형 대신 'GameObject[]' 라고 적혀 있어요.

[ ] → 이 모양은 '배열'을 사용하겠다는 의미입니다. 또한, GameObject는 유니티에서 사용하는 게임 오브젝트들을 의미하는 것이에요. 지금 저의 상황에서는 미리 만들어둔 Enemy 프리팹들을 의미합니다.

 

결국 저건, 미리 만들어 둔 Enemy 프리팹을 enemies라는 이름의 배열로 저장하겠다는 이야기에요.

게임 상에서 몇 개의 Enemy를 사용하든지간에, 그만큼 배열의 공간을 늘리고 또 등록함으로서 자유롭게 쓰려는 의도입니다.

 

enemies 변수 바로 아래에 있는 float 자료형의 arrPosX 변수 또한 배열로 만들어져 있는데, 안에 저장된 값으로 -2, 1, 0, 1, 2까지. 총 5개의 값이 들어가 있죠.

이처럼 배열을 사용하면 하나의 변수 이름에 여러 값을 저장할 수 있습니다.

 

배열에 저장된 값을 사용하는 법은 간단합니다. arrPosX를 예로 들면, 각 데이터는 아래와 같이 불러낼 수 있습니다.

arrPosX[0] = -2
arrPosX[1] = -1
arrPosX[2] = 0
arrPosX[3] = 1
arrPosX[4] = 2

저렇게 [ ] 안에 데이터가 저장된 순서를 (0부터 시작해요!) 넣는 걸로 간단하게 값을 불러낼 수 있습니다.

 

...으음.

사실 지금 제가 작성하는 내용들은 너무나 기초적인 것들이라... 진짜(?) 프로그래머 분들의 입장에서는 너무나 가소로운 설명이겠지만, 저는 저 모든 것들을 하나하나 배우고 익혀가며 개발을 했답니다 헤헤.

이런 나 자신 칭찬해...(쓰담쓰담)

 

어쨌든, 배열이고 자시고 변수들의 역할을 설명하자면 아래와 같습니다.

 

  • enemies = 프리팹으로 저장한 Enemy를 배열로 등록하는 변수.
  • arrPosX = Enemy를 스폰시킬 X축 위치 값을 배열로 등록하는 변수.
  • spawnInterval = Enemy를 다시 스폰시키기까지 필요한 대기 시간.
  • IntervalEnemy = spawnInterval이 downSpawnInterval만큼 줄어들기까지 필요한 Enemy 스폰 횟수. 
  • downSpawnInterval = Enemy가 IntervalEnemy의 수만큼 스폰된 이후에 spawnInterval에서 줄어드는 값.

 

IntervalEnemydownSpawnInterval이 조금 복잡해 보일 수 있겠다 싶어요.

적절한 이름을 찾질 못해서 대충 저렇게 이름을 적긴 했는데, 저걸 쓴 이유는 간단합니다. 게임을 더 재밌게 하려는 이유였지요.

 

게임을 처음 시작했을 때는 Enemy가 여유있게 떨어지다가, 점점 Enemy가 떨어지는 간격이 짧아지면서 게임의 난이도를 점점 높이려는 의도였습니다.

다른 게임에서는 스테이지나 레벨 등으로 이름붙여지는 그 요소를 저 두 변수로 해결한 셈이죠.

 

다음 내용은 간단합니다. Start 함수인데, 이건 게임이 시작하면 같이 실행되는 것이죠.

    void Start()
    {
        StartEnemyRoutine();
    }

안의 내용은 아주 간단합니다.

 

"StartEnemyRoutine 함수를 실행시켜줘."

 

진짜 간단하죠?

그럼, StartEnemyRoution 함수를 살펴봅시다.

    void StartEnemyRoutine()
    {
        StartCoroutine("EnemyRoutien");
    }

StartEnemyRoution안의 내용 또한 아주 간단하죠.

 

"EnemyRoutine 이라는 코루틴 함수를 실행시켜줘."

 

진짜 간단하죠?

그런데 코루틴? 코루틴이 뭐지?? 의아할 수 있습니다.

하지만 의문은 일단 제쳐두고... 그 바로 아래에 있는 StopEnemyRoutine 함수를 살펴볼게요.

    public void StopEnemyRoutine()
    {
        StopCoroutine("EnemyRoutien");
    }

보시다시피 간단합니다. 내용은 이렇네요.

 

"EnemyRoutine 이라는 코루틴 함수를 멈춰줘."

 

함수의 이름이 참으로 직관적입니다.

StartEnemyRoutionEnemyRoutine이라는 이름의 코루틴을 실행시켜달라는 거고,

StopEnemyRoutineEnemyRoutine이라는 이름의 코루틴을 멈춰달라는 거니까요.

 

이처럼 프로그래밍을 할 때는 최대한 직관적인 이름을 붙이는 게 중요한 것 같습니다.

그래야 나중에 헷갈리는 일이 그나마 없는 것 같아요.

 

그런데 아까부터 자꾸 등장하는 코루틴이 도대체 뭐길래 실행시켜달랬다가 멈춰달랬다가 하는 걸까요?

오래 끌 것 없이 바로 구경해봅시다.

    IEnumerator EnemyRoutien()
    {
        yield return new WaitForSeconds(1f);

        int spawnCount = 0;
        while (true)
        {
            int index = Random.Range(0, enemies.Length);
            int posRandom = Random.Range(0, arrPosX.Length);
            SpawnEnemy(arrPosX[posRandom], index);
            spawnCount++;

            if(spawnCount % intervalEnemy == 0)
            {
                spawnInterval -= downSpawnInterval;
            }

            yield return new WaitForSeconds(spawnInterval);
        }
    }

코루틴이란, 제가 간신히 이해한 바에 따르면...

모든 코드는 위에서부터 아래 방향으로 순차적으로 실행되는데, 코루틴을 쓰면 코드를 읽어내려가다가 '잠시 멈추고' 다른 걸 처리하는 거라고 합니다.

요컨데 '직렬로 처리되는 코드를 병렬로 처리하게 만드는 것'이라 보면 되겠는데...

 

긁적

 

솔직히 잘 모르겠습니다. 헤헤..

그냥, 쿨타임 같은 걸 만들 때 사용하는 거라고 생각하고 있어요.

왜 게임에서 스킬을 사용하면 '재사용 대기 시간'동안 스킬을 못쓰잖아요? 여기서 '재사용 대기 시간'을 돌리는 방식이 바로 코루틴을 써서 만드는 것 정도로 이해하고 있습니다.

 

그래서 저 코드를 해석하면, 대충 이런 거에요.

yield return new WaitForSeconds(1f);

"일단 1초 있다가 실행해."

 

        int spawnCount = 0;

"spawnCount라는 변수는 0부터 시작하는 정수야. 이 함수 안에서만 쓸 거야."

 

        while (true)
        {
            int index = Random.Range(0, enemies.Length);
            int posRandom = Random.Range(0, arrPosX.Length);
            SpawnEnemy(arrPosX[posRandom], index);
            spawnCount++;

            if(spawnCount % intervalEnemy == 0)
            {
                spawnInterval -= downSpawnInterval;
            }

            yield return new WaitForSeconds(spawnInterval);
        }

"무한하게 반복할 건데, 뭘 반복할 거냐면..."

 

"index라는 값을 만들거야. 그건 enemies 배열에 저장된 값들 중 하나를 랜덤으로 뽑을 거야."

"posRandom이라는 값도 만들거야. 그건 arrPosX 배열에 저장된 값들 중 하나를 랜덤으로 뽑을 거야."

"그런 다음에 SpawnEnemy함수를 호출할 거야. 그 함수에서 arrPosX배열의 값들 중 랜덤한 값 하나와, enemies 배열의 값들 중 랜덤한 하나를 사용할 거야."

"그리고 spawnCount 값을 1씩 늘릴 거야."

 

"만약, 1씩 늘어나던 spawnCount 값이 intervalEnemy의 배수가 되면..."

"spawnInterval 값은 downSpawnInterval 값만큼 빠지게 될 거야."

 

"그리고 spawnInterval만큼 대기해."

 

이게 도대체 무슨 소리인가 할 수도 있지만...

찬찬히 읽어보면 이해될 거에요! 이게 지금의 제가 할 수 있는 가장 쉬운 설명이에요 ㅠ

 

그리고, SpawnEnemy 함수가 뒤이어집니다.

앞서 봤던 EnemyRoutine 안에서 불러내어지는 함수이기도 하죠. 내용은 이러합니다.

 

    void SpawnEnemy(float posX, int index)
    {
        Vector3 spawnPos = new Vector3(posX, transform.position.y, transform.position.z);
        Instantiate(enemies[index], spawnPos, Quaternion.identity);
    }

"SpawnEnemy 함수를 쓸려면 posX라는 실수값과 index라는 정수값이 필요해."

 

"spawnPos라는 Vector3 자료를 만들거야. x / y / z 값이 들어갈 건데, x는 함수를 쓸 때 받는 posX값이, y는 현재 이 오브젝트의 y축 위치값이, z도 현재 이 오브젝트의 z축 위치값이 들어갈거야."

 

"인스턴트 오브젝트를 생성할 거야. enemies중에서 함수를 쓸 때 받는 index의 오브젝트를 생성해. 생성할 위치는 앞서 만든 spawnPos 위치로 하고, 회전값은 자기 자신의 값을 가지게 될 거야." 

 

 

나름 열심히 해석 + 설명을 붙이긴 했는데...

간추려 말하자면 이미 등록해 둔 Enemy 중에서 랜덤한 녀석을 랜덤한 위치에서 생성해라. 라는 것이에요.

 

이렇게...

Spawner 스크립트가 완료되었습니다.

이제 마무리를 위해 엔진으로 돌아와서 몇가지 설정을 해볼게요.

 

Spawner 스크립트를 보면 아래의 이미지처럼 배열을 만들 수 있게 될 텐데, 여기 내가 등록할 Enemy 프리팹 만큼 수를 입력하시면 됩니다.

만들어 둔 게 하나 뿐이니, 저 칸에다가 숫자 1을 입력하시면 되죠!

 

그러면 Element 0이라는 이름으로 빈 슬롯이 생성될 텐데, 거기에 만들어 둔 프리팹을 드래그 & 드롭으로 넣어줍니다.

이거면 전부 다 끝났어요!

빈 슬롯에 Enemy 프리팹을 넣은 모습입니다.

 

이제 모든게 끝났다고 생각할 수도 있지만...!

아직 하나가 남았습니다.

 

지난 포스팅에서 '프리팹 = Ctrl + C와 같다.'라고 제가 이야길 했었죠?

그런데 프리팹을 미리 만들어 둔 뒤, 스크립트를 작성해서 붙여버리는 만행을 저질렀었습니다.

한마디로 복사가 잘 안된 상황인데, 유니티에서는 그런 상황을 타게하기 위한 기능이 있죠. 바로 'Apply'라는 거에요!

 

만들어 둔 프리팹을 선택한 뒤, Inspector창에서 Overrides를 선택하면 나타나는 Apply All버튼을 누르기만 하면...

Overrides - Apply All 버튼을 눌러주세요.

 

지금까지 해당 프리팹에 수정된 내용이 일괄적으로 적용되는 것입니다.

이제 게임을 실행하면 우리가 작업했던 내용이 정상적으로 나오는 것을 확인할 수 있어요.

랜덤한 위치에서 미리 만든 Enemy 프리팹이 나타나고 있습니다.

 

뿌듯뿌듯합니다.

뭔가 벌써 게임을 다 만든 것 같은 기분이 드네요.

 

하지만... 여기서 끝낼 순 없죠! 아직 갈 길이 멉니다.

다음에는 저렇게 생성된 Enemy와 플레이어가 서로 충돌하도록, 그래서 플레이어가 죽을 수 있도록 만들어야겠어요.

 

 

<다음 편에 계속>

반응형

'도전! N잡러! > 도전! 게임 개발!' 카테고리의 다른 글

TAP FLOAT 개발 일기 #10  (98) 2023.09.25
TAP FLOAT 개발 일기 #9  (82) 2023.09.23
TAP FLOAT 개발 일기 #7  (76) 2023.09.20
TAP FLOAT 개발 일기 #6  (81) 2023.09.19
TAP FLOAT 개발 일기 #5  (60) 2023.09.17

댓글