diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 6e41043b0b..d9e80fa111 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -97,6 +97,25 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestCorrectAnimationStartTime() + { + var decoder = new LegacyStoryboardDecoder(); + + using (var resStream = TestResources.OpenResource("animation-starts-before-alpha.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var storyboard = decoder.Decode(stream); + + StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3); + Assert.AreEqual(1, background.Elements.Count); + + Assert.AreEqual(2000, background.Elements[0].StartTime); + // This property should be used in DrawableStoryboardAnimation as a starting point for animation playback. + Assert.AreEqual(1000, (background.Elements[0] as StoryboardAnimation)?.EarliestTransformTime); + } + } + [Test] public void TestOutOfOrderStartTimes() { diff --git a/osu.Game.Tests/Resources/animation-starts-before-alpha.osb b/osu.Game.Tests/Resources/animation-starts-before-alpha.osb new file mode 100644 index 0000000000..ceef204f3f --- /dev/null +++ b/osu.Game.Tests/Resources/animation-starts-before-alpha.osb @@ -0,0 +1,5 @@ +[Events] +//Storyboard Layer 0 (Background) +Animation,Background,Centre,"img.jpg",320,240,2,150,LoopForever + S,0,1000,1500,0.08 // animation should start playing from this point in time.. + F,0,2000,,0,1 // .. not this point in time diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index f3187d77b7..369a3ee7ba 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Textures; using osu.Framework.Utils; +using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; @@ -115,6 +116,21 @@ namespace osu.Game.Storyboards.Drawables Animation.ApplyTransforms(this); } + [Resolved] + private IGameplayClock gameplayClock { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + // Framework animation class tries its best to synchronise the animation at LoadComplete, + // but in some cases (such as fast forward) this results in an incorrect start offset. + // + // In the case of storyboard animations, we want to synchronise with game time perfectly + // so let's get a correct time based on gameplay clock and earliest transform. + PlaybackPosition = gameplayClock.CurrentTime - Animation.EarliestTransformTime; + } + private void skinSourceChanged() { ClearFrames(); diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 1eeaa0f084..cd7788bb08 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -54,6 +54,14 @@ namespace osu.Game.Storyboards return firstAlpha.startTime; } + return EarliestTransformTime; + } + } + + public double EarliestTransformTime + { + get + { // If we got to this point, either no alpha commands were present, or the earliest had a non-zero start value. // The sprite's StartTime will be determined by the earliest command, regardless of type. double earliestStartTime = TimelineGroup.StartTime;