diff --git a/Source/Editor/GUI/Timeline/Media.cs b/Source/Editor/GUI/Timeline/Media.cs index 5f90b6799..b826e5b6e 100644 --- a/Source/Editor/GUI/Timeline/Media.cs +++ b/Source/Editor/GUI/Timeline/Media.cs @@ -114,6 +114,11 @@ namespace FlaxEditor.GUI.Timeline /// public event Action StartFrameChanged; + /// + /// Gets the end frame of the media (start + duration). + /// + public int EndFrame => _startFrame + _durationFrames; + /// /// Gets or sets the total duration of the media event in the timeline sequence frames amount. /// @@ -175,6 +180,11 @@ namespace FlaxEditor.GUI.Timeline /// public object PropertiesEditObject; + /// + /// Gets a value indicating whether this media can be split. + /// + public bool CanSplit; + /// /// Initializes a new instance of the class. /// @@ -258,6 +268,22 @@ namespace FlaxEditor.GUI.Timeline Width = Duration * Timeline.UnitsPerSecond * _timeline.Zoom; } + /// + /// Splits the media at the specified frame. + /// + /// The frame to split at. + /// The another media created after this media split. + public virtual Media Split(int frame) + { + var clone = (Media)Activator.CreateInstance(GetType()); + clone.StartFrame = frame; + clone.DurationFrames = EndFrame - frame; + DurationFrames = DurationFrames - clone.DurationFrames; + Track?.AddMedia(clone); + Timeline?.MarkAsEdited(); + return clone; + } + /// public override void GetDesireClientArea(out Rectangle rect) { diff --git a/Source/Editor/GUI/Timeline/Timeline.Data.cs b/Source/Editor/GUI/Timeline/Timeline.Data.cs index e1a4b69a7..f5f9bc50b 100644 --- a/Source/Editor/GUI/Timeline/Timeline.Data.cs +++ b/Source/Editor/GUI/Timeline/Timeline.Data.cs @@ -104,8 +104,9 @@ namespace FlaxEditor.GUI.Timeline } break; } - case 2: - case 3: + case 2: // [Deprecated in 2020 expires on 03.09.2023] + case 3: // [Deprecated on 03.09.2021 expires on 03.09.2023] + case 4: { // Load properties FramesPerSecond = stream.ReadSingle(); diff --git a/Source/Editor/GUI/Timeline/Timeline.cs b/Source/Editor/GUI/Timeline/Timeline.cs index 2b5349d2c..570caed62 100644 --- a/Source/Editor/GUI/Timeline/Timeline.cs +++ b/Source/Editor/GUI/Timeline/Timeline.cs @@ -38,7 +38,7 @@ namespace FlaxEditor.GUI.Timeline new KeyValuePair(0, "Custom"), }; - internal const int FormatVersion = 3; + internal const int FormatVersion = 4; /// /// The base class for timeline properties proxy objects. @@ -1584,6 +1584,39 @@ namespace FlaxEditor.GUI.Timeline } } + /// + /// Splits the media (all or selected only) at the given frame. + /// + /// The frame to split at. + public void Split(int frame) + { + List actions = null; + foreach (var track in _tracks) + { + byte[] trackData = null; + for (int i = track.Media.Count - 1; i >= 0; i--) + { + if (track.Media.Count >= track.MaxMediaCount) + break; + var media = track.Media[i]; + if (media.CanSplit && media.StartFrame < frame && media.EndFrame > frame) + { + if (Undo != null && Undo.Enabled) + trackData = EditTrackAction.CaptureData(track); + media.Split(frame); + } + } + if (trackData != null) + { + if (actions == null) + actions = new List(); + actions.Add(new EditTrackAction(this, track, trackData, EditTrackAction.CaptureData(track))); + } + } + if (actions != null) + Undo.AddAction(new MultiUndoAction(actions, "Split")); + } + /// /// Called once to setup the drag drop handling for the timeline (lazy init on first drag action). /// @@ -1979,6 +2012,9 @@ namespace FlaxEditor.GUI.Timeline return true; } break; + case KeyboardKeys.S: + Split(CurrentFrame); + return true; } return false; diff --git a/Source/Editor/GUI/Timeline/Track.cs b/Source/Editor/GUI/Timeline/Track.cs index f61bd87f3..ea1fa6501 100644 --- a/Source/Editor/GUI/Timeline/Track.cs +++ b/Source/Editor/GUI/Timeline/Track.cs @@ -171,6 +171,16 @@ namespace FlaxEditor.GUI.Timeline /// public bool Loop; + /// + /// The minimum amount of media items for this track. + /// + public int MinMediaCount = 0; + + /// + /// The maximum amount of media items for this track. + /// + public int MaxMediaCount = 1024; + /// /// The track archetype. /// diff --git a/Source/Editor/GUI/Timeline/Tracks/AudioTrack.cs b/Source/Editor/GUI/Timeline/Tracks/AudioTrack.cs index 197e889a6..8a1931a14 100644 --- a/Source/Editor/GUI/Timeline/Tracks/AudioTrack.cs +++ b/Source/Editor/GUI/Timeline/Tracks/AudioTrack.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using FlaxEditor.Utilities; using FlaxEditor.GUI.Timeline.Undo; using FlaxEditor.Viewport.Previews; @@ -15,7 +16,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks /// The timeline media that represents an audio clip media event. /// /// - public class AudioMedia : SingleMediaAssetMedia + public class AudioMedia : Media { /// /// True if loop track, otherwise audio clip will stop on the end. @@ -25,19 +26,31 @@ namespace FlaxEditor.GUI.Timeline.Tracks get => Track.Loop; set { - if (Track.Loop != value) - { - Track.Loop = value; - Preview.DrawMode = value ? AudioClipPreview.DrawModes.Looped : AudioClipPreview.DrawModes.Single; - } + if (Loop == value) + return; + Track.Loop = value; + Preview.DrawMode = value ? AudioClipPreview.DrawModes.Looped : AudioClipPreview.DrawModes.Single; + Timeline?.MarkAsEdited(); + } + } + + /// + /// Playback offset of the audio (in seconds). + /// + public float Offset + { + get => Preview.ViewOffset; + set + { + if (Mathf.NearEqual(Preview.ViewOffset, value)) + return; + Preview.ViewOffset = value; + Timeline?.MarkAsEdited(); } } private sealed class Proxy : ProxyBase { - /// - /// Gets or sets the audio clip to play. - /// [EditorDisplay("General"), EditorOrder(10), Tooltip("The audio clip to play.")] public AudioClip Audio { @@ -45,17 +58,21 @@ namespace FlaxEditor.GUI.Timeline.Tracks set => Track.Asset = value; } - /// - /// Gets or sets the audio clip looping mode. - /// [EditorDisplay("General"), EditorOrder(20), Tooltip("If checked, the audio clip will loop when playback exceeds its duration. Otherwise it will stop play.")] public bool Loop { - get => Track.TrackLoop; - set => Track.TrackLoop = value; + get => Media.Loop; + set => Media.Loop = value; + } + + [EditorDisplay("General"), EditorOrder(30), Tooltip("Playback offset of the audio (in seconds).")] + [Limit(0, float.MaxValue, 0.01f)] + public float Offset + { + get => Media.Offset; + set => Media.Offset = value; } - /// public Proxy(AudioTrack track, AudioMedia media) : base(track, media) { @@ -70,6 +87,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks /// public AudioMedia() { + CanSplit = true; Preview = new AudioClipPreview { AnchorPreset = AnchorPresets.StretchAll, @@ -79,12 +97,30 @@ namespace FlaxEditor.GUI.Timeline.Tracks }; } + /// + protected override void OnStartFrameChanged() + { + base.OnStartFrameChanged(); + + if (Track != null && Track.SubTracks.Count != 0 && Track.SubTracks[0] is AudioVolumeTrack volumeTrack) + volumeTrack.UpdateCurve(); + } + + /// + protected override void OnDurationFramesChanged() + { + base.OnDurationFramesChanged(); + + if (Track != null && Track.SubTracks.Count != 0 && Track.SubTracks[0] is AudioVolumeTrack volumeTrack) + volumeTrack.UpdateCurve(); + } + /// public override void OnTimelineChanged(Track track) { base.OnTimelineChanged(track); - PropertiesEditObject = new Proxy(Track as AudioTrack, this); + PropertiesEditObject = track != null ? new Proxy((AudioTrack)track, this) : null; } /// @@ -94,6 +130,17 @@ namespace FlaxEditor.GUI.Timeline.Tracks Preview.ViewScale = Timeline.UnitsPerSecond / AudioClipPreview.UnitsPerSecond * Timeline.Zoom; } + + /// + public override Media Split(int frame) + { + var offset = Offset + ((float)(frame - StartFrame) / DurationFrames) * Duration; + var clone = (AudioMedia)base.Split(frame); + clone.Preview.ViewOffset = offset; + clone.Preview.Asset = Preview.Asset; + clone.Preview.DrawMode = Preview.DrawMode; + return clone; + } } /// @@ -123,46 +170,46 @@ namespace FlaxEditor.GUI.Timeline.Tracks var e = (AudioTrack)track; Guid id = stream.ReadGuid(); e.Asset = FlaxEngine.Content.LoadAsync(id); - var m = e.TrackMedia; - m.StartFrame = stream.ReadInt32(); - m.DurationFrames = stream.ReadInt32(); - m.Preview.DrawMode = track.Loop ? AudioClipPreview.DrawModes.Looped : AudioClipPreview.DrawModes.Single; + if (version <= 3) + { + // [Deprecated on 03.09.2021 expires on 03.09.2023] + var m = e.TrackMedia; + m.StartFrame = stream.ReadInt32(); + m.DurationFrames = stream.ReadInt32(); + m.Preview.ViewOffset = 0.0f; + m.Preview.DrawMode = track.Loop ? AudioClipPreview.DrawModes.Looped : AudioClipPreview.DrawModes.Single; + } + else + { + var count = stream.ReadInt32(); + while (e.Media.Count > count) + e.RemoveMedia(e.Media.Last()); + while (e.Media.Count < count) + e.AddMedia(new AudioMedia()); + for (int i = 0; i < count; i++) + { + var m = (AudioMedia)e.Media[i]; + m.StartFrame = stream.ReadInt32(); + m.DurationFrames = stream.ReadInt32(); + m.Preview.ViewOffset = stream.ReadSingle(); + m.Preview.DrawMode = track.Loop ? AudioClipPreview.DrawModes.Looped : AudioClipPreview.DrawModes.Single; + m.Preview.Asset = e.Asset; + } + } } private static void SaveTrack(Track track, BinaryWriter stream) { var e = (AudioTrack)track; - var assetId = e.Asset?.ID ?? Guid.Empty; - - stream.Write(assetId.ToByteArray()); - - if (e.Media.Count != 0) + stream.WriteGuid(ref e.AssetID); + var count = e.Media.Count; + stream.Write(count); + for (int i = 0; i < count; i++) { - var m = e.TrackMedia; + var m = (AudioMedia)e.Media[i]; stream.Write(m.StartFrame); stream.Write(m.DurationFrames); - } - else - { - stream.Write(0); - stream.Write(track.Timeline.DurationFrames); - } - } - - /// - /// Gets or sets the audio clip looping mode. - /// - public bool TrackLoop - { - get => TrackMedia.Loop; - set - { - AudioMedia media = TrackMedia; - if (media.Loop == value) - return; - - media.Loop = value; - Timeline?.MarkAsEdited(); + stream.Write(m.Offset); } } @@ -172,6 +219,8 @@ namespace FlaxEditor.GUI.Timeline.Tracks public AudioTrack(ref TrackCreateOptions options) : base(ref options) { + MinMediaCount = 1; + // Add button const float buttonSize = 14; _addButton = new Button @@ -219,7 +268,8 @@ namespace FlaxEditor.GUI.Timeline.Tracks { base.OnAssetChanged(); - TrackMedia.Preview.Asset = Asset; + foreach (AudioMedia m in Media) + m.Preview.Asset = Asset; } } @@ -280,7 +330,6 @@ namespace FlaxEditor.GUI.Timeline.Tracks } } - private AudioMedia _audioMedia; private const float CollapsedHeight = 20.0f; private const float ExpandedHeight = 64.0f; private Label _previewValue; @@ -381,17 +430,49 @@ namespace FlaxEditor.GUI.Timeline.Tracks } } + private bool GetRangeFrames(out int startFrame, out int endFrame) + { + if (ParentTrack != null && ParentTrack.Media.Count != 0) + { + startFrame = ParentTrack.Media[0].StartFrame; + endFrame = ParentTrack.Media[0].EndFrame; + for (int i = 1; i < ParentTrack.Media.Count; i++) + { + endFrame = Mathf.Max(endFrame, ParentTrack.Media[i].EndFrame); + } + return true; + } + startFrame = endFrame = 0; + return false; + } + + private bool GetRangeMedia(out Media startMedia, out Media endMedia) + { + if (ParentTrack != null && ParentTrack.Media.Count != 0) + { + startMedia = endMedia = ParentTrack.Media[0]; + for (int i = 1; i < ParentTrack.Media.Count; i++) + { + if (ParentTrack.Media[i].EndFrame >= endMedia.EndFrame) + endMedia = ParentTrack.Media[i]; + } + return true; + } + startMedia = endMedia = null; + return false; + } + /// protected override void OnContextMenu(ContextMenu.ContextMenu menu) { base.OnContextMenu(menu); - if (_audioMedia == null || Curve == null) + if (!GetRangeFrames(out var startFrame, out _) || Curve == null) return; menu.AddSeparator(); menu.AddButton("Copy Preview Value", () => { - var time = (Timeline.CurrentFrame - _audioMedia.StartFrame) / Timeline.FramesPerSecond; + var time = (Timeline.CurrentFrame - startFrame) / Timeline.FramesPerSecond; Curve.Evaluate(out var value, time, false); Clipboard.Text = FlaxEngine.Utils.RoundTo2DecimalPlaces(Mathf.Saturate(value)).ToString("0.00"); }).LinkTooltip("Copies the current track value to the clipboard").Enabled = Timeline.ShowPreviewValues; @@ -400,15 +481,15 @@ namespace FlaxEditor.GUI.Timeline.Tracks /// public override bool GetNextKeyframeFrame(float time, out int result) { - if (_audioMedia != null) + if (GetRangeFrames(out var startFrame, out var endFrame)) { - var mediaTime = time - _audioMedia.StartFrame / Timeline.FramesPerSecond; + var mediaTime = time - startFrame / Timeline.FramesPerSecond; for (int i = 0; i < Curve.Keyframes.Count; i++) { var k = Curve.Keyframes[i]; if (k.Time > mediaTime) { - result = Mathf.FloorToInt(k.Time * Timeline.FramesPerSecond) + _audioMedia.StartFrame; + result = Mathf.FloorToInt(k.Time * Timeline.FramesPerSecond) + startFrame; return true; } } @@ -419,13 +500,13 @@ namespace FlaxEditor.GUI.Timeline.Tracks private void OnAddKeyClicked(Image image, MouseButton button) { var currentFrame = Timeline.CurrentFrame; - if (button == MouseButton.Left && _audioMedia != null && currentFrame >= _audioMedia.StartFrame && currentFrame < _audioMedia.StartFrame + _audioMedia.DurationFrames) + if (button == MouseButton.Left && GetRangeFrames(out var startFrame, out var endFrame) && currentFrame >= startFrame && currentFrame < endFrame) { - var time = (currentFrame - _audioMedia.StartFrame) / Timeline.FramesPerSecond; + var time = (currentFrame - startFrame) / Timeline.FramesPerSecond; for (int i = Curve.Keyframes.Count - 1; i >= 0; i--) { var k = Curve.Keyframes[i]; - var frame = Mathf.FloorToInt(k.Time * Timeline.FramesPerSecond) + _audioMedia.StartFrame; + var frame = Mathf.FloorToInt(k.Time * Timeline.FramesPerSecond) + startFrame; if (frame == Timeline.CurrentFrame) { // Already added @@ -449,15 +530,15 @@ namespace FlaxEditor.GUI.Timeline.Tracks /// public override bool GetPreviousKeyframeFrame(float time, out int result) { - if (_audioMedia != null) + if (GetRangeFrames(out var startFrame, out _)) { - var mediaTime = time - _audioMedia.StartFrame / Timeline.FramesPerSecond; + var mediaTime = time - startFrame / Timeline.FramesPerSecond; for (int i = Curve.Keyframes.Count - 1; i >= 0; i--) { var k = Curve.Keyframes[i]; if (k.Time < mediaTime) { - result = Mathf.FloorToInt(k.Time * Timeline.FramesPerSecond) + _audioMedia.StartFrame; + result = Mathf.FloorToInt(k.Time * Timeline.FramesPerSecond) + startFrame; return true; } } @@ -467,17 +548,17 @@ namespace FlaxEditor.GUI.Timeline.Tracks private void UpdatePreviewValue() { - if (_audioMedia == null || Curve == null || Timeline == null) + if (!GetRangeFrames(out var startFrame, out _) || Curve == null || Timeline == null) return; - var time = (Timeline.CurrentFrame - _audioMedia.StartFrame) / Timeline.FramesPerSecond; + var time = (Timeline.CurrentFrame - startFrame) / Timeline.FramesPerSecond; Curve.Evaluate(out var value, time, false); _previewValue.Text = FlaxEngine.Utils.RoundTo2DecimalPlaces(Mathf.Saturate(value)).ToString("0.00"); } - private void UpdateCurve() + internal void UpdateCurve() { - if (_audioMedia == null || Curve == null || Timeline == null) + if (!GetRangeMedia(out var startMedia, out var endMedia) || Curve == null || Timeline == null) return; bool wasVisible = Curve.Visible; Curve.Visible = Visible; @@ -489,7 +570,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks } Curve.KeyframesEditorContext = Timeline; Curve.CustomViewPanning = Timeline.OnKeyframesViewPanning; - Curve.Bounds = new Rectangle(_audioMedia.X, Y + 1.0f, _audioMedia.Width, Height - 2.0f); + Curve.Bounds = new Rectangle(startMedia.X, Y + 1.0f, endMedia.Right - startMedia.Left, Height - 2.0f); var expanded = IsExpanded; if (expanded) { @@ -542,19 +623,8 @@ namespace FlaxEditor.GUI.Timeline.Tracks { base.OnParentTrackChanged(parent); - if (_audioMedia != null) + if (parent != null) { - _audioMedia.StartFrameChanged -= UpdateCurve; - _audioMedia.DurationFramesChanged -= UpdateCurve; - _audioMedia = null; - } - - if (parent is AudioTrack audioTrack) - { - var media = audioTrack.TrackMedia; - media.StartFrameChanged += UpdateCurve; - media.DurationFramesChanged += UpdateCurve; - _audioMedia = media; UpdateCurve(); UpdatePreviewValue(); } diff --git a/Source/Editor/GUI/Timeline/Tracks/CameraCutTrack.cs b/Source/Editor/GUI/Timeline/Tracks/CameraCutTrack.cs index 76c06c41a..68ee84a7c 100644 --- a/Source/Editor/GUI/Timeline/Tracks/CameraCutTrack.cs +++ b/Source/Editor/GUI/Timeline/Tracks/CameraCutTrack.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; using FlaxEditor.Utilities; using FlaxEngine; using FlaxEngine.GUI; @@ -33,6 +34,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks public CameraCutMedia() { ClipChildren = true; + CanSplit = true; } /// @@ -292,7 +294,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks { base.OnTimelineChanged(track); - PropertiesEditObject = new Proxy(Track as CameraCutTrack, this); + PropertiesEditObject = track != null ? new Proxy((CameraCutTrack)track, this) : null; UpdateThumbnails(); } @@ -673,26 +675,41 @@ namespace FlaxEditor.GUI.Timeline.Tracks { var e = (CameraCutTrack)track; e.ActorID = stream.ReadGuid(); - var m = e.TrackMedia; - m.StartFrame = stream.ReadInt32(); - m.DurationFrames = stream.ReadInt32(); + if (version <= 3) + { + // [Deprecated on 03.09.2021 expires on 03.09.2023] + var m = e.TrackMedia; + m.StartFrame = stream.ReadInt32(); + m.DurationFrames = stream.ReadInt32(); + } + else + { + var count = stream.ReadInt32(); + while (e.Media.Count > count) + e.RemoveMedia(e.Media.Last()); + while (e.Media.Count < count) + e.AddMedia(new CameraCutMedia()); + for (int i = 0; i < count; i++) + { + var m = (CameraCutMedia)e.Media[i]; + m.StartFrame = stream.ReadInt32(); + m.DurationFrames = stream.ReadInt32(); + } + } } private static void SaveTrack(Track track, BinaryWriter stream) { var e = (CameraCutTrack)track; - stream.Write(e.ActorID.ToByteArray()); - if (e.Media.Count != 0) + stream.WriteGuid(ref e.ActorID); + var count = e.Media.Count; + stream.Write(count); + for (int i = 0; i < count; i++) { - var m = e.TrackMedia; + var m = e.Media[i]; stream.Write(m.StartFrame); stream.Write(m.DurationFrames); } - else - { - stream.Write(0); - stream.Write(track.Timeline.DurationFrames); - } } private Image _pilotCamera; @@ -702,10 +719,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks /// public Camera Camera => Actor as Camera; - /// - /// Gets the camera track media. - /// - public CameraCutMedia TrackMedia + private CameraCutMedia TrackMedia { get { @@ -731,6 +745,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks public CameraCutTrack(ref TrackCreateOptions options) : base(ref options, false) { + MinMediaCount = 1; Height = CameraCutThumbnailRenderer.Height + 8; // Pilot Camera button @@ -794,12 +809,18 @@ namespace FlaxEditor.GUI.Timeline.Tracks } } + private void UpdateThumbnails() + { + foreach (CameraCutMedia media in Media) + media.UpdateThumbnails(); + } + /// protected override void OnObjectExistenceChanged(object obj) { base.OnObjectExistenceChanged(obj); - TrackMedia.UpdateThumbnails(); + UpdateThumbnails(); } /// diff --git a/Source/Editor/GUI/Timeline/Tracks/NestedSceneAnimationTrack.cs b/Source/Editor/GUI/Timeline/Tracks/NestedSceneAnimationTrack.cs index df24b0d5c..916d738c2 100644 --- a/Source/Editor/GUI/Timeline/Tracks/NestedSceneAnimationTrack.cs +++ b/Source/Editor/GUI/Timeline/Tracks/NestedSceneAnimationTrack.cs @@ -12,7 +12,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks /// The timeline media that represents a nested scene animation media event. /// /// - public class NestedSceneAnimationMedia : SingleMediaAssetMedia + public class NestedSceneAnimationMedia : Media { private sealed class Proxy : ProxyBase { @@ -48,7 +48,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks { base.OnTimelineChanged(track); - PropertiesEditObject = new Proxy(Track as NestedSceneAnimationTrack, this); + PropertiesEditObject = track != null ? new Proxy((NestedSceneAnimationTrack)track, this) : null; } } @@ -87,10 +87,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks private static void SaveTrack(Track track, BinaryWriter stream) { var e = (NestedSceneAnimationTrack)track; - var assetId = e.Asset?.ID ?? Guid.Empty; - - stream.Write(assetId.ToByteArray()); - + stream.WriteGuid(ref e.AssetID); if (e.Media.Count != 0) { var m = e.TrackMedia; @@ -123,6 +120,8 @@ namespace FlaxEditor.GUI.Timeline.Tracks public NestedSceneAnimationTrack(ref TrackCreateOptions options) : base(ref options) { + MinMediaCount = 1; + MaxMediaCount = 1; } /// diff --git a/Source/Editor/GUI/Timeline/Tracks/ParticleEmitterTrack.cs b/Source/Editor/GUI/Timeline/Tracks/ParticleEmitterTrack.cs index 1a26555ac..d6184e50c 100644 --- a/Source/Editor/GUI/Timeline/Tracks/ParticleEmitterTrack.cs +++ b/Source/Editor/GUI/Timeline/Tracks/ParticleEmitterTrack.cs @@ -12,7 +12,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks /// The timeline media that represents a particle miter playback media event. /// /// - public class ParticleEmitterMedia : SingleMediaAssetMedia + public class ParticleEmitterMedia : Media { private sealed class Proxy : ProxyBase { @@ -38,7 +38,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks { base.OnTimelineChanged(track); - PropertiesEditObject = new Proxy(Track as ParticleEmitterTrack, this); + PropertiesEditObject = track != null ? new Proxy((ParticleEmitterTrack)track, this) : null; } } @@ -78,11 +78,8 @@ namespace FlaxEditor.GUI.Timeline.Tracks private static void SaveTrack(Track track, BinaryWriter stream) { var e = (ParticleEmitterTrack)track; - var emitterId = e.Asset?.ID ?? Guid.Empty; - - stream.Write(emitterId.ToByteArray()); + stream.WriteGuid(ref e.AssetID); stream.Write(((ParticleSystemTimeline)track.Timeline).Emitters.IndexOf(e)); - if (e.Media.Count != 0) { var m = e.TrackMedia; @@ -108,6 +105,8 @@ namespace FlaxEditor.GUI.Timeline.Tracks public ParticleEmitterTrack(ref TrackCreateOptions options) : base(ref options) { + MinMediaCount = 1; + MaxMediaCount = 1; } /// diff --git a/Source/Editor/GUI/Timeline/Tracks/PostProcessMaterialTrack.cs b/Source/Editor/GUI/Timeline/Tracks/PostProcessMaterialTrack.cs index 201a553a6..4310ac577 100644 --- a/Source/Editor/GUI/Timeline/Tracks/PostProcessMaterialTrack.cs +++ b/Source/Editor/GUI/Timeline/Tracks/PostProcessMaterialTrack.cs @@ -2,6 +2,7 @@ using System; using System.IO; +using System.Linq; using FlaxEditor.Utilities; using FlaxEngine; @@ -11,13 +12,10 @@ namespace FlaxEditor.GUI.Timeline.Tracks /// The timeline media that represents a post-process material media event. /// /// - public class PostProcessMaterialMedia : SingleMediaAssetMedia + public class PostProcessMaterialMedia : Media { private sealed class Proxy : ProxyBase { - /// - /// Gets or sets the post process material to show. - /// [EditorDisplay("General"), EditorOrder(10), Tooltip("The post process material to show.")] public MaterialBase PostProcessMaterial { @@ -32,12 +30,20 @@ namespace FlaxEditor.GUI.Timeline.Tracks } } + /// + /// Initializes a new instance of the class. + /// + public PostProcessMaterialMedia() + { + CanSplit = true; + } + /// public override void OnTimelineChanged(Track track) { base.OnTimelineChanged(track); - PropertiesEditObject = new Proxy(Track as PostProcessMaterialTrack, this); + PropertiesEditObject = track != null ? new Proxy((PostProcessMaterialTrack)track, this) : null; } } @@ -68,29 +74,41 @@ namespace FlaxEditor.GUI.Timeline.Tracks var e = (PostProcessMaterialTrack)track; Guid id = stream.ReadGuid(); e.Asset = FlaxEngine.Content.LoadAsync(id); - var m = e.TrackMedia; - m.StartFrame = stream.ReadInt32(); - m.DurationFrames = stream.ReadInt32(); + if (version <= 3) + { + // [Deprecated on 03.09.2021 expires on 03.09.2023] + var m = e.TrackMedia; + m.StartFrame = stream.ReadInt32(); + m.DurationFrames = stream.ReadInt32(); + } + else + { + var count = stream.ReadInt32(); + while (e.Media.Count > count) + e.RemoveMedia(e.Media.Last()); + while (e.Media.Count < count) + e.AddMedia(new PostProcessMaterialMedia()); + for (int i = 0; i < count; i++) + { + var m = e.Media[i]; + m.StartFrame = stream.ReadInt32(); + m.DurationFrames = stream.ReadInt32(); + } + } } private static void SaveTrack(Track track, BinaryWriter stream) { var e = (PostProcessMaterialTrack)track; - var materialId = e.Asset?.ID ?? Guid.Empty; - - stream.Write(materialId.ToByteArray()); - - if (e.Media.Count != 0) + stream.WriteGuid(ref e.AssetID); + var count = e.Media.Count; + stream.Write(count); + for (int i = 0; i < count; i++) { - var m = e.TrackMedia; + var m = e.Media[i]; stream.Write(m.StartFrame); stream.Write(m.DurationFrames); } - else - { - stream.Write(0); - stream.Write(track.Timeline.DurationFrames); - } } /// @@ -100,6 +118,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks public PostProcessMaterialTrack(ref TrackCreateOptions options) : base(ref options) { + MinMediaCount = 1; } } } diff --git a/Source/Editor/GUI/Timeline/Tracks/ScreenFadeTrack.cs b/Source/Editor/GUI/Timeline/Tracks/ScreenFadeTrack.cs index fee15d39c..13b5c6fbc 100644 --- a/Source/Editor/GUI/Timeline/Tracks/ScreenFadeTrack.cs +++ b/Source/Editor/GUI/Timeline/Tracks/ScreenFadeTrack.cs @@ -81,7 +81,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks { base.OnTimelineChanged(track); - PropertiesEditObject = new Proxy(Track as ScreenFadeTrack, this); + PropertiesEditObject = track != null ? new Proxy((ScreenFadeTrack)track, this) : null; } /// diff --git a/Source/Editor/GUI/Timeline/Tracks/SingleMediaAssetTrack.cs b/Source/Editor/GUI/Timeline/Tracks/SingleMediaAssetTrack.cs index adaf354ca..694323e62 100644 --- a/Source/Editor/GUI/Timeline/Tracks/SingleMediaAssetTrack.cs +++ b/Source/Editor/GUI/Timeline/Tracks/SingleMediaAssetTrack.cs @@ -8,18 +8,6 @@ using FlaxEngine.GUI; namespace FlaxEditor.GUI.Timeline.Tracks { - /// - /// The timeline media that represents a media event with an asset reference. - /// - /// - public class SingleMediaAssetMedia : Media - { - /// - /// The asset id. - /// - public Guid Asset; - } - /// /// The base class for timeline tracks that use single media with an asset reference. /// @@ -28,26 +16,29 @@ namespace FlaxEditor.GUI.Timeline.Tracks /// public abstract class SingleMediaAssetTrack : SingleMediaTrack where TAsset : Asset - where TMedia : SingleMediaAssetMedia, new() + where TMedia : Media, new() { /// /// The asset reference picker control. /// protected readonly AssetPicker _picker; + /// + /// The asset id. + /// + public Guid AssetID; + /// /// Gets or sets the asset. /// public TAsset Asset { - get => FlaxEngine.Content.LoadAsync(TrackMedia.Asset); + get => FlaxEngine.Content.LoadAsync(AssetID); set { - TMedia media = TrackMedia; - if (media.Asset == value?.ID) + if (AssetID == value?.ID) return; - - media.Asset = value?.ID ?? Guid.Empty; + AssetID = value?.ID ?? Guid.Empty; _picker.SelectedAsset = value; OnAssetChanged(); Timeline?.MarkAsEdited(); diff --git a/Source/Editor/GUI/Timeline/Tracks/SingleMediaTrack.cs b/Source/Editor/GUI/Timeline/Tracks/SingleMediaTrack.cs index 6ed700954..eb250bff9 100644 --- a/Source/Editor/GUI/Timeline/Tracks/SingleMediaTrack.cs +++ b/Source/Editor/GUI/Timeline/Tracks/SingleMediaTrack.cs @@ -44,9 +44,16 @@ namespace FlaxEditor.GUI.Timeline.Tracks /// public override void OnSpawned() { - // Ensure to have valid media added - // ReSharper disable once UnusedVariable - var media = TrackMedia; + // Ensure to have minimum valid media count + for (int i = Media.Count; i < MinMediaCount; i++) + { + var m = new TMedia + { + StartFrame = 0, + DurationFrames = Timeline?.DurationFrames ?? 60, + }; + AddMedia(m); + } base.OnSpawned(); } diff --git a/Source/Engine/Animations/SceneAnimations/SceneAnimation.cpp b/Source/Engine/Animations/SceneAnimations/SceneAnimation.cpp index 9518bef8e..9fd1e6810 100644 --- a/Source/Engine/Animations/SceneAnimations/SceneAnimation.cpp +++ b/Source/Engine/Animations/SceneAnimations/SceneAnimation.cpp @@ -22,6 +22,11 @@ SceneAnimation::SceneAnimation(const SpawnParams& params, const AssetInfo* info) { } +float SceneAnimation::GetDuration() const +{ + return (float)DurationFrames / FramesPerSecond; +} + const BytesContainer& SceneAnimation::LoadTimeline() { WaitForLoaded(); @@ -110,8 +115,9 @@ Asset::LoadResult SceneAnimation::load() stream.ReadInt32(&version); switch (version) { - case 2: - case 3: + case 2: // [Deprecated in 2020 expires on 03.09.2023] + case 3: // [Deprecated on 03.09.2021 expires on 03.09.2023] + case 4: { stream.ReadFloat(&FramesPerSecond); stream.ReadInt32(&DurationFrames); @@ -143,14 +149,25 @@ Asset::LoadResult SceneAnimation::load() switch (track.Type) { case Track::Types::Folder: - { break; - } case Track::Types::PostProcessMaterial: { const auto trackData = stream.Read(); track.Data = trackData; track.Asset = Content::LoadAsync(trackData->AssetID); + const auto trackRuntime = _runtimeData.Move(); + track.RuntimeData = trackRuntime; + if (version <= 3) + { + // [Deprecated on 03.09.2021 expires on 03.09.2023] + trackRuntime->Count = 1; + trackRuntime->Media = stream.Read(); + } + else + { + stream.ReadInt32(&trackRuntime->Count); + trackRuntime->Media = stream.Read(trackRuntime->Count); + } if (trackData->AssetID.IsValid() && !track.Asset) { LOG(Warning, "Missing material for track {0} in {1}.", track.Name, ToString()); @@ -197,6 +214,20 @@ Asset::LoadResult SceneAnimation::load() track.RuntimeData = trackRuntime; track.TrackStateIndex = TrackStatesCount++; trackRuntime->VolumeTrackIndex = -1; + if (version <= 3) + { + // [Deprecated on 03.09.2021 expires on 03.09.2023] + trackRuntime->Count = 1; + trackRuntime->Media = _runtimeData.Move(); + stream.ReadInt32(&trackRuntime->Media->StartFrame); + stream.ReadInt32(&trackRuntime->Media->DurationFrames); + trackRuntime->Media->Offset = 0.0f; + } + else + { + stream.ReadInt32(&trackRuntime->Count); + trackRuntime->Media = stream.Read(trackRuntime->Count); + } break; } case Track::Types::AudioVolume: @@ -211,7 +242,7 @@ Asset::LoadResult SceneAnimation::load() { if (Tracks[track.ParentIndex].Type == Track::Types::Audio) { - ((AudioTrack::Runtime*)((byte*)_runtimeData.GetHandle() + (intptr)Tracks[track.ParentIndex].RuntimeData))->VolumeTrackIndex = i; + ((AudioTrack::Runtime*)(_runtimeData.GetHandle() + (intptr)Tracks[track.ParentIndex].RuntimeData))->VolumeTrackIndex = i; } else { @@ -365,6 +396,17 @@ Asset::LoadResult SceneAnimation::load() const auto trackRuntime = _runtimeData.Move(); track.RuntimeData = trackRuntime; track.TrackStateIndex = TrackStatesCount++; + if (version <= 3) + { + // [Deprecated on 03.09.2021 expires on 03.09.2023] + trackRuntime->Count = 1; + trackRuntime->Media = stream.Read(); + } + else + { + stream.ReadInt32(&trackRuntime->Count); + trackRuntime->Media = stream.Read(trackRuntime->Count); + } break; } default: diff --git a/Source/Engine/Animations/SceneAnimations/SceneAnimation.h b/Source/Engine/Animations/SceneAnimations/SceneAnimation.h index 3961edf79..538e35cf3 100644 --- a/Source/Engine/Animations/SceneAnimations/SceneAnimation.h +++ b/Source/Engine/Animations/SceneAnimations/SceneAnimation.h @@ -129,24 +129,23 @@ public: } }; + struct Media + { + int32 StartFrame; + int32 DurationFrames; + }; + struct PostProcessMaterialTrack { struct Data { - /// - /// The PostFx material asset ID. - /// Guid AssetID; + }; - /// - /// The start frame of the media play begin. - /// - int32 StartFrame; - - /// - /// The total duration of the media playback in the timeline sequence frames amount. - /// - int32 DurationFrames; + struct Runtime + { + int32 Count; + Media* Media; }; }; @@ -154,19 +153,8 @@ public: { struct Data { - /// - /// The scene animation asset ID. - /// Guid AssetID; - - /// - /// The start frame of the media play begin. - /// int32 StartFrame; - - /// - /// The total duration of the media playback in the timeline sequence frames amount. - /// int32 DurationFrames; }; @@ -185,57 +173,36 @@ public: struct Data { - /// - /// The start frame of the media play begin. - /// int32 StartFrame; - - /// - /// The total duration of the media playback in the timeline sequence frames amount. - /// int32 DurationFrames; - - /// - /// The gradient stops count. - /// int32 GradientStopsCount; }; struct Runtime { - /// - /// The pointer to the gradient stops array (items count is GradientStopsCount). - /// GradientStop* GradientStops; }; }; struct AudioTrack { + struct Media + { + int32 StartFrame; + int32 DurationFrames; + float Offset; + }; + struct Data { - /// - /// The audio clip asset ID. - /// Guid AssetID; - - /// - /// The start frame of the media play begin. - /// - int32 StartFrame; - - /// - /// The total duration of the media playback in the timeline sequence frames amount. - /// - int32 DurationFrames; }; struct Runtime { - /// - /// The index of the volume track. If not used then value is -1. Assigned on load. - /// int32 VolumeTrackIndex; + int32 Count; + Media* Media; }; }; @@ -245,22 +212,12 @@ public: struct Data { - /// - /// The keyframes count. - /// int32 KeyframesCount; }; struct Runtime { - /// - /// The keyframes count. - /// int32 KeyframesCount; - - /// - /// The keyframes array (items count is KeyframesCount). - /// BezierCurveKeyframe* Keyframes; }; }; @@ -269,9 +226,6 @@ public: { struct Data { - /// - /// The object ID. - /// Guid ID; }; @@ -306,37 +260,15 @@ public: { struct Data { - /// - /// The property value data size (in bytes). - /// int32 ValueSize; - - /// - /// The name length. - /// int32 PropertyNameLength; - - /// - /// The typename length. - /// int32 PropertyTypeNameLength; }; struct Runtime { - /// - /// The property value data size (in bytes). - /// int32 ValueSize; - - /// - /// The name of the property (just a member name). - /// char* PropertyName; - - /// - /// The typename of the property value (fullname including namespace but not assembly). - /// char* PropertyTypeName; }; }; @@ -345,17 +277,11 @@ public: { struct Data : PropertyTrack::Data { - /// - /// The keyframes count. - /// int32 KeyframesCount; }; struct Runtime : PropertyTrack::Runtime { - /// - /// The keyframes count. - /// int32 KeyframesCount; /// @@ -382,22 +308,12 @@ public: struct Data : PropertyTrack::Data { - /// - /// The keyframes count. - /// int32 KeyframesCount; }; struct Runtime : PropertyTrack::Runtime { - /// - /// The cached curve data type. - /// DataTypes DataType; - - /// - /// The keyframes count. - /// int32 KeyframesCount; /// @@ -411,17 +327,11 @@ public: { struct Data : PropertyTrack::Data { - /// - /// The keyframes count. - /// int32 KeyframesCount; }; struct Runtime : PropertyTrack::Runtime { - /// - /// The keyframes count. - /// int32 KeyframesCount; // ..followed by the keyframes times and the values arrays (separate) @@ -486,19 +396,12 @@ public: { struct Data : ObjectTrack::Data { - /// - /// The start frame of the media play begin. - /// - int32 StartFrame; - - /// - /// The total duration of the media playback in the timeline sequence frames amount. - /// - int32 DurationFrames; }; struct Runtime : ObjectTrack::Runtime { + int32 Count; + Media* Media; }; }; @@ -534,11 +437,7 @@ public: /// /// Gets the animation duration (in seconds). /// - /// The animation duration (in seconds). - API_PROPERTY() FORCE_INLINE float GetDuration() const - { - return DurationFrames / FramesPerSecond; - } + API_PROPERTY() float GetDuration() const; public: diff --git a/Source/Engine/Animations/SceneAnimations/SceneAnimationPlayer.cpp b/Source/Engine/Animations/SceneAnimations/SceneAnimationPlayer.cpp index 776d34dbd..1890b568d 100644 --- a/Source/Engine/Animations/SceneAnimations/SceneAnimationPlayer.cpp +++ b/Source/Engine/Animations/SceneAnimations/SceneAnimationPlayer.cpp @@ -540,13 +540,18 @@ void SceneAnimationPlayer::Tick(SceneAnimation* anim, float time, float dt, int3 { case SceneAnimation::Track::Types::PostProcessMaterial: { - const auto trackData = track.GetData(); - const float startTime = trackData->StartFrame / fps; - const float durationTime = trackData->DurationFrames / fps; - const bool isActive = Math::IsInRange(time, startTime, startTime + durationTime); - if (isActive && _postFxSettings.PostFxMaterials.Materials.Count() < POST_PROCESS_SETTINGS_MAX_MATERIALS) + const auto runtimeData = track.GetRuntimeData(); + for (int32 k = 0; k < runtimeData->Count; k++) { - _postFxSettings.PostFxMaterials.Materials.Add(track.Asset.As()); + const auto& media = runtimeData->Media[k]; + const float startTime = media.StartFrame / fps; + const float durationTime = media.DurationFrames / fps; + const bool isActive = Math::IsInRange(time, startTime, startTime + durationTime); + if (isActive && _postFxSettings.PostFxMaterials.Materials.Count() < POST_PROCESS_SETTINGS_MAX_MATERIALS) + { + _postFxSettings.PostFxMaterials.Materials.Add(track.Asset.As()); + break; + } } break; } @@ -648,29 +653,40 @@ void SceneAnimationPlayer::Tick(SceneAnimation* anim, float time, float dt, int3 const auto clip = track.Asset.As(); if (!clip || !clip->IsLoaded()) break; - const auto trackData = track.GetData(); const auto runtimeData = track.GetRuntimeData(); - const float startTime = trackData->StartFrame / fps; - const float durationTime = trackData->DurationFrames / fps; - const bool loop = ((int32)track.Flag & (int32)SceneAnimation::Track::Flags::Loop) == (int32)SceneAnimation::Track::Flags::Loop; - float mediaTime = time - startTime; - auto& state = _tracks[stateIndexOffset + track.TrackStateIndex]; - auto audioSource = state.Object.As(); - if (!audioSource) + float mediaTime = -1, mediaDuration, playTime; + for (int32 k = 0; k < runtimeData->Count; k++) { - // Spawn audio source to play the clip - audioSource = New(); - audioSource->SetStaticFlags(StaticFlags::None); - audioSource->HideFlags = HideFlags::FullyHidden; - audioSource->Clip = clip; - audioSource->SetIsLooping(loop); - audioSource->SetParent(this, false, false); - _subActors.Add(audioSource); - state.Object = audioSource; + const auto& media = runtimeData->Media[k]; + const float startTime = media.StartFrame / fps; + const float durationTime = media.DurationFrames / fps; + if (Math::IsInRange(time, startTime, startTime + durationTime)) + { + mediaTime = time - startTime; + playTime = mediaTime + media.Offset; + mediaDuration = durationTime; + break; + } } - if (mediaTime >= 0.0f && mediaTime <= durationTime) + auto& state = _tracks[stateIndexOffset + track.TrackStateIndex]; + auto audioSource = state.Object.As(); + if (mediaTime >= 0.0f && mediaTime <= mediaDuration) { + const bool loop = ((int32)track.Flag & (int32)SceneAnimation::Track::Flags::Loop) == (int32)SceneAnimation::Track::Flags::Loop; + if (!audioSource) + { + // Spawn audio source to play the clip + audioSource = New(); + audioSource->SetStaticFlags(StaticFlags::None); + audioSource->HideFlags = HideFlags::FullyHidden; + audioSource->Clip = clip; + audioSource->SetIsLooping(loop); + audioSource->SetParent(this, false, false); + _subActors.Add(audioSource); + state.Object = audioSource; + } + // Sample volume track float volume = 1.0f; if (runtimeData->VolumeTrackIndex != -1) @@ -680,7 +696,9 @@ void SceneAnimationPlayer::Tick(SceneAnimation* anim, float time, float dt, int3 if (volumeTrackRuntimeData) { SceneAnimation::AudioVolumeTrack::CurveType::KeyFrameData data(volumeTrackRuntimeData->Keyframes, volumeTrackRuntimeData->KeyframesCount); - volumeCurve.Evaluate(data, volume, mediaTime, false); + const auto& firstMedia = runtimeData->Media[0]; + auto firstMediaTime = time - firstMedia.StartFrame / fps; + volumeCurve.Evaluate(data, volume, firstMediaTime, false); } } @@ -688,9 +706,9 @@ void SceneAnimationPlayer::Tick(SceneAnimation* anim, float time, float dt, int3 if (loop) { // Loop position - mediaTime = Math::Mod(mediaTime, clipLength); + playTime = Math::Mod(playTime, clipLength); } - else if (mediaTime >= clipLength) + else if (playTime >= clipLength) { // Stop updating after end break; @@ -708,19 +726,19 @@ void SceneAnimationPlayer::Tick(SceneAnimation* anim, float time, float dt, int3 // Synchronize playback position const float maxAudioLag = 0.3f; const auto audioTime = audioSource->GetTime(); - //LOG(Info, "Audio: {0}, Media : {1}", audioTime, mediaTime); - if (Math::Abs(audioTime - mediaTime) > maxAudioLag && - Math::Abs(audioTime + clipLength - mediaTime) > maxAudioLag && - Math::Abs(mediaTime + clipLength - audioTime) > maxAudioLag) + //LOG(Info, "Audio: {0}, Media : {1}", audioTime, playTime); + if (Math::Abs(audioTime - playTime) > maxAudioLag && + Math::Abs(audioTime + clipLength - playTime) > maxAudioLag && + Math::Abs(playTime + clipLength - audioTime) > maxAudioLag) { - audioSource->SetTime(mediaTime); + audioSource->SetTime(playTime); //LOG(Info, "Set Time (current audio time: {0})", audioSource->GetTime()); } // Keep playing audioSource->Play(); } - else + else if (audioSource) { // End playback audioSource->Stop(); @@ -998,6 +1016,27 @@ void SceneAnimationPlayer::Tick(SceneAnimation* anim, float time, float dt, int3 } case SceneAnimation::Track::Types::CameraCut: { + // Check if any camera cut media on a track is active + bool isActive = false; + const auto runtimeData = track.GetRuntimeData(); + for (int32 k = 0; k < runtimeData->Count; k++) + { + const auto& media = runtimeData->Media[k]; + const float startTime = media.StartFrame / fps; + const float durationTime = media.DurationFrames / fps; + if (Math::IsInRange(time, startTime, startTime + durationTime)) + { + isActive = true; + break; + } + } + if (!isActive) + { + // Skip updating child tracks if the current position is outside the media clip range + j += track.ChildrenCount; + break; + } + // Cache actor to animate const auto trackData = track.GetData(); auto& state = _tracks[stateIndexOffset + track.TrackStateIndex]; @@ -1017,23 +1056,11 @@ void SceneAnimationPlayer::Tick(SceneAnimation* anim, float time, float dt, int3 } state.ManagedObject = state.Object.GetOrCreateManagedInstance(); - // Use camera - _isUsingCameraCuts = true; - const float startTime = trackData->StartFrame / fps; - const float durationTime = trackData->DurationFrames / fps; - float mediaTime = time - startTime; - if (mediaTime >= 0.0f && mediaTime <= durationTime) + // Override camera + if (_cameraCutCam == nullptr) { - if (_cameraCutCam == nullptr) - { - // Override camera - _cameraCutCam = (Camera*)state.Object.Get(); - } - } - else - { - // Skip updating child tracks if the current position is outside the media clip range - j += track.ChildrenCount; + _cameraCutCam = (Camera*)state.Object.Get(); + _isUsingCameraCuts = true; } break; } diff --git a/Source/Engine/Particles/ParticleSystem.cpp b/Source/Engine/Particles/ParticleSystem.cpp index 64b34c718..7d8ffe863 100644 --- a/Source/Engine/Particles/ParticleSystem.cpp +++ b/Source/Engine/Particles/ParticleSystem.cpp @@ -51,7 +51,7 @@ BytesContainer ParticleSystem::LoadTimeline() MemoryWriteStream stream(512); { // Save properties - stream.WriteInt32(3); + stream.WriteInt32(4); stream.WriteFloat(FramesPerSecond); stream.WriteInt32(DurationFrames); @@ -368,7 +368,8 @@ Asset::LoadResult ParticleSystem::load() break; } - case 3: + case 3: // [Deprecated on 03.09.2021 expires on 03.09.2023] + case 4: { // Load properties stream.ReadFloat(&FramesPerSecond);