[진행중]프로젝트 3D_Idle

1일차 - 프레임워크 추가 및 Awaitable을 활용한 타이머 구현

네,가능합니다 2025. 1. 22. 21:16

1. 기존 DOTween 타이머의 문제점

 

기존에 사용하던 DOTween 기반의 타이머는 다음과 같은 문제점이 있었음.

  • 외부 라이브러리 의존성
  • 코드 가독성
  • Unity Update와의 혼용 (DOTween이 자체적으로 Update를 관리하지만 Unity의 기본 Time 관련 기능과는 별도로 작동함)

 

2. Awaitable 타이머로의 변경

 

Unity6에서 도입된 Awaitable 기능을 활용하여 타이머를 비동기적으로 구현했음. 이로 인해 DOTween 의존성을 제거하고 Unity의 기본 기능만으로 타이머를 관리할 수 있게 됨.

 

주요 변경 내용

 

1. Awaitable 도입

  • await Awaitable.NextFrame()을 사용하여 각 프레임에서 비동기적으로 대기하도록 구현함.
  • async/await를 통해 코드 흐름이 간결하고 직관적으로 변경됨.

2. 타이머 관리 방식 변경

  • 기존 DOTween의 Tween 객체 대신 Unity의 Time.deltaTime을 사용하여 직접 경과 시간을 계산함.
  • 타이머 데이터를 관리하는 Dictionary를 유지하여 여러 타이머를 동시에 처리할 수 있도록 함

3. 일시정지 및 재개 기능

  • IsPaused 플래그를 추가하여 타이머를 일시정히하거나 재개할 수 있는 기능을 제공함.

 

실제 코드

 

using System;
using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections.Generic;

/// <summary>
/// Unity 6의 Awaitable을 활용하여 비동기 타이머 기능을 제공하는 클래스입니다.
/// </summary>
public class AwaitableTimer
{
    private static Dictionary<int, TimerInfo> activeTimers = new Dictionary<int, TimerInfo>();
    private static int timerId;
    private static bool isInitialized;
    public static event Action OnTimerStarted;

    private class TimerInfo
    {
        public float StartTime { get; set; }
        public float Duration { get; set; }
        public Action OnComplete { get; set; }
        public Action<float> OnUpdate { get; set; }
        public bool IsPaused { get; set; }
        public float ElapsedTime { get; set; }
    }

    /// <summary>
    /// 현재 실행 중인 타이머가 있는지 확인합니다.
    /// </summary>
    public static bool HasActiveTimers => activeTimers.Count > 0;

    static AwaitableTimer()
    {
        InitializeIfNeeded();
    }

    private static void InitializeIfNeeded()
    {
        if (!isInitialized)
        {
            SceneManager.sceneUnloaded += OnSceneUnloaded;
            Application.quitting += CleanupAllTimers;
            isInitialized = true;
        }
    }

    private static void OnSceneUnloaded(Scene scene)
    {
        CleanupAllTimers();
    }

    /// <summary>
    /// 모든 타이머를 정리하고 메모리를 해제합니다.
    /// </summary>
    public static void CleanupAllTimers()
    {
        activeTimers.Clear();
        timerId = 0;
    }

    /// <summary>
    /// 타이머를 시작하고 설정된 시간 후에 콜백을 호출합니다.
    /// </summary>
    /// <param name="time">타이머 지속 시간</param>
    /// <param name="onComplete">타이머 완료 시 호출할 콜백</param>
    /// <param name="onUpdate">타이머 업데이트 시 호출할 콜백</param>
    /// <returns>타이머 ID</returns>
    public static int StartTimer(float time, Action onComplete, Action<float> onUpdate = null)
    {
        if (time <= 0)
        {
            onComplete?.Invoke();
            return -1;
        }

        InitializeIfNeeded();
        OnTimerStarted?.Invoke();

        timerId++;
        int currentTimerId = timerId;

        var timerInfo = new TimerInfo
        {
            StartTime = Time.time,
            Duration = time,
            OnComplete = onComplete,
            OnUpdate = onUpdate,
            IsPaused = false,
            ElapsedTime = 0f
        };

        activeTimers[currentTimerId] = timerInfo;
        RunTimerAsync(currentTimerId);

        return currentTimerId;
    }

    private static async void RunTimerAsync(int id)
    {
        if (activeTimers.TryGetValue(id, out TimerInfo timerInfo))
        {
            while (timerInfo.ElapsedTime < timerInfo.Duration)
            {
                if (timerInfo.IsPaused)
                {
                    await Awaitable.NextFrameAsync();
                    continue;
                }

                timerInfo.ElapsedTime += Time.deltaTime;
                timerInfo.OnUpdate?.Invoke(timerInfo.ElapsedTime);
                await Awaitable.NextFrameAsync();
            }

            timerInfo.OnComplete?.Invoke();
            activeTimers.Remove(id);
        }
    }

    /// <summary>
    /// 특정 ID의 타이머를 중지합니다.
    /// </summary>
    public static void StopTimer(int id)
    {
        if (activeTimers.ContainsKey(id))
        {
            activeTimers.Remove(id);
        }
    }

    /// <summary>
    /// 특정 ID의 타이머를 일시정지합니다.
    /// </summary>
    public static void PauseTimer(int id)
    {
        if (activeTimers.TryGetValue(id, out TimerInfo timerInfo))
        {
            timerInfo.IsPaused = true;
        }
    }

    /// <summary>
    /// 특정 ID의 타이머를 재개합니다.
    /// </summary>
    public static void ResumeTimer(int id)
    {
        if (activeTimers.TryGetValue(id, out TimerInfo timerInfo))
        {
            timerInfo.IsPaused = false;
        }
    }

    /// <summary>
    /// 특정 ID의 타이머가 실행 중인지 확인합니다.
    /// </summary>
    public static bool IsTimerRunning(int id)
    {
        return activeTimers.ContainsKey(id) && !activeTimers[id].IsPaused;
    }

    /// <summary>
    /// 특정 ID의 타이머의 남은 시간을 반환합니다.
    /// </summary>
    public static float GetRemainingTime(int id)
    {
        if (activeTimers.TryGetValue(id, out TimerInfo timerInfo))
        {
            return timerInfo.Duration - timerInfo.ElapsedTime;
        }
        return 0f;
    }
}

 

3. Awaitable 타이머의 장점

 

1. 간결한 코드

  • async/await를 통해 타이머 동작을 간단하게 표현할 수 있음
  • 이벤트 설정이나 Tween객체 관리와 같은 추가적인 코드가 필요하지 않음

 

2. Unity 기능에 통합

  • Unity의 Time.deltaTime을 사용하여 기본 엔진 기능과 일관되게 작동함
  • 외부 라이브러리 설치나 초기화 과정이 필요하지 않음

 

3. 가독성과 유지보수성 향상

  • 비동기 코드는 흐름이 명확하여 유지보수가 쉬움
  • Awaitable을 통해 프레임 기반 작업을 간편하게 처리할 수 있음