
C++이나 C#을 보면 실행했을때 동작하는 Main()이라는 메인 함수가 있습니다.
다른 곳에 함수를 만들어봤자 그 함수는 작동 되지 않고 이 Main()이라는 함수에 넣어 줘야 작동됩니다.
즉, Main함수는 메인으로 작동하는 메인 루틴이고
그 안에서 작동하는 하나하나의 함수들은 서브 루틴입니다.
서브 루틴은 코드가 끝까지 실행 되거나 return을 만나면
호출됐던 곳으로 돌아가 메인 루틴안의 다른 코드가 실행 됩니다.
Update 함수는 게임 오브젝트가 활성화된 상태에서
매 프레임 호출되어 수행된다고 이야기했었습니다.
그래서 대부분의 게임 동작을 Update 함수에서 작동하도록 구현합니다.
그런데 Update 함수는 멈추지 않고 계속해서 동작하는 함수이기 때문에
여기서 일시적으로 돌아가는 서브 동작을 구현하는 것과
어떤 다른 동작이 처리되는 것을 기다리는 기능을 구현하기는 매우 까다롭습니다.
그리고 Update 함수에서 해당 기능을 구현하기 어렵지 않다고 하더라도,
잠시 돌아가는 기능을 Update 함수에 모두 구현하는 것은
비대한 몸집의 Update 함수를 만들어내서
나중에 게임을 유지보수하는 것이 매우 어려워지는 결과를 낳게 됩니다.
이렇게 한 컴포넌트 내에서 Update 함수와 따로 일시적으로 돌아가는 서브 루틴을 구현하거나,
어떤 다른 작업이 처리되는 것을 기다리는 기능을 구현하는데 쓰이는 것이 바로 코루틴입니다.
참고로, 서브 루틴은 메인 루틴에 종속된 상태로 호출 받는 입장이지만
코루틴은 호출한 메인 루틴과 함께 실행되는 구조입니다.
따라서, 코루틴과 메인 루틴은 대등한 관계라고 할 수 있습니다.
코루틴은 아래와 같은 형식으로 정의할 수 있습니다.
접근제어자 IEnumerator TestCo()
{
//...
//코루틴 시작시 해야 할 일
yield return (명령어);
}
코루틴은 함수의 반환 타입으로 반드시 열거형인 IEumerator를 사용해야하고,
return 대신에 yield return을 무조건 적어 줘야 합니다.
yield는 양보라는 뜻인데,
코루틴안의 코드들이 실행되다가 yield return을 만나면
일시정지하고 유니티에게 제어권을 넘겨줍니다.
그리고 대기 조건만큼 기다린 후
다시 일시정지 한 곳 부터 실행됩니다.
이때 yield return 다음에 쓰는 대기 조건의 종류에 따라 할 수 있는 일이 다릅니다.
이번 시간에는
yield return null;
yield return new WaitForSeconds();
두가지만 알아보고 나머지 대기조건은 다음시간에 알아보도록 하겠습니다.

위에있는 대기 조건 말고도 WaitForSecondsRealTime가 있습니다.
이것과 WaitForSeconds의 차이점은 Time scale의 영향을 받는가 입니다.
유니티 엔진에는 타임 스케일이라는게 있는데 이것은 시간의 길이를 조절합니다.
만약, 타임 스케일을 0.5로 한다면 게임 속 시간의 흐름이 절반으로 느려져서
new WaitForSeconds(1f)을 하면 2초를 기다리지만,
new WaitForSecondsRealTime(1f)은 이 타임 스케일의 영향을 받지 않고
실제 시간에 따라 1초만 기다리는 것입니다.
코루틴은 실행하고 싶은 위치에서 StartCoroutine() 함수에
코루틴를 담고 한번만 실행시켜주면 동작합니다.
void Start()
{
StartCoroutine(TestCo());
}
IEnumerator TestCo()
{
//...
//코루틴 시작시 해야 할 일
yield return null;
}
StartCoroutine은 오브젝트가 활성화된 이후에 실행하여야합니다.
그러므로 Awake()에서 실행하면 안됩니다.
코루틴 스크립트의 오브젝트가 비활성화 되거나 파괴되면 코루틴은 중단됩니다.
코루틴을 중단하고 싶은 위치에서 StopCoroutine(), StopAllCoroutine()을
실행하면 코루틴을 중단시킬 수 있습니다.
StopCoroutine()은 하나의 코루틴을 중단시킬 수 있고,
StopAllCoroutine()은 컴포넌트가 사용하는 모든 코루틴을 중단합니다.

여기서 주의할 점은 StopCoroutine 함수의 인수에는 IEnumerator가 들어가야합니다.
문제는 코루틴 함수가 반환하는 IEnumerator는 실행마다 달라집니다.
따라서 코루틴 함수가 반환하는 IEnumerator를 변수에 담아두고
이것을 사용하여 코루틴을 실행,중단합니다.
IEnumerator enumerator; // IEnumerator 변수 선언
void Start()
{
enumerator = TestCo(); // 사용할 코루틴의 반환값(IEnumerator)을 담아둔다.
StartCoroutine(enumerator); // 실행
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
StopCoroutine(enumerator); // 종료
}
}
IEnumerator TestCo()
{
//...
//코루틴 시작시 해야 할 일
yield return null;
}
IEnumerator enumerator;
void Start()
{
enumerator = TestCo(1.0f,5); // 인수 삽입
StartCoroutine(enumerator);
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
StopCoroutine(enumerator);
}
}
IEnumerator TestCo(float time, int count) // 매개변수 정의
{
int i = 0;
while (i < count)
{
yield return new WaitForSeconds(time);
Debug.Log(i);
i++;
}
}
yield break문을 이용하면 특정 조건을 만족시
코루틴을 중단하도록 만들 수 있습니다.
IEnumerator enumerator;
bool isStop = false; //yield break 조건을 위한 boolean
void Start()
{
enumerator = TestCo(1.0f,5);
StartCoroutine(enumerator);
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
isStop = true; // Space바를 누르면 true로 바뀝니다.
}
}
IEnumerator TestCo(float time, int count)
{
int i = 0;
while (i < count)
{
if (isStop) { yield break; } // yield break가 사용되면 즉시 코루틴이 중단됩니다.
yield return new WaitForSeconds(time);
Debug.Log(i);
i++;
}
}
일시적으로 돌아가는 서브루틴이란,
업데이트 동작 후에 코루틴이 동작하여
마치 업데이트와 코루틴을 동시에 사용하고있는 듯한 효과를 줍니다.
yeild return null을 사용하여 코드를 작성합니다.
그리고 타이머로 원하는 시간동안 코루틴을 사용한다면
일시적으로 돌아가는 서브 루틴을 구현할 수 있습니다.
IEnumerator MyCo()
{
float value05 = 0f;
while (true)
{
value05 += Time.deltaTime;
if(value05 >= 5f) //실행후 5초가 된다면 braek
break;
//...
//5초 동안 실행 할 코드
yield return null;
}
}

로그를 찍어보면 업데이트가 실행되는 동안 코루틴도 동일한 횟수가 실행되는 것을 알수 있습니다.
IEnumerator TestCo()
{
//...
//코루틴 시작시 해야 할 일
yield return new WaitForSeconds(1); // Update에게 제어권 넘기고 1초 일시정지
//...
//1초 뒤 제어권을 다시 받은 후 해야 할 일
}
대기 조건 WaitForSeconds를 사용하면 인자에 넣어준 시간만큼 대기후
다시 코루틴을 실행할 수 있습니다.
(이외에 다양한 대기 조건이 있지만 다음 시간에 알아보도록 하겠습니다.)
IEnumerator enumerator;
WaitForSeconds wait_1Sec = new WaitForSeconds(1f);
void Start()
{
enumerator = TestCo(5);
StartCoroutine(enumerator);
}
IEnumerator TestCo(int count)
{
int i = 0;
while (i < count)
{
yield return wait_1Sec;
Debug.Log(i);
i++;
}
}
루프를 사용하면 특정 시간마다 한번씩(여기서는 1초마다 한번씩) 실행하는 방향으로 사용할 수 있습니다.
참고로 루프안에서 new 연사자를 매번 생성하는것은
가비지 컬렉터가 좋아하지 않는 코드입니다.
때문에 WaitForSeconds wait_1Sec = new WaitForSeconds(1f);를
미리 만들어 놓고 루프안에서 재활용하여 사용하는 편이 좋습니다.
또한 WaitForSeconds waitSec = new WaitForSeconds(1f);를
더욱 활용적이게 사용하고 싶다면
유틸 클래스를 만들어두고 정적으로 변수를 초기화 해둡니다.
(readonly는 '런타임 상수'로 런타임중에 상수로 활용할 수 있게 해줍니다.)
public class Utils
{
public static readonly WaitForSeconds wait_05Sec = new WaitForSeconds(0.5f);
public static readonly WaitForSeconds wait_1Sec = new WaitForSeconds(1f);
public static readonly WaitForSeconds wait_3Sec = new WaitForSeconds(3f);
}
그리고 아래와 같이 필요할때 유틸 클래스를 활용한다면 편하게 사용할 수 있습니다.
yield return Utils.wait_1Sec;
유니티는 .NET(C#)을 사용합니다.
.NET(C#)은 멀티쓰레드를 지원하지만 멀티쓰레드를 가지는 코드를 작성하면
멀티쓰레드간 교착 상태 경합 등, 신경써야 할 부분이 많습니다.
때문에 유니티는 단일 쓰레드로 동작을 합니다.
유니티에서 코루틴을 사용함으로써 멀티 쓰레드를 흉내낼 수 있습니다.
어떻게 이것이 가능 할 수 있을까요?
코루틴 함수가 IEnumerator를 반환하는데 힌트를 얻을 수 있습니다.
C#의 yield 키워드는 호출자(Caller)에게 컬렉션 데이터를 하나씩 리턴할 때 사용합니다.
흔히 Enumerator(Iterator)라고 불리우는 이러한 기능은 집합적인 데이터셋으로부터 데이터를 하나씩 호출자에게 보내주는 역할을 합니다.
yield는 yield return 또는 yield break의 2가지 방식으로 사용되는데,
메소드가 다시 호출되면, 일시정지된 실행을 복구하여 yiled return 또는 yeild break문을 만날때까지 나머지 작업을 실행하게 됩니다.
그래서 여러개의 코루틴을 동작시키고
각기다른 시점에 yield가 반환되도록 한다면
마치 여러개의 쓰래드가 동시에 동작하는것과 같은 효과가 납니다.
유니티의 Update 함수는 1프레임에 한 번 호출 됩니다.
매 프레임마다 yield return 할 코루틴이 있는지 체크하는데
코루틴의 yield 종류마다 그 시점이 다릅니다.
대부분은 Update 에서 체크됩니다.

