걍 new가 쓰기 싫었다.

 

프레임워크에 타이머가 있길래 써봤는데 오브젝트를 생성하고 타이머가 끝나면 파괴하고 있었다.

 

너무 화가 났다 참을 수 없었다.

 

작성 중 우리 프로젝트에서는 ID 같은 건 ID로 하기로 했는데 Id라고 작성한 걸 발견했다.

(몰래 놔둘 생각)

이걸 사용한다면...

지연실행('Invoke', 'StartCoroutine') 대체가능!

업데이트(타이머 작동하는 동안 업데이트처리 가능)!!

쿨다운시스템(콜백 있음) 스킬, 아이템 등 쿨다운 활용 가능!!

UI업데이트!!! 스폰, 페이즈 전환 타이밍 제어 등 시간을 기반하는 건 거~~ 의 대부분 효율적으로 처리가 가능하다.

 

SimpleTimer 클래스 설명 및 사용방법

SimpleTimer 클래스는 DOTween 라이브러리를 활용하여 간단한 타이머 기능을 제공하는 유틸리티 클래스입니다.

이 클래스는 타이머를 시작, 중지, 일시정지, 재개할 수 있으며, 타이머 완료 시 콜백 함수를 호출할 수 있습니다.

또한 씬 전환 시 메모리 누수를 방지하기 위한 기능을 추가해 두었습니다.

 

주요 기능

  • 타이머 시작: 지정된 시간 후에 특정 작업을 수행하도록 타이머를 설정합니다.
  • 타이머 중지: 실행 중인 타이머를 중지하고 리소스를 해제합니다.
  • 타이머 일시정지 및 재개: 타이머의 실행을 일시적으로 멈추거나 다시 시작할 수 있습니다.
  • 타이머 상태 확인: 특정 타이머가 실행 중인지 여부를 확인할 수 있습니다.
  • 모든 타이머 중지 및 정리: 활성화된 모든 타이머를 한 번에 중지하고 메모리를 정리합니다.
  • 씬 전환 시 자동 정리: 씬이 언로드 될 때 모든 타이머를 자동으로 정리하여 메모리 누수를 방지합니다.

코드

(자세한 주석이 있으니 참고할 것)

using System;
using DG.Tweening;
using System.Collections.Generic;
using UnityEngine.SceneManagement;
using System.Linq;

/// <summary>
/// 간단한 타이머 기능을 제공하는 클래스입니다.
/// DOTween을 활용하여 효율적으로 구현되었습니다.
/// </summary>
public class SimpleTimer
{
    private static Dictionary<int, Tween> activeTweens = new Dictionary<int, Tween>();
    private static int timerId;
    private static bool isInitialized;

    static SimpleTimer()
    {
        InitializeIfNeeded();
    }

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

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

    /// <summary>
    /// 모든 타이머를 정리하고 메모리를 정리합니다.
    /// 씬 전환 시 자동으로 호출되며, 필요한 경우 수동으로도 호출할 수 있습니다.
    /// </summary>
    public static void CleanupAllTimers()
    {
        var timerKeys = activeTweens.Keys.ToList();
        foreach (var key in timerKeys)
        {
            if (activeTweens.ContainsKey(key))
            {
                StopTimer(key);
            }
        }
        activeTweens.Clear();
        timerId = 0;
    }

    /// <summary>
    /// 타이머를 시작하고 설정된 시간 후에 콜백을 호출합니다.
    /// </summary>
    /// <param name="time">타이머가 작동할 시간(초)</param>
    /// <param name="onComplete">타이머 완료 시 호출될 콜백</param>
    /// <param name="onUpdate">타이머 진행 중 호출될 업데이트 콜백 (선택사항)</param>
    /// <param name="autoStart">자동 시작 여부 fasle시 ResumeTimer 호출해서 시작</param>
    /// <returns>생성된 타이머의 ID</returns>
    public static int StartTimer(float time, Action onComplete, Action<float> onUpdate = null, bool autoStart = true)
    {
        InitializeIfNeeded();

        timerId++;
        int currentTimerId = timerId;

        var tween = DOTween.To(() => time, x =>
        {
            if (onUpdate != null)
                onUpdate(x);
        }, 0f, time)
            .SetAutoKill(false)
            .SetRecyclable(true)
            .OnComplete(() =>
            {
                if (onComplete != null)
                    onComplete();
                if (activeTweens.ContainsKey(currentTimerId))
                    activeTweens.Remove(currentTimerId);
            })
            .OnKill(() =>
            {
                if (activeTweens.ContainsKey(currentTimerId))
                    activeTweens.Remove(currentTimerId);
            });

        if (!autoStart)
            tween.Pause();

        activeTweens[currentTimerId] = tween;
        return currentTimerId;
    }

    /// <summary>
    /// 특정 ID의 타이머를 중지합니다.
    /// </summary>
    /// <param name="id">중지할 타이머의 ID</param>
    public static void StopTimer(int id)
    {
        if (activeTweens.TryGetValue(id, out Tween tween))
        {
            tween.Kill();
            activeTweens.Remove(id);
        }
    }

    /// <summary>
    /// 특정 ID의 타이머를 일시정지합니다.
    /// </summary>
    /// <param name="id">일시정지할 타이머의 ID</param>
    public static void PauseTimer(int id)
    {
        if (activeTweens.TryGetValue(id, out Tween tween))
        {
            tween.Pause();
        }
    }

    /// <summary>
    /// 특정 ID의 타이머를 재개합니다.
    /// </summary>
    /// <param name="id">재개할 타이머의 ID</param>
    public static void ResumeTimer(int id)
    {
        if (activeTweens.TryGetValue(id, out Tween tween))
        {
            tween.Play();
        }
    }

    /// <summary>
    /// 모든 활성 타이머를 중지합니다.
    /// </summary>
    public static void StopAllTimers()
    {
        foreach (var tween in activeTweens.Values)
        {
            tween.Kill();
        }
        activeTweens.Clear();
    }

    /// <summary>
    /// 특정 ID의 타이머가 실행 중인지 확인합니다.
    /// </summary>
    /// <param name="id">확인할 타이머의 ID</param>
    /// <returns>타이머 실행 여부</returns>
    public static bool IsTimerRunning(int id)
    {
        return activeTweens.ContainsKey(id) && activeTweens[id].IsPlaying();
    }
}

 

사용예제

SimpleTimer를 사용한 지연실행 (Invoke 대체)

void Start()
{
    SimpleTimer.StartTimer(5f, () =>
    {
        Debug.Log("5초 후 실행되었습니다.");
    });
}

 

쿨다운 시스템 구현 (콜백 활용)

public class AbilityController : MonoBehaviour
{
    private bool isCooldown = false;
    private int cooldownTimerId;

    public void UseAbility()
    {
        if (!isCooldown)
        {
            // 능력 발동 로직
            Debug.Log("능력 사용!");

            // 쿨다운 시작
            isCooldown = true;
            cooldownTimerId = SimpleTimer.StartTimer(10f, () =>
            {
                isCooldown = false;
                Debug.Log("쿨다운 종료! 능력을 다시 사용할 수 있습니다.");
            });
        }
        else
        {
            Debug.Log("능력이 쿨다운 중입니다.");
        }
    }
}

 

UI업데이트

(5초 동안 진행되는 타이머를 시작하고, onUpdate 콜백을 통해 남은 시간을 기반으로 UI 진행바를 업데이트)

using UnityEngine.UI;

public class TimerUI : MonoBehaviour
{
    public Image progressBar;

    void Start()
    {
        SimpleTimer.StartTimer(5f, () =>
        {
            Debug.Log("타이머 완료!");
            progressBar.fillAmount = 0f;
        },
        (remainingTime) =>
        {
            progressBar.fillAmount = remainingTime / 5f;
        });
    }
}

 

페이즈 전환 타이밍 조절

(각 페이즈가 일정 시간 동안 진행되고, 타이머 완료 시 다음 페이즈로 전환됩니다.)

public class PhaseManager : MonoBehaviour
{
    private int phase = 1;

    void Start()
    {
        StartPhase(phase);
    }

    void StartPhase(int phaseNumber)
    {
        Debug.Log($"페이즈 {phaseNumber} 시작");

        // 페이즈별 지속 시간 설정 (예: 각 페이즈는 10초)
        float phaseDuration = 10f;

        SimpleTimer.StartTimer(phaseDuration, () =>
        {
            phase++;
            if (phase <= 3)
            {
                StartPhase(phase);
            }
            else
            {
                Debug.Log("모든 페이즈 완료");
            }
        });
    }
}

 

 

현재 씬 전환 시 자동으로 정리가 되도록 해두었지만

 

만약 특별한 상황이 있다면 직접 정리하는 함수를 호출해주어야합니다.(우린아님)