【剧情】后处理场景增加剧情cinemachine兼容

main
刘涛 2026-04-02 16:20:01 +08:00
parent 9821d4159e
commit 7e76c2cab3
1 changed files with 249 additions and 38 deletions

View File

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
@ -1093,87 +1094,297 @@ public abstract class PostProcessor
/// <summary>
/// 设置剧情timeline
/// </summary>
private async UniTask SetTimeline()
private UniTask SetTimeline()
{
GameObject goOld = PostProcessInstructor.timeline;
if (null == goOld)
return;
return UniTask.CompletedTask;
PlayableDirector oldDirector = goOld.GetComponent<PlayableDirector>();
if (null == oldDirector)
{
Debug.LogError("获取PlayableDirector失败");
return;
return UniTask.CompletedTask;
}
if (null == oldDirector.playableAsset)
{
Debug.LogError("获取PlayableAsset失败");
return;
return UniTask.CompletedTask;
}
string oldPath = AssetDatabase.GetAssetPath(oldDirector.playableAsset);
string newPath = $"{ProcessedResourceDir}/{oldDirector.playableAsset.name}.playable";
string cmPath = $"{ProcessedResourceDir}/{oldDirector.playableAsset.name}_cinemachine.playable";
var oldTimelineAsset = oldDirector.playableAsset as TimelineAsset;
var hasCinemachineTrack = oldTimelineAsset != null && _EnumerateTracks(oldTimelineAsset).Any(_IsCinemachineTrack);
if (!AssetDatabase.CopyAsset(oldPath, newPath))
{
Debug.LogError("Timeline拷贝失败");
return;
return UniTask.CompletedTask;
}
if (hasCinemachineTrack && !AssetDatabase.CopyAsset(oldPath, cmPath))
{
Debug.LogError("Cinemachine Timeline拷贝失败");
return UniTask.CompletedTask;
}
var newTimelineAsset = AssetDatabase.LoadAssetAtPath<TimelineAsset>(newPath);
var cmTimelineAsset = hasCinemachineTrack ? AssetDatabase.LoadAssetAtPath<TimelineAsset>(cmPath) : null;
if (newTimelineAsset == null || (hasCinemachineTrack && cmTimelineAsset == null))
{
Debug.LogError("加载后处理Timeline资源失败");
return UniTask.CompletedTask;
}
_FilterTimelineTracks(newTimelineAsset, keepCinemachine: false);
if (hasCinemachineTrack)
{
_FilterTimelineTracks(cmTimelineAsset, keepCinemachine: true);
}
EditorUtility.SetDirty(newTimelineAsset);
AssetDatabase.SaveAssetIfDirty(newTimelineAsset);
if (hasCinemachineTrack)
{
EditorUtility.SetDirty(cmTimelineAsset);
AssetDatabase.SaveAssetIfDirty(cmTimelineAsset);
}
GameObject goNew = new(goOld.name);
goNew.transform.parent = goOld.transform.parent;
goNew.transform.position = Vector3.zero;
goNew.transform.rotation = Quaternion.identity;
PlayableDirector newDirector = goNew.AddComponent<PlayableDirector>();
_CopyDirectorSettings(oldDirector, newDirector);
newDirector.playableAsset = newTimelineAsset;
PlayableDirector cmDirector = null;
if (hasCinemachineTrack)
{
GameObject goCm = new("timeline_cinemachine");
goCm.transform.parent = goOld.transform.parent;
goCm.transform.position = Vector3.zero;
goCm.transform.rotation = Quaternion.identity;
cmDirector = goCm.AddComponent<PlayableDirector>();
_CopyDirectorSettings(oldDirector, cmDirector);
cmDirector.playableAsset = cmTimelineAsset;
}
_CopyDirectorBindings(oldDirector, newDirector, track => !_IsCinemachineTrack(track));
if (cmDirector != null)
{
_CopyDirectorBindings(oldDirector, cmDirector, _IsCinemachineTrack);
_SyncExposedReferences(oldDirector, cmDirector);
}
GameObject.DestroyImmediate(goOld);
PostProcessInstructor.timeline = goNew;
EditorUtility.SetDirty(PostProcessInstructor);
EditorUtility.SetDirty(newDirector);
if (cmDirector != null)
{
EditorUtility.SetDirty(cmDirector);
}
EditorSceneManager.MarkSceneDirty(goNew.scene);
AssetDatabase.SaveAssetIfDirty(newDirector);
if (cmDirector != null)
{
AssetDatabase.SaveAssetIfDirty(cmDirector);
}
return UniTask.CompletedTask;
}
private void _CopyDirectorSettings(PlayableDirector oldDirector, PlayableDirector newDirector)
{
newDirector.timeUpdateMode = oldDirector.timeUpdateMode;
newDirector.playOnAwake = oldDirector.playOnAwake;
newDirector.extrapolationMode = oldDirector.extrapolationMode;
newDirector.initialTime = oldDirector.initialTime;
}
// 新预制体绑定新Playable资源
newDirector.playableAsset = AssetDatabase.LoadAssetAtPath<TimelineAsset>(newPath);
newDirector.playableAsset.name = System.IO.Path.GetFileNameWithoutExtension(newPath);
SerializedObject directorSO = new(oldDirector);
SerializedProperty sceneBindings = directorSO.FindProperty("m_SceneBindings");
newDirector.Play();
await UniTask.NextFrame();
// 删除PlayableDirector.Bindgins的空Key项并把原Timeline文件的轨道Key替换到当前Timeline文件的轨道对象
Dictionary<Object, Object> bindingsKV = new();
for (int i = 0; i < sceneBindings.arraySize; ++i)
private void _CopyDirectorBindings(PlayableDirector oldDirector, PlayableDirector newDirector, Func<TrackAsset, bool> trackFilter)
{
var oldTimelineAsset = oldDirector.playableAsset as TimelineAsset;
var newTimelineAsset = newDirector.playableAsset as TimelineAsset;
if (oldTimelineAsset == null || newTimelineAsset == null)
{
SerializedProperty bindingElement = sceneBindings.GetArrayElementAtIndex(i);
// key是源文件的轨道对象value是当前文件的绑定value
Object key = bindingElement.FindPropertyRelative("key").objectReferenceValue;
Object value = bindingElement.FindPropertyRelative("value").objectReferenceValue;
return;
}
if (null == key)
var oldTracks = oldTimelineAsset.GetOutputTracks()
.Where(track => track != null && trackFilter(track))
.ToList();
var newTracks = newTimelineAsset.GetOutputTracks()
.Where(track => track != null)
.ToList();
for (int i = 0; i < oldTracks.Count && i < newTracks.Count; ++i)
{
var value = oldDirector.GetGenericBinding(oldTracks[i]);
if (value == null)
{
continue;
}
bindingsKV.Add(key, value);
newDirector.SetGenericBinding(newTracks[i], value);
}
}
// 删除无用绑定
while (sceneBindings.arraySize > 0)
private void _FilterTimelineTracks(TimelineAsset timelineAsset, bool keepCinemachine)
{
var allTracks = _EnumerateTracks(timelineAsset).ToList();
var cmTrackAncestors = new HashSet<TrackAsset>();
foreach (var track in allTracks.Where(_IsCinemachineTrack))
{
sceneBindings.DeleteArrayElementAtIndex(0);
for (var current = track; current != null; current = current.parent as TrackAsset)
{
cmTrackAncestors.Add(current);
}
}
directorSO.ApplyModifiedProperties();
// 设置有效绑定新老timeline资源outputs顺序完全一致
List<PlayableBinding> listOldBinding = oldDirector.playableAsset.outputs.ToList();
List<PlayableBinding> listNewBinding = newDirector.playableAsset.outputs.ToList();
for (int i = 0; i < listOldBinding.Count; ++i)
var removeTracks = allTracks
.Where(track => keepCinemachine ? !cmTrackAncestors.Contains(track) : _IsCinemachineTrack(track))
.OrderByDescending(_GetTrackDepth)
.ToList();
foreach (var track in removeTracks)
{
TrackAsset oldTrackAsset = listOldBinding[i].sourceObject as TrackAsset;
TrackAsset newTrackAsset = listNewBinding[i].sourceObject as TrackAsset;
timelineAsset.DeleteTrack(track);
}
}
if (bindingsKV.TryGetValue(oldTrackAsset, out Object value))
newDirector.SetGenericBinding(newTrackAsset, value);
private IEnumerable<TrackAsset> _EnumerateTracks(TimelineAsset timelineAsset)
{
foreach (var rootTrack in timelineAsset.GetRootTracks())
{
if (rootTrack == null)
{
continue;
}
foreach (var track in _EnumerateTrack(rootTrack))
{
yield return track;
}
}
}
private IEnumerable<TrackAsset> _EnumerateTrack(TrackAsset track)
{
yield return track;
foreach (var childTrack in track.GetChildTracks())
{
if (childTrack == null)
{
continue;
}
foreach (var nestedTrack in _EnumerateTrack(childTrack))
{
yield return nestedTrack;
}
}
}
private int _GetTrackDepth(TrackAsset track)
{
var depth = 0;
for (var parent = track.parent as TrackAsset; parent != null; parent = parent.parent as TrackAsset)
{
depth++;
}
GameObject.DestroyImmediate(PostProcessInstructor.timeline);
AssetDatabase.SaveAssetIfDirty(newDirector);
return depth;
}
private bool _IsCinemachineTrack(TrackAsset track)
{
return track != null && track.GetType().FullName.Contains("Cinemachine");
}
/// <summary>
/// 同步 Timeline 的 ExposedReferences 到新场景
/// 主要解决 CinemachineShot 引用的 VirtualCamera 在场景复制后 fileID 变化的问题
/// </summary>
private void _SyncExposedReferences(PlayableDirector oldDirector, PlayableDirector newDirector)
{
SerializedObject oldDirectorSO = new(oldDirector);
SerializedProperty oldExposedRefs = oldDirectorSO.FindProperty("m_ExposedReferences.m_References");
if (oldExposedRefs == null || oldExposedRefs.arraySize == 0)
{
Debug.LogWarning("旧 PlayableDirector 没有 ExposedReferences");
return;
}
// 遍历旧的暴露引用,在新场景中找到对应的对象
for (int i = 0; i < oldExposedRefs.arraySize; ++i)
{
SerializedProperty refElement = oldExposedRefs.GetArrayElementAtIndex(i);
SerializedProperty guidProp = refElement.FindPropertyRelative("first");
SerializedProperty objProp = refElement.FindPropertyRelative("second");
if (guidProp == null || objProp == null) continue;
string exposedName = guidProp.stringValue;
Object oldReferencedObj = objProp.objectReferenceValue;
if (string.IsNullOrEmpty(exposedName) || oldReferencedObj == null) continue;
if (oldReferencedObj is not Component oldComponent)
{
continue;
}
// 先只处理 Cinemachine 相关轨道,避免误改其他 ExposedReference。
if (!oldComponent.GetType().FullName.Contains("Cinemachine"))
{
continue;
}
// 在新场景中找到对应的 GameObject通过完整路径匹配
string oldObjPath = GetGameObjectPath(oldComponent.gameObject);
GameObject newReferencedObjGo = GameObject.Find(oldObjPath);
if (newReferencedObjGo == null)
{
Debug.LogWarning($"在新场景中找不到对应的 GameObject: {oldObjPath} (exposedName: {exposedName})");
continue;
}
var newReferencedObj = newReferencedObjGo.GetComponent(oldComponent.GetType());
if (newReferencedObj == null)
{
Debug.LogWarning($"在新场景中找不到对应组件: {oldComponent.GetType().FullName}, path: {oldObjPath}, exposedName: {exposedName}");
continue;
}
// 设置新的引用
newDirector.SetReferenceValue(new PropertyName(exposedName), newReferencedObj);
Debug.Log($"同步 Cinemachine ExposedReference: {exposedName} -> {oldObjPath} ({oldComponent.GetType().Name})");
}
}
/// <summary>
/// 获取 GameObject 相对于场景根的完整路径
/// </summary>
private string GetGameObjectPath(GameObject go)
{
if (go == null) return "";
var path = go.name;
Transform parent = go.transform.parent;
while (parent != null)
{
path = parent.name + "/" + path;
parent = parent.parent;
}
return path;
}
#endregion
}
}