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;
}
}