using UnityEngine; using UnityEngine.Playables; using UnityEngine.Timeline; using UnityEditor; using System.Collections.Generic; using System.Linq; using System.Reflection; public class AdvancedTimelineAudioBinder : EditorWindow { [SerializeField] private List timelineDirectors = new List(); [SerializeField] private Vector2 scrollPosition; [SerializeField] private Vector2 timelineScrollPosition; // 音频源设置 private GameObject audioSourcePrefab; private AudioSource sceneAudioSource; private bool useSceneObject = false; private bool autoCreateParent = true; private string parentName = "TimelineAudioSources"; // 轨道数据 private Dictionary allAudioTracks = new Dictionary(); private Dictionary trackSelection = new Dictionary(); // 扫描设置 private bool includeInactiveObjects = true; private bool scanOnOpen = true; // UI状态 private bool showTimelineList = true; private bool showTrackList = true; private bool showSettings = false; [System.Serializable] private class AudioTrackInfo { public AudioTrack track; public PlayableDirector director; public string timelineName; public AudioSource currentBinding; } [MenuItem("Tools/AudioTools/Timeline Audio Binder")] public static void ShowWindow() { var window = GetWindow("Timeline Audio Binder"); window.minSize = new Vector2(500, 600); if (window.scanOnOpen) { window.ScanSceneForTimelines(); } } private void OnEnable() { if (scanOnOpen) { ScanSceneForTimelines(); } } private void OnGUI() { DrawHeader(); scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); DrawTimelineSelectionSection(); DrawAudioSourceSection(); DrawTrackManagementSection(); DrawSettingsSection(); EditorGUILayout.EndScrollView(); DrawFooter(); } private void DrawHeader() { GUILayout.Space(10); EditorGUILayout.LabelField("高级 Timeline Audio轨道批量绑定工具", EditorStyles.boldLabel); EditorGUILayout.Space(); } private void DrawTimelineSelectionSection() { EditorGUILayout.BeginVertical("box"); EditorGUILayout.BeginHorizontal(); showTimelineList = EditorGUILayout.Foldout(showTimelineList, $"场景中的Timeline ({timelineDirectors.Count})", true); if (GUILayout.Button("扫描场景", GUILayout.Width(80))) { ScanSceneForTimelines(); } EditorGUILayout.EndHorizontal(); if (showTimelineList && timelineDirectors.Count > 0) { timelineScrollPosition = EditorGUILayout.BeginScrollView(timelineScrollPosition, GUILayout.Height(150)); for (int i = 0; i < timelineDirectors.Count; i++) { EditorGUILayout.BeginHorizontal(); // 显示选择框 bool isSelected = timelineDirectors[i] != null; bool newSelected = EditorGUILayout.Toggle(isSelected, GUILayout.Width(20)); if (newSelected != isSelected && timelineDirectors[i] != null) { if (newSelected) { // 已选中,无需操作 } else { timelineDirectors[i] = null; } } // 显示Timeline信息 if (timelineDirectors[i] != null) { EditorGUILayout.LabelField($"{i + 1}. {timelineDirectors[i].name}", EditorStyles.boldLabel); if (timelineDirectors[i].playableAsset != null) { EditorGUILayout.LabelField($"Asset: {timelineDirectors[i].playableAsset.name}", EditorStyles.miniLabel); } // 操作按钮 if (GUILayout.Button("选择", GUILayout.Width(40))) { Selection.activeObject = timelineDirectors[i].gameObject; } if (GUILayout.Button("扫描轨道", GUILayout.Width(60))) { ScanAudioTracksForDirector(timelineDirectors[i]); } if (GUILayout.Button("移除", GUILayout.Width(40))) { timelineDirectors.RemoveAt(i); i--; } } else { EditorGUILayout.LabelField("无效的Director", EditorStyles.miniLabel); if (GUILayout.Button("移除", GUILayout.Width(40))) { timelineDirectors.RemoveAt(i); i--; } } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); } EditorGUILayout.EndScrollView(); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("清空列表")) { timelineDirectors.Clear(); } if (GUILayout.Button("移除无效项")) { timelineDirectors.RemoveAll(d => d == null); } EditorGUILayout.EndHorizontal(); } else if (showTimelineList) { EditorGUILayout.HelpBox("未找到Timeline,点击\"扫描场景\"按钮查找", MessageType.Info); } EditorGUILayout.EndVertical(); EditorGUILayout.Space(); } private void DrawAudioSourceSection() { EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField("音频源设置", EditorStyles.boldLabel); // 音频源类型选择 EditorGUILayout.BeginHorizontal(); bool prevUseSceneObject = useSceneObject; useSceneObject = GUILayout.Toggle(useSceneObject, "使用场景AudioSource", GUILayout.Width(150)); if (useSceneObject != prevUseSceneObject) { if (useSceneObject) audioSourcePrefab = null; else sceneAudioSource = null; } useSceneObject = !GUILayout.Toggle(!useSceneObject, "使用Prefab创建", GUILayout.Width(150)); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); if (useSceneObject) { // 场景AudioSource模式 AudioSource newAudioSource = (AudioSource)EditorGUILayout.ObjectField( "场景AudioSource", sceneAudioSource, typeof(AudioSource), true); if (newAudioSource != sceneAudioSource) { sceneAudioSource = newAudioSource; } if (sceneAudioSource != null) { EditorGUILayout.HelpBox($"将绑定到: {sceneAudioSource.name}", MessageType.Info); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("定位对象")) { Selection.activeObject = sceneAudioSource.gameObject; SceneView.lastActiveSceneView?.FrameSelected(); } if (GUILayout.Button("创建新AudioSource")) { CreateNewSceneAudioSource(); } EditorGUILayout.EndHorizontal(); } else { if (GUILayout.Button("创建新AudioSource")) { CreateNewSceneAudioSource(); } } } else { // Prefab模式 GameObject newPrefab = (GameObject)EditorGUILayout.ObjectField( "AudioSource Prefab", audioSourcePrefab, typeof(GameObject), false); if (newPrefab != audioSourcePrefab) { if (newPrefab != null) { PrefabAssetType prefabType = PrefabUtility.GetPrefabAssetType(newPrefab); if (prefabType == PrefabAssetType.NotAPrefab) { if (EditorUtility.DisplayDialog("提示", "选择的是场景对象,切换到场景对象模式?", "是", "否")) { useSceneObject = true; sceneAudioSource = newPrefab.GetComponent(); audioSourcePrefab = null; } } else { audioSourcePrefab = newPrefab; } } else { audioSourcePrefab = null; } } if (audioSourcePrefab != null) { // 验证Prefab AudioSource audioSource = audioSourcePrefab.GetComponent(); if (audioSource == null) { EditorGUILayout.HelpBox("Prefab不包含AudioSource组件!", MessageType.Error); } else { EditorGUILayout.HelpBox("Prefab验证通过", MessageType.Info); } if (GUILayout.Button("在Project中定位")) { EditorGUIUtility.PingObject(audioSourcePrefab); } } // Prefab模式设置 autoCreateParent = EditorGUILayout.Toggle("自动创建父对象", autoCreateParent); if (autoCreateParent) { parentName = EditorGUILayout.TextField("父对象名称", parentName); } } EditorGUILayout.EndVertical(); EditorGUILayout.Space(); } private void DrawTrackManagementSection() { if (allAudioTracks.Count == 0) return; EditorGUILayout.BeginVertical("box"); EditorGUILayout.BeginHorizontal(); showTrackList = EditorGUILayout.Foldout(showTrackList, $"音频轨道列表 ({allAudioTracks.Count})", true); // 批量操作按钮 if (GUILayout.Button("全选", GUILayout.Width(60))) { SelectAllTracks(true); } if (GUILayout.Button("反选", GUILayout.Width(60))) { InvertTrackSelection(); } EditorGUILayout.EndHorizontal(); if (showTrackList) { EditorGUILayout.Space(); // 轨道列表 // 修复:创建键的副本以避免枚举时修改 var trackKeys = allAudioTracks.Keys.ToList(); foreach (var trackKey in trackKeys) { if (!allAudioTracks.ContainsKey(trackKey) || allAudioTracks[trackKey].track == null) continue; var trackEntry = allAudioTracks[trackKey]; EditorGUILayout.BeginVertical("box"); // 轨道头信息 EditorGUILayout.BeginHorizontal(); bool isSelected = trackSelection.ContainsKey(trackKey) && trackSelection[trackKey]; bool newSelected = EditorGUILayout.Toggle(isSelected, GUILayout.Width(20)); if (newSelected != isSelected) { trackSelection[trackKey] = newSelected; } EditorGUILayout.LabelField(trackEntry.track.name, EditorStyles.boldLabel); EditorGUILayout.LabelField($"Timeline: {trackEntry.timelineName}", EditorStyles.miniLabel, GUILayout.Width(150)); // 绑定状态 AudioSource currentBinding = trackEntry.currentBinding; if (currentBinding != null) { EditorGUILayout.LabelField($"已绑定: {currentBinding.name}", EditorStyles.miniLabel); } else { EditorGUILayout.LabelField("未绑定", EditorStyles.miniLabel); } EditorGUILayout.EndHorizontal(); // 单个轨道操作 EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("定位轨道", GUILayout.Height(20))) { Selection.activeObject = trackEntry.director; EditorGUIUtility.PingObject(trackEntry.director); } if (GUILayout.Button("解绑", GUILayout.Height(20))) { UnbindSingleTrack(trackKey); } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); EditorGUILayout.Space(); } // 批量操作按钮 int selectedCount = trackSelection.Values.Count(v => v); bool canOperate = selectedCount > 0 && ( (useSceneObject && sceneAudioSource != null) || (!useSceneObject && audioSourcePrefab != null)); EditorGUILayout.BeginHorizontal(); GUI.enabled = canOperate; if (GUILayout.Button($"绑定选中轨道 ({selectedCount})", GUILayout.Height(30))) { BindSelectedTracks(); } GUI.enabled = selectedCount > 0; if (GUILayout.Button($"解绑选中轨道 ({selectedCount})", GUILayout.Height(30))) { UnbindSelectedTracks(); } GUI.enabled = true; EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndVertical(); EditorGUILayout.Space(); } private void DrawSettingsSection() { EditorGUILayout.BeginVertical("box"); showSettings = EditorGUILayout.Foldout(showSettings, "设置", true); if (showSettings) { includeInactiveObjects = EditorGUILayout.Toggle("包含非激活对象", includeInactiveObjects); scanOnOpen = EditorGUILayout.Toggle("打开时自动扫描", scanOnOpen); EditorGUILayout.Space(); if (GUILayout.Button("扫描所有Timeline的音频轨道")) { ScanAllAudioTracks(); } if (GUILayout.Button("清除所有绑定")) { if (EditorUtility.DisplayDialog("确认", "确定要清除所有Timeline的音频轨道绑定吗?", "是", "否")) { UnbindAllTracks(); } } } EditorGUILayout.EndVertical(); } private void DrawFooter() { EditorGUILayout.Space(); EditorGUILayout.HelpBox( "使用说明:\n" + "1. 点击\"扫描场景\"查找所有Timeline\n" + "2. 选择音频源类型(场景对象或Prefab)\n" + "3. 扫描音频轨道并选择要绑定的轨道\n" + "4. 点击绑定按钮完成操作", MessageType.Info); } private void ScanSceneForTimelines() { timelineDirectors.Clear(); // 查找场景中所有PlayableDirector组件 PlayableDirector[] directors = FindObjectsOfType(includeInactiveObjects); foreach (var director in directors) { if (director.playableAsset is TimelineAsset) { timelineDirectors.Add(director); } } // 按名称排序 timelineDirectors = timelineDirectors.OrderBy(d => d.name).ToList(); Debug.Log($"找到 {timelineDirectors.Count} 个Timeline"); } private void ScanAllAudioTracks() { allAudioTracks.Clear(); trackSelection.Clear(); int totalTracks = 0; foreach (var director in timelineDirectors) { if (director == null || director.playableAsset == null) continue; int tracksInDirector = ScanAudioTracksForDirector(director, false); totalTracks += tracksInDirector; } Debug.Log($"扫描完成,共找到 {totalTracks} 个音频轨道"); Repaint(); } private int ScanAudioTracksForDirector(PlayableDirector director, bool showDialog = true) { if (director == null || director.playableAsset == null) return 0; TimelineAsset timeline = director.playableAsset as TimelineAsset; if (timeline == null) return 0; int trackCount = 0; foreach (var track in timeline.GetOutputTracks()) { if (track is AudioTrack audioTrack) { string trackKey = $"{director.GetInstanceID()}_{audioTrack.GetInstanceID()}"; // 获取当前绑定状态 AudioSource currentBinding = director.GetGenericBinding(audioTrack) as AudioSource; allAudioTracks[trackKey] = new AudioTrackInfo { track = audioTrack, director = director, timelineName = director.name, currentBinding = currentBinding }; trackSelection[trackKey] = true; // 默认选中 trackCount++; } } if (showDialog) { Debug.Log($"在 {director.name} 中找到 {trackCount} 个音频轨道"); } return trackCount; } private void SelectAllTracks(bool select) { var keys = new List(trackSelection.Keys); foreach (var key in keys) { trackSelection[key] = select; } Repaint(); } private void InvertTrackSelection() { var keys = new List(trackSelection.Keys); foreach (var key in keys) { trackSelection[key] = !trackSelection[key]; } Repaint(); } private void CreateNewSceneAudioSource() { GameObject audioObject = new GameObject("AudioTimeline"); AudioSource newAudioSource = audioObject.AddComponent(); sceneAudioSource = newAudioSource; // 定位到新创建的对象 Selection.activeObject = audioObject; SceneView.lastActiveSceneView?.FrameSelected(); Debug.Log("创建新的AudioSource: " + audioObject.name); } private void BindSelectedTracks() { if (useSceneObject) { if (sceneAudioSource == null) { EditorUtility.DisplayDialog("错误", "请先选择场景中的AudioSource", "确定"); return; } BindToSceneAudioSource(); } else { if (audioSourcePrefab == null) { EditorUtility.DisplayDialog("错误", "请先选择AudioSource Prefab", "确定"); return; } BindUsingPrefab(); } } private void BindToSceneAudioSource() { int boundCount = 0; // 修复:创建键的副本以避免枚举时修改集合 var keysToProcess = new List(allAudioTracks.Keys); foreach (var key in keysToProcess) { // 检查键是否仍然存在于字典中 if (!allAudioTracks.ContainsKey(key) || !trackSelection.ContainsKey(key) || !trackSelection[key]) continue; AudioTrackInfo trackInfo = allAudioTracks[key]; if (trackInfo.track == null || trackInfo.director == null) continue; Undo.RecordObject(trackInfo.director, "Bind Audio Track"); trackInfo.director.SetGenericBinding(trackInfo.track, sceneAudioSource); // 更新绑定状态 - 创建新对象而不是修改现有对象 allAudioTracks[key] = new AudioTrackInfo { track = trackInfo.track, director = trackInfo.director, timelineName = trackInfo.timelineName, currentBinding = sceneAudioSource }; boundCount++; } RefreshAllEditorWindows(); Debug.Log($"成功绑定了 {boundCount} 个音频轨道到 {sceneAudioSource.name}"); EditorUtility.DisplayDialog("完成", $"成功绑定了 {boundCount} 个音频轨道", "确定"); Repaint(); } private void BindUsingPrefab() { int boundCount = 0; GameObject audioParent = null; if (autoCreateParent) { audioParent = GameObject.Find(parentName); if (audioParent == null) { audioParent = new GameObject(parentName); Undo.RegisterCreatedObjectUndo(audioParent, "Create Audio Parent"); } } // 修复:创建键的副本以避免枚举时修改集合 var keysToProcess = new List(allAudioTracks.Keys); foreach (var key in keysToProcess) { // 检查键是否仍然存在于字典中 if (!allAudioTracks.ContainsKey(key) || !trackSelection.ContainsKey(key) || !trackSelection[key]) continue; AudioTrackInfo trackInfo = allAudioTracks[key]; if (trackInfo.track == null || trackInfo.director == null) continue; // 创建或获取AudioSource实例 string sourceName = $"AudioSource_{trackInfo.timelineName}_{trackInfo.track.name}"; GameObject audioInstance = GameObject.Find(sourceName); if (audioInstance == null) { audioInstance = (GameObject)PrefabUtility.InstantiatePrefab(audioSourcePrefab); audioInstance.name = sourceName; if (audioParent != null) { audioInstance.transform.SetParent(audioParent.transform); } Undo.RegisterCreatedObjectUndo(audioInstance, "Create Audio Source"); } AudioSource audioSource = audioInstance.GetComponent(); if (audioSource != null) { Undo.RecordObject(trackInfo.director, "Bind Audio Track"); trackInfo.director.SetGenericBinding(trackInfo.track, audioSource); // 更新绑定状态 - 创建新对象而不是修改现有对象 allAudioTracks[key] = new AudioTrackInfo { track = trackInfo.track, director = trackInfo.director, timelineName = trackInfo.timelineName, currentBinding = audioSource }; boundCount++; } } RefreshAllEditorWindows(); Debug.Log($"成功绑定了 {boundCount} 个音频轨道"); EditorUtility.DisplayDialog("完成", $"成功绑定了 {boundCount} 个音频轨道", "确定"); Repaint(); } private void UnbindSelectedTracks() { int unboundCount = 0; // 修复:创建键的副本以避免枚举时修改集合 var keysToProcess = new List(allAudioTracks.Keys); foreach (var key in keysToProcess) { // 检查键是否仍然存在于字典中 if (!allAudioTracks.ContainsKey(key) || !trackSelection.ContainsKey(key) || !trackSelection[key]) continue; AudioTrackInfo trackInfo = allAudioTracks[key]; if (trackInfo.track == null || trackInfo.director == null) continue; Undo.RecordObject(trackInfo.director, "Unbind Audio Track"); trackInfo.director.SetGenericBinding(trackInfo.track, null); // 更新绑定状态 - 创建新对象而不是修改现有对象 allAudioTracks[key] = new AudioTrackInfo { track = trackInfo.track, director = trackInfo.director, timelineName = trackInfo.timelineName, currentBinding = null }; unboundCount++; } RefreshAllEditorWindows(); Debug.Log($"成功解绑了 {unboundCount} 个音频轨道"); EditorUtility.DisplayDialog("完成", $"成功解绑了 {unboundCount} 个音频轨道", "确定"); Repaint(); } private void UnbindSingleTrack(string trackKey) { if (allAudioTracks.ContainsKey(trackKey)) { AudioTrackInfo trackInfo = allAudioTracks[trackKey]; if (trackInfo.track != null && trackInfo.director != null) { Undo.RecordObject(trackInfo.director, "Unbind Audio Track"); trackInfo.director.SetGenericBinding(trackInfo.track, null); // 更新绑定状态 - 创建新对象而不是修改现有对象 allAudioTracks[trackKey] = new AudioTrackInfo { track = trackInfo.track, director = trackInfo.director, timelineName = trackInfo.timelineName, currentBinding = null }; RefreshAllEditorWindows(); Debug.Log($"已解绑轨道: {trackInfo.track.name}"); Repaint(); } } } private void UnbindAllTracks() { int unboundCount = 0; // 修复:创建键的副本以避免枚举时修改集合 var keysToProcess = new List(allAudioTracks.Keys); foreach (var key in keysToProcess) { if (!allAudioTracks.ContainsKey(key)) continue; AudioTrackInfo trackInfo = allAudioTracks[key]; if (trackInfo.track == null || trackInfo.director == null) continue; Undo.RecordObject(trackInfo.director, "Unbind Audio Track"); trackInfo.director.SetGenericBinding(trackInfo.track, null); unboundCount++; } // 重新扫描更新状态 ScanAllAudioTracks(); RefreshAllEditorWindows(); Debug.Log($"已解绑所有 {unboundCount} 个音频轨道"); EditorUtility.DisplayDialog("完成", $"已解绑所有 {unboundCount} 个音频轨道", "确定"); } private void RefreshAllEditorWindows() { UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); // 刷新Timeline窗口 System.Type timelineWindowType = System.Type.GetType("UnityEditor.Timeline.TimelineWindow,Unity.Timeline.Editor"); if (timelineWindowType != null) { var timelineWindow = EditorWindow.GetWindow(timelineWindowType); if (timelineWindow != null) { timelineWindow.Repaint(); } } // 立即应用修改 AssetDatabase.Refresh(); EditorApplication.RepaintHierarchyWindow(); } }