mirror of
https://github.com/ppy/osu.git
synced 2024-12-15 11:22:57 +08:00
Merge pull request #12432 from marlinabowring/play-storyboard-outro
Add support for playing storyboards beyond gameplay end time
This commit is contained in:
commit
ed1ae4775f
191
osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
Normal file
191
osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
Normal file
@ -0,0 +1,191 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Storyboards;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public class TestSceneStoryboardWithOutro : PlayerTestScene
|
||||
{
|
||||
protected override bool HasCustomSteps => true;
|
||||
|
||||
protected new OutroPlayer Player => (OutroPlayer)base.Player;
|
||||
|
||||
private double currentStoryboardDuration;
|
||||
|
||||
private bool showResults = true;
|
||||
|
||||
private event Func<HealthProcessor, JudgementResult, bool> currentFailConditions;
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
AddStep("enable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, true));
|
||||
AddStep("set dim level to 0", () => LocalConfig.SetValue<double>(OsuSetting.DimLevel, 0));
|
||||
AddStep("reset fail conditions", () => currentFailConditions = (_, __) => false);
|
||||
AddStep("set storyboard duration to 2s", () => currentStoryboardDuration = 2000);
|
||||
AddStep("set ShowResults = true", () => showResults = true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStoryboardSkipOutro()
|
||||
{
|
||||
CreateTest(null);
|
||||
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
|
||||
AddStep("skip outro", () => InputManager.Key(osuTK.Input.Key.Space));
|
||||
AddAssert("score shown", () => Player.IsScoreShown);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStoryboardNoSkipOutro()
|
||||
{
|
||||
CreateTest(null);
|
||||
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
|
||||
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStoryboardExitToSkipOutro()
|
||||
{
|
||||
CreateTest(null);
|
||||
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
|
||||
AddStep("exit via pause", () => Player.ExitViaPause());
|
||||
AddAssert("score shown", () => Player.IsScoreShown);
|
||||
}
|
||||
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void TestStoryboardToggle(bool enabledAtBeginning)
|
||||
{
|
||||
CreateTest(null);
|
||||
AddStep($"{(enabledAtBeginning ? "enable" : "disable")} storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, enabledAtBeginning));
|
||||
AddStep("toggle storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, !enabledAtBeginning));
|
||||
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOutroEndsDuringFailAnimation()
|
||||
{
|
||||
CreateTest(() =>
|
||||
{
|
||||
AddStep("fail on first judgement", () => currentFailConditions = (_, __) => true);
|
||||
AddStep("set storyboard duration to 1.3s", () => currentStoryboardDuration = 1300);
|
||||
});
|
||||
AddUntilStep("wait for fail", () => Player.HasFailed);
|
||||
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
|
||||
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShowResultsFalse()
|
||||
{
|
||||
CreateTest(() =>
|
||||
{
|
||||
AddStep("set ShowResults = false", () => showResults = false);
|
||||
});
|
||||
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
|
||||
AddWaitStep("wait", 10);
|
||||
AddAssert("no score shown", () => !Player.IsScoreShown);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStoryboardEndsBeforeCompletion()
|
||||
{
|
||||
CreateTest(() => AddStep("set storyboard duration to .1s", () => currentStoryboardDuration = 100));
|
||||
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
|
||||
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
|
||||
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStoryboardRewind()
|
||||
{
|
||||
SkipOverlay.FadeContainer fadeContainer() => Player.ChildrenOfType<SkipOverlay.FadeContainer>().First();
|
||||
|
||||
CreateTest(null);
|
||||
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
|
||||
AddUntilStep("skip overlay content becomes visible", () => fadeContainer().State == Visibility.Visible);
|
||||
|
||||
AddStep("rewind", () => Player.GameplayClockContainer.Seek(-1000));
|
||||
AddUntilStep("skip overlay content not visible", () => fadeContainer().State == Visibility.Hidden);
|
||||
|
||||
AddUntilStep("skip overlay content becomes visible", () => fadeContainer().State == Visibility.Visible);
|
||||
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
|
||||
}
|
||||
|
||||
protected override bool AllowFail => true;
|
||||
|
||||
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||
|
||||
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new OutroPlayer(currentFailConditions, showResults);
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
|
||||
{
|
||||
var beatmap = new Beatmap();
|
||||
beatmap.HitObjects.Add(new HitCircle());
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
|
||||
{
|
||||
return base.CreateWorkingBeatmap(beatmap, createStoryboard(currentStoryboardDuration));
|
||||
}
|
||||
|
||||
private Storyboard createStoryboard(double duration)
|
||||
{
|
||||
var storyboard = new Storyboard();
|
||||
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
||||
sprite.TimelineGroup.Alpha.Add(Easing.None, 0, duration, 1, 0);
|
||||
storyboard.GetLayer("Background").Add(sprite);
|
||||
return storyboard;
|
||||
}
|
||||
|
||||
protected class OutroPlayer : TestPlayer
|
||||
{
|
||||
public void ExitViaPause() => PerformExit(true);
|
||||
|
||||
public new FailOverlay FailOverlay => base.FailOverlay;
|
||||
|
||||
public bool IsScoreShown => !this.IsCurrentScreen() && this.GetChildScreen() is ResultsScreen;
|
||||
|
||||
private event Func<HealthProcessor, JudgementResult, bool> failConditions;
|
||||
|
||||
public OutroPlayer(Func<HealthProcessor, JudgementResult, bool> failConditions, bool showResults = true)
|
||||
: base(false, showResults)
|
||||
{
|
||||
this.failConditions = failConditions;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
HealthProcessor.FailConditions += failConditions;
|
||||
}
|
||||
|
||||
protected override Task ImportScore(Score score)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -48,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
AllowPause = false,
|
||||
AllowRestart = false,
|
||||
AllowSkippingIntro = false,
|
||||
AllowSkipping = false,
|
||||
})
|
||||
{
|
||||
this.userIds = userIds;
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Storyboards;
|
||||
@ -19,6 +20,14 @@ namespace osu.Game.Screens.Play
|
||||
private readonly Storyboard storyboard;
|
||||
private DrawableStoryboard drawableStoryboard;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the storyboard is considered finished.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is true by default in here, until an actual drawable storyboard is loaded, in which case it'll bind to it.
|
||||
/// </remarks>
|
||||
public IBindable<bool> HasStoryboardEnded = new BindableBool(true);
|
||||
|
||||
public DimmableStoryboard(Storyboard storyboard)
|
||||
{
|
||||
this.storyboard = storyboard;
|
||||
@ -49,6 +58,7 @@ namespace osu.Game.Screens.Play
|
||||
return;
|
||||
|
||||
drawableStoryboard = storyboard.CreateDrawable();
|
||||
HasStoryboardEnded.BindTo(drawableStoryboard.HasStoryboardEnded);
|
||||
|
||||
if (async)
|
||||
LoadComponentAsync(drawableStoryboard, onStoryboardCreated);
|
||||
|
@ -104,7 +104,8 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private BreakTracker breakTracker;
|
||||
|
||||
private SkipOverlay skipOverlay;
|
||||
private SkipOverlay skipIntroOverlay;
|
||||
private SkipOverlay skipOutroOverlay;
|
||||
|
||||
protected ScoreProcessor ScoreProcessor { get; private set; }
|
||||
|
||||
@ -244,7 +245,6 @@ namespace osu.Game.Screens.Play
|
||||
HUDOverlay.ShowHud.Value = false;
|
||||
HUDOverlay.ShowHud.Disabled = true;
|
||||
BreakOverlay.Hide();
|
||||
skipOverlay.Hide();
|
||||
}
|
||||
|
||||
DrawableRuleset.FrameStableClock.WaitingOnFrames.BindValueChanged(waiting =>
|
||||
@ -281,8 +281,14 @@ namespace osu.Game.Screens.Play
|
||||
ScoreProcessor.RevertResult(r);
|
||||
};
|
||||
|
||||
DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded =>
|
||||
{
|
||||
if (storyboardEnded.NewValue && completionProgressDelegate == null)
|
||||
updateCompletionState();
|
||||
};
|
||||
|
||||
// Bind the judgement processors to ourselves
|
||||
ScoreProcessor.HasCompleted.ValueChanged += updateCompletionState;
|
||||
ScoreProcessor.HasCompleted.BindValueChanged(_ => updateCompletionState());
|
||||
HealthProcessor.Failed += onFail;
|
||||
|
||||
foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>())
|
||||
@ -355,10 +361,15 @@ namespace osu.Game.Screens.Play
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
},
|
||||
skipOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime)
|
||||
skipIntroOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime)
|
||||
{
|
||||
RequestSkip = performUserRequestedSkip
|
||||
},
|
||||
skipOutroOverlay = new SkipOverlay(Beatmap.Value.Storyboard.LatestEventTime ?? 0)
|
||||
{
|
||||
RequestSkip = () => updateCompletionState(true),
|
||||
Alpha = 0
|
||||
},
|
||||
FailOverlay = new FailOverlay
|
||||
{
|
||||
OnRetry = Restart,
|
||||
@ -385,12 +396,15 @@ namespace osu.Game.Screens.Play
|
||||
}
|
||||
};
|
||||
|
||||
if (!Configuration.AllowSkipping || !DrawableRuleset.AllowGameplayOverlays)
|
||||
{
|
||||
skipIntroOverlay.Expire();
|
||||
skipOutroOverlay.Expire();
|
||||
}
|
||||
|
||||
if (GameplayClockContainer is MasterGameplayClockContainer master)
|
||||
HUDOverlay.PlayerSettingsOverlay.PlaybackSettings.UserPlaybackRate.BindTarget = master.UserPlaybackRate;
|
||||
|
||||
if (!Configuration.AllowSkippingIntro)
|
||||
skipOverlay.Expire();
|
||||
|
||||
if (Configuration.AllowRestart)
|
||||
{
|
||||
container.Add(new HotkeyRetryOverlay
|
||||
@ -525,6 +539,10 @@ namespace osu.Game.Screens.Play
|
||||
Pause();
|
||||
return;
|
||||
}
|
||||
|
||||
// if the score is ready for display but results screen has not been pushed yet (e.g. storyboard is still playing beyond gameplay), then transition to results screen instead of exiting.
|
||||
if (prepareScoreForDisplayTask != null)
|
||||
updateCompletionState(true);
|
||||
}
|
||||
|
||||
this.Exit();
|
||||
@ -564,17 +582,23 @@ namespace osu.Game.Screens.Play
|
||||
private ScheduledDelegate completionProgressDelegate;
|
||||
private Task<ScoreInfo> prepareScoreForDisplayTask;
|
||||
|
||||
private void updateCompletionState(ValueChangedEvent<bool> completionState)
|
||||
/// <summary>
|
||||
/// Handles changes in player state which may progress the completion of gameplay / this screen's lifetime.
|
||||
/// </summary>
|
||||
/// <param name="skipStoryboardOutro">If in a state where a storyboard outro is to be played, offers the choice of skipping beyond it.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown if this method is called more than once without changing state.</exception>
|
||||
private void updateCompletionState(bool skipStoryboardOutro = false)
|
||||
{
|
||||
// screen may be in the exiting transition phase.
|
||||
if (!this.IsCurrentScreen())
|
||||
return;
|
||||
|
||||
if (!completionState.NewValue)
|
||||
if (!ScoreProcessor.HasCompleted.Value)
|
||||
{
|
||||
completionProgressDelegate?.Cancel();
|
||||
completionProgressDelegate = null;
|
||||
ValidForResume = true;
|
||||
skipOutroOverlay.Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -614,6 +638,20 @@ namespace osu.Game.Screens.Play
|
||||
return score.ScoreInfo;
|
||||
});
|
||||
|
||||
if (skipStoryboardOutro)
|
||||
{
|
||||
scheduleCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
|
||||
|
||||
if (storyboardHasOutro)
|
||||
{
|
||||
skipOutroOverlay.Show();
|
||||
return;
|
||||
}
|
||||
|
||||
using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY))
|
||||
scheduleCompletion();
|
||||
}
|
||||
|
@ -21,8 +21,8 @@ namespace osu.Game.Screens.Play
|
||||
public bool AllowRestart { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the player should be allowed to skip the intro, advancing to the start of gameplay.
|
||||
/// Whether the player should be allowed to skip intros/outros, advancing to the start of gameplay or the end of a storyboard.
|
||||
/// </summary>
|
||||
public bool AllowSkippingIntro { get; set; } = true;
|
||||
public bool AllowSkipping { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
@ -8,19 +8,19 @@ using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Input.Bindings;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
@ -92,6 +92,18 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private double fadeOutBeginTime => startTime - MasterGameplayClockContainer.MINIMUM_SKIP_TIME;
|
||||
|
||||
public override void Hide()
|
||||
{
|
||||
base.Hide();
|
||||
fadeContainer.Hide();
|
||||
}
|
||||
|
||||
public override void Show()
|
||||
{
|
||||
base.Show();
|
||||
fadeContainer.Show();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
@ -147,7 +159,7 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
}
|
||||
|
||||
private class FadeContainer : Container, IStateful<Visibility>
|
||||
public class FadeContainer : Container, IStateful<Visibility>
|
||||
{
|
||||
public event Action<Visibility> StateChanged;
|
||||
|
||||
@ -170,7 +182,7 @@ namespace osu.Game.Screens.Play
|
||||
switch (state)
|
||||
{
|
||||
case Visibility.Visible:
|
||||
// we may be triggered to become visible mnultiple times but we only want to transform once.
|
||||
// we may be triggered to become visible multiple times but we only want to transform once.
|
||||
if (stateChanged)
|
||||
this.FadeIn(500, Easing.OutExpo);
|
||||
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using osuTK;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
@ -19,6 +20,13 @@ namespace osu.Game.Storyboards.Drawables
|
||||
[Cached]
|
||||
public Storyboard Storyboard { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the storyboard is considered finished.
|
||||
/// </summary>
|
||||
public IBindable<bool> HasStoryboardEnded => hasStoryboardEnded;
|
||||
|
||||
private readonly BindableBool hasStoryboardEnded = new BindableBool();
|
||||
|
||||
protected override Container<DrawableStoryboardLayer> Content { get; }
|
||||
|
||||
protected override Vector2 DrawScale => new Vector2(Parent.DrawHeight / 480);
|
||||
@ -39,6 +47,8 @@ namespace osu.Game.Storyboards.Drawables
|
||||
|
||||
public override bool RemoveCompletedTransforms => false;
|
||||
|
||||
private double? lastEventEndTime;
|
||||
|
||||
private DependencyContainer dependencies;
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
|
||||
@ -73,6 +83,14 @@ namespace osu.Game.Storyboards.Drawables
|
||||
|
||||
Add(layer.CreateDrawable());
|
||||
}
|
||||
|
||||
lastEventEndTime = Storyboard.LatestEventTime;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
hasStoryboardEnded.Value = lastEventEndTime == null || Time.Current >= lastEventEndTime;
|
||||
}
|
||||
|
||||
public DrawableStoryboardLayer OverlayLayer => Children.Single(layer => layer.Name == "Overlay");
|
||||
|
@ -14,4 +14,17 @@ namespace osu.Game.Storyboards
|
||||
|
||||
Drawable CreateDrawable();
|
||||
}
|
||||
|
||||
public static class StoryboardElementExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the end time of this storyboard element.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This returns the <see cref="IStoryboardElementWithDuration.EndTime"/> where available, falling back to <see cref="IStoryboardElement.StartTime"/> otherwise.
|
||||
/// </remarks>
|
||||
/// <param name="element">The storyboard element.</param>
|
||||
/// <returns>The end time of this element.</returns>
|
||||
public static double GetEndTime(this IStoryboardElement element) => (element as IStoryboardElementWithDuration)?.EndTime ?? element.StartTime;
|
||||
}
|
||||
}
|
||||
|
21
osu.Game/Storyboards/IStoryboardElementWithDuration.cs
Normal file
21
osu.Game/Storyboards/IStoryboardElementWithDuration.cs
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Storyboards
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="IStoryboardElement"/> that ends at a different time than its start time.
|
||||
/// </summary>
|
||||
public interface IStoryboardElementWithDuration : IStoryboardElement
|
||||
{
|
||||
/// <summary>
|
||||
/// The time at which the <see cref="IStoryboardElement"/> ends.
|
||||
/// </summary>
|
||||
double EndTime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The duration of the StoryboardElement.
|
||||
/// </summary>
|
||||
double Duration => EndTime - StartTime;
|
||||
}
|
||||
}
|
@ -36,6 +36,16 @@ namespace osu.Game.Storyboards
|
||||
/// </remarks>
|
||||
public double? EarliestEventTime => Layers.SelectMany(l => l.Elements).OrderBy(e => e.StartTime).FirstOrDefault()?.StartTime;
|
||||
|
||||
/// <summary>
|
||||
/// Across all layers, find the latest point in time that a storyboard element ends at.
|
||||
/// Will return null if there are no elements.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This iterates all elements and as such should be used sparingly or stored locally.
|
||||
/// Videos and samples return StartTime as their EndTIme.
|
||||
/// </remarks>
|
||||
public double? LatestEventTime => Layers.SelectMany(l => l.Elements).OrderBy(e => e.GetEndTime()).LastOrDefault()?.GetEndTime();
|
||||
|
||||
/// <summary>
|
||||
/// Depth of the currently front-most storyboard layer, excluding the overlay layer.
|
||||
/// </summary>
|
||||
|
@ -11,7 +11,7 @@ using JetBrains.Annotations;
|
||||
|
||||
namespace osu.Game.Storyboards
|
||||
{
|
||||
public class StoryboardSprite : IStoryboardElement
|
||||
public class StoryboardSprite : IStoryboardElementWithDuration
|
||||
{
|
||||
private readonly List<CommandLoop> loops = new List<CommandLoop>();
|
||||
private readonly List<CommandTrigger> triggers = new List<CommandTrigger>();
|
||||
|
Loading…
Reference in New Issue
Block a user