C#과 JAVA의 차이점중에 하나인 코루틴,
C#에는 코루틴이 있지만
JAVA에는 코루틴이 없는걸로 알고있다.
그럼 코루틴이란 정확히 무엇일까?
일반적으로 어떤 함수(루틴)를 구현할 때 return을 넣어 함수가 종료될 때를 정하게 된다.
void Func()
{
print("A");
print("B");
return;
}
위 함수를 호출하면 순차적으로 A를 출력하고 B를 출력할 것이다.
return을 만나게 되면 제어권을 호출자에게 넘겨주게 되는데,
문제는 호출자는 Func()가 return 해주기를 하염없이 기다려야 한다는 것이다.
만약 Func()가 서버에서 데이터를 로드하는등 시간이 오래걸리는 작업이라면
사용자는 프로그램이 멈춘듯한 느낌을 받을 수 있다.
이것은 동기 프로그래밍의 단점인데, 이를 해결하기 위해 await, async를 이용해 비동기 프로그래밍을 할 수 있다.
코루틴은 이 비동기 프로그래밍을 보다 쉽게 해주기 위한 도구이다.
엄밀히 말하자면 await를 이용한 비동기 프로그래밍과 살짝 다른데
기존의 비동기 프로그래밍은 멀티쓰레드를 이용해 여러 작업들을 제각각 처리하는 방법이라면,
코루틴은 멀티쓰레드가 아닌 싱글쓰레드를 이용해 비동기처럼 보여지게 해주는것이다.
그럼 return을 하고 나서 Func()를 또 호출하면 어떻게 될까?
앞의 결과와 동일하게 전에 호출했던 사실을 기억하지 못하는 것처럼 처음부터 다시 똑같이 실행될 것이다.
Func(); // A,B 출력
Func(); // A,B 출력
다음 함수는 컴파일은 되지만 B를 출력할 일은 절대 없을것이다.
void Func()
{
print("A");
return;
print("B");
return;
}
Func(); // A출력
Func(); // A출력
하지만 어떤 이유에 의해서, 해당 함수가 어디까지 실행됐는지 기억하고싶다면?
Func(); // A출력
Func(); // B출력
즉, 위의 코드처럼 동작하길 원한다면?
그때 필요한게 코루틴이다.
C#과 Unity C#에서의 코루틴 코드는 살짝 다르지만 Unity를 기준으로 보자면
IEnumerator Func()
{
print("A");
yield return null;
print("B");
yield return null;
}
이런식으로 코루틴이 될 함수는 IEnumerator를 반환형으로 해야한다.
여기서 다소 생소한 yield라는 키워드가 나왔는데
yield는 양도하다 라는 뜻으로 제어권을 호출자에게 넘겨준다는 뜻이다.
return 값으로 null을 넘겨주고 있는데
유니티에서는 null은 1프레임 기준으로
A출력과 B출력 사이의 시간 차이를 1프레임으로 하겠다는 뜻이다
일단 yield문을 만나면 코루틴은 호출자에게 제어권을 넘겨준 다음
유니티 엔진 내부에서 코루틴을 다시 실행시킬 수 있는 상황이라고 판단되면
이전에 제어권을 넘겨줬던 yield문 다음 코드로 넘어가
마치 호출했던 사실을 기억하는것처럼 동작하게 된다.
다시 실행시킬 수 있는 상황은 우리가 정해줘야 하는데
이는 yield return값에 의해 결정된다.
null 이외에 return할 수 있는 것들을 크게 정리해보자면
yield return null; // 1프레임을 기다린다
yield return new WaitForSeconds(1f); // 1초를 기다린다
yield return new WaitForSecondsRealtime(1f); // 1초를 기다린다(Time.Scale에 영항을 받지 않음)
yield return new WaitUntil(Func<bool> predicate) // predicate를 만족할 때까지 대기
yield return new Custom(); // YieldInstruction이나 IEnumerator를 상속받은 Custom 클래스
yield return break; // 코루틴을 종료한다
이렇게 된다.
또한 코루틴을 호출하는 방식은
void Start()
{
StartCoroutine(Func());
}
MonoBehaviour에 들어있는 StartCoroutine함수를 통해 호출해야 한다.
이 코드의 실행결과는 어떻게 될까?
A
B
한번의 StartCoroutine을 이용한 Func() 호출로 A, B가 순차적으로 print되고 끝난다.
Func()를 한번밖에 호출하지 않았는데 왜 두번 호출한 결과가 나오는것인가? 라는 의문을 품을 수 있는데
일단 StartCoroutine을 이용하면 유니티 엔진 내부에서 자체적으로 처리를 해준다.
우리는 그저 비동기적으로 수행됐으면 하는 함수를 StartCoroutine을 이용해 호출만 해주면 된다.
코루틴은 언제 쓰이는가?
1. 해당 함수의 실행을 지연시키고 싶을 때
플레이어가 마우스를 눌렀을때 어떤 오브젝트의 투명도를 점점 낮춰 0으로 만드는 함수는 다음과 같다.
void Fade()
{
for (float ft = 1f; ft >= 0; ft -= 0.1f)
{
Color c = renderer.material.color;
c.a = ft;
renderer.material.color = c;
}
}
void Update()
{
if (Input.GetMouseButton(0))
{
Fade();
}
}
이때 Fade()는 1프레임내에서 실행되므로 플레이어는 오브젝트가 바로 사라진것처럼 느낄 수 있다.
IEnumerator Fade()
{
for (float ft = 1f; ft >= 0; ft -= 0.1f)
{
Color c = renderer.material.color;
c.a = ft;
renderer.material.color = c;
yield return new WaitForSeconds(0.1f);
}
}
void Update()
{
if (Input.GetMouseButton(0))
{
StartCoroutine(Fade());
}
}
위와 같은 코드는 플레이어에게 0.1초마다 투명도가 줄어드는 효과를 주게된다.
2. 매 프레임마다 처리할 필요가 없는 함수를 사용할 때
만약 플레이어가 적을 공격하는데에 시간이 0.5초가 걸리고 그동안 새로운 공격을 못한다고 할때
void Update()
{
if(Input.GetKeyDown("A") && lastAttackTime >= 0.5f)
{
Attack();
}
lastAttackTime += Time.deltaTime;
}
void Attack()
{
lastAttackTime = 0f;
// 몬스터 때리기
}
보통의 방법으로는 이렇게 처리한다.
하지만 한번 공격하고나서 0.5초동안은 공격 못한다는 사실을 알기 때문에
굳이 매 프레임마다 플레이어가 공격명령을 내렸는가를 계속 조사할 필요는 없다.
void Start()
{
StartCoroutine(Attack());
}
IEnumerator Attack()
{
var cached = new WaitForSeconds(0.5f);
while(true)
{
if(Input.GetKeyDown("A"))
{
// 몬스터 때리기
yield return cached;
}
yield return null; // 없으면 무한 루프!!
}
}
위와 같이 처리하면 연산의 부하를 줄일 수 있다.
기존에 코루틴을 알기전에 Update에서 처리했던 부분들 중 실제로 코루틴으로 완벽하게 대체될 수 있는것이 많다.
이제부터 Update에는 정말 필요한 부분들만 넣으면 될거같다.
코루틴을 시작하는 방법
위의 Func()를 예로 들면
StartCoroutine(Func());
StartCoroutine(Func);
둘다 가능하다.
함수 그대로의 이름을 보내도 되는데 이때 주의할 것은
코루틴을 끝낼때, 시작했던 방법과 동일하게 끝내야 된다는 것이다.
함수 이름으로 시작했으면 함수 이름으로 끝내야 되고
함수를 직접 전달했다면 똑같이 함수를 직접 전달해 끝내야 한다.
코루틴을 끝내는 방법
StopCoroutine(Func());
StopCoroutine(Func);
StopAllCoroutines(); // 해당 스크립트에서 시작했던 코루틴을 모두 종료시킨다
두번째 방법에서 조심해야 할 점은 Func()에 대한 인스턴스를 가지고 있어야 된다는 점이다.
가지고 있지 않다면 StopCoroutine(Func())는 우리가 원하는대로 동작하지 않는다.
코루틴을 최적화 하는 방법
코루틴 내부에 루프문이 있을 때 계속 WaitForSeconds 객체를 생성해서 yield return 하는것보다
지역변수에 캐싱을 하게되면 부담이 덜해진다.
IEnumerator Func()
{
var cached = new WaitForSeconds(1f);
while(true)
{
yield return cached;
}
}
참고
https://docs.unity3d.com/kr/2019.4/Manual/Coroutines.html
코루틴 - Unity 매뉴얼
함수를 호출하면 값을 반환하기 전에 실행 완료됩니다. 이는 함수에서 수행되는 모든 액션이 하나의 프레임 업데이트 내에서 발생해야 한다는 것을 의미합니다. 시간이 지남에 따라 절차식 애
docs.unity3d.com
'etc > Unity' 카테고리의 다른 글
[Unity] UI 크기를 WorldSpace 기준으로 바꾸기 (0) | 2022.02.25 |
---|---|
[Unity] 코루틴 일시정지시에 시점 복구하기 (0) | 2022.02.24 |
[Unity] VS에서 Unity 메세지 띄우는 법 (0) | 2021.07.12 |