using System; using System.Collections.Generic; using System.Linq; using Framework; using Gameplay.Story.Cmp.Data; using Gameplay.Story.Cmp.Timeline.AudioAction; using Gameplay.Story.Cmp.Timeline.BlockChoiceAction; using Gameplay.Story.Cmp.Timeline.CameraFadeAction; using Gameplay.Story.Cmp.Timeline.CameraPosAction; using Gameplay.Story.Cmp.Timeline.CameraPropAction; using Gameplay.Story.Cmp.Timeline.CameraRotAction; using Gameplay.Story.Cmp.Timeline.CommonAction; using Gameplay.Story.Cmp.Timeline.MsgAction; using Gameplay.Story.Cmp.Timeline.PlayerAction; using Gameplay.Story.Cmp.Timeline.PlayerEmojiAction; using Gameplay.Story.Cmp.Timeline.PlayerPosAction; using Gameplay.Story.Cmp.Timeline.PlayerRotAction; using Gameplay.Story.Cmp.Timeline.PlayerTalkAction; using Gameplay.Story.Cmp.Timeline.SceneSwitchAction; using Gameplay.Story.Cmp.Timeline.SetNameAction; using Gameplay.Story.Cmp.Timeline.UIImageSetAction; using Gameplay.Story.Cmp.Timeline.UIModChangeAction; using Gameplay.Story.Cmp.Timeline.WeaponHideAction; using GameWrapper.Story.Timeline.UIImageSelectAction; using Sirenix.Utilities; using Sirenix.Utilities.Editor; using UnityEditor; using UnityEngine; using UnityEngine.Playables; using UnityEngine.Timeline; namespace Gameplay.Story.Cmp.Utils { public static class TimelineUtils { private static readonly Dictionary PlayerActionTrackDic = new Dictionary(); private static readonly Dictionary PlayerEmojiActionTrackDic = new Dictionary(); private static readonly Dictionary PlayerPosTrackDic = new Dictionary(); private static readonly Dictionary PlayerTalkTrackDic = new Dictionary(); private static readonly Dictionary PlayerRotTrackDic = new Dictionary(); private static readonly Dictionary PlayerClipDic = new Dictionary(); private static readonly Dictionary WeaponHideActionTrackDic = new Dictionary(); private static readonly Dictionary SetNameActionTrackDic = new Dictionary(); private static readonly Dictionary AudioActionTrackDic = new Dictionary(); private static readonly List PlayerDataList = new List(); private static MsgActionTrack _msgActionTrack; private static BlockChoiceActionTrack _blockChoiceTrack; private static SceneSwitchActionTrack _sceneSwitchTrack; private static CameraPosActionTrack _cameraPosActionTrack; private static CameraRotActionTrack _cameraRotActionTrack; private static CameraPropActionTrack _cameraPropActionTrack; private static CameraFadeActionTrack _cameraFadeActionTrack; private static UIModChangeActionTrack _uiModChangeTrack; private static AudioActionTrack _soundActionTrack; private static AudioActionTrack _musicActionTrack; private static AudioActionTrack _evActionTrack; private const float SampleTime = 1f; public static AudioActionTrack SoundActionTrack => _soundActionTrack; public static AudioActionTrack MusicActionTrack => _musicActionTrack; public static TimelineAsset ReplaceTimelineAsset(StoryExcelData storyExcelData) { // TODO: 暂未实现 return null; } public static TimelineAsset CreateTimelineAsset(string savePath, StoryExcelData storyExcelData) { // 判断文件是否存在,如果存在则先删除 var fullPath = Application.dataPath.Replace("Assets", "") + savePath; if (System.IO.File.Exists(fullPath)) { // 先进行checkout // VersionControlSystem.Checkout(savePath); AssetDatabase.DeleteAsset(savePath); AssetDatabase.Refresh(); } var timelineAsset = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(timelineAsset, savePath); PlayerActionTrackDic.Clear(); PlayerEmojiActionTrackDic.Clear(); PlayerPosTrackDic.Clear(); PlayerTalkTrackDic.Clear(); PlayerRotTrackDic.Clear(); SetNameActionTrackDic.Clear(); PlayerClipDic.Clear(); WeaponHideActionTrackDic.Clear(); PlayerDataList.Clear(); PlayerDataList.AddRange(storyExcelData.playerList); _CreateGroupTracks(timelineAsset); var currentTime = 0f; var onlyOnce = true; for (var i = 0; i < storyExcelData.blockList.Count; i++) { var block = storyExcelData.blockList[i]; var totalTime = _CreatePlayers(block, currentTime); if (onlyOnce) { _CreateCameraPosTrack(block, currentTime, totalTime); _CreateCameraRotTrack(block, currentTime, totalTime); _CreateSceneSwitch(currentTime, totalTime); onlyOnce = false; } _CreateMsgAction(_msgActionTrack, block, currentTime, totalTime); _CreateBlockChoice(currentTime, totalTime); currentTime += totalTime; } // 角色相关的只创建第一次 var playerList = PlayerDataList; for (var i = 0; i < playerList.Count; i++) { var playerData = playerList[i]; _CreatePlayerPos(PlayerPosTrackDic[playerData.playerId], playerData, 0, SampleTime); _CreatePlayerRot(PlayerRotTrackDic[playerData.playerId], playerData, 0, SampleTime); _CreateWeaponHides(WeaponHideActionTrackDic[playerData.playerId], playerData, 0, SampleTime); } // 保存到指定路径 // 保存时间轴资源为.asset文件 EditorUtility.SetDirty(timelineAsset); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); return timelineAsset; } private static void _CreateCameraRotTrack(StoryExcelBlockData block, float startTime, float during) { var clip = _cameraRotActionTrack.CreateClip(); clip.start = startTime; clip.duration = during; } private static void _CreateCameraPosTrack(StoryExcelBlockData block, float startTime, float during) { var clip = _cameraPosActionTrack.CreateClip(); clip.start = startTime; clip.duration = during; } private static void _CreateGroupTracks(TimelineAsset timelineAsset) { // 创建剧情轨道 var groupBlockTrack = timelineAsset.CreateTrack(null, "剧情") as GroupTrack; _msgActionTrack = timelineAsset.CreateTrack(groupBlockTrack, "剧情文字"); _blockChoiceTrack = timelineAsset.CreateTrack(groupBlockTrack, "剧情选项"); _sceneSwitchTrack = timelineAsset.CreateTrack(groupBlockTrack, "场景切换"); // StoryManager.instance.msgManager.msgTrack = _blockLine; // 创建角色轨道 var playerGroupTrack = timelineAsset.CreateTrack(null, "角色") as GroupTrack; foreach (var playerData in PlayerDataList) { var combieName = playerData.playerName + playerData.playerId; var groupTrackSplit = timelineAsset.CreateTrack(playerGroupTrack, combieName); // 创建角色说话轨道 var playerTalkActionTrack = timelineAsset.CreateTrack(groupTrackSplit, "说话") as PlayerTalkActionTrack; playerTalkActionTrack.playerId = playerData.playerId; PlayerTalkTrackDic.Add(playerData.playerId, playerTalkActionTrack); // 创建角色动作轨道 // var playerActionTrack = timelineAsset.CreateTrack(groupTrackSplit, "动作") as PlayerActionTrack; // playerActionTrack.playerId = playerData.playerId; var playerActionTrack = timelineAsset.CreateTrack(groupTrackSplit, "动作") as AnimationTrack; playerActionTrack.applyAvatarMask = false; PlayerActionTrackDic.Add(playerData.playerId, playerActionTrack); PlayerClipDic.Add(playerData.playerId, new CachePlayerAnimClips() { playerId = playerData.playerId }); // 创建角色表情轨道 var playerEmojiActionTrack = timelineAsset.CreateTrack(groupTrackSplit, "表情") as PlayerEmojiActionTrack; playerEmojiActionTrack.playerId = playerData.playerId; PlayerEmojiActionTrackDic.Add(playerData.playerId, playerEmojiActionTrack); // 创建角色位置轨道 var playerPosActionTrack = timelineAsset.CreateTrack(groupTrackSplit, "位置"); PlayerPosTrackDic.Add(playerData.playerId, playerPosActionTrack); playerPosActionTrack.playerId = playerData.playerId; // 创建角色朝向轨道 var playerRotActionTrack = timelineAsset.CreateTrack(groupTrackSplit, "朝向"); playerRotActionTrack.playerId = playerData.playerId; PlayerRotTrackDic.Add(playerData.playerId, playerRotActionTrack); // 创建角色武器隐藏轨道 var weaponHideActionTrack = timelineAsset.CreateTrack(groupTrackSplit, "武器隐藏"); weaponHideActionTrack.playerId = playerData.playerId; WeaponHideActionTrackDic.Add(playerData.playerId, weaponHideActionTrack); var setNameActionTrack = timelineAsset.CreateTrack(groupTrackSplit, "名字"); setNameActionTrack.playerId = playerData.playerId; SetNameActionTrackDic.Add(playerData.playerId, setNameActionTrack); //创建角色音效轨道 // var audioActionTrack = timelineAsset.CreateTrack(groupTrackSplit, "音频"); // audioActionTrack.playerId = playerData.playerId; //AudioActionTrackDic.Add(playerData.playerId, audioActionTrack); } // 创建相机轨道 var cameraGroupTrack = timelineAsset.CreateTrack(null, "相机"); { // 创建相机位置轨道 var cameraPosActionTrack = timelineAsset.CreateTrack(cameraGroupTrack, "位置"); _cameraPosActionTrack = cameraPosActionTrack; // 创建相机朝向轨道 var cameraRotActionTrack = timelineAsset.CreateTrack(cameraGroupTrack, "朝向"); _cameraRotActionTrack = cameraRotActionTrack; // 创建相机size轨道 var cameraSizeActionTrack = timelineAsset.CreateTrack(cameraGroupTrack, "属性"); _cameraPropActionTrack = cameraSizeActionTrack; // 创建一个空的clip var clip = cameraSizeActionTrack.CreateClip(); clip.start = 0f; clip.duration = 1f; // 创建相机淡入淡出轨道 _cameraFadeActionTrack = timelineAsset.CreateTrack(cameraGroupTrack, "淡入淡出"); } // 创建UI轨道 var UIGroupTrack = timelineAsset.CreateTrack(null, "UI"); { // 创建UI模式切换 var uimodChangeTrack = timelineAsset.CreateTrack(UIGroupTrack, "模式切换"); _uiModChangeTrack = uimodChangeTrack; // 默认显示3D相机内容 var clip = uimodChangeTrack.CreateClip(); clip.start = 0f; clip.duration = 5f; // 创建前景切换 var uiFrontChangeTrack = timelineAsset.CreateTrack(UIGroupTrack, "前景切换"); uiFrontChangeTrack.imgPosType = 1; // 创建背景切换 var uiBackChangeTrack = timelineAsset.CreateTrack(UIGroupTrack, "背景切换"); uiBackChangeTrack.imgPosType = 0; var uiBgSelectTrack = timelineAsset.CreateTrack(UIGroupTrack, "背景切换(可选)"); uiBgSelectTrack.imgPosType = 0; var uiFrontSelectTrack = timelineAsset.CreateTrack(UIGroupTrack, "前景切换(可选)"); uiFrontSelectTrack.imgPosType = 1; } // 创建通用轨道 var commonGroupTrack = timelineAsset.CreateTrack(null, "通用"); { // 创建通用轨道 var commonActionTrack = timelineAsset.CreateTrack(commonGroupTrack, "通用1"); } //创建音频轨道 AddAudioTracks(timelineAsset); } public static void ReGenAudioTracks(TimelineAsset timelineAsset) { var tracks = timelineAsset.GetOutputTracks(); foreach (var track in tracks) { if (track is AudioActionTrack audioActionTrack) { timelineAsset.DeleteTrack(track); } } AddAudioTracks(timelineAsset); } public static void AddAudioTracks(TimelineAsset timelineAsset) { var audioGroupTrack = timelineAsset.CreateTrack(null, "音频"); _soundActionTrack = timelineAsset.CreateTrack(audioGroupTrack, "音效"); _musicActionTrack = timelineAsset.CreateTrack(audioGroupTrack, "BGM"); _evActionTrack = timelineAsset.CreateTrack(audioGroupTrack, "Ev"); _soundActionTrack.trackType = AudioActionTrack.AudioTrackType.Sound; _musicActionTrack.trackType = AudioActionTrack.AudioTrackType.BGM; _evActionTrack.trackType = AudioActionTrack.AudioTrackType.Ev; // audioActionTrack.playerId = playerData.playerId; //AudioActionTrackDic.Add(playerData.playerId, audioActionTrack); } private static string _GetMotionFullPath(string fileName) { if (fileName.StartsWith("ani_s_")) return $"Assets/Art/Animations/Show/Story/{fileName}.fbx"; return $"Assets/Art/Animations/Show/Battle/{fileName}.fbx"; } private static float _ReadStartMotionLength(string motionName) { if (string.IsNullOrEmpty(motionName)) { Debug.LogError("无效的动作名字:" + motionName); return 1f; } var motionConfig = EditorTableManager.instance.tables.MotionMapperConfig.Get(motionName); if (motionConfig == null) { Debug.LogError("无效的动作名字:" + motionName); return 1f; } var motionPath = _GetMotionFullPath(motionConfig.MotionFullPath); var loadDatas = AssetDatabase.LoadAllAssetsAtPath(motionPath); AnimationClip clipStart = null; for (int i = 0; i < loadDatas.Length; i++) { var loadData = loadDatas[i]; if (loadData is AnimationClip loadClip) { if (loadClip.name.EndsWith("_start")) { clipStart = loadClip; } } } if (clipStart == null) { return 1f; } return clipStart.length; } private static float _CreatePlayers(StoryExcelBlockData block, float currentTime) { var totalTime = SampleTime; var blockMsg = block.blockMsg; var msgTime = blockMsg == null ? 0f : blockMsg.Length / 8f; msgTime = Mathf.Max(block.blockTime, msgTime); var playerDatas = block.playerDatas; for (var i = 0; i < playerDatas.Count; i++) { var playerData = playerDatas[i]; // 读取startAnim playerData = _SearchPlayerData(playerDatas, playerData.playerId); var motionName = playerData.motionName; var startMotionLength = _ReadStartMotionLength(motionName); totalTime = Mathf.Max(totalTime, startMotionLength, msgTime); if (playerData.isTalk) { _CreatePlayerTalks(PlayerTalkTrackDic[playerData.playerId], playerData, currentTime); } _CreatePlayerEmojis(PlayerEmojiActionTrackDic[playerData.playerId], playerData, currentTime, totalTime); } // 所有角色都会创建位置轨道 var playerList = PlayerDataList; for (var i = 0; i < playerList.Count; i++) { var playerData = playerList[i]; // _CreatePlayerPos(PlayerPosTrackDic[playerData.playerId], playerData, currentTime, totalTime); // _CreatePlayerRot(PlayerRotTrackDic[playerData.playerId], playerData, currentTime, totalTime); // _CreateWeaponHides(WeaponHideActionTrackDic[playerData.playerId], playerData, currentTime, totalTime); playerData = _SearchPlayerData(playerDatas, playerData.playerId); _CreatePlayerActions(PlayerActionTrackDic[playerData.playerId], playerData, currentTime, totalTime); _CreateSetNameClips(playerData, currentTime); } return totalTime; } private static void _CreateSetNameClips(StoryExcelPlayerData playerData, float currentTime) { var cacheData = PlayerClipDic[playerData.playerId]; var track = SetNameActionTrackDic[playerData.playerId]; var needCreate = false; if (string.IsNullOrEmpty(cacheData.name)) { // 创建新的 cacheData.name = playerData.playerName; needCreate = true; } else { if (cacheData.name != playerData.playerName) { cacheData.name = playerData.playerName; needCreate = true; } } if (needCreate) { var clip = track.CreateClip(); clip.start = currentTime; clip.duration = 1f; var setNameActionClip = clip.asset as SetNameActionClip; if (setNameActionClip == null) { return; } setNameActionClip.setName = playerData.playerName; } } private static StoryExcelPlayerData _SearchPlayerData(List searchList, int playerId) { for (int i = 0; i < searchList.Count; i++) { var searchData = searchList[i]; if (searchData.playerId == playerId) { return searchData; } } return new StoryExcelPlayerData() { playerId = playerId }; } private static void _CreateWeaponHides(WeaponHideActionTrack track, StoryExcelPlayerData playerData, float currentTime, float during) { var clip = track.CreateClip(); clip.start = currentTime; clip.duration = during; var weaponHideActionClip = clip.asset as WeaponHideActionClip; if (weaponHideActionClip == null) { return; } weaponHideActionClip.playerId = playerData.playerId; } private static void _CreatePlayerTalks(PlayerTalkActionTrack track, StoryExcelPlayerData playerData, float currentTime) { var clip = track.CreateClip(); clip.start = currentTime; } private static void _CreatePlayerEmojis(PlayerEmojiActionTrack track, StoryExcelPlayerData playerData, float currentTime, float during) { var emojiNames = playerData.faceNames; var splitDuring = during / emojiNames.Count; for (int i = 0; i < emojiNames.Count; i++) { var emojiName = emojiNames[i]; _CreatePlayerEmoji(track, playerData, currentTime, emojiName, splitDuring); currentTime += splitDuring; } } private static void _CreatePlayerEmoji(PlayerEmojiActionTrack track, StoryExcelPlayerData playerData, float currentTime, string emojiName, float during) { var clip = track.CreateClip(); clip.start = currentTime; clip.duration = during; var playerEmojiActionClip = clip.asset as PlayerEmojiActionClip; if (playerEmojiActionClip == null) { return; } playerEmojiActionClip.playerId = playerData.playerId; playerEmojiActionClip.emojiName = emojiName; var emojiConfig = EditorTableManager.instance.tables.EmojiMapperConfig.Get(emojiName); if (emojiConfig == null) { Debug.LogError("无效的表情名字:" + emojiName); return; } playerEmojiActionClip.emojiId = emojiConfig.EmojiId; } private static void _CreatePlayerPos(PlayerPosActionTrack track, StoryExcelPlayerData playerData, float currentTime, float during) { var clip = track.CreateClip(); var playerPosActionClip = clip.asset as PlayerPosActionClip; if (playerPosActionClip == null) { return; } clip.start = currentTime; clip.duration = during; } private static void _CreatePlayerRot(PlayerRotActionTrack track, StoryExcelPlayerData playerData, float currentTime, float during) { var clip = track.CreateClip(); var playerRotActionClip = clip.asset as PlayerRotActionClip; if (playerRotActionClip == null) { return; } playerRotActionClip.playerId = playerData.playerId; clip.start = currentTime; clip.duration = during; } private static void _CreatePlayerActions(AnimationTrack track, StoryExcelPlayerData playerData, double currentTime, double during) { var cacheData = PlayerClipDic[playerData.playerId]; var motionName = playerData.motionName; if (string.IsNullOrEmpty(motionName)) { // 如果之前有,则把之前的loop拉长,end后移 if (cacheData.clipLoop != null && cacheData.clipEnd != null) { cacheData.clipLoop.duration += during + 0.1f; cacheData.clipEnd.start = currentTime + during + 0.1f; // 这里需要进行融合 var mixTime = cacheData.clipEnd.end - cacheData.clipLoop.start; if (mixTime > 0) { cacheData.clipEnd.blendInDuration = mixTime; cacheData.clipLoop.blendOutDuration = mixTime; } } return; } var motionConfig = EditorTableManager.instance.tables.MotionMapperConfig.Get(motionName); if (motionConfig == null) { Debug.LogError("无效的动作名字:" + motionName); return; } var motionPath = _GetMotionFullPath(motionConfig.MotionFullPath); var loadDatas = AssetDatabase.LoadAllAssetsAtPath(motionPath); AnimationClip clipStart = null, clipLoop = null, clipEnd = null; for (int i = 0; i < loadDatas.Length; i++) { var loadData = loadDatas[i]; if (loadData is AnimationClip loadClip) { if (loadClip.name.EndsWith("_start")) { clipStart = loadClip; } if (loadClip.name.EndsWith("_loop")) { clipLoop = loadClip; } if (loadClip.name.EndsWith("_end")) { clipEnd = loadClip; } } } var workTime = currentTime; var passTime = 0.0; if (clipStart != null) { var createClip = track.CreateClip(clipStart); createClip.start = workTime; var startOffset = 0.0; if (cacheData.clipEnd != null) { startOffset = cacheData.clipEnd.duration * 0.7; createClip.start += startOffset; // 这里需要进行融合 var mixTime = cacheData.clipEnd.end - createClip.start; if (mixTime > 0) { cacheData.clipEnd.blendOutDuration = mixTime; createClip.blendInDuration = mixTime; } } createClip.duration = clipStart.length; workTime += clipStart.length + startOffset; passTime += clipStart.length + startOffset; cacheData.clipStart = createClip; } else { Debug.Log("未找到start动作:" + motionName); cacheData.clipStart = null; } if (clipLoop != null) { var createStartTime = workTime; if (cacheData.clipStart != null) { createStartTime = cacheData.clipStart.end; } var createClip = track.CreateClip(clipLoop); createClip.start = createStartTime; createClip.duration = during - passTime + 0.1f; cacheData.clipLoop = createClip; } else { Debug.Log("未找到loop动作:" + motionName); cacheData.clipLoop = null; } if (clipEnd != null) { var createClip = track.CreateClip(clipEnd); createClip.start = currentTime + during + 0.1f; createClip.duration = clipEnd.length; // 融合 if (cacheData.clipLoop != null) { var mixTime = cacheData.clipLoop.end - createClip.start; if (mixTime > 0) { cacheData.clipLoop.blendOutDuration = mixTime; createClip.blendInDuration = mixTime; } } cacheData.clipEnd = createClip; } else { Debug.Log("未找到end动作:" + motionName); cacheData.clipEnd = null; } } private static float _CreatePlayerAction(AnimationTrack track, StoryExcelPlayerData playerData, float currentTime, string motionName) { var clip = track.CreateClip(); var playerActionClip = clip.asset as PlayerActionClip; if (playerActionClip == null) { return 0f; } playerActionClip.playerId = playerData.playerId; playerActionClip.playerName = playerData.playerName; playerActionClip.motionName = motionName; clip.start = currentTime; var motionConfig = EditorTableManager.instance.tables.MotionMapperConfig.Get(motionName); if (motionConfig == null) { Debug.LogError("无效的动作名字:" + motionName); return 0f; } var motionPath = _GetMotionFullPath(motionConfig.MotionFullPath); var motion = AssetDatabase.LoadAssetAtPath(motionPath); if (motion == null) { Debug.LogError("无效的动作路径:" + motionPath); return 0f; } playerActionClip.motionClip = motion; clip.duration = motion.length; return motion.length; } private static void _CreateSceneSwitch(float currentTime, float during) { var clip = _sceneSwitchTrack.CreateClip(); clip.start = currentTime; clip.duration = during; var sceneSwitchActionClip = clip.asset as SceneSwitchActionClip; if (sceneSwitchActionClip == null) { return; } sceneSwitchActionClip.showIndex = 0; } private static void _CreateBlockChoice(float currentTime, float during) { var clip = _blockChoiceTrack.CreateClip(); clip.start = currentTime; clip.duration = during; var blockChoiceActionClip = clip.asset as BlockChoiceActionClip; if (blockChoiceActionClip == null) { return; } blockChoiceActionClip.choiceMsgs = Array.Empty(); blockChoiceActionClip.choiceBlockIds = Array.Empty(); } private static void _CreateMsgAction(MsgActionTrack blockLine, StoryExcelBlockData block, float currentTime, float during) { var clip = blockLine.CreateClip(); var msgActionClip = clip.asset as MsgActionClip; if (msgActionClip == null) { return; } msgActionClip.blockId = block.blockId; msgActionClip.msg = block.blockMsg; clip.start = currentTime; clip.duration = during; } public static TimelineClip SearchClip(TimelineAsset timeline, PlayableAsset clip) { var allTracks = timeline.GetOutputTracks(); foreach (var track in allTracks) { var clips = track.GetClips(); foreach (var trackClip in clips) { if (trackClip.asset == clip) { return trackClip; } } } return null; } public static TrackAsset SearchTrack(TimelineAsset timeline, PlayableAsset clip) { var allTracks = timeline.GetOutputTracks(); foreach (var track in allTracks) { var clips = track.GetClips(); foreach (var trackClip in clips) { if (trackClip.asset == clip) { return track; } } } return null; } public static TimelineClip SearchBeforeClip(TrackAsset track, PlayableAsset clip) { var clips = track.GetClips().ToArray(); clips.SortMinToMax(); for (int i = 0; i < clips.Length; i++) { var trackClip = clips[i]; if (trackClip.asset == clip) { if (i == 0) { return null; } return clips[i - 1]; } } return null; } public static TimelineClip SearchBehindClip(TrackAsset track, PlayableAsset clip) { var clips = track.GetClips().ToArray(); clips.SortMinToMax(); for (int i = 0; i < clips.Length; i++) { var trackClip = clips[i]; if (trackClip.asset == clip) { if (i == clips.Length - 1) { return null; } var nextIndex = i + 1; if (nextIndex >= clips.Length) { return null; } return clips[nextIndex]; } } return null; } public static void SortMinToMax(this TimelineClip[] clips) { clips.Sort((a, b) => a.start.CompareTo(b.start)); } } }