유니티 코루틴 정리 [유니티 C#]

게임을 개발하다 보면 이런 로직을 구현해야 되는 일이 생깁니다.

  • 스킬의 쿨타임 기능을 구현하고 싶을 때
  • 화면을 페이드 아웃 시키고 싶을 때

이럴 때 사용되는 문법이 “코루틴(Coroutine)” 입니다.
오늘은 코루틴을 왜 사용하는 지, 그리고 어떻게 사용하는 지 알아보겠습니다.

1. 왜 코루틴(Coroutine)을 사용할까?

코루틴을 사용하지 않은 상태에서 스킬 쿨타임 기능을 구현한다면, 보통 아래와 같은 코드를 작성하게 됩니다.

C#
using UnityEngine;

public class Player : MonoBehaviour
{
    float _cooldown = 2.0f; // 스킬 쿨타임
    float _timer; // 쿨타임 타이머
    bool _canUseSkill; // 스킬 사용 가능 여부 (true, false)

    private void Update()
    {
        _timer += Time.deltaTime;
        if (_timer >= _cooldown)
        {
            _canUseSkill = true;
            Debug.Log("스킬 쿨타임 종료. 스킬 사용 가능.");
        }
    }
}

_timer 변수가 Update문에서 시간의 변화량을 매 프레 추가 후 스킬 쿨타임(_cooldown) 시간에 도달 했는지 확인합니다.

스킬 쿨타임 기능을 위해 변수를 선언 해야되고, Update문이 지저분해져 가독성을 저해합니다.

하지만, ‘시간’에 관련한 로직을 구현할 때 코루틴을 사용하면 이를 해결할 수 있습니다.

2. 코루틴(Coroutine)의 정의

유니티 매뉴얼에 따른 코루틴의 정의를 간단히 요약해보자면 이렇습니다.

(정의)
“작업을 다수의 프레임에 분산하며, 실행을 일시 정지할 수도, 중단한 부분에서 다음 프레임을 계속할 수도 있는 메서드

‘작업을 다수의 프레임에 분산한다‘는 얼핏 보면 멀티스레딩을 통한 병렬처리를 하는 걸로 볼 수 있지만,
코루틴은 멀티스레드가 아닌 메인스레드에서 처리됩니다.

따라서 병렬처리가 아닌 메인스레드에서 시간을 잘게 나누어 메인 로직과 각 코루틴에 배정하여 실행시키는 비동기 방식입니다.

코루틴과 멀티스레드 비교

그렇다면 ‘실행을 일시 정지할 수도, 중단한 부분에서 다음 프레임을 계속할 수도 있는 메서드’는 무슨 의미일까요?

일반적인 함수(메서드)와 비교해보면,
일반적인 함수는 실행이 되면 중간에 멈추지 않고 함수 내의 모든 코드를 실행합니다.

하지만, 코루틴은…
메서드 내 ‘yield’ 문법을 만나면 제어권을 유니티에 넘겨주며 대기합니다.
함수가 종료되는 게 아닌 대기이므로 코루틴 내의 값들은 유지됩니다.
(정확하게는 yield문이 함수의 상태를 메모리에 저장하기 때문에 함수 내 변수 등이 보존됩니다.)

따라서, 다수의 프레임에 분산 될 수 있으며, ‘대기’가 가능하므로 코루틴시간과 관련된 로직을 구현하기에 최적의 문법입니다!

3. 코루틴(Coroutine) 사용법

코루틴 문법은 다음 5가지만 기억하시면 쉽게 사용하실 수 있습니다.

  • using System.Collections을 선언한다.
  • IEnumerator를 반환하는 형태여야 한다
  • 메서드 내에 yield return 문법이 들어가야 된다.
  • StartCoroutine()을 사용해 코루틴을 실행 시킨다.
  • StopCoroutine()을 사용해 코루틴을 중지 할 수 있다.

예제 코드로 보면 이렇습니다.

C#
using System.Collections;
using UnityEngine;

public class Player : MonoBehaviour
{
    float _cooldown = 2.0f;
    bool _canUseSkill = true;
    
    // 실행 중인 코루틴을 저장할 변수
    private Coroutine _coSkill; 


    private void OnDisable()
    {
        // 코루틴 정지
        // (실제로는 오브젝트 비활성화 시 자동으로 코루틴도 종료되나, 예제를 위해 추가)
        if (_coSkill != null)
        {
            StopCoroutine(_coSkill);
        }
    }
    

    private void Update()
    {
        // Q 입력 시 스킬 사용
        if (Input.GetKeyUp(KeyCode.Q) && _canUseSkill)
        {
            // 스킬 코루틴 실행 및 변수에 저장
            _coSkill = StartCoroutine(SkillCollDownRoutine());
            Debug.Log("스킬 사용");
        }
    }


    // 스킬 쿨타임 코루틴
    private IEnumerator SkillCollDownRoutine()
    {
        // 스킬 사용 여부 true
        _canUseSkill = false;
        
        // 대기
        yield return new WaitForSeconds(_cooldown);
        
        // 스킬 사용 여부 false
        _canUseSkill = true;
    }
}

앞서 살펴보았던 스킬 쿨타임을 구현하는 로직을 코루틴으로 구현한 코드입니다.

코루틴 yield return을 만나면 제어권을 잠시 반환하는데 얼마나 길게 반환할 지를 설정하는 문법이
new WaitForSeconds(_cooldown) 부분입니다.

(참고)
왜 ‘new’를 사용할까요?

yield return으로 WaitForSeconds등의 값을 반환하면 유니티는 이 값을 보고
얼마나 대기할 지 판단합니다.

다만, WaitForSeconds등의 클래스에 값을 넣고 반환하기 위해서는 new 키워드를 통해 새로운 인스턴스를 생성한 후 반환해야 되기에 new’를 사용하셔야 됩니다!

그러면 주요 코드를 살펴보겠습니다.


using System.Collections

IEnumerator 인터페이스를 사용하기 위해 선언합니다. 코루틴은 유니티의 문법이지만, IEnumerator는 C#의 문법이기 때문에 별개의 라이브러리 입니다.

StopCoroutine();

*오브젝트가 비활성화 되면 코루틴도 자동으로 종료되나, StopCoroutine을 설명하기 위해 넣었습니다.

소괄호() 내 코루틴을 중지합니다.

(참고)
코루틴 중지 시 변수에 저장한 코루틴을 넣지 않고 “StopCoroutine(SkillCollDownRoutine())” 식으로 작성 하게 되면 이는 새로운 코루틴 인스턴스를 생성 후 중지한다는 의미입니다.

위의 경우 기존 코루틴은 중지되지 않으니 주의 하셔야 됩니다.

StartCoroutine();

소괄호() 내 코루틴을 실행합니다.

private IEnumerator SkillCollDownRoutine()

코루틴 메서드의 문법입니다. IEnumerator를 반환하는 형태여야 합니다.

yield return new WaitForSeconds();

yield return을 만나 대기하며, 몇 초 동안 대기할 지 설정하는 문법입니다.


4. 대기 방식

예제 코드는 WaitForSeconds를 사용하여 몇 초 동안 기다릴 지를 설정하였습니다.

하지만 경우에 따라 한 프레임만 대기할 수도, 특정 조건이 참이 될 때까지 대기를 할 수도 있습니다.

이럴 때 사용하는 대표적인 몇 가지 대기 방식에 대해서 알아보겠습니다.

대기 방식설명용도
yield return null;다음 Update 직전까지 대기Update문처럼 사용 해야 될 때 while문과 같이 사용
yield return new WaitForSeconds();n초 동안 대기쿨타임, 리스폰 등 대기 시간
yield return new WaitForFixedUpdate();모든 FixedUpdate가 호출된 직후물리 연산 처리 시
yield return new WaitForSecondsRealtime();n초 (실시간) 동안 대기게임이 일시정지 되어도 처리되는 로직
yield return new WaitWhile();조건이 참인 동안특정 조건이 필요한 로직
yield return new WaitUntil();조건이 참이 될 때까지특정 조건이 필요한 로직

5. 코루틴 최적화

코루틴‘yield return new ~~’ 문법을 사용하며 지속적으로 새로운 클래스 인스턴스를 생성합니다.

그리고 코루틴이 종료 된 후에는 생성된 인스턴스들이 가비지 컬렉션(GC)이 되어버립니다!
특히 코루틴이 반복적이고 짧다면 더 많은 가비지 컬렉션이 생성 될 겁니다.

이를 어느 정도 방지하는 방법이 바로 ‘캐싱(Caching)’ 입니다.

캐싱(Caching)은 값을 미리 할당 해주는 것을 의미합니다. 이를 코루틴에 적용 해보면…

C#
    // ----- 기존 (캐싱 X) -----
    // 스킬 쿨타임 코루틴
    private IEnumerator SkillCollDownRoutine()
    {
        // _cooldown(초) 만큼 대기
        yield return new WaitForSeconds(2.0f);

        // 대기 종료 후 스킬 사용 가능 상태로 변경
        _canUseSkill = true;
    }
C#
    // ----- 캐싱 -----
    // * WaitForSeconds 캐싱! *
    private WaitForSeconds _waitForSeconds = new WaitForSeconds(2.0f);

    // 스킬 쿨타임 코루틴
    private IEnumerator SkillCollDownRoutine()
    {
        // _cooldown(초) 만큼 대기
        yield return _waitForSeconds;

        // 대기 종료 후 스킬 사용 가능 상태로 변경
        _canUseSkill = true;
    }

선언 단계에서 WaitForSeconds를 캐싱하면 더 이상 코루틴을 실행 시킬 때마다 인스턴스를 생성하는 게 아닌,
생성 된 인스턴스를 사용하여 가비지 컬렉션이 쌓이는 걸 방지할 수 있습니다.

선언 시 값 할당이 불가능한 경우 (이미 선언 된 값을 할당해야 되는 경우)라면,
Awake, Start등의 초기화 단계 함수에서 초기화를 진행해주시면 됩니다.

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다