안녕하세요.
지난 3번의 포스팅에 걸쳐 유니티의 코루틴을 이해하기 위한 사전적인 지식들을 살펴봤습니다.
혹시나 처음 이 글을 읽으시는 분들은 처음부터 읽고 돌아오시는 것을 추천드립니다.
2024.05.03 - [유니티 기초 스킬] - 코루틴(Coroutine)과 IEnumerator을 이해하기 위해(1) - Interface
코루틴(Coroutine)과 IEnumerator을 이해하기 위해(1) - Interface
안녕하세요. 유니티에서 게임을 개발할 때 많이 사용하는 함수 중 코루틴이 있습니다. 그리고 코루틴 함수를 사용할 때는 항상 IEnumerator를 사용해야한다고만 배웠죠. 그런데 C#문법을 공부하
developer-coffee.tistory.com
2024.05.03 - [유니티 기초 스킬] - 코루틴(Coroutine)과 IEnumerator을 이해하기 위해(2) - IEnumerator와 IEnumerable
코루틴(Coroutine)과 IEnumerator을 이해하기 위해(2) - IEnumerator와 IEnumerable
안녕하세요. 지난번 포스팅에서는 IEnumerator와 IEnumerable를 이해하기 위해서 먼저 인터페이스에 대해서 살펴보았습니다. 혹시 아직 보시지 못하신 분이 있으시다면 이 전 포스팅을 읽고 오시는
developer-coffee.tistory.com
2024.05.09 - [분류 전체보기] - 코루틴(Coroutine)과 IEnumerator을 이해하기 위해(3) - IEnumerator와 yield return
코루틴(Coroutine)과 IEnumerator을 이해하기 위해(3) - IEnumerator와 yield return
안녕하세요. 지난 두 번의 포스팅으로 인터페이스(Interface)와 IEnumerator 그리고 IEnumerable에 대해 살펴보았습니다. 코루틴을 이해하는데 인터페이스 부터 시작하는건 좀 너무한것 아니냐는 분도
developer-coffee.tistory.com
자, 그럼 앞의 3번의 포스팅에서 다룬 내용들을 이해하고 있다는 전제하에 드디어 유니티엔진에서 제공하는 코루틴 함수에 대해서 살펴보도록 하겠습니다.
코루틴 (Corutine)
코루틴은 보통 하나의 함수 안에 포함 되어 있는 작업을 다수의 프레임 안에 분산하기 위해 사용도록 고안된 함수입니다.
일반적으로 하나의 함수 안에 포함된 모든 동작들은 하나의 프레임안에서 작동을 합니다. 그래서 하나의 함수안에 어떤 요소가 점진적으로 변화하는 과정을 모두 표현하고 싶을때는 하나의 함수안에서 표현하는 것이 어렵습니다.
예를 하나 들어보겠습니다.
게임을 만들때 우리가 지속데미지를 3초동안 1초에 10씩 주는 함수를 만들고 싶다고 해보겠습니다.
void ContinuousDamage()
{
for(int i=0; i<3; i++)
{
hp -= 10;
}
}
단순하게 이런 식으로 하면 될까요?
당연히 안됩니다. ContinuousDamage()라는 하나의 함수는 하나의 프레임 안에서 실행되기 때문에 우리가 눈으로 볼 수 있는 점차적인 감소는 보이지 않고, 그저 눈깜짝할 사이에 HP가 30이 빠져있는 것을 볼 수 있을 뿐입니다.
어떤 작업을 여러 프레임에 걸쳐서 늘려서 실행할 수 있게 고안된 함수가 바로 코루틴(Coruitne) 함수입니다.
코루틴 함수로 위의 예제의 함수를 만들면 이렇게 할 수 있습니다.
private void Start()
{
StartCoroutine(ContinuousDamage());
}
IEnumerator ContinuousDamage()
{
int time = 0;
while(time <3)
{
time++;
hp -= 10;
yield return new WaitForSeconds(1f);
}
}
일단 핵심적인 구현 함수는 IEnumerator 타입의 함수로 만들어야 합니다. 그리고 그 함수를 StartCoroutine()로 호출 할 수 있습니다.
그러면 1초마다 한번씩 10초동안 hp에서 -10을 연산하는 함수를 구현했습니다.
코루틴의 사용방식은 다양할 수 있지만 저는 기본적인 부분만 언급하고 넘어가도록 하겠습니다. 이 포스팅의 의도는 코루틴을 사용하는 법을 익히는데 있지 않고 코루틴을 통해 이전의 포스팅에서 살펴본 개념들을 적용할 수 있게 되는 것이기 때문입니다.
이 부분에서 우리가 함께 자세히 들여다 보고 싶은 것은 IEnumerator와 yield return 그리고 코루틴의 작동 방식입니다.
지난 포스팅에서 우리는 IEnumerator와 yield return가 어떻게 작동하는지 그 원리를 함께 살펴봤습니다.
IEnumerator는 어떤 인덱스적인 요소가 있는 대상 혹은 객체가 있다면 그 인덱스적 요소에 순차적으로 접근하고 그 접근한 요소를 반환해주는 기능을 하는 인터페이스입니다.
그리고 yield return은 함수 안에 존재하는 내용을 인덱스 적인 부분으로 나눠주는 역할을 하는 키워입니다.
그런 의미를 생각하면서 위의 예제 부분을 다시 살펴보겠습니다.
while이 사용되어서 그렇지 사실 for문을 사용해도 무방하고 3초밖에 안되니 풀어써도 무방합니다. 이해를 돕기 위해서 반복문의 사용없이 함수를 풀어 써보겠습니다.
IEnumerator ContinuousDamage()
{
hp -= 10; // 1번 부분
yield return new WaitForSeconds(1f);
hp -= 10; // 2번 부분
yield return new WaitForSeconds(1f);
hp -= 10; // 3번부분
yield return new WaitForSeconds(1f);
}
IEnumerator 타입의 함수에 yield return이 사용되는 순간 함수는 기준으로 3개의 부분으로 나뉘게 됩니다.
요소가 3게인 인덱스적인 대상으로 바뀌었다고 표현할 수 있게 됩니다.
이 함수의 반환타입인 IEnumerator의 변수에 넣는다면 IEnumerator의 기능에 구현된 Move.Next()와 Current를 이용해서 이 함수의 각 요소에 순차적으로 접근할 수 있게 됩니다.
이 경우 순차적인 요소란 함수의 각 부분에 실행되어야 하는 코드들입니다.
IEnumerator의 기능으로 1번부분의 코드와 2번 부분의 코드, 3번 부분의 코드를 반환 받을 수 있다는 겁니다.
자 그럼 이 IEnumerator함수를 호출하는 Startcorutine() 함수는 어떤 의미의 함수일까요?
제가 편의상 IEnumerator함수를 호출하는 함수라고 했지만 사실 형식으로 보면 IEnumerator함수를 호출하는게 아니라 매개변수로 사용하는 겁니다.
그래서 StartCoroutine(ContinuousDamage()); 이런 형식으로 사용하게 되는 겁니다.
StartCoroutine 함수는 유니티에서 만든 비공개 함수임으로 그 내용을 정확하게 알 수는 없지만 대략 IEnumerator와 yield return을 이해한다면 어느정도 유추가 가능합니다.
아마 StartCoroutine 함수는 이런 형식이 아닐까 싶습니다.
void StartCoroutine(IEnumerator enumerator)
{
while (enumerator.MoveNext())
{
WaitTime(enumerator.Currnet)
}
}
void WaitTime(object waittime)
{
// waitForSeconds 객체에서 받아온 float 값의 시간 만큼
// 메인 스레드의 다른 코드를 먼저 실행시키는 함수
}
당연히 유니티에서 제공하는 StartCorutine 함수는 이런 로직보다 훨씬 복잡하겠지만 작동원리를 이해하기 위해서 아주 간략하게 만들어봤습니다.
우리가 유니티 스크립트에서 StartCorutine() 함수를 호출하면 아마도 위에 있는 예제와 비슷하게 생긴 함수를 호출하는 것입니다. 그리고 StartCorutine는 IEnumerator타입의 객체를 매개변수로 받기 때문에 우리가 코루틴을 사용할때 만드는 IEnumerator 함수의 반환 값이 매개 변수에 할당해서 StartCorutine( IEnumerator 반환값) 이 호출되게 되는 겁니다.
그럼 StartCorutine( IEnumerator 반환값)이 호출될때 어떤 일이 일어나는지 한번 자세히 살펴보겠습니다.
void StartCoroutine(IEnumerator enumerator)
{
while (enumerator.MoveNext())
{
WaitTime(enumerator.Currnet)
}
}
StartCorutine 함수에서 매개변수 enumerator에 IEnumerator 타입의 객체를 받으면 이제 enumerator.MoveNext()가 true를 반환할 동안은 반복문을 실행하게 됩니다.
enumerator.MoveNext()가 true를 반환한다는 것은 어떤 의미일까요?
enumerator.MoveNext()는 bool값을 반환하는데 IEnumerator에 할당된 객체 혹은 대상에게 현재요소에서 다음으로 넘어갈 요소가 있다면 true를 만일 요소가 없다면 false를 반환합니다. 다시 말해서 마지막요소까지 가기 전까지 true를 반환하고 다음요소로 가지 못한다면 false를 반환합니다.
그러니 enumerator.MoveNext() true를 반환한다면 다시 말해서 IEnumerator 객체에 다음요소가 존재한다면 while문 안에 있는 로직을 실행하라는 의미입니다.
그럼 while문 안에는 어떤 내용이 있을까요?
WaitTime(enumerator.Currnet)
WaitTime은 제가 임의로 만들어낸 함수입니다. 내용은 아마 너무 복잡한 내용일것 같아서 간단히 주석으로 대략적인 기능만 설명해 놓았습니다.
void WaitTime(object waittime)
{
// waitForSeconds 객체에서 받아온 float 값의 시간 만큼
// 메인 스레드의 다른 코드를 먼저 실행시키는 함수
}
여기서 중요한 것은 매개변수입니다.
WaitTime(enumerator.Currnet) 을 본다면 이 함수가 매개변수로 전달하고 있는 것은 enumerator 객체가 가지고 있는 현재 반환 값입니다.
그런데 코루틴에서는 어떤 값이 반환될까요?
yield return new WaitForSeconds(1f);
네, yield return 뒤에 오는 new WaitForSeconds(1f) 값이 반환됩니다.
따라서 TimeAttack (enumerator.Currnet)은 TimeAttack(new WaitForSeconds(1f)) 이 됩니다.
그럼 new WaitForSeconds(1f)이 의미하는 바는 뭘까요?
WaitForSeconds는 사실 유니티에서 만든 클래스 이름입니다.
그러니까 WaitForSeconds() 라고 하면 WaitForSeconds 클래스의 생성자가 되는 겁니다.
WaitForSeconds생성자는 매개변수로 float값을 받습니다. 그것을 시간이라고 치고 계산을 하겠다는 겁니다.
그럼 왜 앞에 new가 오는지 이해가시죠?
바로 WaitForSeconds클래스의 새로운 객체를 생성하는 겁니다.
그러니까 일반적으로 코루틴을 사용할때 new WaitForSeconds()를 사용하면 계속해서 새로운 객체가 생성되는 겁니다. 만일 이 객체를 파괴하지 않고 새로운 코루틴을 계속해서 호출한다면 사용하지 않는 객체가 계속 쌓여서 성능에 악영향을 끼치게 되는 겁니다.
그래서 보통 코루틴을 사용할때 미리 전역변수에 new WaitForSeconds() 을 할당해서 사용하라고 하는 겁니다.
아무튼 이 WaitForSeconds 클래스가 어떤 기능을 가지고 있는지 정확히는 알지 못하지만 우리는 코루틴을 사용하는 법을 알고 있으므로 어느정도 유추가 가능합니다.
아마 매개 변수로 받는 float값을 시간으로 생각하고 그만큼의 시간을 계산해주는 역할을 하지 않을까 싶습니다.
마치 우리가 Update() 함수에서 Time.deltaTime을 통해 시간 계산 식을 만드는 것 처럼 말입니다.
그리고 이제 임의로 만든 TimeAttack함수의 매개변수로 사용해 WaitForSeconds 이 계산한 시간 동안에 동일한 스레드의 다른 코드들을 실행하는 로직이 구현되어 있을 겁니다.
아마 우리가 잘 알지는 못해도 유니티는 내부적으로 무슨 코드를 먼저 실행하지에 대한 일련의 원칙들을 정해놨을 것 같습니다. 그리고 WaitForSeconds이 반환해주는 시간 값 만큼 후순위의 다른 코드들을 먼저 실행시키는 식으로 아마 내부적인 처리를 했을 것으로 예상할 수 있습니다.
물론 이건 어디까지나 제 예상입니다. 실제로 어떤 식으로 구성되어있는지는 공개되어 있지 않고 코루틴이 하나만 동작되는 것도 아니기때문에 훨씬 더 여러개의 IEnumerator 객체를 다루는 시스템이 구현되어 있을 것입니다.
예제로 다시 돌아오겠습니다.
void StartCoroutine(IEnumerator enumerator)
{
while (enumerator.MoveNext())
{
TimemAttack(enumerator.Currnet)
}
}
위의 코드를 보시면 enumerator의 다음 요소가 없을때까지 실행하는 것은 enumerator.Currnet으로 인해 받은 시간 만큼 코드 실행을 유니티 엔진이 가지고 있는 우선순위를 가지고 있는 다른 코드들을 먼저 실행시키는 과정을 거치는 겁니다.
그리고 그 시간이 지나면 다시 enumerator.MoveNext()를 실행해서 yield return new WaitForSeconds(1f) 의 다음 코드들을 실행하고 다시 유니티 엔진에게 다른 정해진 시간 만큼 다른 코드들을 먼저 실행시켜달라고 요청하는 겁니다.
이런 식으로 더 이상 반환 받을 yield return new WaitForSeconds(1f) 가 없을때 까지 작동하게 되는 겁니다.
굳이 좀 더 예상해보자면 StopCoroutine()함수는 아마 코루틴 함수로 받은 IEnumerator 객체를 관리하는 리스트에서 해당 IEnumerator 객체를 제거하는 기능을 하지 않을까 싶습니다.
아무튼 여기까지 이해하셨다면 대략적으로 코루틴과 IEnumerator 그리고 yield return에 대해서 이해하신 겁니다.
유니티에서 제공하고 있는 코루틴 사용시 사용할 수 있는 클래스들이 있습니다.
- WaitForSeconds: 지정된 시간(초) 동안 대기합니다.
- WaitForFixedUpdate: 고정된 물리 프레임마다 대기합니다.
- WaitForEndOfFrame: 현재 프레임의 렌더링이 완료될 때까지 대기합니다.
- WaitUntil: 조건이 참이 될 때까지 대기합니다.
- WaitWhile: 조건이 거짓인 동안 대기합니다.
이런 클래스들은 모두 유니티에서 코루틴을 사용할 시 yield return 뒤에 붙여 사용하도록 만든 클래스들이고 이들은 클래스들이기 떄문에 이들을 사용할때 최소 한번은 객체를 생성하는 New 키워드를 사용해야하는 겁니다.
지금까지 꽤 긴 분량으로 유니티의 코루틴에 대해서 함께 살펴봤습니다.
계속 언급했지만 이번 포스팅의 목적은 코루틴의 실행 방법을 익히는 것이 아니라 코루틴이라는 기능을 공부하면서 C#의 문법들을 좀 더 이해하고 응용할 수 있는 방식을 살펴보는 것이었습니다.
C#문법을 공부할때 따로 따로 공부했던 개념들을 코루틴이라는 하나의 주제로 모아서 연관지어서 이해하면 좀 더 실전적인 이해와 응용력을 기르는데 도움이 되지 않을까 싶어서 정리해봤습니다.
제가 부족한 점이 많아서 모호한 표현이나 잘못된 이야기가 많이 있을지 모릅니다.
그점은 너그러이 봐주시기 바합니다.
혹시나 이 글들을 끝까지 읽어주셨다면 정말 감사드립니다.
'C#과 유니티의 이해' 카테고리의 다른 글
| 코루틴(Coroutine)과 IEnumerator을 이해하기 위해(3) - IEnumerator와 yield return (0) | 2024.05.09 |
|---|---|
| 코루틴(Coroutine)과 IEnumerator을 이해하기 위해(2) - IEnumerator와 IEnumerable (0) | 2024.05.03 |
| 코루틴(Coroutine)과 IEnumerator을 이해하기 위해(1) - Interface (0) | 2024.05.03 |