using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; using UnityEngine.Playables; using UnityEngine.Timeline; namespace Streamingle.Editor { public class TimelineBindingTransferWindow : EditorWindow { private PlayableDirector sourceDirector; private PlayableDirector targetDirector; private bool matchByHierarchyPath = true; private bool requireSameTrackType = true; private bool showPreview = true; private Vector2 scroll; private struct TrackMap { public TrackAsset sourceTrack; public TrackAsset targetTrack; public Object boundObject; } private List previewMappings = new List(); [MenuItem("Tools/Timeline Tools/Transfer Bindings")] public static void ShowWindow() { GetWindow("Timeline Binding Transfer"); } private void OnGUI() { EditorGUILayout.LabelField("타임라인 바인딩 복사", EditorStyles.boldLabel); EditorGUILayout.Space(); sourceDirector = (PlayableDirector)EditorGUILayout.ObjectField("소스 디렉터", sourceDirector, typeof(PlayableDirector), true); targetDirector = (PlayableDirector)EditorGUILayout.ObjectField("타겟 디렉터", targetDirector, typeof(PlayableDirector), true); EditorGUILayout.Space(); matchByHierarchyPath = EditorGUILayout.ToggleLeft("트랙 계층 경로로 매칭 (권장)", matchByHierarchyPath); requireSameTrackType = EditorGUILayout.ToggleLeft("트랙 타입이 동일할 때만 매칭", requireSameTrackType); showPreview = EditorGUILayout.ToggleLeft("미리보기 표시", showPreview); EditorGUILayout.Space(); EditorGUI.BeginDisabledGroup(!CanBuildMapping()); if (GUILayout.Button("매핑 미리보기 갱신")) { BuildPreviewMapping(); } EditorGUI.EndDisabledGroup(); EditorGUI.BeginDisabledGroup(previewMappings.Count == 0); if (GUILayout.Button("바인딩 복사 실행")) { ApplyMappings(); } EditorGUI.EndDisabledGroup(); if (showPreview) { DrawPreview(); } } private bool CanBuildMapping() { if (sourceDirector == null || targetDirector == null) return false; var srcAsset = sourceDirector.playableAsset as TimelineAsset; var dstAsset = targetDirector.playableAsset as TimelineAsset; return srcAsset != null && dstAsset != null; } private void BuildPreviewMapping() { previewMappings.Clear(); var srcAsset = sourceDirector.playableAsset as TimelineAsset; var dstAsset = targetDirector.playableAsset as TimelineAsset; if (srcAsset == null || dstAsset == null) { EditorUtility.DisplayDialog("오류", "소스/타겟 디렉터에 유효한 TimelineAsset이 필요합니다.", "확인"); return; } var sourceTracks = GetAllOutputTracks(srcAsset).ToList(); var targetTracks = GetAllOutputTracks(dstAsset).ToList(); // 타겟 트랙 인덱스 구성 Dictionary> nameToTargetTracks = new Dictionary>(); Dictionary pathToTargetTrack = new Dictionary(); foreach (var t in targetTracks) { string nameKey = t.name; if (!nameToTargetTracks.TryGetValue(nameKey, out var list)) { list = new List(); nameToTargetTracks[nameKey] = list; } list.Add(t); string pathKey = GetTrackPath(t); if (!pathToTargetTrack.ContainsKey(pathKey)) pathToTargetTrack.Add(pathKey, t); } foreach (var s in sourceTracks) { var bound = sourceDirector.GetGenericBinding(s); if (bound == null) continue; TrackAsset matched = null; if (matchByHierarchyPath) { pathToTargetTrack.TryGetValue(GetTrackPath(s), out matched); } else { if (nameToTargetTracks.TryGetValue(s.name, out var candidates)) { if (requireSameTrackType) { matched = candidates.FirstOrDefault(c => c.GetType() == s.GetType()); } else { matched = candidates.FirstOrDefault(); } } } if (matched == null) continue; if (requireSameTrackType && matched.GetType() != s.GetType()) continue; previewMappings.Add(new TrackMap { sourceTrack = s, targetTrack = matched, boundObject = bound as Object }); } } private void ApplyMappings() { if (targetDirector == null) { EditorUtility.DisplayDialog("오류", "타겟 디렉터가 필요합니다.", "확인"); return; } int applied = 0; Undo.RecordObject(targetDirector, "Timeline Binding Transfer"); foreach (var map in previewMappings) { if (map.targetTrack == null || map.boundObject == null) continue; targetDirector.SetGenericBinding(map.targetTrack, map.boundObject); applied++; } EditorUtility.SetDirty(targetDirector); var go = targetDirector.gameObject; if (go != null && go.scene.IsValid()) { UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(go.scene); } EditorUtility.DisplayDialog("완료", $"바인딩 복사 완료: {applied}개 적용", "확인"); } private void DrawPreview() { EditorGUILayout.Space(); EditorGUILayout.LabelField("매핑 미리보기", EditorStyles.boldLabel); scroll = EditorGUILayout.BeginScrollView(scroll, GUILayout.MinHeight(150)); if (previewMappings.Count == 0) { EditorGUILayout.HelpBox("미리보기가 비어 있습니다. '매핑 미리보기 갱신'을 눌러 생성하세요.", MessageType.Info); } else { foreach (var m in previewMappings) { EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField($"소스: {GetTrackDisplay(m.sourceTrack)}"); EditorGUILayout.LabelField($"타겟: {GetTrackDisplay(m.targetTrack)}"); EditorGUILayout.ObjectField("바인딩 객체", m.boundObject, typeof(Object), true); EditorGUILayout.EndVertical(); } } EditorGUILayout.EndScrollView(); } private static IEnumerable GetAllOutputTracks(TimelineAsset asset) { // GetOutputTracks는 서브트랙을 평탄화하여 반환하므로 그대로 사용 return asset.GetOutputTracks(); } private static string GetTrackPath(TrackAsset track) { // 상위 트랙 이름을 포함한 경로 문자열 생성 List names = new List(); var current = track; while (current != null) { names.Add(current.name); current = current.parent as TrackAsset; } names.Reverse(); return string.Join("/", names); } private static string GetTrackDisplay(TrackAsset track) { if (track == null) return "(없음)"; return $"{GetTrackPath(track)} <{track.GetType().Name}>"; } } }