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 See also (added 2026-11-10): [[Boss Battles with Phases]] / [[4. Timeline Intros and Phase Transitions]] - a PlayableDirector intro that hands control back to a boss FSM.