코루틴을 공부하며

송제백
∙∙조회 2

이번 좀비게임을 개발하면서 코루틴이라는 개념을 처음 접했다. 총격 효과, 재장전, 좀비 재사용 기능을 구현하면서 코루틴의 동작 원리를 공부했고, 그 과정에서 학교에서 배웠던 운영체제 개념도 떠올랐다.

1. 코루틴을 처음 접하며

코루틴은 실행 도중 잠시 멈췄다가 정해진 조건이 충족되면 중단했던 위치부터 이어서 실행할 수 있는 기능이다.

처음 yield return을 접했을 때 문득 운영체제에서 배웠던 Context Switching(문맥 교환)이 떠올랐다.

실행 중인 작업을 멈췄다가 나중에 이어서 실행한다는 점이 비슷하게 느껴졌기 때문이다.

궁금해서 찾아보니 Unity 코루틴은 별도의 스레드를 생성하는 것이 아니라 일반적으로 메인 스레드에서 실행된다는 사실을 알게 되었다. 하나의 실행 흐름 안에서 여러 작업을 번갈아 진행하는 멀티태스킹과 비슷하다는 생각이 들었었다.

조금 더 찾아보니, 특히 작업이 자발적으로 제어권을 양보하는 협력적 멀티태스킹(Cooperative Multitasking)과 유사한 부분이 있었다.

물론 결론적으로 운영체제에서 프로세스나 스레드를 전환하는 Context Switching과 Unity 코루틴은 실제 동작 방식이 다르다. 코루틴은 yield return을 기준으로 실행을 나누고, Unity가 정해진 조건에 따라 다시 실행하는 구조다.

2. 코루틴의 주요 작동 원리

일반 함수는 호출되면 보통 끝까지 실행된다. 반면 코루틴은 중간에 실행을 중단하고, 일정 시간이 지나거나 조건이 충족되면 이어서 실행할 수 있다.

csharp
StartCoroutine(CoReloadEffect()); // 코루틴 시작

private IEnumerator CoReloadEffect()
{
    // 바로 실행할 내용

    yield return new WaitForSeconds(gundata.reloadTime);

    // 장전 시간이 지난 후 실행할 내용
}

  • IEnumerator: 코루틴 메서드의 반환형
  • StartCoroutine(): 코루틴 실행 시작
  • yield return: 현재 실행을 잠시 중단하고 제어권을 Unity에 반환
  • WaitForSeconds(): 지정한 게임 시간만큼 대기

실행 흐름을 정리하면 다음과 같다.

코루틴 시작 → yield return → 실행 중단 → 다른 게임 로직 진행 → 대기 조건 충족 → 실행 재개

Unity는 C#의 상태 머신(State Machine)을 활용해 코루틴의 실행 위치와 필요한 지역변수를 보존한다. 따라서 yield return 이후에도 이전에 실행하던 부분부터 이어갈 수 있다.

여기서 중요한 점은 코루틴이 게임 전체를 멈추거나 새로운 스레드를 만들어 병렬 실행하는 기능은 아니라는 것이다.

또한 좀비게임 제작에서 많이 썼던WaitForSeconds()와 Time.timeScale의 영향을 받으며, 정확히 지정한 순간이 아니라 대기 조건이 충족된 이후의 프레임에서 실행이 재개될 수 있다.

1) 총격 효과: 0.03초 후 탄환 궤적 숨기기

Gun.cs의 CoShotEffect()에서는 총을 발사할 때 총구 불빛과 탄피 효과, 발사음을 재생하고 LineRenderer로 탄환 궤적을 표시한다.

csharp
private IEnumerator CoShotEffect(Vector3 hitPosition)
{
    muzzleFlashEffect.Play();
    shellEjectEffect.Play();
    gunAudioPlayer.PlayOneShot(gundata.shotClip);

    bulletLineRenderer.SetPosition(0,fireTransform.position);
    bulletLineRenderer.SetPosition(1,hitPosition);
    bulletLineRenderer.enabled = true;

    yield return new WaitForSeconds(0.03f);

    bulletLineRenderer.enabled = false;
}

동작 순서는 간단하다.

총격 효과 실행 → 탄환 궤적 표시 → 0.03초 대기 → 탄환 궤적 숨김

이 코루틴은 Shoot()에서 StartCoroutine(CoShotEffect(hitPosition))으로 호출한다.

참고로 실제 명중 여부는 코루틴이 아니라 Physics.Raycast()를 사용하는 Hitscan 방식으로 하기로 했다.

2) 재장전: 일정 시간이 지난 후 탄약 채우기

Gun.cs에서 장전 버튼을 누르면 Reload()가 장전 가능 여부를 검사한 뒤 CoReloadEffect()를 실행한다.

csharp
private IEnumerator CoReloadEffect()
{
    CurrentState = State.Reloading;
    gunAudioPlayer.PlayOneShot(gundata.reloadClip);

    yield return new WaitForSeconds(gundata.reloadTime);

    int ammoToFill = gundata.magCapacity - magAmmo;

    if(ammoRemain < ammoToFill)
    {
        ammoToFill = ammoRemain;
    }

    magAmmo += ammoToFill;
    ammoRemain -= ammoToFill;

    uiManager.SetAmmoText(magAmmo,ammoRemain);
    CurrentState = State.Ready;
}

장전이 시작되면 총의 상태를 Reloading으로 변경한다. 이후 gundata.reloadTime만큼 대기하고, 탄창에 부족한 탄약을 채운 뒤 Ready 상태로 돌아간다.

예비 탄약이 부족할 경우에는 남은 탄약만큼만 채우도록 조건문을 사용했다.

여기서 코루틴은 장전 시간을 처리하고, enum State는 장전 중 발사를 방지하는 역할을 한다.

시간에 따른 실행과 현재 상태를 관리하는 기능이 서로 다르다는 생각을 했다.

3) 죽은 좀비를 5초 후 재사용하기

ZombieSpawner.cs에서는 오브젝트 풀링을 사용해 좀비를 재사용하도록 구현했다.

좀비가 죽었을 때 곧바로 삭제하지 않고, 5초 뒤 비활성화한 후 재사용 목록으로 반환한다.

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

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

이렇게 하면 사망한 좀비가 일정 시간 동안 화면에 남아 있을 수 있고, 이후에는 새로운 좀비를 생성하는 대신 기존 오브젝트를 재사용할 수 있다.

사망 → 5초 대기 → 비활성화 → 오브젝트 풀에 반환하는 순서다.

4) Update 타이머와 코루틴의 차이

좀비게임 실습을 하며 코딩중에 만들던 중 옆 친구가 "좀비가 공격할 때도 코루틴을 사용하셨나요?" 라고 물어봤다.

당시에는 수업에 너무 많은 양을 받아들이다보니 별 생각이 없었었다. 그리고 이미Update()에서 시간을 비교하는 방식으로 구현했기 때문에 아니라고 대답했는데, 집와서 갑자기 생각이 들었다. "그러게, 좀비의 공격에도 코루틴을 사용할 수 있지 않았을까?"

현재 Zombie.cs의 UpdateAttack()에서는 다음과 같이 공격 간격을 처리하고 있다.

csharp
if(Time.time - lastAttackTime >= attackInterval)
{
    lastAttackTime = Time.time;

    // 공격 처리
}

Time.time으로 현재 시간을 확인하고 마지막 공격 시간과 비교하여, 일정 시간이 지났을 때 다시 공격하도록 구현했다.

그런데 코루틴을 사용한다면 WaitForSeconds(attackInterval)로 공격 사이에 대기 시간을 주는 방식도 가능할 것 같았다.

구분Update 타이머Coroutine
동작 방식매 프레임 시간 조건 검사지정된 조건까지 실행 중단 후 재개
시간 처리Time.time 비교WaitForSeconds()
활용 예시공격 가능 여부 확인공격 → 대기 → 다음 공격

좀비 공격을 코루틴으로 구현한다면 이런 느낌이지 않았을까? 생각이 든다. 다만 코루틴으로 변경한다면 좀비가 죽거나 공격 범위를 벗어났을 때 공격을 중단하는 처리같은 문제도 함께 고려해야 했을 것 같다.

고려해야할 처리 문제가 많아서 현재 방식과 같이 UpdateAttack()에서 공격 상태와 거리를 확인한 뒤 시간 조건을 검사는 구조가 잘 맞으니 강사님도 해당 방법을 채택하신게 아닌가란 생각이 들었다.

마무리

처음 코루틴을 접했을 때는 운영체제의 Context Switching과 비슷한 개념인지 궁금했다. 직접 찾아보면서 Unity 코루틴은 일반적으로 메인 스레드에서 동작하며, 협력적 멀티태스킹과 유사한 실행 구조를 가진다는 점을 알게 되었다.

무엇보다도 학교에서 이론으로 배웠던 운영체제 개념이 실제 게임 개발을 이해하는 데 도움이 된다는 사실이 꽤 기분 좋았다.

당시에는 시험을 위해 공부했던 내용들이었고, 하드웨어나 시스템 개발쪽이라 생각한 이론들이였는데 그냥 조금 신기한 기분이 들었다. 완벽하게 코루틴을 쓸 정도로 이해한 것 같지는 않지만 코루틴의 사용법을 익히는 것에서 그치지 않고, 기존에 알고 있던 개념과 연결해 동작 원리를 이해해 볼 수 있었다.

앞으로도 새로운 기술을 배울 때 단순히 사용법을 외우기보다도 왜 이런 방식으로 동작하는지 생각하고 이해하며, 직접 찾아보며 이해하는 습관을 이어가고 싶다. 물론 취업도 해야하고 주어진 시간이 많지 않아 너무 깊지 않게 적절한 선을 유지하며 공부하지 않을까 생각이 든다.