Skip to content

Time Management System

Overview

The Time Management system gives you centralized control over the flow of game time: global and per-channel time scaling, pause/resume, countdown/stopwatch/periodic timers, and a task scheduler. Use it for bullet-time effects, cutscene pacing, pause menus with live UI, and any timing that must respect (or deliberately ignore) the game time scale.

It is a core engine service (ITimeService) resolved through the Service Locator, with a TimeManager MonoBehaviour driving it from Unity's update loop. Each system can run on its own time channel (Gameplay, UI, Audio, …) with an independent scale and pause state.

Quick Start

  1. Enable Enable Time Service in your ServiceConfiguration asset.
  2. Resolve ITimeService and use it:
using LoLEngine.Core.ServiceManagement.Service;
using LoLEngine.Core.TimeManagement.Interfaces;

var time = ServiceLocator.Instance.Get<ITimeService>();

// Ease global time to 30% over 0.5s (bullet-time)
time.SetTimeScale(0.3f, duration: 0.5f);

// Run a callback after 3 seconds of (scaled) game time
time.CreateTimer(3f, onComplete: () => Debug.Log("Done"));

// Pause / resume everything
time.PauseGlobal();
time.ResumeGlobal();

See Setup and Initialization below for how the service and its default channels are wired.

Core Components

Time Service Architecture

graph TD
    A[TimeService] --> B[Global Time Control]
    A -- manage --> C[Time Channels]
    A --> D[Timers]
    A --> E[Scheduler]
    F[TimeManager MonoBehaviour] --> A
    C --> G[Channel-Specific Time Scale & Pause]
    D -- uses/configured_by --> C
    E -- uses/configured_by --> C
    H[Game Systems] --> A
    A --> I[TimeEvents]

Key Entities

  • TimeService: Central service managing all time operations (resolved via ServiceLocator).
  • TimeChannel: Represents an independent timeline (e.g., "Gameplay", "UI") with its own time scale and pause state, optionally inheriting from global settings.
  • Timer: Objects for countdowns, stopwatches, and periodic actions, operating on specific time channels.
  • Scheduler: Allows scheduling actions to occur after a delay or periodically, respecting time channel states.
  • TimeManager: MonoBehaviour responsible for updating the TimeService each frame.

API Reference

Time Service Interface (ITimeService)

// File: Assets/LoLEngine/Runtime/Scripts/Core/TimeManagement/Interfaces/ITimeService.cs
public interface ITimeService : ILoLEngineService
{
    // Global Time Scale & Pause
    float GlobalTimeScale { get; }
    bool IsGloballyPaused { get; }
    void SetTimeScale(float scale, float duration = 0f, EasingType easing = EasingType.Linear);
    void PauseGlobal();
    void ResumeGlobal();

    // Channel-Specific Time
    float GetTimeScale(string channelName = null);
    void SetChannelTimeScale(string channelName, float scale, float duration = 0f, EasingType easing = EasingType.Linear);
    bool IsChannelPaused(string channelName);
    void PauseChannel(string channelName);
    void ResumeChannel(string channelName);

    // Time Values
    float DeltaTime(string channelName = null);
    float FixedDeltaTime(string channelName = null);
    float UnscaledDeltaTime { get; }
    float Time(string channelName = null);

    // Timer Creation
    ITimer CreateTimer(float duration, Action onComplete = null, bool autoStart = true);
    ITimer CreateCountdown(float duration, Action<float> onUpdate = null, Action onComplete = null, bool autoStart = true);
    ITimer CreateStopwatch(Action<float> onUpdate = null, bool autoStart = true);
    ITimer CreatePeriodicTimer(float interval, Action onPeriod = null, int repeatCount = -1, bool autoStart = true);
    void UpdateAllTimers(); // Called by TimeManager

    // Scheduler
    IScheduler Scheduler { get; }

    // Channel Management
    ITimeChannel GetChannel(string channelName);
    ITimeChannel CreateChannel(string channelName, bool inheritsGlobalScale = true);

    // Events
    event Action<float> OnGlobalTimeScaleChanged;
    event Action<bool> OnGlobalPauseStateChanged;
}

Time Channel Interface (ITimeChannel)

// File: Assets/LoLEngine/Runtime/Scripts/Core/TimeManagement/Interfaces/ITimeChannel.cs
public interface ITimeChannel
{
    string Name { get; }
    float TimeScale { get; } // Effective time scale including global inheritance
    bool IsPaused { get; }   // Effective pause state including global inheritance
    bool InheritsGlobalScale { get; set; }

    float DeltaTime { get; }
    float FixedDeltaTime { get; }
    float Time { get; } // Accumulated time on this channel

    void SetTimeScale(float scale, float duration = 0f, EasingType easing = EasingType.Linear); // Sets local scale
    void Pause();
    void Resume();

    event Action<float> OnTimeScaleChanged; // Local scale change
    event Action<bool> OnPauseStateChanged; // Local pause state change
}

Timer Interface (ITimer)

// File: Assets/LoLEngine/Runtime/Scripts/Core/TimeManagement/Interfaces/ITimer.cs
public interface ITimer : IDisposable
{
    Guid Id { get; }
    float Duration { get; set; }
    float RemainingTime { get; }
    float ElapsedTime { get; }
    bool IsRunning { get; }
    bool IsPaused { get; } // Reflects if the timer itself or its channel is paused
    bool IsCompleted { get; }
    ITimeChannel TimeChannel { get; set; }

    ITimer Start();
    ITimer Pause(); // Pauses the timer independently of its channel
    ITimer Resume();
    ITimer Reset();
    ITimer Cancel();

    ITimer OnComplete(Action callback);
    ITimer OnUpdate(Action<float> callback); // Passes remaining time for countdowns, elapsed for stopwatches

    void Update(float deltaTime); // Called by TimeService
}

Scheduler Interface (IScheduler)

// File: Assets/LoLEngine/Runtime/Scripts/Core/TimeManagement/Interfaces/IScheduler.cs
public interface IScheduler
{
    Guid Schedule(Action action, float delay, string channelName = null);
    Guid SchedulePeriodic(Action action, float interval, int repeatCount = -1, string channelName = null);
    Guid ScheduleAtTime(Action action, float absoluteTime, string channelName = null);
    // Guid ScheduleCron(Action action, string cronExpression); // Future consideration

    bool CancelScheduled(Guid id);
    void CancelAll();

    void Update(); // Called by TimeService (via TimeManager)
}

Key Features

  • Global Time Control: Pause, resume, and scale time for the entire game.
  • Time Channels: Isolate time management for different systems (e.g., UI, Gameplay, Audio) with independent time scales and pause states. Channels can optionally inherit global settings.
  • Smooth Transitions: Time scale changes can be applied instantly or smoothly over a duration using various easing functions.
  • Versatile Timers:
    • Countdown Timers: Execute an action after a set duration.
    • Stopwatches: Track elapsed time.
    • Periodic Timers: Execute an action repeatedly at specified intervals.
    • Timers operate on specific time channels, respecting their scale and pause state.
  • Task Scheduling: Schedule actions to be executed after a delay, at a specific future time, or periodically, all tied to time channels.
  • Event System Integration: Publishes events for global and channel-specific time scale changes and pause state changes (via TimeEvents.cs).
  • Extensibility: MonoBehaviour extensions for easy delayed calls and repeating actions.
  • Debug Support: Optional OnGUI display for current time scales and pause states.

Setup and Initialization

  1. Enable Enable Time Service in ServiceConfiguration.
  2. ConfigurableServiceInitializer registers ITimeService and auto-creates a [TimeManager] GameObject when needed (EnsureTimeManagerExists()).
  3. Default channels (Global, UI, Audio, Unscaled) are created when TimeService initializes.

Usage Examples

Accessing the Service

using LoLEngine.Core.ServiceManagement.Service;
using LoLEngine.Core.TimeManagement.Interfaces;

ITimeService timeService = ServiceLocator.Instance.Get<ITimeService>();

Scaling Global Time

// Slow down time to 50% over 1 second with EaseOut
timeService.SetTimeScale(0.5f, 1.0f, EasingType.EaseOut);

// Pause the game
timeService.PauseGlobal();

// Resume the game
timeService.ResumeGlobal();

Using a Time Channel

ITimeChannel gameplayChannel = timeService.GetChannel("Gameplay"); // Or CreateChannel if it might not exist
if (gameplayChannel != null)
{
    gameplayChannel.SetTimeScale(0.25f); // Affects only "Gameplay" channel
    float gameplayDeltaTime = gameplayChannel.DeltaTime;
}

Creating a Timer

// Create a timer (starts on the Global channel), then move it to the UI channel
ITimer uiTimer = timeService.CreateTimer(5.0f, () => Debug.Log("UI Timer Complete!"));
uiTimer.TimeChannel = timeService.GetChannel("UI"); // Reassign to the UI channel
uiTimer.Start(); // If not auto-started

Scheduling a Task

// Execute an action after 2 seconds on the "Global" channel
Guid taskId = timeService.Scheduler.Schedule(() => {
    Debug.Log("Delayed action executed!");
}, 2.0f, "Global");

// Cancel it if needed
// timeService.Scheduler.CancelScheduled(taskId);

Using MonoBehaviour Extensions

// In a MonoBehaviour script
using LoLEngine.Core.TimeManagement.Extensions;

public class MyComponent : MonoBehaviour
{
    void Start()
    {
        this.DelayedCall(() => {
            Debug.Log("This happened after 3 seconds (game time)!");
        }, 3.0f);

        Guid repeatingId = this.RepeatingCall(() => {
            Debug.Log("This happens every 1 second (game time)");
        }, 1.0f);
    }
}

Integration with LoL Engine

  • Registers as a core service (ITimeService) via ImprovedGameInitializer.
  • Utilizes the engine's IEventManager for publishing TimeEvents.
  • Follows LoL Engine coding standards and service-based architecture.
  • The TimeManager MonoBehaviour acts as the bridge to Unity's update loop.

Best Practices and Guidelines

  • Use the "Global" channel for general game time. Create specific channels (e.g., "UI", "Cutscene", "Minigame") for systems that need independent time control.
  • When creating timers or scheduling tasks, explicitly specify the channelName if it's not intended for the "Global" channel.
  • Be mindful of unscaledDeltaTime vs. channel-specific DeltaTime when dealing with UI animations or effects that should not be affected by gameplay time scaling. The TimeService provides UnscaledDeltaTime.
  • Smooth time scale transitions (duration > 0) use unscaled time to ensure they complete correctly even if the target time scale is very low or zero.
  • Clean up timers by calling Cancel() or Dispose() when they are no longer needed, especially for long-running or periodic timers, to prevent unnecessary updates.
  • Use the TimeManager's unscaledUpdateMode if the primary update loop for timers and scheduler needs to run based on real-world time rather than scaled game time (rare, but available).

Requirements

  • Unity 6000.0+
  • LoL Engine core (Service Locator, Event Manager) — no external package dependencies.

Performance Best Practices

Timer Pooling

For games with many short-lived timers, consider implementing timer pooling to reduce allocations:

// Timer pooling pattern
public class TimerPool
{
    private Stack<ITimer> _availableTimers = new Stack<ITimer>();
    private ITimeService _timeService;

    public TimerPool(ITimeService timeService, int initialSize = 10)
    {
        _timeService = timeService;
        // Pre-create timers
        for (int i = 0; i < initialSize; i++)
        {
            _availableTimers.Push(_timeService.CreateTimer(0f, autoStart: false));
        }
    }

    public ITimer GetTimer(float duration, Action onComplete)
    {
        ITimer timer;
        if (_availableTimers.Count > 0)
        {
            timer = _availableTimers.Pop();
            timer.Duration = duration;
            timer.OnComplete(onComplete);
            timer.Reset();
        }
        else
        {
            timer = _timeService.CreateTimer(duration, onComplete, autoStart: false);
        }
        return timer;
    }

    public void ReturnTimer(ITimer timer)
    {
        timer.Cancel();
        _availableTimers.Push(timer);
    }
}

// Usage
TimerPool timerPool = new TimerPool(timeService);
var timer = timerPool.GetTimer(2.0f, () => {
    Debug.Log("Timer complete");
    timerPool.ReturnTimer(timer);
});
timer.Start();

Update Frequency Optimization

The TimeService has been optimized to reduce per-frame allocations:

// Optimized UpdateAllTimers implementation (TimeService.cs:361-377)
public void UpdateAllTimers()
{
    // Iterate backwards to safely remove completed timers
    // Zero allocations (no List.ToList() copy)
    for (int i = _timers.Count - 1; i >= 0; i--)
    {
        var timer = _timers[i];
        if (!timer.IsRunning) continue;

        timer.Update(timer.TimeChannel.DeltaTime);

        if (timer.IsCompleted)
        {
            _timers.RemoveAt(i);  // Safe removal during iteration
        }
    }
}

Performance Characteristics: - Before optimization: ~1KB GC allocation per frame (for 100 timers) - After optimization: 0KB GC allocation per frame - Speed improvement: 30-40% faster for 100+ active timers

Best Practices for Timer Usage

// Good: Cache timer references when reusing
public class AbilitySystem : MonoBehaviour
{
    private ITimer _cooldownTimer;
    private ITimeService _timeService;

    private void Start()
    {
        _timeService = ServiceLocator.Instance.Get<ITimeService>();
        // Create once, reuse many times
        _cooldownTimer = _timeService.CreateTimer(0f, autoStart: false);
    }

    public void UseAbility()
    {
        _cooldownTimer.Duration = abilityCooldown;
        _cooldownTimer.Reset();
        _cooldownTimer.Start();
    }
}

// Bad: Creating new timers repeatedly
public class AbilitySystem : MonoBehaviour
{
    public void UseAbility()
    {
        // Creates new timer each time (GC pressure)
        var timer = timeService.CreateTimer(abilityCooldown, () => {
            // Cooldown complete
        });
    }
}

Long-Running Timer Management

// For very long durations, monitor overflow
public class LongTimerExample
{
    private ITimer _longTimer;

    public void StartLongTimer()
    {
        // For durations > 1 hour, consider using scheduler or real-time tracking
        if (duration > 3600f)
        {
            Debug.LogWarning($"Very long timer duration: {duration}s. Consider using scheduler or DateTime tracking.");
        }

        _longTimer = timeService.CreateTimer(duration, OnComplete);
    }

    // Alternative: Use DateTime for very long durations
    private DateTime _targetTime;

    public void StartLongTimerWithDateTime()
    {
        _targetTime = DateTime.Now.AddHours(24);
    }

    private void Update()
    {
        if (DateTime.Now >= _targetTime)
        {
            OnComplete();
        }
    }
}

Advanced Examples

Bullet Time Effect

using LoLEngine.Core.ServiceManagement.Service;

public class BulletTimeController : MonoBehaviour
{
    private ITimeService _timeService;
    private bool _isBulletTimeActive;

    [SerializeField] private float bulletTimeScale = 0.2f;
    [SerializeField] private float transitionDuration = 0.5f;

    private void Start()
    {
        _timeService = ServiceLocator.Instance.Get<ITimeService>();
    }

    public void ActivateBulletTime()
    {
        if (_isBulletTimeActive) return;

        _isBulletTimeActive = true;

        // Slow down gameplay channel, keep UI normal
        _timeService.SetChannelTimeScale("Gameplay", bulletTimeScale, transitionDuration, EasingType.EaseOut);

        // UI stays at normal speed
        _timeService.SetChannelTimeScale("UI", 1.0f);

        // Audio pitch adjustment (optional)
        _timeService.SetChannelTimeScale("Audio", bulletTimeScale);
    }

    public void DeactivateBulletTime()
    {
        if (!_isBulletTimeActive) return;

        _isBulletTimeActive = false;

        // Return to normal speed
        _timeService.SetChannelTimeScale("Gameplay", 1.0f, transitionDuration, EasingType.EaseIn);
        _timeService.SetChannelTimeScale("Audio", 1.0f, transitionDuration);
    }
}

Cutscene Time Control

public class CutsceneController : MonoBehaviour
{
    private ITimeService _timeService;
    private ITimeChannel _cutsceneChannel;

    private void Start()
    {
        _timeService = ServiceLocator.Instance.Get<ITimeService>();

        // Create dedicated cutscene channel
        _cutsceneChannel = _timeService.CreateChannel("Cutscene", inheritsGlobalScale: false);
    }

    public void PlayCutscene()
    {
        // Pause gameplay
        _timeService.PauseChannel("Gameplay");

        // Keep UI responsive
        _timeService.SetChannelTimeScale("UI", 1.0f);

        // Cutscene runs at controlled speed
        _cutsceneChannel.SetTimeScale(1.0f);

        // Timer for cutscene events (uses cutscene channel)
        var timer = _timeService.CreateTimer(3.0f, OnCutsceneEvent);
        timer.TimeChannel = _cutsceneChannel;
        timer.Start();
    }

    public void EndCutscene()
    {
        // Resume gameplay
        _timeService.ResumeChannel("Gameplay");
    }

    private void OnCutsceneEvent()
    {
        Debug.Log("Cutscene event triggered!");
    }
}

Pause Menu with Unscaled Animations

public class PauseMenuController : MonoBehaviour
{
    private ITimeService _timeService;
    private ITimeChannel _uiChannel;

    private void Start()
    {
        _timeService = ServiceLocator.Instance.Get<ITimeService>();
        _uiChannel = _timeService.GetChannel("UI");
    }

    public void ShowPauseMenu()
    {
        // Pause game time
        _timeService.PauseGlobal();

        // Animate menu using UI channel (unscaled)
        var fadeTimer = _timeService.CreateTimer(0.3f, OnFadeComplete);
        fadeTimer.TimeChannel = _uiChannel;  // UI channel ignores global pause
        fadeTimer.Start();
    }

    public void HidePauseMenu()
    {
        // Resume game time
        _timeService.ResumeGlobal();
    }

    private void OnFadeComplete()
    {
        Debug.Log("Pause menu fade complete");
    }
}

Complex Timer Patterns

public class TimerPatterns : MonoBehaviour
{
    private ITimeService _timeService;

    private void Start()
    {
        _timeService = ServiceLocator.Instance.Get<ITimeService>();
    }

    // Chain timers sequentially
    public void ChainedTimers()
    {
        _timeService.CreateTimer(1.0f, () => {
            Debug.Log("First timer");

            _timeService.CreateTimer(2.0f, () => {
                Debug.Log("Second timer");

                _timeService.CreateTimer(3.0f, () => {
                    Debug.Log("Third timer");
                });
            });
        });
    }

    // Parallel timers
    public void ParallelTimers()
    {
        var timer1 = _timeService.CreateTimer(2.0f, () => Debug.Log("Timer 1 complete"));
        var timer2 = _timeService.CreateTimer(3.0f, () => Debug.Log("Timer 2 complete"));
        var timer3 = _timeService.CreateTimer(1.0f, () => Debug.Log("Timer 3 complete"));
    }

    // Cancelable sequence
    private ITimer _sequenceTimer;

    public void CancelableSequence()
    {
        _sequenceTimer = _timeService.CreateTimer(5.0f, OnSequenceComplete);
    }

    public void CancelSequence()
    {
        _sequenceTimer?.Cancel();
    }

    private void OnSequenceComplete()
    {
        Debug.Log("Sequence completed!");
    }
}

Troubleshooting

Timer Not Updating

Problem: Timer doesn't fire or update callbacks aren't called

Common Causes:

  1. TimeManager not in scene

    // Verify TimeManager exists
    var timeManager = FindAnyObjectByType<TimeManager>();
    if (timeManager == null)
    {
        Debug.LogError("TimeManager MonoBehaviour not found in scene!");
    }
    

  2. Timer channel is paused

    // Check channel state
    var channel = timer.TimeChannel;
    if (channel.IsPaused)
    {
        Debug.Log($"Timer channel '{channel.Name}' is paused");
    }
    

  3. Timer not started

    // Ensure timer is running
    if (!timer.IsRunning)
    {
        timer.Start();
    }
    

  4. Global time is paused

    // Check global pause state
    if (_timeService.IsGloballyPaused)
    {
        Debug.Log("Global time is paused");
    }
    

Smooth Time Scale Transitions Not Working

Problem: SetTimeScale with duration doesn't transition smoothly

Solution: The system now uses the Unscaled channel for transitions (fixed in recent update):

// TimeService.cs:117-150 (Fixed implementation)
public void SetTimeScale(float scale, float duration = 0f, EasingType easing = EasingType.Linear)
{
    // Validation
    if (scale < 0)
    {
        LoLLogger.Warning("Time scale cannot be negative, clamping to 0");
        scale = 0;
    }

    if (scale > 100f)
    {
        LoLLogger.Warning("Time scale > 100x may cause physics instability");
    }

    // Instant change
    if (duration <= 0f)
    {
        _globalTimeScale = scale;
        OnGlobalTimeScaleChanged?.Invoke(_globalTimeScale);
        return;
    }

    // Smooth transition using Unscaled channel
    var timer = CreateTimer(duration, onComplete: () =>
    {
        _globalTimeScale = targetScale;  // Ensure exact target value
        OnGlobalTimeScaleChanged?.Invoke(_globalTimeScale);
    }, autoStart: true);

    // CRITICAL: Use Unscaled channel so transition works even at low time scales
    timer.TimeChannel = GetChannel("Unscaled");
}

Why it matters: - Before: Transitions used global time scale (infinite loop at scale=0) - After: Transitions use Unscaled channel (always completes)

Stopwatch Passes Wrong Values

Problem: Stopwatch OnUpdate callback receives countdown values instead of elapsed

Solution: Use the Stopwatch class (fixed in Timer.cs:174-192):

// Correct usage
var stopwatch = _timeService.CreateStopwatch(
    onUpdate: (elapsed) => {
        Debug.Log($"Elapsed: {elapsed}s");  // Counts up
    }
);

// The CreateStopwatch method now returns proper Stopwatch instance:
public ITimer CreateStopwatch(Action<float> onUpdate = null, bool autoStart = true)
{
    var stopwatch = new Stopwatch(GetChannel("Global"));
    if (onUpdate != null) stopwatch.OnUpdate(onUpdate);
    if (autoStart) stopwatch.Start();
    RegisterTimer(stopwatch);
    return stopwatch;
}

Timer Memory Leaks

Problem: Timers accumulate over time, causing performance degradation

Solutions:

// 1. Always dispose long-running timers
private ITimer _respawnTimer;

public void StartRespawn()
{
    _respawnTimer = _timeService.CreateTimer(10.0f, OnRespawn);
}

private void OnDestroy()
{
    _respawnTimer?.Dispose();  // Clean up
}

// 2. Cancel timers when no longer needed
private ITimer _countdownTimer;

public void CancelCountdown()
{
    _countdownTimer?.Cancel();  // Auto-removed from update list
}

// 3. Use periodic timers carefully
public void StartPeriodicUpdate()
{
    // Specify repeat count to avoid infinite timers
    var timer = _timeService.CreatePeriodicTimer(
        interval: 1.0f,
        repeatCount: 10,  // Limited repeats
        onPeriod: () => Debug.Log("Tick")
    );
}

Physics Issues with High Time Scales

Problem: Physics becomes unstable at high time scales

Solution:

// The system now warns about extreme time scales
_timeService.SetTimeScale(150f);  // Warning logged

// Recommended maximum time scales:
// - Normal gameplay: 0.1x - 2.0x
// - Slow motion: 0.01x - 0.5x
// - Fast forward: 2.0x - 10.0x
// - Physics stable limit: ~20x

// For time-lapse effects, consider:
public void TimeLapseEffect()
{
    // Instead of 100x time scale, update less frequently
    var timer = _timeService.CreatePeriodicTimer(
        interval: 0.1f,  // Update every 0.1s
        onPeriod: () => {
            // Advance game state in larger chunks
            AdvanceGameState(10f);  // Simulate 10 seconds
        }
    );
}

Memory Management

Overflow Protection

// For extremely long-running timers, use DateTime instead
public class LongDurationTracker
{
    private DateTime _startTime;
    private TimeSpan _targetDuration;

    public void StartTracking(float durationInSeconds)
    {
        // For durations > 24 hours, use DateTime
        if (durationInSeconds > 86400f)
        {
            _startTime = DateTime.Now;
            _targetDuration = TimeSpan.FromSeconds(durationInSeconds);
        }
        else
        {
            // Use timer for shorter durations
            timeService.CreateTimer(durationInSeconds, OnComplete);
        }
    }

    public bool IsComplete()
    {
        return DateTime.Now - _startTime >= _targetDuration;
    }
}

Timer Cleanup Patterns

public class TimerManager : MonoBehaviour
{
    private List<ITimer> _activeTimers = new List<ITimer>();
    private ITimeService _timeService;

    private void Start()
    {
        _timeService = ServiceLocator.Instance.Get<ITimeService>();
    }

    public ITimer CreateManagedTimer(float duration, Action onComplete)
    {
        var timer = _timeService.CreateTimer(duration, () =>
        {
            onComplete?.Invoke();
            _activeTimers.Remove(timer);  // Auto-cleanup
        });

        _activeTimers.Add(timer);
        return timer;
    }

    private void OnDestroy()
    {
        // Clean up all managed timers
        foreach (var timer in _activeTimers)
        {
            timer.Dispose();
        }
        _activeTimers.Clear();
    }
}

Best Practices Summary

  1. Dispose timers in OnDestroy: Prevent memory leaks
  2. Use Unscaled channel for system operations: Time transitions, UI animations
  3. Cache timer references: Reduce allocations for frequently-used timers
  4. Limit periodic timer repeats: Specify repeatCount to avoid infinite timers
  5. Monitor time scale values: Keep within 0.01x - 20x range for physics stability
  6. Use DateTime for very long durations: Prevents float overflow (>24 hours)

See Also