ZombieSpawner 이해하기 — 좀비 생성과 오브젝트 풀링

공재원
∙∙조회 3

ZombieSpawner 이해하기 — 좀비 생성과 오브젝트 풀링

오늘은 ZombieSurvival 제작 과정에서 ZombieSpawner 코드를 살펴보았다.

이번 코드에서는 좀비를 생성하고, 웨이브마다 등장하는 좀비의 수를 늘리며, 죽은 좀비를 다시 사용할 수 있도록 관리하는 기능을 다룬다.

특히 List, Instantiate, SetActive, IEnumerator와 함께 오브젝트 풀링(Object Pooling) 이라는 개념이 사용되었다.

1. ZombieSpawner의 역할

ZombieSpawner는 좀비의 생성과 웨이브 진행을 관리하는 스크립트다.

주요 기능은 다음과 같다.

  • 좀비가 모두 사라지면 다음 웨이브 시작
  • 웨이브에 따라 생성할 좀비 수 증가
  • 지정된 위치에서 무작위로 좀비 생성
  • 좀비의 종류를 무작위로 선택
  • 죽은 좀비를 목록에서 제거
  • 죽은 좀비를 일정 시간 후 비활성화하고 재사용

이 기능들을 이해하려면 먼저 두 개의 리스트를 구분해야 한다.

csharp
private readonly List<Zombie> zombies = new List<Zombie>();
private readonly List<Zombie> zombiePool = new List<Zombie>();

변수역할
zombies현재 웨이브에서 활동 중인 좀비를 관리
zombiePool비활성화되어 재사용할 수 있는 좀비를 관리

쉽게 말해 zombies는 현재 전투에 참여하는 좀비 목록이고, zombiePool은 나중에 다시 사용할 수 있도록 보관하는 좀비 목록이다.

여기서 readonly는 리스트 변수 자체를 다른 리스트로 교체할 수 없도록 한다. 리스트에 좀비를 추가하거나 제거하는 것은 가능하다.

2. Update() — 다음 웨이브 확인

csharp
private void Update()
{
    if (zombies.Count == 0)
    {
        SpawnWave();
    }
}

Update()는 매 프레임마다 실행되는 함수다.

zombies.Count는 현재 zombies 리스트에 들어 있는 좀비의 수를 의미한다.

따라서 zombies.Count == 0은 현재 관리 중인 좀비가 한 마리도 없다는 뜻이다.

이 조건이 참이면 SpawnWave()를 호출하여 다음 웨이브를 시작한다.

다만 이 코드는 좀비가 없으면 매 프레임마다 조건을 확인한다. 다음 웨이브가 생성되기 전에 zombies가 비어 있는 상태가 유지되면 웨이브가 중복 생성될 수 있으므로, 실제 게임에서는 웨이브 진행 상태를 별도로 관리하는 방법도 고려할 수 있다.

3. SpawnWave() — 웨이브 진행과 좀비 수 결정

csharp
private void SpawnWave()
{
    wave++;

    int count = Mathf.RoundToInt(wave * 1.5f);

    for (int i = 0; i < count; ++i)
    {
        CreateZombie();
    }

    uIManager.SetWaveText(wave, zombies.Count);
}

이 함수는 새로운 웨이브를 시작하고 생성할 좀비의 수를 결정한다.

① 웨이브 증가

csharp
wave++;

새로운 웨이브가 시작될 때마다 wave를 1씩 증가시킨다.

처음에는 wave = 0이므로 첫 번째 웨이브가 시작되면 wave = 1이 된다.

② 생성할 좀비 수 계산

csharp
int count = Mathf.RoundToInt(wave * 1.5f);

현재 웨이브에 1.5를 곱한 뒤 가장 가까운 정수로 반올림하여 좀비 수를 결정한다.

웨이브계산생성할 좀비 수
11 × 1.5 = 1.52
22 × 1.5 = 33
33 × 1.5 = 4.55
44 × 1.5 = 66
55 × 1.5 = 7.58

이렇게 하면 웨이브가 진행될수록 더 많은 좀비가 등장한다.

③ 반복문으로 좀비 생성

csharp
for (int i = 0; i < count; ++i)
{
    CreateZombie();
}

for 반복문을 사용하여 계산한 수만큼 CreateZombie()를 호출한다.

예를 들어 첫 번째 웨이브에서 count가 2라면 CreateZombie()가 두 번 실행된다.

마지막으로 SetWaveText()를 호출하여 현재 웨이브와 좀비 수를 UI에 표시한다.

4. CreateZombie() — 좀비 생성과 배치

csharp
Transform point = spawnPoints[Random.Range(0, spawnPoints.Length)];
ZombieData data = zombieDatas[Random.Range(0, zombieDatas.Length)];

먼저 좀비가 등장할 위치와 좀비의 종류를 무작위로 선택한다.

① 생성 위치 선택

csharp
Transform point = spawnPoints[Random.Range(0, spawnPoints.Length)];

spawnPoints는 좀비가 등장할 수 있는 위치들을 담고 있는 배열이다.

Random.Range()를 사용하면 배열에 들어 있는 위치 중 하나를 무작위로 선택할 수 있다.

예를 들어 생성 위치가 3개라면 그중 하나를 선택한다.

참고로 정수형 Random.Range(min, max)는 최솟값을 포함하고 최댓값은 포함하지 않는다.

따라서 Random.Range(0, spawnPoints.Length)는 배열의 유효한 인덱스 중 하나를 반환한다.

② 좀비 데이터 선택

csharp
ZombieData data = zombieDatas[Random.Range(0, zombieDatas.Length)];

zombieDatas에는 좀비의 종류별 데이터가 저장되어 있다.

이 코드 역시 무작위 인덱스를 선택하여 사용할 ZombieData를 가져온다.

예를 들어 일반 좀비와 강한 좀비의 데이터가 들어 있다면, 그중 하나를 선택하여 좀비를 설정할 수 있다.

③ 좀비 오브젝트 확보

csharp
Zombie zombie;

if (zombiePool.Count == 0)
{
    zombie = Instantiate(zombiePrefab);
    zombie.gameObject.SetActive(false);
    zombiePool.Add(zombie);
}

여기서는 재사용할 수 있는 좀비가 있는지 확인한다.

zombiePool.Count == 0은 재사용할 좀비가 없다는 뜻이다.

이 경우 Instantiate()를 사용하여 새로운 좀비를 생성한다.

생성한 좀비는 우선 SetActive(false)로 비활성화한 뒤 zombiePool에 추가한다.

Instantiate()는 새로운 오브젝트를 생성하는 함수이고, SetActive(false)는 해당 게임 오브젝트를 비활성화하는 함수다.

이후 코드에서는 풀에 들어 있는 좀비를 가져와 사용한다.

csharp
zombie = zombiePool[0];
zombie.Setup(data);

zombie.transform.position = point.position;
zombie.transform.rotation = point.rotation;

zombie.gameObject.SetActive(true);

각 코드의 역할은 다음과 같다.

  • zombiePool[0]: 풀에 보관된 첫 번째 좀비를 가져온다.
  • zombie.Setup(data): 선택한 데이터에 맞게 좀비를 설정한다.
  • position: 좀비가 등장할 위치를 설정한다.
  • rotation: 좀비가 등장할 때의 회전을 설정한다.
  • SetActive(true): 좀비를 활성화한다.

즉, 새로 생성한 좀비든 재사용하는 좀비든 필요한 설정을 마친 뒤 활성화하는 것이다.

④ 좀비 목록 이동

csharp
zombies.Add(zombie);
zombiePool.Remove(zombie);

활성화한 좀비는 zombies에 추가하고 zombiePool에서는 제거한다.

이렇게 하면 현재 활동 중인 좀비와 재사용할 좀비를 구분할 수 있다.

예를 들어 풀에 있던 좀비 한 마리를 꺼냈다면 다음과 같이 변경된다.

목록변경 전변경 후
zombies활동 중인 좀비 1마리활동 중인 좀비 2마리
zombiePool재사용 가능한 좀비 1마리재사용 가능한 좀비 0마리

5. OnDead.AddListener() — 좀비 사망 이벤트 등록

csharp
zombie.OnDead.AddListener(() => zombies.Remove(zombie));

zombie.OnDead.AddListener(() => StartCoroutine(CoReturnToPoolZombie(zombie)));

zombie.OnDead.AddListener(() => uIManager.SetWaveText(wave, zombies.Count));

이 코드는 좀비가 죽었을 때 실행할 작업을 등록한다.

OnDead는 좀비가 사망했음을 알리는 이벤트로 사용되고, AddListener()는 해당 이벤트가 발생했을 때 실행할 동작을 추가한다.

여기서 () =>는 람다식이다. 간단히 말하면 실행할 코드를 전달하는 문법이다.

세 가지 동작은 다음과 같다.

첫 번째: 활동 중인 좀비 목록에서 제거

csharp
zombie.OnDead.AddListener(() => zombies.Remove(zombie));

좀비가 죽으면 zombies 목록에서 해당 좀비를 제거한다.

두 번째: 일정 시간이 지난 뒤 오브젝트 풀로 반환

csharp
zombie.OnDead.AddListener(() => StartCoroutine(CoReturnToPoolZombie(zombie)));

좀비가 죽으면 코루틴을 시작한다.

이 코루틴은 5초를 기다린 뒤 좀비를 비활성화하고 풀에 반환한다.

세 번째: UI 갱신

csharp
zombie.OnDead.AddListener(() => uIManager.SetWaveText(wave, zombies.Count));

좀비가 죽어서 목록에서 제거되면 현재 웨이브의 좀비 수를 UI에 다시 표시한다.

단, 이 코드는 사망 이벤트가 발생할 때마다 리스너를 등록한다. 좀비를 재사용하는 구조이므로 같은 좀비가 여러 번 생성될 때 리스너가 중복 등록될 수 있다는 점에 주의해야 한다. 실제 게임에서는 리스너 등록과 해제 시점을 관리하는 것이 좋다.

6. CoReturnToPoolZombie() — 죽은 좀비 재사용하기

csharp
private IEnumerator CoReturnToPoolZombie(Zombie zombie)
{
    yield return new WaitForSeconds(5f);

    zombie.gameObject.SetActive(false);
    zombiePool.Add(zombie);
}

이 함수는 코루틴을 사용하여 죽은 좀비를 일정 시간 후 재사용할 수 있도록 만든다.

① 5초 대기

csharp
yield return new WaitForSeconds(5f);

좀비가 죽은 직후 바로 비활성화하지 않고 5초를 기다린다.

이 시간 동안 죽는 애니메이션이나 사망 효과를 보여줄 수 있다.

② 좀비 비활성화

csharp
zombie.gameObject.SetActive(false);

5초가 지나면 좀비 오브젝트를 비활성화한다.

비활성화하면 해당 오브젝트는 게임에서 활성 상태로 동작하지 않게 된다.

③ 오브젝트 풀에 반환

csharp
zombiePool.Add(zombie);

비활성화한 좀비를 zombiePool에 추가한다.

이제 다음에 좀비를 생성해야 할 때 새로운 오브젝트를 만드는 대신, 풀에 보관된 좀비를 다시 사용할 수 있다.

오브젝트 풀링이란?

이처럼 오브젝트를 필요할 때마다 생성하고 파괴하는 대신, 미리 만들어 둔 오브젝트를 재사용하는 방식을 오브젝트 풀링(Object Pooling) 이라고 한다.

text
좀비 생성
    ↓
zombies에 추가
    ↓
플레이어와 전투
    ↓
좀비 사망
    ↓
zombies에서 제거
    ↓
5초 대기
    ↓
좀비 비활성화
    ↓
zombiePool에 반환
    ↓
다음 웨이브에서 재사용

오브젝트 풀링은 반복적인 오브젝트 생성과 파괴를 줄이는 데 도움이 된다. 특히 좀비나 총알처럼 자주 생성되는 오브젝트에 유용하다.

7. 전체 실행 순서

지금까지 살펴본 내용을 종합하면 다음과 같다.

  1. Update()에서 활동 중인 좀비가 있는지 확인한다.
  2. 좀비가 없다면 SpawnWave()를 호출한다.
  3. wave를 증가시키고 생성할 좀비 수를 계산한다.
  4. CreateZombie()를 반복해서 호출한다.
  5. 생성 위치와 좀비 데이터를 무작위로 선택한다.
  6. 오브젝트 풀에 좀비가 없다면 새로 생성하고, 있다면 기존 좀비를 재사용한다.
  7. 좀비를 설정하고 활성화한 뒤 zombies에 추가한다.
  8. 좀비가 죽으면 활동 목록에서 제거하고 UI를 갱신한다.
  9. 5초 후 좀비를 비활성화하고 zombiePool에 반환한다.
  10. 모든 좀비가 사라지면 다음 웨이브가 시작된다.

이번 코드를 살펴보면서 좀비 생성뿐만 아니라 웨이브 관리, 이벤트 처리, 코루틴, 오브젝트 풀링이 서로 연결되어 동작한다는 것을 알게 되었다.

특히 오브젝트 풀링은 한 번 생성한 좀비를 다시 사용할 수 있게 해 주기 때문에, 게임에서 반복적으로 생성되는 오브젝트를 효율적으로 관리하는 데 중요한 개념이라는 것을 이해하게 되었다.

다만 아직 모든 코드를 완전히 이해한 것은 아니기에 코드 리뷰와 지속적인 코드 작성 연은 필요하다.