Scheduler

One-shot delays, repeating timers, next-frame callbacks, and a safe bridge from background threads to the main thread.

Run code later — after a delay, every interval, next frame, or back on the main thread.

InterfaceIScheduler
Off switchNullScheduler
AssemblyCommonGameSystem.Core
StartupRegistered automatically at boot — no setup needed

What it does

The Scheduler runs your actions later: once after a delay (After), repeatedly at a fixed interval (Every), on the next frame (NextFrame), or on the main thread when posted from a background thread (Post / RunOnMain).

Every timer is bound to a clock — a named time stream owned by the Time service. There are three: Gameplay pauses when the game pauses and slows under slow motion, UI keeps running during pause menus, and Background always runs. A cooldown timer on the Gameplay clock freezes automatically while a menu is open — you write no pause logic at all.

Post is the one thread-safe entry point in the framework's timing stack, so background tasks (file loading, network calls) can hand results back to the main thread without touching Unity APIs off-thread.

Quick start

Resolve once in Awake() or Start() and cache the reference:

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)] // Run after the framework has started.
public class MyTimerUser : MonoBehaviour
{
    private IScheduler _scheduler;

    private void Awake()
    {
        _scheduler = ServiceLocator.Resolve<IScheduler>();
    }
}

SL is a shorter alias for ServiceLocator: var scheduler = SL.Resolve<IScheduler>();

API reference

One-shot delays

Each call returns a token; dispose the token to cancel the timer before it fires.

MemberNotes
IDisposable After(float delaySeconds, Action callback)Fires once after the delay, on the Gameplay clock (pauses with the game).
IDisposable After(float delaySeconds, Clock clock, Action callback)Fires once after the delay, on the clock you choose.

Repeating timers

You must dispose the returned token to stop the timer.

MemberNotes
IDisposable Every(float intervalSeconds, Action callback)Repeats at a fixed interval on the Gameplay clock.
IDisposable Every(float intervalSeconds, Clock clock, Action callback)Repeats at a fixed interval on the clock you choose.

Next frame

Frame-based, not time-based — runs even while the game is paused.

MemberNotes
IDisposable NextFrame(Action callback)Runs the callback on the next frame.

Background thread to main thread (thread-safe)

MemberNotes
void Post(Action action)Queues the action from any thread. Queued actions run on the next main-thread tick, in the order they were posted.
void RunOnMain(Action action)On the main thread, runs the action immediately. On a background thread, queues it like Post. Safe to call from inside another scheduled callback.

Diagnostics

MemberNotes
int PendingCount { get; }Approximate count of live timers plus queued posts. For profiling only.

Examples

using System;
using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]
public class CooldownManager : MonoBehaviour
{
    [SerializeField] private GameObject enemyPrefab; // assign in the Inspector

    private IScheduler _scheduler;
    private IDisposable _aiLoopToken;

    private void Awake()
    {
        _scheduler = ServiceLocator.Resolve<IScheduler>();
    }

    // One-shot: "in 2 seconds, spawn an enemy".
    public void SpawnEnemyIn2Seconds()
    {
        _scheduler.After(2f, () => Instantiate(enemyPrefab));
    }

    // Repeating: "tick the AI every 0.5 seconds; pause when the game pauses".
    public void StartAiLoop()
    {
        _aiLoopToken = _scheduler.Every(0.5f, Clock.Gameplay, TickAi);
    }

    private void TickAi()
    {
        // Runs every 0.5 game-seconds. Pauses automatically while menus are open.
    }

    // Next frame: "refresh the UI on the next frame".
    public void RefreshUiNextFrame()
    {
        _scheduler.NextFrame(RefreshUi);
    }

    private void RefreshUi()
    {
        // Update health bars, labels, and so on.
    }

    // Background thread to main thread: "loading finished, update the UI safely".
    public void StartAsyncLoad()
    {
        System.Threading.Tasks.Task.Run(() =>
        {
            string data = LoadDataExpensively();
            _scheduler.Post(() => OnDataLoaded(data)); // safe from any thread
        });
    }

    private string LoadDataExpensively() => "loaded"; // your real loading work here

    private void OnDataLoaded(string data)
    {
        // Update the UI with the loaded data.
    }

    // Stop the repeating timer when this object goes away.
    private void OnDestroy()
    {
        _aiLoopToken?.Dispose();
    }
}

Turning it off

ServiceLocator.Replace<IScheduler>(new NullScheduler());

This silences all scheduling. After, Every, and NextFrame register nothing and return a token that does nothing. Post and RunOnMain drop their actions. PendingCount is 0. Useful for stress tests or for temporarily disabling scheduled logic. The first call logs a warning, so an accidental swap stays visible.

Common pitfalls

  • Pick the right clock. A wrong clock fails silently — no error, just wrong pause behavior. Use Clock.Gameplay (the default) for cooldowns and AI. Use Clock.UI for menu and HUD animation that must keep running during pause. Use Clock.Background for work that always runs, such as music fades. See the Time service for the full clock contract.
  • Every requires disposal. If you discard the returned token, the timer runs forever. Store it in a field and call Dispose() when done — usually in OnDestroy().
  • Post is the only thread-safe entry point. Only Post(), and RunOnMain() when called from a background thread, may be used off the main thread. Every other method throws InvalidOperationException when called from a worker thread.
  • Callbacks run on the frame. An exception in one callback is logged and contained — other timers still fire. A slow callback, however, delays the frame like any other main-thread work.
  • NullScheduler mutes everything, including Post. If you rely on background-to-main dispatch, replacing the scheduler with NullScheduler silently drops those results.
  • Time — clocks, pause, and time scale
  • Tween — value animation on the same clocks