Time
One clock surface for wall time, server time, frame time and a scaled game clock, so pause and slow motion are one call.
- Realm
- Shared
- Require
ReplicatedStorage.ChloeKernel .Time - Since
- v0.7.0
- Source
- Time.luau
Time.attach(kernel) sets kernel.Time to one object with every clock a game needs: monotonic wall time, the shared
server clock, frame time, and a scaled game clock that honors pause and slow motion. The point is the scaled clock. When
every gameplay timer reads kernel.Time:scaled(), “pause the game” is one pause() and “half-speed bullet time” is one
setScale(0.5), instead of an audit of every system that reads os.clock() on its own.
VfxSuite follows the same switch, so engine particles slow and freeze with the game.
Mental model
Section titled “Mental model”Time keeps no per-frame state and connects to no frame event. Since 0.9.0 it computes frame() and scaled() from
stored anchors when you read them; before, a Heartbeat connection added to two accumulators every frame. The values are
the same.
now()returns the injectedClock,os.clockby default.server()returnsworkspace:GetServerTimeNow().frame()returns the seconds since attach, read from the engine’s frame-quantizedtime()(or theFrameClockoption), plus anything added throughstep(dt). It never pauses and ignores the scale.scaled()returnsAnchorScaled + (frame() - AnchorFrame) * Scale, orAnchorScaledalone while paused.
setScale, pause and resume each re-anchor before they change anything: the span since the last anchor is folded
into AnchorScaled at the old rate, and AnchorFrame moves to now. So a change applies from the moment it is made and
never rewrites time already passed. Dropping the scale from 1 to 0.5 slows scaled() from then on; it never jumps
backwards, which is what happens if you multiply total elapsed time by the current scale. scaled() can only hold still
(paused, or scale 0) or climb.
A scale change while paused re-anchors without adding anything and takes effect when the game resumes.
Spec seams
Section titled “Spec seams”SkipLoop = true leaves out the frame clock entirely, so frame time moves only through step(dt). Clock injects the
source for now(), and FrameClock (added in 0.9.0) injects the source for frame():
local Clock = Time.attach(Kernel, { SkipLoop = true })Clock:step(1) -- frame() is 1, scaled() is 1Clock:setScale(0.5)Clock:step(1) -- frame() is 2, scaled() is 1.5Attach in a service’s init, so every service that depends on it can read kernel.Time in start:
--!strict-- ClockService: owns kernel.Time and the bullet-time ability.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Time = require(ReplicatedStorage.ChloeKernel.Time)
local ClockService = { Name = "ClockService",}
function ClockService.init(self: any, kernel: any) self.Kernel = kernel self.Clock = Time.attach(kernel)end
function ClockService.start(self: any) local Kernel = self.Kernel self.Connection = Kernel.Bus:subscribe("Ability.BulletTime", function(_topic, seconds: number) self.Clock:setScale(0.5) -- every scaled() reader runs at half rate from now if self.Restore then self.Restore:cancel() end -- Scheduler deadlines count real seconds; the scale does not stretch them self.Restore = Kernel.Scheduler:after(seconds, function() self.Restore = nil self.Clock:setScale(1) end) end)end
function ClockService.stop(self: any) if self.Restore then self.Restore:cancel() end if self.Connection then self.Connection:disconnect() end self.Clock:destroy()end
return ClockServiceA timer that pauses with the game stores a scaled deadline and compares when it is used:
local EndsAt = kernel.Time:scaled() + 60
local function secondsLeft(): number return math.max(EndsAt - kernel.Time:scaled(), 0)endThe countdown, a pause menu calling pause() and resume(), and the ability above never coordinate. They all read
scaled(), and the control calls change what every reader sees at once. now() and frame() stay available for code
that must ignore pause and scale, such as the pause menu’s own animation or wall-clock logging.
API reference
Section titled “API reference”| Member | Description |
|---|---|
Time.attach(kernel, options?) -> TimeService |
Builds the clock, sets kernel.Time, and returns it. Connects nothing. |
time:now() -> number |
The Clock option, os.clock by default. |
time:server() -> number |
workspace:GetServerTimeNow(). Rewind validates claims against the same clock by default. |
time:frame() -> number |
Seconds since attach from the frame clock, plus step time. Never pauses. |
time:scaled() -> number |
Game seconds at the current scale, frozen while paused. The clock gameplay timers read. |
time:setScale(scale) |
Re-anchors, sets the scale and publishes Time.ScaleChanged on every call, even with the same value. Asserts a number that is not NaN and not negative. |
time:getScale() -> number |
The current scale. |
time:pause() |
Re-anchors and publishes Time.Paused, only when running. |
time:resume() |
Re-anchors and publishes Time.Resumed, only when paused. |
time:isPaused() -> boolean |
Whether the clock is paused. |
time:step(dt) |
Adds dt to frame time. With SkipLoop it is the only way frame time moves. |
time:destroy() |
Clears kernel.Time if it still points at this clock. |
Options, all optional:
| Field | Default | Description |
|---|---|---|
Clock |
os.clock |
Source for now(). |
FrameClock |
time |
Source for frame(). Ignored with SkipLoop. Added in 0.9.0. |
SkipLoop |
false |
No frame clock: frame time moves only through step(dt). |
Bus topics
Section titled “Bus topics”| Topic | Payload | Fired |
|---|---|---|
Time.ScaleChanged |
(scale) |
Every setScale call, including a repeat of the current value. |
Time.Paused |
() |
pause(), on the change from running to paused. |
Time.Resumed |
() |
resume(), on the change from paused to running. |
VfxSuite reads kernel.Time when it attaches and follows these topics: it sets the TimeScale of
live emitters, fire, smoke, sparkles and explosions to their authored value times the kernel scale, clamped to 0 to 1,
and 0 while paused. Attach Time before VfxSuite.