# **Timeline Basics in Unity 6 (6000.5+)** > **Target:** Unity 6000.5 and above. Packages: **Timeline** (`com.unity.timeline`, 1.8.x) and **Cinemachine 3** (`com.unity.cinemachine` 3.x — namespace `Unity.Cinemachine`, component `CinemachineCamera`). Input uses the Input System package. What we'll build: a short door-opening cutscene — a door animates, a light switches on, a sound plays, the camera cuts between two Cinemachine cameras, a Signal fires a gameplay event at the end, and a custom track fades a UI/overlay. Then we control it from code: play, pause, skip, and scrub. --- ## 1. Concepts in one minute - **Timeline Asset** (`.playable`) — the data: tracks and clips. Reusable across scenes. - **PlayableDirector** — the scene component that plays a Timeline Asset and holds **bindings** (which scene object each track drives). - **Tracks** — Activation, Animation, Audio, Signal, Control, Playable, plus package tracks (Cinemachine) and your own custom tracks. - **Clips** — time ranges on a track. Under the hood each clip creates a `Playable` in a `PlayableGraph`. --- ## 2. Create the Timeline 1. Install **Timeline** and **Cinemachine** from Package Manager (Unity Registry). 2. Create an empty GameObject `Cutscene_Door`. With it selected, open **Window > Sequencing > Timeline** and click **Create** → save `Door.playable`. Unity adds a **PlayableDirector** and an Animator track bound to the object. 3. On the PlayableDirector: - **Play On Awake** — off (we'll trigger from code/trigger zone). - **Wrap Mode** — *Hold* (stay on last frame), *Loop*, or *None* (reset and stop). - **Update Method** — *Game Time* (scaled), *Unscaled Game Time* (plays while paused/timeScale 0), *DSP* (audio sync), *Manual* (you call `Evaluate`). 4. Lock the Timeline window (padlock) so it doesn't switch when you select other objects. --- ## 3. Tracks ### Activation track Add (+) **Activation Track**, bind the `DoorLight` GameObject. The clip range = when the object is active. **Post-playback state** (track inspector): Active / Inactive / Revert / Leave As Is. ### Animation track 1. + **Animation Track**, bind the `Door` (needs an Animator — Unity offers to add one). 2. Click the red **Record** button on the track, move to frame 0, set rotation, move to 1s, rotate door 90°. Stop recording. This creates an *Infinite clip*; right-click > **Convert To Clip Track** to get a regular movable clip. 3. Or drag existing AnimationClips onto the track. Use **Clip In**, **Speed Multiplier**, **Ease In/Out** and overlap two clips to blend. 4. **Track Offsets** (track inspector): *Apply Transform Offsets* plays root motion relative to the object's start position; *Apply Scene Offsets* uses current scene pose. 5. Override tracks: right-click an Animation track > **Add Override Track** with an Avatar Mask for upper-body only. ### Audio track + **Audio Track**, bind an **AudioSource** (optional; without binding it plays 2D). Drag a creak sound clip in. Volume/pan per track; clips support ease-in/out fades. Set Director **Update Method = DSP** if tight audio sync matters. ### Signal track Signals fire events at a point in time. 1. Project > **Create > Timeline > Signal** → `DoorOpened.signal`. 2. + **Signal Track**, bind the GameObject that will have a **SignalReceiver** (Unity offers to add one). 3. Right-click the track at 2.5s > **Add Signal Emitter**, choose `DoorOpened`. 4. Select the emitter; in its inspector the bound **SignalReceiver** reaction appears — hook a UnityEvent to e.g. `GameEvents.OnDoorOpened()`. 5. Emitter options: **Retroactive** (fire if playback starts after the marker), **Emit Once** (only once per loop). You can also place markers on the timeline's **Markers** row (top) — then the receiver must be on the PlayableDirector object itself. ### Control track Activates child GameObjects, plays particle systems, or nested PlayableDirectors (sub-timelines) for the clip duration. ### Cinemachine track (Cinemachine 3) 1. Main Camera needs a **CinemachineBrain** (GameObject > Cinemachine > Cinemachine Camera adds one automatically). 2. Create two cameras: `CM_Wide` and `CM_Close` (**GameObject > Cinemachine > Cinemachine Camera**), set Tracking Target to the door. 3. + **Cinemachine Track**, bind the Main Camera (CinemachineBrain). 4. Right-click > **Add Cinemachine Shot Clip**; in the clip choose `CM_Wide`. Add another later with `CM_Close`. **Overlap the clips to blend**; a hard cut otherwise. 5. Timeline overrides the Brain's priority logic while the track is active; when it ends, normal priority blending resumes. --- ## 4. Signal receiver script ```csharp // Assets/Scripts/Timeline/DoorEvents.cs using UnityEngine; using UnityEngine.Events; public class DoorEvents : MonoBehaviour { public UnityEvent onDoorOpened; // Hook this from the SignalReceiver reaction in the inspector. public void DoorOpened() { Debug.Log("Door opened signal received"); onDoorOpened?.Invoke(); } } ``` For signals with parameters, write a custom marker. `SignalEmitter` is a `Marker` that implements `INotification`; you can create your own: ```csharp // Assets/Scripts/Timeline/DialogueMarker.cs using UnityEngine; using UnityEngine.Playables; using UnityEngine.Timeline; // A marker that carries data, placed on any track's marker area or the Markers row. public class DialogueMarker : Marker, INotification { public string line = "Hello."; public PropertyName id => new PropertyName("DialogueMarker"); } ``` ```csharp // Assets/Scripts/Timeline/DialogueMarkerReceiver.cs using UnityEngine; using UnityEngine.Playables; // Put on the GameObject bound to the track (or on the PlayableDirector object for Markers-row markers). public class DialogueMarkerReceiver : MonoBehaviour, INotificationReceiver { public void OnNotify(Playable origin, INotification notification, object context) { if (notification is DialogueMarker m) Debug.Log(quot;[Dialogue] {m.line}"); } } ``` --- ## 5. Triggering, skipping and scrubbing from code ```csharp // Assets/Scripts/Timeline/CutsceneController.cs using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.Playables; using UnityEngine.Timeline; [RequireComponent(typeof(PlayableDirector))] public class CutsceneController : MonoBehaviour { public bool playOnce = true; [Range(0f, 1f)] public float scrub; // drag in Play Mode to scrub (when paused) PlayableDirector director; bool played; void Awake() { director = GetComponent<PlayableDirector>(); director.played += d => Debug.Log("Cutscene started"); director.paused += d => Debug.Log("Cutscene paused"); director.stopped += d => Debug.Log("Cutscene finished/stopped"); } // Trigger zone entry (2D or 3D — keep the one you use). void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) Play(); } void OnTriggerEnter2D(Collider2D other) { if (other.CompareTag("Player")) Play(); } public void Play() { if (playOnce && played) return; played = true; director.time = 0; director.Play(); } // Swap timelines and rebind at runtime (e.g., same cutscene, different actor). public void PlayWith(TimelineAsset asset, GameObject actor) { director.playableAsset = asset; foreach (var output in asset.outputs) { if (output.sourceObject is AnimationTrack) director.SetGenericBinding(output.sourceObject, actor.GetComponent<Animator>()); } director.RebuildGraph(); director.Play(); } void Update() { var kb = Keyboard.current; if (kb == null) return; if (kb.pKey.wasPressedThisFrame) TogglePause(); if (kb.escapeKey.wasPressedThisFrame) Skip(); // Scrub only while paused so we don't fight playback. if (director.state == PlayState.Paused && director.playableGraph.IsValid()) { director.time = scrub * director.duration; director.Evaluate(); // apply the pose/state at that time immediately } } void TogglePause() { if (director.state == PlayState.Playing) { director.Pause(); scrub = (float)(director.time / director.duration); } else director.Resume(); } // Jump to the end so final state (door open, signals with Retroactive) applies. public void Skip() { director.time = director.duration; director.Evaluate(); director.Stop(); } } ``` *Notes:* - `Evaluate()` applies the timeline at `time` without advancing — the basis for scrubbing, previews, and Manual update mode. - With **Wrap Mode = Hold**, the graph keeps playing on the final frame; `stopped` won't fire until you call `Stop()`. Use *None* if you need the event. - Skipping past signals: non-retroactive emitters won't fire on a jump. Mark important ones **Retroactive**. --- ## 6. Custom Playable: a screen-fade track Three classes: a **PlayableBehaviour** (runtime logic per clip), a **PlayableAsset** (the clip asset that creates the behaviour), and a **TrackAsset** (the track type, its binding, and optionally a mixer). ```csharp // Assets/Scripts/Timeline/FadeBehaviour.cs using System; using UnityEngine; using UnityEngine.Playables; [Serializable] public class FadeBehaviour : PlayableBehaviour { public Color color = Color.black; [Range(0f, 1f)] public float alpha = 1f; } ``` ```csharp // Assets/Scripts/Timeline/FadeClip.cs using System; using UnityEngine; using UnityEngine.Playables; using UnityEngine.Timeline; [Serializable] public class FadeClip : PlayableAsset, ITimelineClipAsset { public FadeBehaviour template = new FadeBehaviour(); // Blending lets clip ease-in/out drive the fade. public ClipCaps clipCaps => ClipCaps.Blending; public override Playable CreatePlayable(PlayableGraph graph, GameObject owner) { return ScriptPlayable<FadeBehaviour>.Create(graph, template); } } ``` ```csharp // Assets/Scripts/Timeline/FadeMixerBehaviour.cs using UnityEngine; using UnityEngine.Playables; using UnityEngine.UI; // Blends all clip inputs by weight and writes to the bound Image. public class FadeMixerBehaviour : PlayableBehaviour { Color defaultColor; bool initialized; Image image; public override void ProcessFrame(Playable playable, FrameData info, object playerData) { image = playerData as Image; if (image == null) return; if (!initialized) { defaultColor = image.color; initialized = true; } Color blended = Color.clear; float totalWeight = 0f; int count = playable.GetInputCount(); for (int i = 0; i < count; i++) { float w = playable.GetInputWeight(i); if (w <= 0f) continue; var input = (ScriptPlayable<FadeBehaviour>)playable.GetInput(i); FadeBehaviour b = input.GetBehaviour(); Color c = b.color; c.a = b.alpha; blended += c * w; totalWeight += w; } // Outside any clip, fall back to the original color. image.color = blended + defaultColor * (1f - totalWeight); } public override void OnPlayableDestroy(Playable playable) { if (initialized && image != null) image.color = defaultColor; initialized = false; } } ``` ```csharp // Assets/Scripts/Timeline/FadeTrack.cs using UnityEngine; using UnityEngine.Playables; using UnityEngine.Timeline; using UnityEngine.UI; [TrackColor(0.1f, 0.1f, 0.1f)] [TrackClipType(typeof(FadeClip))] [TrackBindingType(typeof(Image))] public class FadeTrack : TrackAsset { public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount) { return ScriptPlayable<FadeMixerBehaviour>.Create(graph, inputCount); } } ``` **Use it:** 1. Canvas (Screen Space - Overlay) > full-screen **UI Image** `FadeOverlay`, color black, alpha 0, Raycast Target off. (Requires the `com.unity.ugui` package, included by default.) 2. In the Timeline: + > **Fade Track** (appears under your custom tracks), bind `FadeOverlay`. 3. Add a **Fade Clip** at the start (alpha 1 → ease-out 0.5s reveals the scene) and one at the end (ease-in to black). 4. Scrub the Timeline in Edit Mode — the mixer runs in preview too. *Why a mixer:* without `CreateTrackMixer`, each clip would write independently and overlaps/ease curves couldn't blend. The mixer receives all clip playables as inputs with weights computed by Timeline from clip timing and easing. --- ## 7. Worked example checklist 1. `Cutscene_Door` (PlayableDirector + `CutsceneController` + BoxCollider trigger, Play On Awake off, Wrap Mode Hold). 2. Tracks: Animation (Door 0–1.5s), Activation (DoorLight 1.0–end), Audio (creak at 0.2s), Cinemachine (CM_Wide 0–1.2s blending into CM_Close 1.0–3s), Signal (`DoorOpened` at 2.5s, Retroactive), Fade (in at start, out at end). 3. `GameEvents` object with `SignalReceiver` + `DoorEvents`; reaction → `DoorEvents.DoorOpened`. 4. Play: walk into trigger → cutscene; **P** pauses and enables scrub slider; **Esc** skips to the end state. **Troubleshooting** - Track does nothing → check binding in the PlayableDirector's Bindings list (rebinding needed when the asset is used in a new scene). - Animated object snaps to world origin → enable *Apply Transform Offsets* or use Scene Offsets. - Cinemachine shots ignored → Main Camera missing CinemachineBrain, or track not bound to it. - Custom track not listed → script compile errors, or the class isn't a `TrackAsset` with `[TrackClipType]`. **Docs:** Timeline package manual (com.unity.timeline); Cinemachine 3 manual › Cinemachine and Timeline; Scripting API `PlayableDirector`, `PlayableBehaviour`, `TrackAsset`, `INotificationReceiver`. ## See also - [[Animation]] — Animator tracks / Avatar Mask overrides pair with Mecanim fundamentals