선행지식
동기 vs 비동기
(I)Enumerator/(I)Enumerable
Main 루틴
- 프로그램의 주 실행 흐름으로 순차적으로 명령을 수행
- 프로세스가 종료될 때까지 실행
Sub 루틴
- 메인 루틴에서 호출되는 메서드
- 특정 작업(함수) 수행 후, 함수 종료 시 메인 루틴으로 복귀
코루틴 : Coroutine
메인 루틴과 독립적으로 여러 프레임에 걸쳐 실행되는 별도 실행 흐름
비동기처럼(실제론 싱글스레드 동기) 작업이 수행되도록 여러 프레임에 분산해주는 특별한 메소드
yield return을 통해 작업을 일시 중단(제어권 양보)한 뒤 다른 프레임/시간에 제어권을 넘겨받아 재개(중단된 곳부터)하는 방식
중지 / 재개 / 반복 제어
- [★]
게임오브젝트 비활성화/파괴되면 코루틴 종료
IEnumerator 반환
public class Test : MonoBehaviour
{
void Start()
{
Func();
}
void Func()
{
...
}
IEnumerator Co(){
yield return null;
}
}
코루틴 작동 원리[심화]
- 코루틴은
IEnumerator를 반환
- 왜 yield return ...이 IEnumerator를 반환하는가?
- yield return 문이 선언된 매서드는
C# 컴파일러에 의해 자동으로 상태 머신으로 변환됨
상태 머신이 IEnumerator 인터페이스를 상속하는 Enumerator 클래스를 생성
- Object Current property
- MoveNext();
- Reset()
- Dispose()
- 위 사항 모두 자동 구현
- 때문에, 자동적으로 Enumerator를 얻어와 실행히키는 것
- StartCoroutine() 같은 경우 내부적으로 while(enumerator.MoveNext()) { ... }를 돌림
- 이를 통해 코루틴이 수행됨
코루틴 사용법
1. MonoBehaviour 상속
public class Test : MonoBehaviour { ... }
2. 선언 : IEnumerator
코루틴은 IEnumerator 타입을 반환하는 함수
IEnumerator FadeIn(){
while(alpha < 1.0f)
{
alpha += Time.deltaTime / fadeTime;
image.color = new Color(1,1,1,alpha);
yield return null;
}
}
3. 호출 : StartCoroutine
- StartCoroutine(IEnumerator)을 통해 호출
StartCoroutine("FadeIn");
StartCoroutine(FadeIn());
Coroutine co = StartCoroutine(FadeIn());
StartCoroutine(co);
4. yield return (조건) 사용
제어권을 넘긴다(양보=yield)
조건 달성 시, 재개
yield return을 만나는 순간, 프레임 변경가 발생
IEnumerator Count()
{
Debug.Log("1");
yield return new WaitForSeconds(1);
Debug.Log("1");
yield return new WaitForSeconds(1);
gameObject.SetActive(false);
Debug.Log("1");
yield return new WaitForSeconds(1);
}
5. 코루틴 종료 방법 : StopCoroutine
StopCoroutine("FadeIn");
StopCoroutine(FadeIn());
Coroutine co = StartCoroutine(FadeIn());
StopCoroutine(co);
StopAllCoroutine: 해당 스크립트의 모든 코루틴 종료
StopAllCoroutine();
Yield
Yield: 주도권을 다른 프레임에 양보하는 것
YieldInstruction 클래스
1. yield return null;
Update의 한 프레임(=Time.deltaTime)동안 대기 후 실행
IEnumerator CoroutineTest(){
yield return new WaitForSeconds(time);
while (time < 1.0f)
{
yield return null;
time += Time.deltaTime;
}
}
2. yield return new WaitForFixedUpdate();
FixedUpdate의 한 프레임(=Time.fixedDeltaTime)동안 대기 후 실행
IEnumerator CoroutineTest(){
...
yield return new WaitForFixedUpdate();
}
3. yield return new WaitForEndOfFrame();
Unity Event 순서 상 렌더링이 마무리되는 거의 마지막 시점에 호출
모든 렌더링 작업이 완료되는 프레임이 끝날때까지 대기 후 실행
IEnumerator CoroutineTest(){
...
yield return new WaitForEndOfFrame();
}
4. yield return new WaitForSeconds(time);
IEnumerator CoroutineTest(){
...
yield return new WaitForSeconds(time);
}
IEnumerator CoroutineTest(){
...
while (time < 1.0f)
{
yield return null;
time += Time.deltaTime;
}
}
5. yield return new WaitForSecondsRealtime(time);
TimeScale에 영향받지 않음
float time만큼 시간이 지난 후 첫 프레임까지 대기 후 실행
IEnumerator CoroutineTest(){
...
yield return new WaitForSecondsRealtime(time);
}
6. yield return new WaitUntil(Func*);
IEnumerator CoroutineTest(){
...
yield return new WaitUntil(() => time > 10);
}
7. yield return new WaitWhile(Func*);
조건이 참이라면 대기(거짓일 때까지 대기 후 실행)
IEnumerator CoroutineTest(){
...
yield return new WaitWhile(() => time > 10);
}
코루틴 체인[★]
StartCoroutine()은 Coroutine 형식을 반환
- 이를 이용해 코루틴 내에 코루틴을
연쇄적으로 호출 가능
간결하면서도 유동적인 절차적 움직임 구현에 용이
IEnumerator Coroutine1()
{
yield return StartCoroutine(Coroutine2());
yield return StartCoroutine(Coroutine3());
print("모든 코루틴 끝남");
}
IEnumerator Coroutine2()
{
while(true){
time2 += Time.deltaTime;
if(time2 >= 2){
time2 = 2f;
break;
}
yield return null;
}
}
IEnumerator Coroutine3()
{
yield return new WaitForSeconds(3f);
}
캐싱 : 코루틴
코루틴 수행 시마다 Garbage가 생성된다.
yield return new를 수행시마다 Garbage 생성
성능 저하 문제
특정 코루틴을 반복 사용할 필요가 있으면, Update로 대체하는 경우도 존재
자주 사용하는 YieldInstruction은 캐싱을 통해 재활용해야 함
public class CoroutineTest : MonoBehaviour
{
WaitForFixedUpdate wf = new WaitForFixedUpdate();
IEnumerator Start()
{
...
yield return wf;
}
IEnumerator OnCollisionEnter()
{
yield return wf;
}
IEnumerator OnMouseEnter()
{
yield return wf;
}
private IEnumerator OnBecameInvisible()
{
yield return wf;
}
IEnumerator CoTest()
{
yield return wf;
}
Util.cs 캐싱
- Util.cs
Util.cs 파일엔 매우 효율적이거나 전역적으로 사용하는 공용 함수를 작성해, 효율적인 작업을 위한 개발자들의 협업 공간으로 사용한다.
- 이런 번거로운 과정을 해결할 때 사용한다.[★]
Util.cs 클래스에 캐싱을 전역적으로 만들고 생성 및 사용
- WaitForSeconds를 캐싱하는 방법은 번거롭다.
WaitForSeconds wf0.1 = new WaitForSeconds(0.1f);
WaitForSeconds wf0.2 = new WaitForSeconds(0.2f);
WaitForSeconds wf0.3 = new WaitForSeconds(0.3f);
WaitForSeconds wf0.4 = new WaitForSeconds(0.4f);
WaitForSeconds wf0.5 = new WaitForSeconds(0.5f);
...
- 이 번거로운 과정을 극복하는 방법
Util.cs에 static readonly Dictionary<시간,WaitForSeconds객체> 자료구조를 생성
- 해당 자료구조에 접근하는
전역 함수 WaitForSecond생성
원하는 시간의 WaitForSecond 객체가 존재하면 반환
원하는 시간의 WaitForSecond 객체가 없다면 생성/저장/반환
using System.Collections.Generic;
using UnityEngine;
public class Util : MonoBehaviour
{
public static readonly WaitForFixedUpdate m_WaitForFixedUpdate = new WaitForFixedUpdate();
public static readonly WaitForEndOfFrame m_WaitForEndOfFrame = new WaitForEndOfFrame();
private static readonly Dictionary<float, WaitForSeconds> m_WaitForSecondsDict = new Dictionary<float, WaitForSeconds>();
public static WaitForSeconds WaitForSecond(float waitTime)
{
WaitForSeconds wfs;
if (m_WaitForSecondsDict.TryGetValue(waitTime, out wfs)) return wfs;
else
{
wfs = new WaitForSeconds(waitTime);
m_WaitForSecondsDict.Add(waitTime, wfs);
return wfs;
}
}
}
Invoke
매개변수 없는 간단한 지연을 구현할 땐, Coroutine 말고 Invoke를 사용할 수도 있다.
[★]코루틴과 달리 게임 오브젝트가 비활성화 되어도 수행됨
CancelInvoke를 매우 적절히 사용해야 함
Invoke
public class Test : MonoBehaviour
{
void Start()
{
Invoke(nameof(Delay), 0.5f);
}
void Delay()
{
print("출력");
}
}
public class Test : MonoBehaviour
{
void Start()
{
InvokeRepeating(nameof(Delay), 0.5f, 1f);
}
void Delay()
{
print("출력");
}
}
public class Test : MonoBehaviour
{
void Start()
{
CancelInvoke(nameof(Delay));
CancelInvoke();
}
}
public class Test : MonoBehaviour
{
[SerializeField] bool attackable = true;
void Update()
{
if(attackable && Input.GetKeyDown(KeyCode.Space))
{
attackable = false;
Invoke(nameof(AttackDelay), 1f);
}
}
void AttackDelay()
{
attackable = true;
}
}