Humility

아무리 노력해도 최고가 되지 못할 수 있다⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀그럼에도 노력하는자가 가장 겸손한 것 아닌가

공부하는 블로그

유니티/문법

[Unity] 유니티 C#) async와 코루틴의 차이

새벽_글쓴이 2025. 1. 22. 19:01
반응형

async/await와 코루틴의 주요 차이점

1. 반환값 처리

// 코루틴 - 직접적인 반환값을 받을 수 없음
IEnumerator LoadDataCoroutine()
{
    yield return new WaitForSeconds(1f);
    string data = "데이터";
    // 반환값을 직접 받을 수 없어서 다른 변수나 콜백으로 처리해야 함
}

// async - 직접 반환값을 받을 수 있음
async Task<string> LoadDataAsync()
{
    await Task.Delay(1000);
    return "데이터"; // 직접 반환 가능
}

2. 예외처리

// 코루틴 - try-catch 사용 불가
IEnumerator ErrorCoroutine()
{
    yield return new WaitForSeconds(1f);
    throw new Exception("에러 발생!");  // 예외 처리가 어려움
}

// async - try-catch 사용 가능
async Task ErrorHandlingAsync()
{
    try
    {
        await Task.Delay(1000);
        throw new Exception("에러 발생!");
    }
    catch (Exception e)
    {
        Debug.LogError(e);  // 정상적으로 예외 처리
    }
}

3. Unity 엔진과의 통합

// 코루틴 - Unity의 Time.timeScale 영향 받음
IEnumerator WaitCoroutine()
{
    yield return new WaitForSeconds(1f);  // timeScale이 0이면 멈춤
}

// async - Time.timeScale 영향 받지 않음
async Task WaitAsync()
{
    await Task.Delay(1000);  // timeScale과 무관하게 실행
}

4.취소와 제어

// 코루틴 - 간단한 취소
private Coroutine currentCoroutine;

void StartAndStopCoroutine()
{
    currentCoroutine = StartCoroutine(SomeCoroutine());
    StopCoroutine(currentCoroutine);  // 쉽게 취소 가능
}

// async - CancellationToken 필요
private CancellationTokenSource cts = new CancellationTokenSource();

async Task CancellableAsync()
{
    try
    {
        await Task.Delay(1000, cts.Token);
    }
    catch (OperationCanceledException)
    {
        Debug.Log("작업 취소됨");
    }
}

5. 주요 사용 사례

// 코루틴 - Unity 관련 작업에 적합
IEnumerator AnimationCoroutine()
{
    yield return new WaitForSeconds(1f);
    transform.position += Vector3.up;
    yield return new WaitForEndOfFrame();
    // Unity의 프레임 기반 작업에 좋음
}

// async - 데이터 처리, 네트워크 작업에 적합
async Task LoadPlayerDataAsync()
{
    var data = await GetDataFromServerAsync();
    await SaveToFileAsync(data);
    // 복잡한 비동기 작업 체이닝에 좋음
}

권장하는 방식

코루틴
Unity의 물리/애니메이션 관련 작업
프레임 기반 로직
간단한 시간 지연

 

async/await
파일 입출력
네트워크 통신
복잡한 비동기 로직
예외 처리가 필요한 경우
반환값이 필요한 경우

이러한 차이가 발생하는 이유

코루틴의 동작 방식
IEnumerator ExampleCoroutine()
{
    Debug.Log("시작");
    yield return new WaitForSeconds(1f);
    Debug.Log("1초 후");
}

 

  • 코루틴은 유니티 엔진이 직접 관리하는 특별한 반복자(Iterator) 패턴이다
  • 매 프레임 마다 Unity 엔진이 yield return 지점까지 실행하고 제어를 가져간다
  • 유니티의 게임 루프와 밀접하게 연결되어 있다
  • Time.timescale의 영향을 받고, waitforseconds 같은 유니티 전용 기능을 사용할 수 있다

async/await의 동작 방식
async Task ExampleAsync()
{
    Debug.Log("시작");
    await Task.Delay(1000);
    Debug.Log("1초 후");
}

 

  • async/await는 C# 언어 자체의 기능으로, .NET 런타임이 관리한다
  • 상태 머신(State Machine)으로 컴파일되어 비동기 작업을 관리한다
  • Unity 엔진과 독립적으로 동작을 한다
  • try-catch나 반환값 같은 C#의 일반적인 기능들을 모두 사용할 수 있다

 

이러한 구현 방식의 차이 때문에
코루틴은 유니티에 종속적이지만 유니티 기능을 잘 활용할 수 있고
async/await는 더 범용적이고 강력하지만 유니티의 특수한 기능들과는 거리가 있다

활용예시

게임 저장 시스템
// 비동기 방식 (async/await)
public class SaveSystemAsync
{
    public async Task SaveGameAsync(GameData data)
    {
        try 
        {
            string json = JsonUtility.ToJson(data);
            string path = Path.Combine(Application.persistentDataPath, "save.json");
            
            // 파일 저장 (무거운 I/O 작업)
            await File.WriteAllTextAsync(path, json);
            Debug.Log("게임 저장 완료!");
            
            // 클라우드 백업 (네트워크 작업)
            await UploadToCloudAsync(json);
            Debug.Log("클라우드 백업 완료!");
        }
        catch (Exception e)
        {
            Debug.LogError($"저장 실패: {e.Message}");
        }
    }
}

// 사용 예시
public class GameManager : MonoBehaviour
{
    private SaveSystemAsync saveSystem;
    
    public async void OnSaveButtonClick()
    {
        var saveButton = GetComponent<Button>();
        saveButton.interactable = false;  // 저장 중 버튼 비활성화
        
        await saveSystem.SaveGameAsync(currentGameData);
        
        saveButton.interactable = true;   // 저장 완료 후 버튼 활성화
    }
}

 

SaveSystemAsync의 장점

  • 파일 I/O 작업을 비동기로 처리하여 게임이 멈추지 않음
  • 에러 처리가 깔끔함
  • 클라우드 저장같은 네트워크 작업과 연계가 쉬움

 

스킬 시스템
public class SkillSystem : MonoBehaviour
{
    // 코루틴 방식 - 시각적 효과와 타이밍이 중요한 경우
    public IEnumerator CastFireballCoroutine()
    {
        // 캐스팅 시작 이펙트
        var castingEffect = Instantiate(castingEffectPrefab, transform.position, Quaternion.identity);
        
        // 캐스팅 시간 (2초)
        float castTime = 2f;
        float elapsed = 0f;
        
        while (elapsed < castTime)
        {
            // 캐스팅 게이지 업데이트
            float castProgress = elapsed / castTime;
            UpdateCastingBar(castProgress);
            
            elapsed += Time.deltaTime;
            yield return null;
        }
        
        // 발사 이펙트
        var fireball = Instantiate(fireballPrefab, spawnPoint.position, transform.rotation);
        fireball.GetComponent<Rigidbody>().AddForce(transform.forward * fireballSpeed);
        
        // 마나 소모
        playerStats.UseMana(30);
        
        // 쿨다운 시작
        yield return new WaitForSeconds(5f);
        
        // 스킬 다시 사용 가능
        EnableSkillButton();
    }
    
    // async/await 방식 - 서버 통신이 필요한 경우
    public async Task ValidateAndCastSkillAsync(int skillId)
    {
        try
        {
            // 서버에 스킬 사용 가능 여부 확인
            var response = await serverClient.CheckSkillAvailabilityAsync(skillId);
            
            if (response.canUseSkill)
            {
                // 서버에 스킬 사용 알림
                await serverClient.NotifySkillUseAsync(skillId);
                
                // 실제 스킬 효과 실행 (코루틴)
                StartCoroutine(CastFireballCoroutine());
            }
            else
            {
                Debug.Log($"스킬 사용 불가: {response.reason}");
            }
        }
        catch (Exception e)
        {
            Debug.LogError($"스킬 사용 중 에러 발생: {e.Message}");
        }
    }
}

 

SkillSystem의 특징

  • 코루틴: 시각적 효과와 게임 타이밍에 맞춘 처리
  • async/await: 서버 통신과 같은 네트워크 작업 처리
  • 두 방식의 적절한 조합 사용

 

스킬 시스템을 보다 의문점이 들었다
라인 게임이라면 굳이 코루틴으로 만들 필요가 있을까?
통합할 순 없을까? 라는 의문점이다

 

온라인스킬시스템
public class OnlineSkillSystem : MonoBehaviour
{
    public async Task CastFireballAsync()
    {
        try
        {
            // 1. 서버에 스킬 사용 가능 여부 확인 (쿨타임, 마나 등)
            var canUseSkill = await serverClient.CheckSkillAvailableAsync("Fireball");
            if (!canUseSkill)
            {
                Debug.Log("스킬을 현재 사용할 수 없습니다.");
                return;
            }

            // 2. 스킬 사용 시작을 서버에 알림
            await serverClient.NotifySkillStartAsync("Fireball");

            // 3. 클라이언트의 시각적 효과 처리
            var castingEffect = Instantiate(castingEffectPrefab);
            
            // 4. 캐스팅 시간 (2초) 처리
            float castTime = 2.0f;
            float startTime = Time.time;
            
            while (Time.time - startTime < castTime)
            {
                float progress = (Time.time - startTime) / castTime;
                UpdateCastingBar(progress);
                await Task.Yield(); // 다음 프레임까지 대기
            }

            // 5. 스킬 발동 효과
            var fireball = Instantiate(fireballPrefab, spawnPoint.position, transform.rotation);
            fireball.GetComponent<Rigidbody>().AddForce(transform.forward * fireballSpeed);

            // 6. 스킬 완료를 서버에 알림
            await serverClient.NotifySkillCompletedAsync("Fireball", transform.position, transform.rotation);
        }
        catch (Exception e)
        {
            Debug.LogError($"스킬 사용 중 오류 발생: {e.Message}");
            // 서버에 스킬 취소 알림
            await serverClient.NotifySkillCancelledAsync("Fireball");
        }
    }

    // UI 버튼에서 호출
    public async void OnFireballButtonClick()
    {
        var skillButton = GetComponent<Button>();
        skillButton.interactable = false;
        
        await CastFireballAsync();
        
        // 서버에서 쿨타임 정보를 받아와서 버튼 상태 업데이트
        var cooldownInfo = await serverClient.GetSkillCooldownAsync("Fireball");
        UpdateSkillButtonCooldown(cooldownInfo);
    }
}

 

통합 장점

  • 서버 통신이 많은 온라인 게임의 특성상 일관된 비동기 패턴 사용이 유지보수에 더 좋습니다
  • 예외 처리가 더 깔끔합니다
  • 서버와의 통신 실패 시 롤백 처리가 더 쉽습니다

 

통합 주의할 점

  • Unity의 Transform, Rigidbody 등 물리/애니메이션 관련 작업은 메인 스레드에서 해야 합니다
  • Time.time 같은 Unity API는 메인 스레드에서만 접근 가능합니다
  • 프레임 단위의 업데이트가 필요한 경우 Task.Yield()를 사용해야 합니다

 

여기서 개인적인 궁금증이 생겼는데,
poe2나 원신 같은 게임은 혼자할 땐 게임이 일시정지가 가능하지만
파티를 한다면 일시정지가 불가능하다

 

이러한 게임들을 생각한 구현 방식
public class SkillSystem : MonoBehaviour
{
    private bool isOnlineMode = false;

    public void SetOnlineMode(bool isOnline)
    {
        isOnlineMode = isOnline;
    }

    // 스킬 실행을 위한 통합 진입점
    public void CastSkill()
    {
        if (isOnlineMode)
        {
            // 온라인 모드: async/await 사용
            _ = CastSkillAsync();
        }
        else
        {
            // 오프라인 모드: 코루틴 사용
            StartCoroutine(CastSkillCoroutine());
        }
    }

    // 온라인용 async 구현
    private async Task CastSkillAsync()
    {
        try
        {
            // 서버와 동기화가 필요한 온라인 로직
            await serverClient.CheckSkillAvailableAsync();
            await serverClient.NotifySkillStartAsync();

            // 시각 효과 등 공통 로직
            await PlaySkillEffectAsync();

            await serverClient.NotifySkillCompletedAsync();
        }
        catch (Exception e)
        {
            Debug.LogError($"온라인 스킬 사용 실패: {e.Message}");
        }
    }

    // 오프라인용 코루틴 구현
    private IEnumerator CastSkillCoroutine()
    {
        if (Time.timeScale == 0) // 일시정지 상태 체크
        {
            yield break;
        }

        // 로컬에서만 처리되는 오프라인 로직
        if (!CheckLocalSkillAvailable())
        {
            yield break;
        }

        // 시각 효과 등 공통 로직
        yield return StartCoroutine(PlaySkillEffectCoroutine());

        // 로컬 상태 업데이트
        UpdateLocalSkillState();
    }
}

// 게임 매니저에서 모드 전환 처리
public class GameManager : MonoBehaviour
{
    private SkillSystem skillSystem;

    public void OnPartyJoined()
    {
        // 파티 참가시 온라인 모드로 전환
        skillSystem.SetOnlineMode(true);
        Time.timeScale = 1f; // 강제로 일시정지 해제
        DisablePauseMenu(); // 일시정지 메뉴 비활성화
    }

    public void OnPartyLeft()
    {
        // 파티 퇴장시 오프라인 모드로 전환
        skillSystem.SetOnlineMode(false);
        EnablePauseMenu(); // 일시정지 메뉴 활성화
    }

    public void OnEscKeyPressed()
    {
        if (!skillSystem.isOnlineMode)
        {
            Time.timeScale = Time.timeScale == 0 ? 1f : 0f; // 일시정지 토글
            UpdatePauseMenu();
        }
    }
}

 

싱글플레이 때는 코루틴으로 동작하면서 일시정지가 가능하게
파티 참가시 자동으로 async 방식으로 전환되도록 하였다
반응형