using UnityEditor; using UnityEngine; using UnityEngine.Playables; /// /// 自动检测并清理 PlayableDirector 中 missing 的 Binding 引用。 /// 不使用 [CustomEditor],不影响原生 Inspector 显示。 /// 触发时机:选中带有 PlayableDirector 的物体时自动检测清理。 /// 手动触发:PlayableDirector 组件右上角菜单 → "检查并清理 Missing Binding 引用"。 /// [InitializeOnLoad] public class PlayableDirectorBindingValidator { static PlayableDirectorBindingValidator() { Selection.selectionChanged += OnSelectionChanged; } [MenuItem("CONTEXT/PlayableDirector/检查并清理 Missing Binding 引用")] private static void ContextMenuClean(MenuCommand command) { var director = command.context as PlayableDirector; if (director == null) return; int cleaned = CleanMissingBindings(director); if (cleaned > 0) { Debug.Log($"[PlayableDirector Cleaner] {director.gameObject.name}: 清理了 {cleaned} 个 missing 引用"); ActiveEditorTracker.sharedTracker.ForceRebuild(); } else { Debug.Log($"[PlayableDirector Cleaner] {director.gameObject.name}: 未发现 missing 引用,一切正常 ✓"); } } private static void OnSelectionChanged() { if (EditorApplication.isPlayingOrWillChangePlaymode) return; if (Selection.activeGameObject == null) return; var directors = Selection.activeGameObject.GetComponents(); if (directors == null || directors.Length == 0) return; // 先快速扫描是否有需要清理的,没有则跳过 bool hasMissing = false; foreach (var director in directors) { if (director == null) continue; if (HasMissingBindings(director)) { hasMissing = true; break; } } if (!hasMissing) return; // 有 missing 引用时,延迟到下一帧清理,然后强制 Inspector 重建 EditorApplication.delayCall += () => { bool anyCleaned = false; foreach (var director in directors) { if (director == null) continue; int cleaned = CleanMissingBindings(director); if (cleaned > 0) { anyCleaned = true; Debug.Log($"[PlayableDirector Cleaner] 自动清理 {director.gameObject.name}: {cleaned} 个 missing 引用"); } } if (anyCleaned) { // 强制 Inspector 重建,让原生 DirectorEditor 重新读取已修改的序列化数据 ActiveEditorTracker.sharedTracker.ForceRebuild(); } }; } private static bool HasMissingBindings(PlayableDirector director) { var so = new SerializedObject(director); var sceneBindings = so.FindProperty("m_SceneBindings"); if (sceneBindings != null) { for (int i = 0; i < sceneBindings.arraySize; i++) { var element = sceneBindings.GetArrayElementAtIndex(i); if (IsMissingReference(element.FindPropertyRelative("key")) || IsMissingReference(element.FindPropertyRelative("value"))) { so.Dispose(); return true; } } } var exposedRefs = so.FindProperty("m_ExposedReferences.m_References"); if (exposedRefs != null) { for (int i = 0; i < exposedRefs.arraySize; i++) { if (IsMissingReference(exposedRefs.GetArrayElementAtIndex(i).FindPropertyRelative("second"))) { so.Dispose(); return true; } } } so.Dispose(); return false; } private static int CleanMissingBindings(PlayableDirector director) { int cleaned = 0; var so = new SerializedObject(director); // 清理 m_SceneBindings 中 missing 的条目 var sceneBindings = so.FindProperty("m_SceneBindings"); if (sceneBindings != null) { for (int i = sceneBindings.arraySize - 1; i >= 0; i--) { var element = sceneBindings.GetArrayElementAtIndex(i); var key = element.FindPropertyRelative("key"); if (IsMissingReference(key)) { sceneBindings.DeleteArrayElementAtIndex(i); cleaned++; continue; } // key 有效但 value 是 missing 引用时,清空 value(只解绑,不删除整条记录) var value = element.FindPropertyRelative("value"); if (IsMissingReference(value)) { value.objectReferenceValue = null; cleaned++; } } } // 清理 m_ExposedReferences 中 missing 的条目 var exposedRefs = so.FindProperty("m_ExposedReferences.m_References"); if (exposedRefs != null) { for (int i = exposedRefs.arraySize - 1; i >= 0; i--) { var second = exposedRefs.GetArrayElementAtIndex(i).FindPropertyRelative("second"); if (IsMissingReference(second)) { exposedRefs.DeleteArrayElementAtIndex(i); cleaned++; } } } if (cleaned > 0) { so.ApplyModifiedProperties(); EditorUtility.SetDirty(director); } so.Dispose(); return cleaned; } /// /// 判断一个 ObjectReference 属性是否为 missing 引用: /// 对象已被删除(无法解析),但序列化数据中仍残留非零的 instanceID。 /// private static bool IsMissingReference(SerializedProperty prop) { if (prop == null) return false; if (prop.propertyType != SerializedPropertyType.ObjectReference) return false; return prop.objectReferenceValue == null && prop.objectReferenceInstanceIDValue != 0; } }