
Unity UniTask와 C# Async/Await으로 대기 시간·애니메이션 프레임 제어하기
Unity에서 UniTask와 C# Async/Await을 사용해 게임 시간을 기준으로 안전하게 대기하고 Animator 상태와 프레임 타이밍을 제어하는 실전 패턴을 정리합니다.
Direct Answer: Unity에서 지연 실행과 애니메이션 동기화에는
Task.Delay보다 Unity PlayerLoop에 연결되는 UniTask를 쓰는 편이 안전합니다. 시간 대기는UniTask.Delay, 다음 프레임 대기는UniTask.Yield또는UniTask.NextFrame, 애니메이션 완료 대기는AnimatorStateInfo.normalizedTime과CancellationToken을 조합해 구현합니다.
UniTask가 Unity 대기 처리에 적합한 이유
C#의 async/await는 비동기 흐름을 순차 코드처럼 작성하게 해 준다. 하지만 일반 Task는 Unity의 프레임 루프, Time.timeScale, GameObject 수명과 직접 연결되지 않는다. 반면 UniTask는 Unity PlayerLoop의 특정 시점에 이어서 실행할 수 있어 게임플레이와 UI 코드에 잘 맞는다.
| 목적 | 권장 API | 기준 |
|---|---|---|
| 일정 시간 뒤 실행 | UniTask.Delay | 게임 시간 또는 실제 시간 |
| 다음 프레임까지 대기 | UniTask.NextFrame | 다음 렌더 프레임 |
| 특정 PlayerLoop 시점까지 대기 | UniTask.Yield | Update, LateUpdate 등 |
| 조건이 참이 될 때까지 대기 | UniTask.WaitUntil | 매 프레임 조건 검사 |
| 애니메이션 종료 대기 | 반복 확인 + UniTask.Yield | AnimatorStateInfo |
await는 메인 스레드를 멈추는 호출이 아니다. 현재 메서드를 일시 중단하고 지정한 조건이나 프레임 시점이 왔을 때 이후 코드를 재개한다.
Unity에서 대기 시간은 어떻게 구현할까?
1. 게임 시간 기준 지연 실행
일시 정지에서 타이머도 멈춰야 한다면 DelayType.DeltaTime을 사용한다. 이 방식은 Time.timeScale의 영향을 받는다.
using Cysharp.Threading.Tasks;
using System;
using System.Threading;
using UnityEngine;
public sealed class SkillCooldown : MonoBehaviour
{
[SerializeField] private float cooldownSeconds = 2f;
public async UniTask ActivateAsync(CancellationToken cancellationToken)
{
Debug.Log("스킬 사용");
await UniTask.Delay(
TimeSpan.FromSeconds(cooldownSeconds),
DelayType.DeltaTime,
PlayerLoopTiming.Update,
cancellationToken);
Debug.Log("재사용 가능");
}
}
게임 시간은 프레임마다 누적되는 델타 시간의 합으로 이해할 수 있다.
Time.timeScale이 0이면 일반적인 델타 시간도 0이므로 DeltaTime 기반 대기는 완료되지 않는다.
2. 일시 정지와 무관한 실제 시간 대기
로딩 화면 최소 노출 시간, 네트워크 재시도 간격, 광고 SDK 콜백처럼 일시 정지 중에도 지나가야 하는 시간에는 DelayType.UnscaledDeltaTime을 선택한다.
await UniTask.Delay(
TimeSpan.FromSeconds(1.5f),
DelayType.UnscaledDeltaTime,
cancellationToken: cancellationToken);
| 상황 | 추천 시간 기준 |
|---|---|
| 스킬 쿨다운, 버프 지속 시간 | DeltaTime |
| 게임 일시 정지 중에도 유지할 UI 타이머 | UnscaledDeltaTime |
| 씬 전환 취소, 오브젝트 파괴 대응 | CancellationToken 추가 |
다음 프레임 대기는 왜 필요할까?
Start() 직후 생성된 UI의 레이아웃 값이나 Animator 상태는 같은 프레임에 아직 갱신되지 않았을 수 있다. 이때 다음 프레임 또는 특정 PlayerLoop 구간까지 기다리면 갱신 순서를 명시할 수 있다.
using Cysharp.Threading.Tasks;
using UnityEngine;
public sealed class FrameTimingExample : MonoBehaviour
{
[SerializeField] private Animator animator;
private async UniTaskVoid Start()
{
animator.SetTrigger("Open");
// Animator 파라미터 반영 이후의 프레임까지 대기한다.
await UniTask.NextFrame();
AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0);
Debug.Log($"현재 상태 해시: {state.fullPathHash}");
}
}
UniTask.NextFrame()은 다음 프레임까지 기다리는 의도가 명확하다. 특정 업데이트 순서가 중요하다면 UniTask.Yield(PlayerLoopTiming.LastUpdate)처럼 PlayerLoop 시점을 지정한다.

Animator 애니메이션이 끝날 때까지 안전하게 기다리는 방법
애니메이션 클립 길이를 하드코딩해 Delay하는 방식은 재생 속도, 전환, 블렌드 트리 변경에 취약하다. 대신 AnimatorStateInfo에서 실제 상태와 정규화 시간을 확인한다.
정규화 시간은 대체로 다음처럼 해석한다.
루프하지 않는 상태에서 normalizedTime >= 1.0f이면 한 번의 재생이 끝났다고 볼 수 있다. 단, 전환 중에는 이전 상태 정보가 반환될 수 있으므로 IsInTransition도 함께 확인해야 한다.
using Cysharp.Threading.Tasks;
using System.Threading;
using UnityEngine;
public sealed class AttackAnimationController : MonoBehaviour
{
[SerializeField] private Animator animator;
private static readonly int Attack = Animator.StringToHash("Attack");
private static readonly int AttackState = Animator.StringToHash("Base Layer.Attack");
public async UniTask PlayAttackAsync(CancellationToken cancellationToken)
{
animator.SetTrigger(Attack);
// Trigger가 Animator 상태에 반영되는 다음 프레임까지 기다린다.
await UniTask.NextFrame(cancellationToken);
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0);
bool isAttack = state.fullPathHash == AttackState;
bool isFinished = isAttack
&& !animator.IsInTransition(0)
&& state.normalizedTime >= 1f;
if (isFinished)
{
break;
}
await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
}
Debug.Log("공격 애니메이션 완료 후 다음 로직 실행");
}
}
상태 이름 문자열을 매 프레임 비교하지 않도록 Animator.StringToHash로 해시를 미리 만든다. 상태 머신 구조가 바뀔 수 있다면 Base Layer.Attack 경로와 레이어 인덱스도 상수 또는 설정값으로 관리하는 편이 좋다.
CancellationToken은 왜 반드시 연결해야 하는가?
비동기 메서드가 기다리는 중에 씬이 바뀌거나 GameObject가 파괴될 수 있다. 취소 토큰이 없으면 이미 사라진 UI나 Animator를 다시 접근해 예외 또는 의도치 않은 동작을 만들 수 있다.
Unity 2022.2 이상에서는 MonoBehaviour.destroyCancellationToken을 사용할 수 있다.
private async UniTaskVoid OnEnable()
{
try
{
await UniTask.Delay(
TimeSpan.FromSeconds(3),
cancellationToken: destroyCancellationToken);
gameObject.SetActive(false);
}
catch (OperationCanceledException)
{
// 오브젝트가 파괴되어 취소된 정상 경로다.
}
}
이전 Unity 버전에서는 UniTask 확장 메서드인 this.GetCancellationTokenOnDestroy()를 사용한다. 호출자가 취소를 처리할 책임이 있다면 예외를 잡아 삼키기보다 상위 메서드로 전파하는 설계도 가능하다.
실전 구현 순서
- 대기의 기준을 정한다. 게임 시간에 묶을지 일시 정지와 무관한 실제 시간으로 처리할지 결정한다.
- 프레임 의존성이 있으면
NextFrame또는Yield(PlayerLoopTiming...)을 먼저 사용해 갱신 순서를 보장한다. CancellationToken을 모든 대기 API에 전달한다. 오브젝트 수명 또는 화면 전환 시 비동기 작업이 함께 끝나야 한다.- 애니메이션은 클립 길이 대신
AnimatorStateInfo의 상태 해시, 전환 여부,normalizedTime을 검사한다.
자주 묻는 질문 (FAQ)
Task.Delay 대신 항상 UniTask.Delay를 써야 하나?
Unity 프레임, Time.timeScale, GameObject 수명과 연관된 코드라면 UniTask.Delay가 적합하다. 순수 C# 라이브러리처럼 Unity에 의존하지 않는 코드라면 Task를 유지해도 된다.
UniTask.Yield와 UniTask.NextFrame의 차이는 무엇인가?
NextFrame은 다음 프레임까지의 대기를 명확히 표현한다. Yield는 Update, LateUpdate, LastPostLateUpdate 등 재개할 PlayerLoop 시점을 세밀하게 지정할 때 사용한다.
애니메이션 이벤트와 폴링 중 무엇을 선택해야 하나?
타격 판정처럼 클립의 정확한 특정 시점이 중요하면 Animation Event가 알맞다. 상태가 실제로 끝났는지 확인하거나 이벤트를 추가하기 어려운 공통 UI·게임플레이 코드에는 AnimatorStateInfo 폴링이 유용하다.
정리
UniTask는 Unity의 프레임 루프 안에서 비동기 흐름을 제어하게 해 준다. 시간 대기에는 UniTask.Delay, 프레임 순서 보장에는 NextFrame과 Yield, 애니메이션 완료 대기에는 AnimatorStateInfo, 수명 안전성에는 CancellationToken을 조합하면 지연 실행 코드를 예측 가능하게 유지할 수 있다.


