diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 42474ce80b..b000f08369 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -54,6 +54,7 @@ namespace osu.Game.Configuration // Graphics Set(OsuSetting.ShowFpsDisplay, false); + Set(OsuSetting.ShowStoryboard, true); Set(OsuSetting.CursorRotation, true); Set(OsuSetting.MenuParallax, true); @@ -89,6 +90,7 @@ namespace osu.Game.Configuration GameplayCursorSize, AutoCursorSize, DimLevel, + ShowStoryboard, KeyOverlay, FloatingComments, PlaybackSpeed, diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs index 0e348f3791..00e6b8b722 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs @@ -14,6 +14,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics { Children = new[] { + new SettingsCheckbox + { + LabelText = "Storyboards", + Bindable = config.GetBindable(OsuSetting.ShowStoryboard) + }, new SettingsCheckbox { LabelText = "Rotate cursor when dragging", diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 593abb7d26..e120c7f193 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -24,6 +24,8 @@ using osu.Game.Screens.Ranking; using osu.Framework.Audio.Sample; using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Storyboards.Drawables; +using OpenTK.Graphics; namespace osu.Game.Screens.Play { @@ -59,6 +61,7 @@ namespace osu.Game.Screens.Play #region User Settings private Bindable dimLevel; + private Bindable showStoryboard; private Bindable mouseWheelDisabled; private Bindable userAudioOffset; @@ -66,6 +69,9 @@ namespace osu.Game.Screens.Play #endregion + private Container storyboardContainer; + private DrawableStoryboard storyboard; + private HUDOverlay hudOverlay; private FailOverlay failOverlay; @@ -77,6 +83,7 @@ namespace osu.Game.Screens.Play this.api = api; dimLevel = config.GetBindable(OsuSetting.DimLevel); + showStoryboard = config.GetBindable(OsuSetting.ShowStoryboard); mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); @@ -145,6 +152,12 @@ namespace osu.Game.Screens.Play Children = new Drawable[] { + storyboardContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Clock = offsetClock, + Alpha = 0, + }, pauseContainer = new PauseContainer { AudioClock = decoupledClock, @@ -196,6 +209,9 @@ namespace osu.Game.Screens.Play scoreProcessor = RulesetContainer.CreateScoreProcessor(); + if (showStoryboard) + initializeStoryboard(false); + hudOverlay.BindProcessor(scoreProcessor); hudOverlay.BindRulesetContainer(RulesetContainer); @@ -211,6 +227,16 @@ namespace osu.Game.Screens.Play scoreProcessor.Failed += onFail; } + private void initializeStoryboard(bool asyncLoad) + { + var beatmap = Beatmap.Value.Beatmap; + + storyboard = beatmap.Storyboard.CreateDrawable(Beatmap.Value); + storyboard.Masking = true; + + storyboardContainer.Add(asyncLoad ? new AsyncLoadWrapper(storyboard) { RelativeSizeAxes = Axes.Both } : (Drawable)storyboard); + } + public void Restart() { sampleRestart?.Play(); @@ -266,12 +292,12 @@ namespace osu.Game.Screens.Play return; (Background as BackgroundScreenBeatmap)?.BlurTo(Vector2.Zero, 1500, Easing.OutQuint); - Background?.FadeTo(1 - (float)dimLevel, 1500, Easing.OutQuint); + + dimLevel.ValueChanged += dimLevel_ValueChanged; + showStoryboard.ValueChanged += showStoryboard_ValueChanged; + updateBackgroundElements(); Content.Alpha = 0; - - dimLevel.ValueChanged += newDim => Background?.FadeTo(1 - (float)newDim, 800); - Content .ScaleTo(0.7f) .ScaleTo(1, 750, Easing.OutQuint) @@ -310,8 +336,33 @@ namespace osu.Game.Screens.Play return true; } + private void dimLevel_ValueChanged(double newValue) + => updateBackgroundElements(); + + private void showStoryboard_ValueChanged(bool newValue) + => updateBackgroundElements(); + + private void updateBackgroundElements() + { + var opacity = 1 - (float)dimLevel; + + if (showStoryboard && storyboard == null) + initializeStoryboard(true); + + var beatmap = Beatmap.Value; + var storyboardVisible = showStoryboard && beatmap.Beatmap.Storyboard.HasDrawable; + + storyboardContainer.FadeColour(new Color4(opacity, opacity, opacity, 1), 800); + storyboardContainer.FadeTo(storyboardVisible && opacity > 0 ? 1 : 0); + + Background?.FadeTo(!storyboardVisible || beatmap.Background == null ? opacity : 0, 800, Easing.OutQuint); + } + private void fadeOut() { + dimLevel.ValueChanged -= dimLevel_ValueChanged; + showStoryboard.ValueChanged -= showStoryboard_ValueChanged; + const float fade_out_duration = 250; RulesetContainer?.FadeOut(fade_out_duration); diff --git a/osu.Game/Storyboards/CommandLoop.cs b/osu.Game/Storyboards/CommandLoop.cs index 02b5eb0122..0d8b57e46c 100644 --- a/osu.Game/Storyboards/CommandLoop.cs +++ b/osu.Game/Storyboards/CommandLoop.cs @@ -10,8 +10,8 @@ namespace osu.Game.Storyboards public double LoopStartTime; public int LoopCount; - public override double StartTime => LoopStartTime; - public override double EndTime => LoopStartTime + CommandsDuration * LoopCount; + public override double StartTime => LoopStartTime + CommandsStartTime; + public override double EndTime => StartTime + CommandsDuration * LoopCount; public CommandLoop(double startTime, int loopCount) { diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index f88e5d118f..5df88b342f 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -5,6 +5,7 @@ using OpenTK; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.IO; @@ -14,6 +15,16 @@ namespace osu.Game.Storyboards.Drawables { public Storyboard Storyboard { get; private set; } + private readonly Background background; + public Texture BackgroundTexture + { + get { return background.Texture; } + set { background.Texture = value; } + } + + private readonly Container content; + protected override Container Content => content; + protected override Vector2 DrawScale => new Vector2(Parent.DrawHeight / 480); public override bool HandleInput => false; @@ -39,6 +50,18 @@ namespace osu.Game.Storyboards.Drawables Size = new Vector2(640, 480); Anchor = Anchor.Centre; Origin = Anchor.Centre; + + AddInternal(background = new Background + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + AddInternal(content = new Container + { + Size = new Vector2(640, 480), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); } [BackgroundDependencyLoader] @@ -55,5 +78,10 @@ namespace osu.Game.Storyboards.Drawables foreach (var layer in Children) layer.Enabled = passing ? layer.Layer.EnabledWhenPassing : layer.Layer.EnabledWhenFailing; } + + private class Background : Sprite + { + protected override Vector2 DrawScale => Texture != null ? new Vector2(Parent.DrawHeight / Texture.DisplayHeight) : base.DrawScale; + } } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index d8b7d05ee9..9757756316 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -14,9 +14,6 @@ namespace osu.Game.Storyboards.Drawables { public StoryboardAnimation Animation { get; private set; } - protected override bool ShouldBeAlive => Animation.HasCommands && base.ShouldBeAlive; - public override bool RemoveWhenNotAlive => !Animation.HasCommands || base.RemoveWhenNotAlive; - public bool FlipH { get; set; } public bool FlipV { get; set; } @@ -59,11 +56,8 @@ namespace osu.Game.Storyboards.Drawables Position = animation.InitialPosition; Repeat = animation.LoopType == AnimationLoopType.LoopForever; - if (animation.HasCommands) - { - LifetimeStart = animation.StartTime; - LifetimeEnd = animation.EndTime; - } + LifetimeStart = animation.StartTime; + LifetimeEnd = animation.EndTime; } [BackgroundDependencyLoader] diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs index 2b5db5b6fa..737704f6d0 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs @@ -28,9 +28,8 @@ namespace osu.Game.Storyboards.Drawables { foreach (var element in Layer.Elements) { - var drawable = element.CreateDrawable(); - if (drawable != null) - Add(drawable); + if (element.IsDrawable) + Add(element.CreateDrawable()); } } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 4b491fa008..9153b3e514 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -14,9 +14,6 @@ namespace osu.Game.Storyboards.Drawables { public StoryboardSprite Sprite { get; private set; } - protected override bool ShouldBeAlive => Sprite.HasCommands && base.ShouldBeAlive; - public override bool RemoveWhenNotAlive => !Sprite.HasCommands || base.RemoveWhenNotAlive; - public bool FlipH { get; set; } public bool FlipV { get; set; } @@ -58,11 +55,8 @@ namespace osu.Game.Storyboards.Drawables Origin = sprite.Origin; Position = sprite.InitialPosition; - if (sprite.HasCommands) - { - LifetimeStart = sprite.StartTime; - LifetimeEnd = sprite.EndTime; - } + LifetimeStart = sprite.StartTime; + LifetimeEnd = sprite.EndTime; } [BackgroundDependencyLoader] diff --git a/osu.Game/Storyboards/IStoryboardElement.cs b/osu.Game/Storyboards/IStoryboardElement.cs index d5fc86b0f7..74b6a8d8bc 100644 --- a/osu.Game/Storyboards/IStoryboardElement.cs +++ b/osu.Game/Storyboards/IStoryboardElement.cs @@ -8,6 +8,8 @@ namespace osu.Game.Storyboards public interface IStoryboardElement { string Path { get; } + bool IsDrawable { get; } + Drawable CreateDrawable(); } } diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 111cdd5d41..59cbe74650 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Beatmaps; using osu.Game.Storyboards.Drawables; using System.Collections.Generic; using System.Linq; @@ -12,6 +13,8 @@ namespace osu.Game.Storyboards private readonly Dictionary layers = new Dictionary(); public IEnumerable Layers => layers.Values; + public bool HasDrawable => Layers.Any(l => l.Elements.Any(e => e.IsDrawable)); + public Storyboard() { layers.Add("Background", new StoryboardLayer("Background", 3)); @@ -29,7 +32,32 @@ namespace osu.Game.Storyboards return layer; } - public DrawableStoryboard CreateDrawable() - => new DrawableStoryboard(this); + /// + /// Whether the beatmap's background should be hidden while this storyboard is being displayed. + /// + public bool ReplacesBackground(BeatmapInfo beatmapInfo) + { + var backgroundPath = beatmapInfo.BeatmapSet?.Metadata?.BackgroundFile?.ToLowerInvariant(); + if (backgroundPath == null) + return false; + + return GetLayer("Background").Elements.Any(e => e.Path.ToLowerInvariant() == backgroundPath); + } + + public float AspectRatio(BeatmapInfo beatmapInfo) + => beatmapInfo.WidescreenStoryboard ? 16 / 9f : 4 / 3f; + + public DrawableStoryboard CreateDrawable(WorkingBeatmap working = null) + { + var drawable = new DrawableStoryboard(this); + if (working != null) + { + var beatmapInfo = working.Beatmap.BeatmapInfo; + drawable.Width = drawable.Height * AspectRatio(beatmapInfo); + if (!ReplacesBackground(beatmapInfo)) + drawable.BackgroundTexture = working.Background; + } + return drawable; + } } } diff --git a/osu.Game/Storyboards/StoryboardSample.cs b/osu.Game/Storyboards/StoryboardSample.cs index bcf6a4329d..e7a157c2f4 100644 --- a/osu.Game/Storyboards/StoryboardSample.cs +++ b/osu.Game/Storyboards/StoryboardSample.cs @@ -2,12 +2,15 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Graphics; +using System; namespace osu.Game.Storyboards { public class StoryboardSample : IStoryboardElement { public string Path { get; set; } + public bool IsDrawable => false; + public double Time; public float Volume; @@ -19,6 +22,8 @@ namespace osu.Game.Storyboards } public Drawable CreateDrawable() - => null; + { + throw new InvalidOperationException(); + } } } diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 598167d720..349a59dee0 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -16,6 +16,8 @@ namespace osu.Game.Storyboards private readonly List triggers = new List(); public string Path { get; set; } + public bool IsDrawable => HasCommands; + public Anchor Origin; public Vector2 InitialPosition; diff --git a/osu.Game/Tests/Visual/TestCaseStoryboard.cs b/osu.Game/Tests/Visual/TestCaseStoryboard.cs index f7ac58c632..c6ef3f4ecf 100644 --- a/osu.Game/Tests/Visual/TestCaseStoryboard.cs +++ b/osu.Game/Tests/Visual/TestCaseStoryboard.cs @@ -79,11 +79,13 @@ namespace osu.Game.Tests.Visual storyboardContainer.Remove(storyboard); var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; - decoupledClock.ChangeSource(working.Track); storyboardContainer.Clock = decoupledClock; - storyboardContainer.Add(storyboard = working.Beatmap.Storyboard.CreateDrawable()); + storyboard = working.Beatmap.Storyboard.CreateDrawable(beatmapBacking); storyboard.Passing = false; + + storyboardContainer.Add(storyboard); + decoupledClock.ChangeSource(working.Track); } } }