[Unity] Coroutine

Lingtea_luv·2025년 4월 18일
0

Unity

목록 보기
6/30
post-thumbnail

Coroutine


코루틴은 유니티에서 함수의 실행을 일시중지하고 나중에 다시 이어서 실행할 수 있도록 만들어진 개념이다. 비동기식 작업처럼 보이지만 작업을 다수의 프레임에 분산하여 처리하는 동기식 작업이기에 코루틴은 스레드가 아니며 코루틴의 작업은 메인 스레드에서 진행된다.

코루틴은 유니티에서 비동기 작업과 같은 느낌의 처리를 할 때 유용하게 사용되지만 사용시 주의할 점이 2가지가 있다.

1. 사용이 끝나면 반드시 중지할 것

사용이 끝난 코루틴을 중단하지 않으면 오브젝트가 삭제된 후에도 동작을 시도하여 Null 참조 오류와 같은 예상치 못한 오류가 발생할 수 있다. 따라서 StopCoroutine() 혹은 StopAllCoroutine() 으로 중단하는 것이 안전하다.

2. 코루틴을 변수에 담아 사용할 것

private void Update()
{
	if(Input.GetKeyDown(KeyCode.Space))
    {
    	if(coroutine == null)
        {
        	// 코루틴 변수에 담아 사용 - 중복 방지, 중단 처리 용이
        	coroutine = StartCoroutine(Routine());
        }
    }
	if(Input.GetKeyDown(KeyCode.Escape))
    {
    	if(coroutine != null)
        {
        	// 캐싱한 코루틴을 멈추는 함수
        	StopCoroutine(coroutine);
            coroutine = null; 	 // 캐싱 참조 해제
        }
    }
}

IEnumerator Routine()
{
	for(int i = 5; i > 0; i--)
    {
    	Debug.Log($"{i}");
        yield return new WaitForSeconds(1f);
    }
    
    Debug.Log("Go!");
    coroutine = null; 	 // 캐싱 참조 해제
}

yield return

코루틴은 일반 함수의 반환인 return 대신에 yield return이라는 키워드를 사용하며, 해당 키워드 사용시 코루틴은 일시중지되어 유니티에게 제어권을 반납하고 다음 실행시 중단점부터 이어서 실행된다.

// Update 끝날 때
yield return null;
// 게임시간 n초간 기다리고 Update 끝날 때
yield return new WaitForSeconds(n);
// 현실시간 n초간 기다리고 Update 끝날 때
yield return new WaitForSecondsRealtime(n);
// FixedUpdate 끝날 때
yield return new WaitForFixedUpdate;
// 프레임이 끝날 때 (LateUpdate 다음)
yield return new WaitForEndOfFrame;

// 조건을 만족할 때
yield return new WaitUntil(() => Input.GetKeyDown());
public class CoroutineTest : MonoBehaviour
{
	[SerializeField] Rigidbody rigid;
    [SerializeField] float jumpPower;
    
	private void Update()
    {
    	if(Input.GetKeyDown(KeyCode.Space))
        {
        	StartCoroutine(Routine());
        }        
    }

	IEnumerator Routine()
    {
    	Debug.Log("점프 대기 시작");
    	yield return null
        Debug.Log("1프레임 후 점프");
        rigid.AddForce(Vector3.up * jumpPower, ForceMode.Impulse);
        yield return new WaitForSeconds(2f);
        Debug.Log("2초 뒤 점프!");
        rigid.AddForce(Vector3.up * jumpPower, ForceMode.Impulse);   
    }
}

Practice - 재장전 쿨타임

Coroutine X

public class Tank : MonoBehaviour
{
	[SerializeField] Shooter shooter;
	[SerializeField] float coolTime;
	private float shootTime;

	private void Update()
	{
    	shootTime += Time.deltaTime;
        
		if(Input.GetKeyDown()&& ShootTime > coolTime)
	    {
	    	shootTime = 0;
	        shooter.Fire();
	    }
	}
}

Coroutine O

public class Tank : MonoBehaviour
{
	// 인스펙터에 노출시켜 Shooter 지정
	[SerializeField] Shooter shooter;
    // 인스펙터에서 조절 가능한 시간
    [SerializeField][Range(0,1f)] float delayTime = 1f;
    // 메모리 단편화 방지를 위한 내부 캐싱
    private Coroutine fireRoutine;    
    private WaitForSeconds delay;
    // 코루틴이 동시에 2개 이상 실행되는 것을 방지
    private bool isFiring = false;
    
    // 코루틴 내부 초기화 작업은 Awake()에서!
    private void Awake()
    {
    	Init();
    }
        
    private void Update()
    {
    	if(Input.GetKeyDown(KeyCode.Space))
        {
        	// 코루틴의 시작은 StartCoroutine()으로!
        	// StartCoroutine(Routine());
            // 코루틴을 내부 캐싱해서 사용하는 것도 가능!
            fireRoutine = StartCoroutine(Routine());
        }   	
    }
    
    private void Init()
    {    	
    	delay = new WaitForSeconds(delayTime);
    }
    
    IEnumerator Routine()
    {    
    	// 코루틴에서는 break대신 yield break를 사용
    	if(isFiring) yield break;
        {
        	isFiring = true;
            
        	shooter.Fire();
        	yield return delay;
            
            isFiring = false;
            fireRoutine = null;
        }    	
    }
}

Practice - 차지 공격

private void Update()
{
	if(Input.GetKeyDown(KeyCode.Space) && chargeCoroutine == null)
    {
        chargeCoroutine = StartCoroutine(ChargeRoutine())
    }
}
IEnumerator ChargeRoutine()
{
	float timer = 0;
    while(!Input.GetKeyUP(KeyCode.Space)
    {
    	timer += Time.deltaTime * 20;
    	yield return null;
    }
    float bulletSpeed = Mathf.Clamp(timer, 5f, 30f);
    shooter.Fire(bulletSpeed);
    
    chargeCoroutine = null;
}

public void Fire(float speed)
{
	bulletRigidbody.velocity = muzzlePoint.forward *speed
}

기타


Caching

캐싱(caching)은 객체를 변수로 저장한다는 의미로 2가지의 용도로 사용될 수 있다.

1. 메모리 단편화 방지

내부적으로 매번 새로 생성되는 객체라면 사용이 끝날 때마다 GC가 계속 청소해야하므로 메모리 단편화의 위험이 존재한다.

private WaitForSeconds delay;
delay = new WaitForSeconds(1f);

private IEnumerator Routine()
{
    yield return delay;  // 매번 생성되는 인스턴스를 캐싱하여 사용
}

따라서 위 코드처럼 필드에 미리 캐싱을 하여 사용하게 될 경우 캐싱으로써 메모리 단편화를 방지하여 성능 최적화 효과를 발휘한다.

2. 메서드 호출 제어

coroutine = StartCoroutine(Routine());

위의 코드와 같은 캐싱은 IEnumerator 인스턴스의 정보를 저장함으로써 코루틴의 중복 실행을 막거나, StopCoroutine()으로 중단하기 위한 용도이다. 실제로는 Routine()이 호출될 때마다 새로운 IEnumerator 인스턴스가 생성되기 때문에 메모리 단편화 방지와는 직접적인 관련이 없다.

다만 논리적인 흐름 관리에 효과적이기 때문에 코루틴의 내부 캐싱은 분명 의미를 갖으며, 메모리 최적화를 위한 캐싱과 흐름 제어를 위한 캐싱은 목적이 다르므로 구분해서 사용해야한다.

profile
뚠뚠뚠뚠

0개의 댓글