Quick answer: Time-comparison triggers like Timeline.Time ≥ 1.5 can fire one frame late because the playhead only samples on tick. Use On keyframe reached with a named keyframe as the trigger — Construct fires it exactly once when playback crosses that point, regardless of frame timing.

Here is how to fix Construct 3 timeline events that fire at slightly the wrong time. You author a timeline with a keyframe at 1.5 seconds. You add an event On Timeline.Time ≥ 1.5 to trigger a sound. The sound plays roughly 1.5 seconds in — but sometimes at 1.516 seconds, sometimes at 1.483 seconds, and on slower machines it can drift by a noticeable amount. Your timeline feels loose.

The Symptom

You are syncing game events to a timeline animation. Sound effects should land on keyframes. Particle bursts should align with an object arriving at its target pose. Dialogue should advance in time with character lip-sync. Instead, sounds fire a frame or two after the visual beat. Particles appear after the impact pose has already begun moving out. Lip-sync drifts over a long timeline.

If you log Timeline.Time when your event fires, you see values like 1.5167, 1.5333, 1.5500 on a 60 fps project. The event condition Timeline.Time ≥ 1.5 evaluated true on the first tick where the sampled time met or exceeded 1.5, which depending on the frame cadence could be anywhere from 1.5 to 1.516 or worse.

On a 30 fps project the jitter doubles. On projects with playback speed scales applied to the timeline, the drift stretches in real time proportionally.

What Causes This

Construct 3 samples a timeline exactly once per tick. The engine advances Timeline.Time by dt multiplied by playback scale, reads interpolated keyframe values at the new time, and dispatches events. A condition like Timeline.Time ≥ 1.5 is checked at the new time; the earliest tick on which it can be true is the tick where the sampled time first crosses 1.5. If the frame before was at 1.4833 and the next frame lands at 1.5 exactly, you get a clean hit — but if it lands at 1.5167, you are already 1/60 of a second late.

Time-comparison triggers are effectively polling. They cannot fire more often than once per tick and cannot distinguish which instant during the tick the threshold was crossed. For one-shot events synchronized to specific time values, polling always carries frame-aligned jitter.

Additionally, timeline playback scale affects real-world timing but not internal timeline time. A scale of 0.5 means every tick advances internal time by half of dt. The polling still happens once per tick, but the time window in which a tick could cross the threshold is now compressed, so jitter is proportionally smaller in internal time but identical in real-world wall clock time.

A subtler cause: authoring events against Timeline.CurrentTime when the timeline is paused or reversed. If you expect monotonic increasing time and the user scrubs the timeline, your comparison fires repeatedly or not at all.

The Fix

Step 1: Use On keyframe reached. In the Events sheet, add a condition from the Timeline object called On keyframe reached. Specify the keyframe tag. Construct fires this trigger exactly once per playback pass across the keyframe, regardless of whether the sampled time landed exactly on it. Multiple keyframes can share tags or have unique ones depending on how you want to route events.

Add keyframes at the precise times your events should fire, tag them descriptively (impact, attack_hit, dialogue_next), and attach each event to its corresponding keyframe-reached trigger.

Step 2: For time-driven logic without a dedicated keyframe, use On timeline tick with a crossing check. If you cannot add a keyframe (for example, triggering on a value interpolated by the timeline rather than a time position), compare the previous and current time to catch the exact tick of crossing:

// Event sheet pseudocode
Global variable prev_time = 0

On Timeline "Attack" tick:
  If prev_time < 1.5 and Timeline.Time >= 1.5:
    Audio.Play("hit_sfx")
  Set prev_time = Timeline.Time

This fires exactly once on the first tick whose sampled time is ≥ 1.5, even if the engine is running at 15 fps. You still have frame-aligned jitter in when the event fires relative to wall clock, but you will never fire it twice and never miss it.

Step 3: Adjust the timeline step rate. In the Timeline bar, the Step property controls how frequently Construct samples interpolated values. If step is 0.1 and you have keyframes at 0.05-second intervals, the engine interpolates but only re-dispatches events at 0.1-second intervals. Set step to match your frame time (approximately 0.0166 for 60 fps) for tight event timing.

Step 4: Account for playback scale. When you change playback scale, internal time advances differently from real time. For any logic that needs to happen at a real-world moment (like starting background music in sync), use system time or schedule a wait rather than a timeline condition.

If you need sub-frame timing for audio, consider scheduling the audio via an audio clip offset rather than a timeline event. Audio engines run on a sample clock much finer than the frame tick and will be in tighter sync.

Why This Works

On keyframe reached bypasses the polling model. When the engine advances the playhead from time a to time b in a single tick, it enumerates every keyframe in the interval (a, b] and fires the reached trigger for each. You get exactly one event per crossing, regardless of how much time elapsed in one tick. This makes the timing frame-rate independent at the event level.

The previous-current comparison pattern achieves similar correctness for non-keyframe triggers. By comparing the sampled time in the previous and current tick, you detect the moment of crossing without double-firing on subsequent ticks.

Related Issues

If your Construct 3 timelines play at inconsistent speeds between devices, see Fix: Construct 3 Delta Time Variable Frame Rate Issues — the root cause is often minimum framerate settings.

For audio events that desync from visual events over time, see Fix: Construct 3 Audio Sync Drift in Long Cutscenes.

Keyframe reached for one-shots. Prev/current comparison for value crossings. Never use Time ≥ x alone.