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.
| Interface | IScheduler |
| Off switch | NullScheduler |
| Assembly | CommonGameSystem.Core |
| Startup | Registered 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.
| Member | Notes |
|---|---|
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.
| Member | Notes |
|---|---|
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.
| Member | Notes |
|---|---|
IDisposable NextFrame(Action callback) | Runs the callback on the next frame. |
Background thread to main thread (thread-safe)
| Member | Notes |
|---|---|
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
| Member | Notes |
|---|---|
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. UseClock.UIfor menu and HUD animation that must keep running during pause. UseClock.Backgroundfor work that always runs, such as music fades. See the Time service for the full clock contract. Everyrequires disposal. If you discard the returned token, the timer runs forever. Store it in a field and callDispose()when done — usually inOnDestroy().Postis the only thread-safe entry point. OnlyPost(), andRunOnMain()when called from a background thread, may be used off the main thread. Every other method throwsInvalidOperationExceptionwhen 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.
NullSchedulermutes everything, includingPost. If you rely on background-to-main dispatch, replacing the scheduler withNullSchedulersilently drops those results.