Programming 

코루틴(Coroutine) 다루기 2(코루틴 중단하기 + 코루틴 매개변수 + yield break)

 

작성 기준 버전 ::2019.2

 

[이 포스트는 유튜브 영상으로도 시청하실 수 있습니다]

 

이번 포스트에서는 지난 코루틴 포스트에서 다루지 못했던 코루틴 중단하기와 코루틴 함수에 매개변수 전달하기 그리고 yield break에 대해서 다뤄보도록 하자.

 

코루딘 중단하기

 

public class CoroutineTest : MonoBehaviour
{
    IEnumerator enumerator;

    void Start()
    {
        // 코루틴 함수를 직접 호출해서 중단시키려면 IEnumerator를 저장해서 사용
        enumerator = TestCoroutine();
        StartCoroutine(enumerator);
    }

    void Update()
    {
        if(Input.GetKeyDown(KeyCode.Space))
        {
            // 코루틴 함수를 직접 호출해서 중단시키려면 IEnumerator를 저장해서 사용
            StopCoroutine(enumerator);
        }
    }

    IEnumerator TestCoroutine()
    {
        int i = 0;
        while(true)
        {
            yield return null;
            Debug.Log("Coroutine " + i);
            i++;
        }
    }
}

 

코루틴을 정지시키는 기본적인 방법의 위의 코드와 같다.

 

위 코드는 게임이 시작되면 Start 함수에 StartCoroutine으로 TestCoroutine 함수를 실행시켜 준다. 그러면 코루틴 함수 내용에 따라서 "Coroutine"이라는 로그와 함께 반복된 횟수를 출력한다. 그리고 플레이 도중에 스페이스 키를 누르면 코루틴이 멈추면서 로그 출력이 중단된다.

 

StartCoroutine(TestCoroutine());

 

보통 때는 바로 위 코드처럼 코루틴 함수를 호출했을 텐데 IEnumerator에 저장해서 실행한 이유는 TestCoroutine 함수에서 받아온 IEnumerator를 StopCoroutine에 넣어주기 위해서 이다.

 

StartCoroutine과 StopCoroutine은 이 IEnumerator를 통해서 어떤 코루틴을 실행하고 중단시킬지 확인할 수 있다.

 

코루틴 함수 이름을 문자열로 이용하기

 

StartCoroutine("TestCoroutine");

StopCoroutine("TestCoroutine");

 

이전 코루틴 포스트에서는 알려주지 않은 방법이 있는데, 코루틴을 실행시킬 때 그 코루틴 함수의 이름만으로도 실행시킬 수 있다는 것이다. 그리고 코루틴의 이름으로도 코루틴을 정지시킬 수 있.

 

어떤 면에서 보면 코루틴의 이름으로 코루틴을 시작하고 멈추는 방법이 더 낫다고 여길 수도 있다. 하지만 한 오브젝트에서 같은 이름의 코루틴이 2개 이상 실행되고 있는 상태에서 코루틴의 함수 이름 문자열로 StopCoroutine을 호출하면 이름으로 실행된 모든 코루틴이 동시에 멈춰버린다.

 

StartCoroutine(TestCoroutine());   // 코루틴 함수를 호출해서 실행한 코루틴은

StopCoroutine("TestCoroutine");    // 코루틴 이름 문자열로 중단시킬 수 없음

 

그와 더불어 위 코드처럼 코루틴 함수를 호출해서 실행한 코루틴은 코루틴 이름 문자열로 중단시킬 수 없다.

 

문자열로 코루틴을 실행하는 방법은 편리하지만 사용을 권장하지는 않는다. 그 이유는 문자열이기 때문에 오타가 발생해도 에러 표시가 되지 않아서 문제가 발생할 소지가 높기 때문이다. 그리고 이렇게 상수 문자열은 게임을 출시하는 과정에서 앱 보안을 위한 암호화 과정을 거칠 때, 코루틴 이름과 다른 문자열로 변경되어서 코루틴 함수 호출에 실패하게 되는 경우가 발생하기도 한다.

 

모든 코루틴 동시에 중단시키기

 

StopAllCoroutines();

 

코루틴을 중단시키는 마지막 방법으로는 StopAllCoroutines가 있다. StopAllCoroutines 함수를 사용하면 이 컴포넌트가 실행하고있는 모든 코루틴을 중단시킨다.

 

코루틴 함수에 매개변수 전달하기

 

IEnumerator TestCoroutine(int count)
{
    int i = 0;
    while(i < count)
    {
        yield return null;
        Debug.Log("Coroutine " + i);
        i++;
    }
}

 

그럼 이번에는 코루틴 함수에 매개변수를 전달하는 방법을 알아보자.

 

TestCoroutine 코루틴 함수를 위와 같이 매개변수를 받을 수 있게 약간 변경한다. int 타입의 count 매개변수를 받아서 count 횟수만큼 반복하게 되었다.

 

StartCoroutine(TestCoroutine(10));

 

코루틴 함수 호출 방식에서는 TestCoroutine의 매개변수에 바로 값을 넣어주면된다.

 

StartCoroutine("TestCoroutine", 10);

 

코루틴 이름 문자열로 코루틴을 실행시킬 때는 문자열의 이름 뒤에 콤마를 찍고 매개변수를 넣어주면 된다.

 

Coroutine StartCoroutine(string methodName, object value);

 

코루틴 이름 문자열로 코루틴을 실행시키면서 매개변수를 받는 StartCoroutine의 오버로드 형식은 string으로 코루틴 함수의 이름을 받고 object 타입으로 매개변수를 받는다.

 

참고로 object 타입은 C# 프로그래밍에서 모든 변수 타입의 최상위 타입이다. 그래서 이렇게 int 타입의 숫자를 넣어주면 object 타입으로 바꿔서 받는다. 그리고 TestCoroutine 함수를 호출하면서 내부적으로 int 타입으로 바꿔서 전달해준다다.

 

StartCoroutine(TestCoroutine(10));

StartCoroutine("TestCoroutine", 10);

 

그렇기 때문에 이 두 가지 호출 방식은 똑같이 동작한다.

 

코루틴 함수 이름 문자열 실행 방식의 매개변수 전달 방식의 약점

 

박싱/언박싱의 오버헤드

 

// 박싱(Boxing)
int i = 10;
object obj = i;

// 언박싱(Unboxing)
int j = (int)obj;

 

하지만 이렇게 object로 매개변수를 전달하는 방법에는 약점이 몇 가지 있다. 먼저 프로그래밍에서 다른 타입의 변수를 object 타입으로 만드는 과정을 박싱(Boxing)이라고 부르고 object 타입의 변수를 원래 타입의 변수로 되돌리는 과정을 언박싱(Unboxing)이라고 부른다.

 

이 박싱/언박싱 과정은 미세하지만 분명히 성능적인 오버헤드를 일으킨다. 게임 최적화를 위해서는 남발하지 않는게 좋다.

 

전달 가능한 매개변수의 갯수

 

IEnumerator TestCoroutine(int count, float time)
{
    yield return new WaitForSeconds(time);

    int i = 0;
    while(i < count)
    {
        yield return null;
        Debug.Log("Coroutine " + i);
        i++;
    }
}

 

그리고 두 번째 약점을 설명하기 위해서 TestCoroutine의 기능을 조금 변경하기로 했다고 가정해보자.

 

매개변수에 float 타입으로 time 변수를 추가로 받아서 그 시간만큼 기다렸다가 count 횟수만큼 반복하도록 변경했다. 이렇게 되면 매개변수가 2개로 바뀌게 된다. 

 

StartCoroutine(TestCoroutine(10, 3f));

 

그러면 코루틴 함수 자체로 호출하는 방식에서는 새로운 매개변수를 넣어달라고 에러가 표시되기 때문에 추가된 기능에 맞게 매개변수를 넣어주기만 하면 된다.

 

하지만 코루틴 함수의 이름으로 호출하는 방식에서는 매개변수를 전달할 object가 하나 뿐이라 두 번째 매개변수를 전달할 방법이 없다. 그러니까 이름으로만 호출할 때는 매개변수를 하나 밖에 쓸 수 없는 것이다.

 

물론 클래스나 구조체로 묶어서 보내는 방법도 있겠지만 굳이 그렇게 번거로운 방법쓰는 것 보다는 코루틴 함수에 바로 매개변수 여러 개를 사용하는 것이 편할 것이다.

 

yield break

 

IEnumerator ReturnCoroutine()
{
    Debug.Log("Return 1");
    yield return null;  // 코드의 제어권을 잠시 양보했다가 돌려받아서 아래 코드를 계속 진행
    Debug.Log("Return 2");
}

IEnumerator BreakCoroutine()
{
    Debug.Log("Break 1");
    yield break;    // 코루틴 함수를 이 시점에 종료
    Debug.Log("Break 2");
}

 

마지막으로는 yield break 문에 대해서 알아보도록 하자.

 

yield return을 사용할 ReturnCoroutine과 yield break를 사용할 BreakCoroutine을 만든다.

 

이 두 코루틴을 실행하면 ReturnCoroutine은 Return 1과 Return 2가 모두 출력되지만 BreakCoroutine에서는 Break 1만 출력되고 Break 2는 출력되지 않는 것을 볼 수 있다.

 

yield break가 호출된 순간에 코루틴 함수가 완전히 멈춰버린 것이다.

 

yield return은 코드의 제어권을 유니티 엔진에 잠시 넘겼다가 특정 시점이 되면 다시 받아서 코드를 진행하지만 yield break는 그 시점에 코루틴을 완전히 멈춰버린다.

 

코루틴 내부에서 특정 조건을 만족하면 yield break로 코루틴을 멈추는 방식으로 사용할 수 있다.

반응형

Programming 

코루틴(Coroutine) 다루기

 

작성 기준 버전 :: 2019.2

 

[이 포스트는 유튜브 영상을 통해서도 시청하실 수 있습니다]

 

Update 함수는 게임 오브젝트가 활성화된 상태에서 매 프레임 호출되어 수행된다. 그래서 유니티 엔진으로 게임을 만들 때는 대부분의 게임 동작을 Update 함수에서 작동하도록 구현한다.

 

그런데 Update 함수는 멈추지 않고 계속해서 동작하는 함수이기 때문에 여기서 일시적으로 돌아가는 서브 동작을 구현하는 것과 어떤 다른 동작이 처리되는 것을 기다리는 기능을 구현하기는 매우 까다롭다.

 

그리고 Update 함수에서 해당 기능을 구현하기 어렵지 않다고 하더라도, 잠시 돌아가는 기능을 Update 함수에 모두 구현하는 것은 비대한 몸집의 Update 함수를 만들어 내서 나중에 게임을 유지보수하는 것이 매우 어려워지는 결과를 낳게 된다.

 

이렇게 한 컴포넌트 내에서 Update 함수와 따로 일시적으로 돌아가는 서브 동작을 구현하거나, 어떤 다른 작업이 처리되는 것을 기다리는 기능을 구현하는데 쓰이는 것이 바로 코루틴이다.

 

Update로 구현한 공격 딜레이

 

코루틴이 필요할 법한 간단한 예시를 들기 위해서 스페이스 키를 누르면 캐릭터가 공격했다고 가정하고 딜레이를 줘서 그 시간 동안에는 다시 공격을 하지 못하게 만드는 기능을 만들어 보자.

 

public class Attacker : MonoBehaviour

{

    public bool isDelay;

    public float delayTime = 2f;

 

    float timer = 0f;

    void Update()

    {

        if (Input.GetKeyDown(KeyCode.Space))

        {

            if (!isDelay)

            {

                isDelay = true;

                Debug.Log("Attack");

            }

            else

            {

                Debug.Log("Delay");

            }

        }

 

        // 업데이트로 구현한 공격 딜레이

        if (isDelay)

        {

            timer += Time.deltaTime;

            if (timer >= delayTime)

            {

                timer = 0f;

                isDelay = false;

            }

        }

    }

}

 

위와 같이 코드를 작성하고 플레이 해보면, 처음 스페이스 키를 누르면 "Attack" 로그가 나오지만, 딜레이가 지나기 전에 다시 누르면 "Delay" 로그가 표시된다.

 

그리고 로그가 지난 이후에 스페이스 키를 눌러야 "Attack" 로그가 출력된다.

 

간단한 기능이라 구현이 그리 어렵지는 않았지만, 앞에서 이야기한 것과 같이 게임 기능이 계속해서 추가될 때마다 "공격 딜레이가 발생했을 때만" 동작하는 코드 같은 일시적 동작 코드가 Update 함수에 계속해서 늘어나면, Update 함수가 비대화되고 유지보수가 어려워진다.

 

코루틴으로 구현한 공격 딜레이

 

똑같은 기능을 이번에는 코루틴으로 구현해보자.

 

// 코루틴으로 구현한 공격 딜레이

IEnumerator CountAttackDelay()

{

    yield return new WaitForSeconds(delayTime);

    isDelay = false;

}

 

먼저 코루틴을 사용하기 위해서는 코루틴 함수를 만들어야 한다.

 

코루틴 함수를 만드는 방법은 간단하게 반환형만 IEnumerator로 만들어주면 된다.

 

그리고 yield return이란 코드를 작성해주면 된다. 이것은 코루틴에서 동작하는 제어권을 유니티에 다시 돌려준다는 뜻이다. 이 yield return 지점에 도착하면 코루틴은 반환 타입으로 정의한 만큼 코드 동작을 중지하고 제어권을 유니티에 돌려준다. 그리고 반환 타입의 조건이 충족되면 이 다음 줄부터 다시 코루틴이 동작한다.

 

코루틴이 제어권을 얼마나 양보할 지 정하는 반환 타입에는 여러 가지가 있다.

 

// 한 프레임 기다림

yield return null;

// 게임 시간으로 1초 기다림(time scale에 영향받음)

yield return new WaitForSeconds(1f);

// 실제 시간으로 1초 기다림(time scale에 영향받지 않음)

yield return new WaitForSecondsRealtime(1f);

// 다음 FixedUpdate 끝날 때까지 기다림

yield return new WaitForFixedUpdate();

// 다음 프레임의 Update와 모든 렌더링이 끝날 때까지 기다림

yield return new WaitForEndOfFrame();

 

코루틴을 모두 작성하고 나면 Update 함수를 아래와 같이 수정하면 된다.

 

void Update()

{

    if (Input.GetKeyDown(KeyCode.Space))

    {

        if (!isDelay)

        {

            isDelay = true;

            Debug.Log("Attack");

            StartCoroutine(CountAttackDelay());

        }

        else

        {

            Debug.Log("Delay");

        }

    }

 

    // 업데이트로 구현한 공격 딜레이

    //if (isDelay)

    //{

    //    timer += Time.deltaTime;

    //    if (timer >= delayTime)

    //    {

    //        timer = 0f;

    //        isDelay = false;

    //    }

    //}

}

 

코루틴 함수는 실행할 때 일반 함수처럼 호출하는 것이 아니라, StartCoroutine 함수를 이용해서 호출해야 한다.

 

코루틴으로 공격 딜레이를 구현하면 Update 함수에서 전부 구현하는 것보다 훨씬 쉽고 간단하게 똑같은 기능을 구현할 수 있다.

 

코루틴과 관련된 포스트

 

코루틴과 관련하여 추가로 확인할 만한 포스트는 아래와 같다.

 

[코루틴 내부에서 무한 루프를 사용할 때 주의점]

 

[코루틴의 호출 시점에 대한 주의점]

 

[커스텀으로 yield return 조건 만들기]

 

반응형
  1. psh 2021.11.15 02:40

    베르님 감사합니당!

당신을 고통에 빠뜨릴 문제 (1)


코루틴과 Instantiate 그리고 Start


게임을 개발하면서 생겨나는 많은 버그들은 개발자들을 고통에 빠뜨리지만, 그 중에서도 엔진의 특성과 맞물려서 발생하는 버그는 깊이가 다른 고통을 느끼게 해준다. 이런 종류의 버그는 개발자가 생각한 로직으로는 완벽하지만 엔진의 특성으로 인해서 그 로직이 완벽하게 동작하지 않는 것이기 때문에 해당 엔진의 동작과 특성을 확실하게 알고 있지 않으면 어디서 버그가 발생했는지, 원인이 무엇인지 알기 어렵다.


이제부터 이러한 버그들에 대해서 알아보도록 하자. 첫 번째 내용은 코루틴과 Instantiate 그리고 Start에 얽힌 문제에 관한 내용이다.



코루틴(Coroutine)의 특성


코루틴은 유니티로 게임을 개발할 때, 자주 사용되는 기능 중에 하나다. 주로 비동기 처리나, 프로그램을 멈추지 않고 다른 동작을 기다리거나, 메인 로직 뒤에서 돌아가는 서브 루틴을 구현할 때 사용된다.


using System.Collections;
using UnityEngine;

public class B : MonoBehaviour
{
    public void CallCoroutine()
    {
        StartCoroutine(SomeCoroutine());
    }

    private IEnumerator SomeCoroutine()
    {
        Debug.Log("Start Coroutine");
        yield return null;
        Debug.Log("End Coroutine");
    }
}


using UnityEngine;

public class A : MonoBehaviour
{
    public B b;

    void Start()
    {
        b.CallCoroutine();
    }
}


위의 코드를 보면 A라는 클래스가 B라는 클래스의 CallCoroutine() 함수를 호출하여 B 클래스의 SomeCoroutine()을 동작하게 만들고 있다.


 

이 코드는 만약 B 오브젝트가 활성화(enabled)되어 있다면 정상적으로 동작하겠지만, 비활성화(disabled)된 상태라면 위의 이미지처럼 오브젝트가 활성화되지 않은 상태에서 코루틴을 호출했다는 에러 로그가 발생하고 해당 코루틴의 코드가 하나도 실행되지 않는다.


이것은 코루틴을 사용할 때 기본적인 주의 사항으로 해당 오브젝트가 활성화(Enabled)된 상태에서 코루틴을 호출해야 한다는 것을 의미한다.


이런 문제는 에러 로그가 명확하기 때문에 코루틴을 호출하기 전에 게임 오브젝트의 액티브를 확인해주거나 게임 오브젝트가 활성화된 이후에만 코루틴을 호출하도록 수정하면 간단하게 해결된다.





진짜 문제


이전의 예시처럼 이미 생성되어 있는 게임 오브젝트의 코루틴을 호출하는 문제는 활성화 상태만 잘 체크하면 된다. 진짜 문제는 이 코루틴과 Start, Instantiate가 엮였을 때부터 발생한다.


using UnityEngine;

public class A : MonoBehaviour
{
    public GameObject bPrefab;

    void Start()
    {
        var b = Instantiate(bPrefab).GetComponent<B>();
        b.CallCoroutine();

    }
}


이미 생성되어 있는 B 오브젝트를 참조하는 이전의 예시와 다르게 이번에는 B 오브젝트의 프리팹을 소유하고 있다가 A 오브젝트가 Start() 되면 B 오브젝트를 새로 생성해서 B 오브젝트의 코루틴을 호출하는 상황을 가정해보자.


using System.Collections;
using UnityEngine;

public class B : MonoBehaviour
{
    private void Start()
    {
        Debug.Log("Start");
    }

    public void CallCoroutine()
    {
        StartCoroutine(SomeCoroutine());
    }

    private IEnumerator SomeCoroutine()
    {
        // yield return null;
        Debug.Log("Start Coroutine");
      
        Debug.Log("Ing Coroutine");
       
        Debug.Log("End Coroutine");
    }
}


그리고 B 클래스는 Start() 함수가 호출되면 "Start"라는 로그를 띄우고 SomeCoroutine()에서 각 과정에 맞는 로그를 띄울 것이다.


자, 그러면 아래와 같이 여러 상황의 코루틴들을 가정해보자.


private IEnumerator SomeCoroutine()
{
    yield return null;
    Debug.Log("Start Coroutine");
      
    Debug.Log("Ing Coroutine");
       
    Debug.Log("End Coroutine");


}



private IEnumerator SomeCoroutine()
{
   
    Debug.Log("Start Coroutine");
   
yield return null;
    Debug.Log("Ing Coroutine");
       
    Debug.Log("End Coroutine");


}



private IEnumerator SomeCoroutine()
{

    Debug.Log("Start Coroutine");
      
    Debug.Log("Ing Coroutine");
   
yield return null;
    Debug.Log("End Coroutine");


}



private IEnumerator SomeCoroutine()
{

    Debug.Log("Start Coroutine");
      
    Debug.Log("Ing Coroutine");
       
    Debug.Log("End Coroutine");

    yield return null;
}


여러 상황의 코루틴에서 과연 Start() 함수는 같은 시점에 호출될까? 아니다. 유니티에서 게임 오브젝트의 Start() 함수는 게임 오브젝트가 Instantiate()로 생성되고 한 프레임 뒤에 호출되기 때문에 코루틴이 돌다가 만난 첫 번째, yield return 시점에 Start() 함수가 호출되게 된다. 만약 Start() 함수가 별다른 역할을 하지 않는다거나, 호출순서에 상관없는 일을 처리한다면 큰 문제는 없겠지만, 만약 코루틴의 동작과 연관된 중요한 작업을 Start()가 처리하고 있다면, 이 yield return의 위치에 따라서 심각하고도 감지하기 어려운 난감한 문제를 만나게 될 확률이 높아진다.


그리고 이것보다 심각한 문제로, Instantiate() 직후에 코루틴을 호출한 경우, 희박한 확률로 코루틴의 앞 부분 코드는 정상 동작을 하다가 첫 번째 yield return을 만나는 순간, 그 이후의 코드를 실행하지 않는 문제도 발생한다. 게다가 이 경우에는 어떠한 에러나 경고 로그도 발생하지 않기 때문에, 이 문제의 원인이나 해결법을 찾아내기가 아주 어렵다.


using System.Collections;
using UnityEngine;

public class B : MonoBehaviour
{
    private void Start()
    {
        Debug.Log("Start");
        StartCoroutine(SomeCoroutine());
    }

    private IEnumerator SomeCoroutine()
    {
        Debug.Log("Start Coroutine");
        yield return null;
        Debug.Log("Ing Coroutine");
       
        Debug.Log("End Coroutine");
    }
}


그렇기 때문에 만약 Instantiate()와 Start() 그리고 코루틴이 긴밀하게 엮여있거나, Instantiate()로 게임 오브젝트를 생성한 직후에 코루틴을 호출해서 어떤 작업을 처리해야 하는 경우라면, 별도로 코루틴을 호출하는 것보다는, 위의 코드처럼 Start() 함수에서 코루틴을 실행하는 것을 권장한다.

반응형

Programming

-

커스텀 IEnumerator 클래스 만들기(Custon yield instruction)


코루틴(Coroutine)은 유니티에서 아주 많이 사용되는 기능들 중에 하나다. 코루틴의 대표적인 사용법은 게임의 메인 흐름과 다르게 후방에서 무언가가 돌아가야 하는 경우와 무언가가 완료되기를 기다리는 것이다.


이번 섹션에서 이야기할 방법은 주로 후방에서 돌아가는 경우보다는 무언가가 완료되기를 기다리는 경우에 더욱 유용한 것이다. 일반적으로 가장 많이 기다리게 되는 것은 일정한 시간이 지나기를 기다리는 것이다. 그 경우 사용되는 것이 바로 WaitForSeconds 클래스이다. 유니티 개발을 해본 개발자라면 다음과 같은 종류의 코드를 많이 보았을 것이다.


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

public class CoroutineExample : MonoBehaviour
{
    private void Start()
    {
        StartCoroutine(WaitForSecondsCoroutine());
    }

    IEnumerator WaitForSecondsCoroutine()
    {
        yield return new WaitForSeconds(5.0f);
        Debug.Log("5초가 지남!");
    }
}


5초가 지난 이후에 로그 하나의 띄우는 코드이다. 위와 비슷한 방식으로 WWW 클래스를 이용해서 웹 서버에서 무언가가 전송되기를 기다린다던지, 위의 코드처럼 WaitForSeconds 클래스를 이용해서 얼마간의 시간이 지나기를 기다린다던가 할 수 있고, 이 외에도 여러가지의 것을 기다렸다가 이후의 작업을 진행할 수 있다.


유니티에서는 이렇게 원하는 동작을 기다릴 수 있는 IEnumerator 클래스를 제공한다. 하지만 유니티에서 모든 종류의 기능을 제공하는 것은 아니며, 개발자가 새로운 동작을 기다리는 기능을 만들기를 원할 수도 있다.


만약 개발자가 코루틴 내에서 Space 키를 입력하기를 기다려야 된다고 가정해보자. 하지만 기본적으로 키 입력을 기다리는 Custom yield Instruction은 존재하지 않는다. 그렇기 때문에 개발자는 이러한 기능을 직접 구현해야하는데 그 방법은 2가지가 있다.



1. CustomYieldInstruction 상속받기

첫 번째 방법은 CustomYieldInstruction을 상속받는 것이다. 다음은 그 예제이다.


using System.Collections;
using UnityEngine;

public class CustomIEnumeratorType1 : CustomYieldInstruction
{
    public override bool keepWaiting
    {
        get
        {
            return !Input.GetKeyDown(KeyCode.Space);
        }
    }
}


간단하게 새로운 클래스를 구현하고 CustomYieldInstruction을 상속 받은 뒤에 keepWaiting을 오버로딩해주고 getter에서 원하는 동작을 구현하면 된다. 이 예제에서는 Space 키의 입력을 기다릴 것이기 때문에 Input.GetKeyDown(KeyCode.Space)로 입력했다. Yield instruction의 동작은 "계속 기다릴 것인가?"를 묻고 있기 때문에 계속 기다려야 한다면 true를 반환해야하고 더 이상 기다릴 필요가 없다면 false를 반환해야 한다.



2. IEnumerator 상속받기

using System.Collections;
using UnityEngine;

public class CustomIEnumeratorType2 : IEnumerator
{
    public object Current
    {
        get
        {
            Debug.Log("대기하는 동안에 처리할 동작");
            return null;
        }
    }

    public bool MoveNext()
    {
        return !Input.GetKeyDown(KeyCode.Space);
    }

    public void Reset()
    {
    }
}


두 번째 방법은 새로운 클래스를 구현한 뒤에 IEnumerator를 상속받는 것이다. 이 방법은 CustomYieldInstruction을 상속받는 방법보다는 약간 복잡하지만,

좀 더 많은 기능을 사용할 수 있게 된다. Current를 통해서 코루틴이 대기하는 동안에도 다른 동작을 처리할 수 있고 MoveNext를 통해서 CustomYieldInstruction의 keepWaiting처럼 더 기다려야 하는지에 대한 여부를 대기중인 코루틴에 전달할 수 있다. reset 메서드의 경우는 COM의 상호운용성을 위해 제공되며 반드시 구현해야 하지만 반드시 필요하지는 않다.



위의 두 가지 방법을 이용하면 코루틴을 수행하는 도중에 원하는 동작을 기다릴 수 있게 된다. 이 기능의 유용한 한 가지 예를 들자면, 동시에 턴이 진행되고 두 사람이 둘 다 완료 버튼을 눌러야 다음 턴으로 진행되는 게임이 있다면, 턴을 넘기는 코루틴을 커스텀으로 제작된 yield 객체를 이용해서 정지시켜두고 두 사람 모두에게서 신호를 받아야 턴 코루틴이 진행되는 방식으로 만들 수 있다.

반응형

Programming 

Coroutine 내에서의 무한 루프 작성시 주의점


유니티 엔진을 이용해서 게임을 제작하다 보면 코루틴을 자주 사용하게 된다. 몇 초후에 문이 자동으로 열렸다가 닫힌다든지, 화상 데미지로 1초마다 데미지가 얼마씩 들어온다든지 하는 형식으로 말이다.


특정한 경우에는 코루틴을 실행시킨 다음에 안에 무한 루프문을 작동시키는 방식으로 코루틴을 유지하면서 사용하는 방법도 있다.


IEnumerator CoroutineFunction()

{

yield return null;

while(true)

{

Function();

}

}


다만 코루틴 내에서 무한 루프를 처리할 때 주의할 점이 있는데 위의 코드처럼 코루틴을 작성하면 유니티 에디터에서 실행하든 빌드를 해서 실행을 하든 프로그램이 응답없음이 뜨면서 정지를 할 것이다. 이 문제를 해결하는 방법은 매우 간단한데, 바로 while 무한 반복문 안에 yield return을 넣어주는 것이다.


올바른 코루틴 내의 무한 루프 사용법

IEnumerator CoroutineFunction()

{

while(true)

{

yield return null;

Function();

}

}


이 사태의 원인은 코루틴이 돌리는 무한 반복문이 시스템에 독점적으로 돌아가는 상태가 되었기 때문이다. 그렇기 때문에 이를 해결하기 위해서 반복문안에 반드시 yield return을 이용해서 시스템을 다른 코드에 양보해주는 것이 필수이다.

반응형

+ Recent posts