Quick answer: Timeline signals that work in the editor but not in a build are almost always an asset-stripping problem. Unity ships the Timeline but cannot see that a SignalAsset is referenced, so it either strips the receiver methods or loses the asset reference. Move SignalAsset files next to the Timeline, lower Managed Stripping, and add [Preserve] attributes to your receiver methods.
You wire up a cinematic: Timeline plays, a SignalEmitter marker fires, and a SignalReceiver on your door object opens the door. Editor: perfect. You build and run on Windows or iOS, and the cutscene plays fine but the door never opens. No errors. No warnings. Just silence. This is one of the most frustrating Timeline issues because every visible signal in the editor looks healthy.
The Symptom
The Timeline plays through in the built player — animations, audio, and Cinemachine cuts all work. But the SignalEmitter marker on the Signal Track does not trigger its corresponding UnityEvent. You add Debug.Log statements inside the receiver handler: nothing prints. You add Debug.Log to OnEnable of the SignalReceiver: that prints. You attach a development build, enable deep profiling, and watch for SignalReceiver.OnNotify — it is called, but the event does no work.
Alternate variant: you get a single-line warning like SignalAsset not found in Player.log but the game continues.
What Causes This
1. SignalAsset was stripped from the build. A SignalAsset is a ScriptableObject that acts as a globally unique identifier. The Timeline references it by GUID, and the SignalReceiver uses asset identity to look up the matching UnityEvent. If the asset lives in a folder that never gets referenced by a scene or a direct asset path (for example, a shared Signals/ folder that is outside Resources), Unity may exclude it from the build.
2. Managed Stripping removed the receiver methods. Unity’s IL2CPP and Mono strippers remove methods that appear unreachable. UnityEvent bindings use reflection to call target methods, which the stripper cannot see. Medium or High stripping removes those methods.
3. SignalReceiver mapping was lost during duplication. If you duplicated a Timeline with signals and forgot to reassign the SignalAsset on the receiver, the serialized mapping still exists but points to the original asset.
4. PlayableDirector never played the Timeline. This sounds obvious but is common: the build uses a different scene flow and the PlayableDirector starts disabled. Animations appear to play because of another director, but your signal-bearing timeline never runs.
5. SignalReceiver component disabled or destroyed. AOT platforms can occasionally re-serialize prefabs with their signal component toggled off if the prefab overrides disagreed with the source.
The Fix
Step 1: Move SignalAssets into a preserved location. The simplest fix is to put SignalAsset files in the same folder as the Timeline that uses them. This guarantees the dependency graph keeps them.
// Project structure that works in builds:
// Assets/Cinematics/Intro/
// IntroTimeline.playable
// OpenDoor.signalasset ← next to the timeline
// SpawnEnemies.signalasset
// Alternative: put in Resources/ to force inclusion
// Assets/Resources/Signals/OpenDoor.signalasset
Step 2: Preserve receiver methods from stripping.
using UnityEngine;
using UnityEngine.Scripting;
public class DoorController : MonoBehaviour {
[Preserve] // tells the linker to keep this method
public void OnSignalOpenDoor() {
animator.SetTrigger("Open");
}
[Preserve]
public void OnSignalSpawnEnemies() {
spawner.SpawnWave();
}
}
Step 3: Add a link.xml for the entire receiver type. If you have many receiver classes, preserving each method is tedious. A link.xml file in Assets/ is cleaner.
<!-- Assets/link.xml -->
<linker>
<assembly fullname="Assembly-CSharp">
<type fullname="DoorController" preserve="all" />
<type fullname="EnemySpawner" preserve="all" />
</assembly>
<assembly fullname="Unity.Timeline" preserve="all" />
</linker>
Step 4: Validate at runtime. Add a diagnostic that logs which signals are wired so you notice issues in Player.log without reproducing them.
using UnityEngine;
using UnityEngine.Timeline;
using UnityEngine.Playables;
[RequireComponent(typeof(SignalReceiver))]
public class SignalReceiverDiagnostic : MonoBehaviour {
void Start() {
var receiver = GetComponent<SignalReceiver>();
int count = receiver.Count();
Debug.Log($"[SignalDiag] {name} has {count} signal bindings");
for (int i = 0; i < count; i++) {
var asset = receiver.GetSignalAssetAtIndex(i);
Debug.Log($" [{i}] {(asset ? asset.name : "NULL")}");
}
}
}
Step 5: Ensure the PlayableDirector actually starts. Watch for Play On Awake being off or the director being on an inactive GameObject.
Step 6: Rebuild after reimporting Timeline assets. After changing asset locations or adding link.xml, do a full rebuild, not an incremental one. Asset dependency graphs are cached and stale data can hide the fix.
Why This Works
The Timeline signal system is three loosely-coupled pieces: a SignalEmitter (a marker on the track) references a SignalAsset, the PlayableDirector forwards fired emitters to a bound SignalReceiver, and the receiver looks up a UnityEvent by SignalAsset identity. Any one of these links can be broken by the build pipeline without producing an error — the lookup just returns no match and silently does nothing.
Managed stripping is the single biggest culprit because Unity’s static analysis cannot follow UnityEvent reflection. The event stores the target type and method name as strings, so the method body looks unreferenced at compile time. [Preserve] and link.xml tell the linker to keep the method regardless.
Co-locating SignalAsset files with their Timeline ensures they are in the same dependency subtree that the build includes. Resources folders work too but introduce runtime-loadable overhead you may not want.
"Silent signal failures in builds are not a Unity bug — they are a stripping outcome. Preserve your receivers and keep SignalAssets close to their Timelines."
Related Issues
If your Timeline animations drift out of sync with audio, see Fix: Unity Timeline Audio Drift in Build. For general IL2CPP stripping issues, Fix: Unity IL2CPP MethodNotFound Exception covers the broader pattern.
Editor runs on reflection; builds run on what the stripper kept. Preserve everything signals touch.