TsvrcTimer
Tsvrc.Timing.TsvrcTimer is a networked, pausable timer built on Process.
Its generated shadow is TsTimer. All clients share server
time anchors and compute elapsed time
locally, so a running timer sends nothing per tick beyond the periodic resync of
Process. See Run a synced
countdown for a task-oriented walkthrough.
Getting an instance
TsvrcTimer is one of the builtin pool entries, so a
[WirePool] field of this type resolves with nothing to register, and codegen calls TsConstruct
on it. If you place the shipped TsTimer prefab in your scene yourself instead, call
TsConstruct on it before using it.
Usage
public class RoundClock : TsBehaviour
{
[WirePool][SerializeField] private TsvrcTimer _timer;
protected override void TsStart()
{
_timer.TsSubscribe(this, TsvrcTimer.OnTimerCompletedEvent, nameof(_OnRoundEnded));
_timer.StartTimer(60_000); // 60-second round
}
public void _OnRoundEnded() => LogInfo("Round timer finished.");
}
To show the countdown, read _timer.GetRemainingSeconds() from your own update loop.
OnTimerUpdatedEvent is for reacting to state changes, not for redrawing a UI every frame.
Starting, stopping, pausing
StartTimer()counts up with no end.DurationMsstays0.StartTimer(int durationMilliseconds)completes once elapsed time reaches the duration. A negative value counts as0, a timer with no end.
Either overload makes the calling player the owner first, so the timer runs on the player who starts it. Calling either one while the timer runs does nothing: it doesn't restart the timer or change its duration.
StopTimer()and the inheritedCompleteProcess()end the timer early or complete it. Both send the call to the owner when another player makes it.PauseTimer()andResumeTimer()freeze and unfreeze elapsed time. A pause extends the run: the completion check inOnProcessUpdateusesGetElapsedMilliseconds(), which stays frozen while paused, so the full duration always runs. Another player calling either one sends it to the owner throughRequestPauseTimerorRequestResumeTimer, both[NetworkCallable]and limited to two calls per second.
Reading elapsed and remaining time
GetElapsedMilliseconds() and GetElapsedSeconds() compute from the synced server-time anchor
while running, and return the frozen offset while stopped or paused. GetRemainingMilliseconds()
and GetRemainingSeconds() return DurationMs - GetElapsedMilliseconds(), and 0 for a timer
with no end, one past its duration, or a completed one.
LastElapsedMilliseconds holds the last value the timer computed. Read it when you need what the
timer last reported: it updates on every start, stop, pause, resume and deserialization, and every
0.25 seconds while running and not paused, from the same local loop that drives
OnTimerUpdatedEvent.
Events
Each transition has a protected virtual hook to override in a subclass and a string constant to
pass to TsSubscribe. Both fire on every client, the hook first:
| Hook | Event constant | Fires when |
|---|---|---|
OnTimerStarted | OnTimerStartedEvent | The timer starts, including when a late joiner finds it running |
OnTimerStopped | OnTimerStoppedEvent | The timer stops before completion |
OnTimerCompleted | OnTimerCompletedEvent | The timer completes: duration reached, or CompleteProcess called |
OnTimerPaused | OnTimerPausedEvent | The timer pauses, including when a late joiner finds it paused |
OnTimerResumed | OnTimerResumedEvent | The timer resumes |
OnTimerUpdated | OnTimerUpdatedEvent | The elapsed time, running state or paused state seen locally changed |
OnTimerDeserialization | OnTimerDeserializationEvent | Every OnDeserialization call, whether or not anything changed |
How other players detect transitions
On the owner, the OnProcessStarted, Stopped and Completed hooks of Process call the timer
hooks directly. Other players compare the running and paused flags in each packet against what they
saw last, inside OnDeserialization. Two cases matter if you rely on the exact order of events:
- A late joiner arriving mid-run sees the timer go from never seen to running in one packet.
That counts as a start:
OnTimerStartedfires, andOnTimerPausedright after it if the timer is paused. - A stop and a restart in one packet. Manual sync doesn't send on
RequestSerializationright away. It waits for its rate limit and sends whatever the synced fields hold at that moment. A listener that starts a new run fromOnTimerStoppedorOnTimerCompleted(possible becauseSendCustomNetworkEvent(All, ...)runs on the sender right away) can produce one packet in which the timer never stopped.TsvrcTimercounts runs in_runId, increased in everyOnProcessStarted. When the timer stays running across a packet but_runIdchanged, the timer treats it as a new start. Whether the old run stopped or completed is lost by then, so only the start of the new run (and its pause, if any) fires.
Edge cases
- The read methods return
0on a timer that never started, and none of them throw. - Elapsed time never goes negative. If
_elapsedOffsetMs + deltaoverflowsint, it becomes0instead of wrapping to a negative value. OnProcessCleanupkeeps the server-time anchors of the last run, so other players can still show the final elapsed time after the timer stops.OnProcessStartedresets them at the start of the next run.