Files
MinFt/Client/Assets/Editor/VideoTool/TimelineAudioTrackBinder.cs
Liubing\LB 3d8d4d18f3 first commit
先修复一下,错误的场景

删除不必要的钓场资产

修复into场景

update:更新meta文件,修复报错

update:修复资源

修复第一章节建造

修复建造第三章

update:删除多余内容

update:更新README.md

update:还原图标

update:更新README

update:更新配置
2026-04-28 02:17:22 +08:00

791 lines
27 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<PlayableDirector> timelineDirectors = new List<PlayableDirector>();
[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<string, AudioTrackInfo> allAudioTracks = new Dictionary<string, AudioTrackInfo>();
private Dictionary<string, bool> trackSelection = new Dictionary<string, bool>();
// 扫描设置
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<AdvancedTimelineAudioBinder>("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<AudioSource>();
audioSourcePrefab = null;
}
}
else
{
audioSourcePrefab = newPrefab;
}
}
else
{
audioSourcePrefab = null;
}
}
if (audioSourcePrefab != null)
{
// 验证Prefab
AudioSource audioSource = audioSourcePrefab.GetComponent<AudioSource>();
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<PlayableDirector>(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<string>(trackSelection.Keys);
foreach (var key in keys)
{
trackSelection[key] = select;
}
Repaint();
}
private void InvertTrackSelection()
{
var keys = new List<string>(trackSelection.Keys);
foreach (var key in keys)
{
trackSelection[key] = !trackSelection[key];
}
Repaint();
}
private void CreateNewSceneAudioSource()
{
GameObject audioObject = new GameObject("AudioTimeline");
AudioSource newAudioSource = audioObject.AddComponent<AudioSource>();
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<string>(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<string>(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<AudioSource>();
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<string>(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<string>(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();
}
}