Scheduler
Run a callback later in UCC’s shared Update or FixedUpdate loop, and keep a cancellable handle when the work must stop with its owner. Use the Scheduler for small game-time delays and repeating controller work, not as an ownerless replacement for every coroutine or update method.
When to use the Scheduler
The Scheduler is a good fit when code needs to:
- finish a projectile, effect, state, or animation fallback after a short game-time delay;
- defer physics or locomotion work to a later FixedUpdate;
- repeat a small callback every Update or FixedUpdate without adding another
MonoBehaviourupdate method; or - cancel pending work when an ability, item, pooled object, or scene owner stops.
Keep an ordinary Update, FixedUpdate, coroutine, or purpose-built UCC lifecycle when the operation needs complex sequencing, yields, unscaled time, per-character time, or several independently visible states.
Set up the Scheduler
No scene component is required for a basic call. The first Scheduler.Schedule or Scheduler.ScheduleFixed finds an existing SchedulerBase; if none exists, it creates a GameObject named Scheduler with a SchedulerBase component.
Add one Scheduler component deliberately when capacity and scene ownership matter. Its Max Event Count defaults to 200 and allocates separate arrays of that size for Update and FixedUpdate callbacks during Awake. Change the value before Play Mode. In Play Mode, its Inspector shows the scheduled callback target and method for diagnosis.
Keep that Scheduler in a scene that remains loaded for as long as its callbacks are expected to run. It is a global queue, not a queue owned by the component that scheduled each callback.
Choose when the callback runs
Both scheduling methods calculate their end time from TimeUtility.Time. Without a project TimeManagerBase, that is Unity’s scaled Time.time.
| Requirement | Choice | Observable behavior |
|---|---|---|
| General gameplay, presentation, respawn, or UI-adjacent work | Scheduler.Schedule |
The callback runs from the Scheduler’s next eligible Update after the global end time. |
| Physics, locomotion, or item work that must align with a physics step | Scheduler.ScheduleFixed |
The callback runs from the next eligible FixedUpdate after the same global end time. |
| Run synchronously | A delay of 0 |
On an enabled Scheduler, the action runs inside the Schedule call and the method returns null; nothing enters the queue. |
| Repeat every rendered frame | Scheduler.Schedule(-1, action) |
The action runs every Update until explicitly cancelled. |
| Repeat every physics step | Scheduler.ScheduleFixed(-1, action) |
The action runs every FixedUpdate until explicitly cancelled. |
A positive delay is a minimum, not an exact timestamp: the callback waits until its selected loop next checks the queue. Do not use negative delays other than the documented -1 repeating value.
The released Shared Scheduler has no unscaled-time overload. At the default clock, a positive delay pauses when Time.timeScale is 0; FixedUpdate also stops running. Use an owner-managed timer based on Time.unscaledTime or Time.unscaledDeltaTime when a pause menu, connection timeout, or other real-time operation must continue.
The -1 sentinel is loop-driven rather than elapsed-time-driven. An Update repeater continues while global time is paused because Unity still calls Update; a FixedUpdate repeater waits because Unity stops physics steps at a zero time scale.
The Scheduler also does not read an individual UltimateCharacterLocomotion.TimeScale. A slowed or paused character’s scheduled callback still follows the global TimeUtility.Time. Keep character-time work in the owning UCC lifecycle, or accumulate time explicitly from that character’s scale when it must adapt to changes during the delay. A project TimeManagerBase changes the Scheduler’s clock globally, not per character.
Own the handle lifecycle
Every nonzero call returns a ScheduledEventBase handle. Store it when the callback might need to be cancelled.
- Cancel the previous handle before replacing it.
- Assign the new handle returned by
ScheduleorScheduleFixed. - At the start of a one-shot callback, set the owning field to
null. - On early completion, disable, destruction, or pool return, call
Scheduler.Cancel(handle)and immediately set the field tonull. - Explicitly cancel every
-1repeating event; it never removes itself.
Scheduled event objects are internally obtained from GenericObjectPool. A one-shot handle is removed from the active queue before its callback and returned to that pool after the callback finishes. Cancellation also returns it. The same object can then become another system’s event, so a completed or cancelled handle is no longer yours: do not retain it, poll its Active property later, or cancel it again.
Active is useful only while the owner still controls a pending handle. A stale reference may become active again when the internal object is reused for an unrelated callback.
Clean up with the owner
The Scheduler does not inspect the callback target’s lifetime and does not automatically cancel by GameObject, component, ability, item, or scene. A disabled component’s callback still runs unless it is cancelled, and a delegate can retain captured objects until the event is reused.
- Cancel owner-bound work in
OnDisablewhen disabling means the operation has ended. - Use
OnDestroyas a final cleanup path for work that intentionally survives a temporary disable. - For a pooled GameObject, cancel in
OnDisable, clear the handle, and schedule a fresh event after each checkout. - For an ability or item module, cancel when the action stops or the module is no longer active, not only when the character is destroyed.
- Avoid lambdas that capture a large object graph when a method plus one of the Scheduler’s typed parameter overloads can pass the required values directly.
Some lifecycles intentionally outlive a disabled target. The built-in Respawner, for example, can schedule a respawn while its GameObject is inactive because the independent Scheduler remains active. Decide that ownership explicitly rather than applying OnDisable cancellation to every use case.
The Scheduler singleton reference resets at subsystem registration and after its scene unloads. Active callbacks do not migrate to a replacement Scheduler. Stop or transfer the owning operation before unloading the Scheduler’s scene.
Verify in Play Mode
- Schedule one visible one-shot result with a positive delay. Confirm it does not run immediately and runs once from the intended Update or FixedUpdate phase.
- Schedule it again, cancel the handle before the end time, set the field to
null, and confirm the result never occurs. - Disable or return the owner to its pool before the delay completes. Confirm no callback changes the inactive or reused object.
- Re-enable or reuse the owner and trigger it twice. Confirm one current handle exists and the callback occurs only once for the latest use.
- Pause global time. Confirm ordinary Scheduler delays wait until global time resumes; separately test an owner-managed unscaled timer if the feature requires one.
- If the feature uses a character-specific time scale, slow only that character and confirm whether the intended behavior should follow the character or the global Scheduler clock.
- Select the scene Scheduler in Play Mode. Confirm its active count returns to the baseline after completion or cancellation and the Console has no capacity or missing-reference error.
Troubleshooting
| Symptom | Check | Fix |
|---|---|---|
The callback runs immediately and the saved handle is null. |
The supplied delay is 0. |
Call the method directly when synchronous execution is intended, or use a positive delay when a queued, cancellable callback is required. |
| A callback runs after an ability, component, or pooled object has stopped. | Its handle was not cancelled in the owner’s stop, disable, destroy, or return path. | Cancel the current handle and set the field to null before the owner can be reused. |
| Cancelling one operation stops an unrelated callback. | Code retained a handle after its callback or cancellation, and that pooled handle was reused. | Clear the field during both completion and cancellation. Never reuse or inspect an expired handle. |
| A delay does not progress while the game is paused. | Scheduler uses scaled TimeUtility.Time, and ScheduleFixed also needs FixedUpdate to run. |
Use an explicit unscaled timer for real-time work; do not expect a Scheduler overload to change clocks. |
| A slowed character’s callback fires at normal game speed. | Scheduler has no per-character time-scale parameter. | Keep the timer in the character-owned lifecycle and advance it using the intended character-scale rule. |
The Console reports that the ActiveEvents array is full. |
Concurrent Update or FixedUpdate events reached Max Event Count, commonly because -1 events or ownerless callbacks were not cancelled. |
Cancel leaked work, reduce repeating callbacks, then raise Max Event Count before Play Mode only if the measured workload requires it. |
| No callback runs after a scene transition. | The scene that owned the Scheduler unloaded, or the explicit Scheduler component was disabled. | Keep the Scheduler in the required long-lived scene and reschedule work under the new scene owner. Do not schedule against a disabled Scheduler. |
Related pages
API examples
Cancel a one-shot event safely
This component assumes its root was checked out through the Object Pool. It schedules a fresh return on every activation, clears the field before the callback returns the GameObject, and cancels an early disable.
using Opsive.Shared.Game;
using UnityEngine;
public sealed class TimedPooledEffect : MonoBehaviour
{
[Min(0.001f)]
[SerializeField] private float m_Lifetime = 1.5f;
private ScheduledEventBase m_ReturnEvent;
private void OnEnable()
{
CancelReturn();
m_ReturnEvent = Scheduler.Schedule(m_Lifetime, ReturnToPool);
}
private void ReturnToPool()
{
m_ReturnEvent = null;
ObjectPool.Destroy(gameObject);
}
private void OnDisable()
{
CancelReturn();
}
private void CancelReturn()
{
if (m_ReturnEvent == null) {
return;
}
Scheduler.Cancel(m_ReturnEvent);
m_ReturnEvent = null;
}
}
For an object that is not pool-owned, replace ObjectPool.Destroy with the completion behavior owned by that object.
Pass values without a capturing lambda
The released API provides zero-, one-, two-, and three-parameter overloads for both loops. Each public overload returns ScheduledEventBase.
using Opsive.Shared.Game;
using UnityEngine;
public sealed class DelayedMarker : MonoBehaviour
{
private ScheduledEventBase m_MarkerEvent;
public void ShowLater(GameObject marker, Vector3 position, float delay)
{
CancelMarker();
m_MarkerEvent = Scheduler.Schedule(
delay, ShowMarker, marker, position);
}
private void ShowMarker(GameObject marker, Vector3 position)
{
m_MarkerEvent = null;
marker.transform.position = position;
marker.SetActive(true);
}
private void OnDisable()
{
CancelMarker();
}
private void CancelMarker()
{
if (m_MarkerEvent == null) {
return;
}
Scheduler.Cancel(m_MarkerEvent);
m_MarkerEvent = null;
}
}
The principal signatures are Schedule(float, Action), Schedule<T>(float, Action<T>, T), and the matching two- and three-parameter forms. ScheduleFixed provides the same shapes for FixedUpdate. Cancel(ScheduledEventBase) removes a still-active handle. ScheduledEventBase.EndTime, Location, and Active are readable, but only while the caller still owns that pending handle.