Unity's Timeline is highly extensible, and by using not only the standard Animation and Activation tracks, but also the Playable API (`PlayableAsset` and `PlayableBehaviour`), you can place completely unique C# control scripts on the timeline. However, when implementing unique parameter rewriting (e.g. flickering lights, transitioning shader parameters, changing the color of materials, etc.), parameter fixation problems occur almost 100% of the time in practice, such as ``Even though the cutscene has finished playing, the character's face remains pale and does not return'' or ``Property values ​​become messed up when you move the seek bar.'' In this article, we present a robust implementation pattern that fundamentally solves this bug in lifecycle control.

1. Root of the problem: Deactivation of Playable graph and absence of state restoration

Timeline's evaluation system cycles through each node (Playable) in the graph every frame and calls the `ProcessFrame` callback to calculate and reflect the parameters. However, when Timeline playback reaches the end point and stops, or when a Timeline component is deactivated, the Playable system suddenly pauses without any aftercare. **

In other words, `ProcessFrame` suddenly stops being called at a certain frame, and as a result, the controlled object is left with the value of the last called frame (usually at the moment when the effect is at its maximum or during a transition). If it is a standard Unity Animation track, the internal animation clip binding will handle state restoration (Write Default, etc.), but in the case of a `PlayableBehaviour` created by yourself using a C# script, the parameter will be stuck at the overwritten value forever unless you explicitly program the value restoration process yourself.

2. Remaining reference bindings causing memory leaks

Even more serious is C#'s reference management problem. `PlayableBehaviour` internally holds scene objects (references such as `Light` and `MeshRenderer`) bound from `PlayableDirector`. If these reference pointers are not cleaned up when a scene transitions or the Director is destroyed, the Playable graph's internal data structure will crush those references and remain in memory, making it impossible for the GC (garbage collector) to destroy the target object from memory. This is the cause of chronic memory leaks, where memory is gradually compressed as scenes are repeatedly loaded and unloaded.

3. Solution: Robust code pattern for state rollback and cleanup

In order to prevent this problem, it is necessary to write an implementation pattern that correctly handles the life cycle of `PlayableBehaviour` (playback start, stop, graph destruction), and ensures that initial values ​​are saved and restored and references are cleared.

Below is a robust implementation code that takes as an example a "custom playable that dynamically controls the brightness of a light" that can be used in practice.

① PlayableAsset (class that holds data and sets binding)

using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

[System.Serializable]
public class CustomLightControlAsset : PlayableAsset, ITimelineClipAsset
{
    // Parameter data set in the inspector
 public Color lightColor = Color.white;
 public float intensityMultiplier = 1.0f;

 // Define the function of the clip on the Timeline
 public ClipCaps clipCaps => ClipCaps.Blending;

 public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
 {
 // Create an instance of Behavior and pour the parameters
 var playable = ScriptPlayable.Create(graph);
 var behavior = playable.GetBehaviour();
 
 behavior.lightColor = lightColor;
 behavior.intensityMultiplier = intensityMultiplier;

 return playable;
 }
}

② PlayableBehaviour (control and restoration/leak prevention class for each frame)

using UnityEngine;
using UnityEngine.Playables;

public class CustomLightControlBehaviour : PlayableBehaviour
{
    // Control parameters (copied from Asset)
 public Color lightColor;
 public float intensityMultiplier;

 // Reference cache of controlled object
 private Light targetLight;

 // Save variable to remember the state before playback
 private float originalIntensity;
 private Color originalColor;
 private bool hasCachedState = false; // Flag to prevent double caching

 // 1. Called at the start of playback (when seeking or entering a clip)
 public override void OnBehaviourPlay(Playable playable, FrameData info)
 {
 // Resolve the operation target object (Light) from the graph binding and obtain a reference
 if (targetLight == null)
 {
 // info.output is the binding destination object (if it cannot be resolved, search from the caller etc.)
 // In order to simplify the implementation, here we use a configuration that caches from the scene
 }
 }

 // Update processing every frame
 public override void ProcessFrame(Playable playable, FrameData info, object playerData)
 {
 targetLight = playerData as Light;
 if (targetLight == null) return;

 // Strictly remember (save) the original state (initial value) at the moment when processing is performed for the first time
 if (!hasCachedState)
 {
 originalIntensity = targetLight.intensity;
 originalColor = targetLight.color;
 hasCachedState = true;
 }

 // Interpolate calculation based on the clip progress (0 to 1)
 double time = playable.GetTime();
 double duration = playable.GetDuration();
 float progress = (float)(time / duration);

 // Dynamically overwrite the value
 targetLight.intensity = originalIntensity + (intensityMultiplier * progress);
 targetLight.color = Color.Lerp(originalColor, lightColor, progress);
 }

 // 2. Called when playback ends/pauses (when exiting clip, stopping timeline)
 public override void OnBehaviourPause(Playable playable, FrameData info)
 {
 RestoreOriginalState();
 }

 // 3. Called when destroying playable instance
 public override void OnPlayableDestroy(Playable playable)
 {
 RestoreOriginalState();
 }

 // Completely restore state (rollback)
 private void RestoreOriginalState()
 {
 if (targetLight != null && hasCachedState)
 {
 targetLight.intensity = originalIntensity;
 targetLight.color = originalColor;
 hasCachedState = false; // Reset flag
 }
 }

 // 4. Called when the Playable graph itself is completely destroyed (key to prevent memory leaks)
 public override void OnGraphDestroy(Playable playable)
 {
 // Make all references to scene objects null and allow collection by GC
 targetLight = null;
 }
}

The most important thing about this implementation is that the `hasCachedState` flag saves the initial values ​​only once, and that `RestoreOriginalState()` is called in both `OnBehaviourPause` / `OnPlayableDestroy`. This ensures that the light intensity returns to its original brightness even if the timeline is sought in the middle or playback ends. Also, setting `targetLight = null` in `OnGraphDestroy` completely prevents leaks where object references continue to remain in memory.