diff --git a/osu.Desktop.Tests/Visual/TestCaseScrollingHitObjects.cs b/osu.Desktop.Tests/Visual/TestCaseScrollingHitObjects.cs deleted file mode 100644 index efc129a678..0000000000 --- a/osu.Desktop.Tests/Visual/TestCaseScrollingHitObjects.cs +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Framework.Configuration; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Timing; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Desktop.Tests.Visual -{ - public class TestCaseScrollingHitObjects : OsuTestCase - { - public override string Description => "SpeedAdjustmentContainer/DrawableTimingSection"; - - private readonly BindableDouble timeRangeBindable; - private readonly OsuSpriteText bottomLabel; - private readonly SpriteText topTime; - private readonly SpriteText bottomTime; - - public TestCaseScrollingHitObjects() - { - OsuSpriteText timeRangeText; - SpeedAdjustmentCollection adjustmentCollection; - - timeRangeBindable = new BindableDouble(2000) - { - MinValue = 200, - MaxValue = 4000, - }; - - SliderBar timeRange; - Add(timeRange = new BasicSliderBar - { - Size = new Vector2(200, 20), - SelectionColor = Color4.Pink, - KeyboardStep = 100 - }); - - Add(timeRangeText = new OsuSpriteText - { - X = 210, - TextSize = 16, - }); - - timeRange.Current.BindTo(timeRangeBindable); - timeRangeBindable.ValueChanged += v => timeRangeText.Text = $"Visible Range: {v:#,#.#}"; - timeRangeBindable.ValueChanged += v => bottomLabel.Text = $"t minus {v:#,#}"; - - AddRange(new Drawable[] - { - new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(100, 500), - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.25f - }, - adjustmentCollection = new SpeedAdjustmentCollection(Axes.Y) - { - RelativeSizeAxes = Axes.Both, - VisibleTimeRange = timeRangeBindable, - Masking = true, - }, - new OsuSpriteText - { - Text = "t minus 0", - Margin = new MarginPadding(2), - TextSize = 14, - Anchor = Anchor.TopRight, - }, - bottomLabel = new OsuSpriteText - { - Text = "t minus x", - Margin = new MarginPadding(2), - TextSize = 14, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomLeft, - }, - topTime = new OsuSpriteText - { - Margin = new MarginPadding(2), - TextSize = 14, - Anchor = Anchor.TopLeft, - Origin = Anchor.TopRight, - }, - bottomTime = new OsuSpriteText - { - Margin = new MarginPadding(2), - TextSize = 14, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomRight, - }, - } - } - }); - - timeRangeBindable.TriggerChange(); - - adjustmentCollection.Add(new TestSpeedAdjustmentContainer(new MultiplierControlPoint())); - - AddStep("Add hit object", () => adjustmentCollection.Add(new TestDrawableHitObject(new HitObject { StartTime = Time.Current + 2000 }))); - } - - protected override void Update() - { - base.Update(); - - topTime.Text = Time.Current.ToString("#,#"); - bottomTime.Text = (Time.Current + timeRangeBindable.Value).ToString("#,#"); - } - - private class TestSpeedAdjustmentContainer : SpeedAdjustmentContainer - { - public override bool RemoveWhenNotAlive => false; - - public TestSpeedAdjustmentContainer(MultiplierControlPoint controlPoint) - : base(controlPoint) - { - } - - protected override DrawableTimingSection CreateTimingSection() => new TestDrawableTimingSection(ControlPoint); - - private class TestDrawableTimingSection : DrawableTimingSection - { - private readonly MultiplierControlPoint controlPoint; - - public TestDrawableTimingSection(MultiplierControlPoint controlPoint) - { - this.controlPoint = controlPoint; - } - - protected override void Update() - { - base.Update(); - - Y = (float)(controlPoint.StartTime - Time.Current); - } - } - } - - private class TestDrawableHitObject : DrawableHitObject, IScrollingHitObject - { - private readonly Box background; - private const float height = 14; - - public BindableDouble LifetimeOffset { get; } = new BindableDouble(); - - public TestDrawableHitObject(HitObject hitObject) - : base(hitObject) - { - AutoSizeAxes = Axes.Y; - RelativeSizeAxes = Axes.X; - RelativePositionAxes = Axes.Y; - - Y = (float)hitObject.StartTime; - - Children = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.X, - Height = height, - }, - new Box - { - RelativeSizeAxes = Axes.X, - Colour = Color4.Cyan, - Height = 1, - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = Color4.Black, - TextSize = height, - Font = @"Exo2.0-BoldItalic", - Text = $"{hitObject.StartTime:#,#}" - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - this.FadeInFromZero(250, Easing.OutQuint); - } - - protected override void Update() - { - base.Update(); - if (Time.Current >= HitObject.StartTime) - background.Colour = Color4.Red; - } - } - } -} diff --git a/osu.Desktop.Tests/Visual/TestCaseScrollingPlayfield.cs b/osu.Desktop.Tests/Visual/TestCaseScrollingPlayfield.cs new file mode 100644 index 0000000000..79a9fab91c --- /dev/null +++ b/osu.Desktop.Tests/Visual/TestCaseScrollingPlayfield.cs @@ -0,0 +1,158 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using OpenTK; +using osu.Desktop.Tests.Beatmaps; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; + +namespace osu.Desktop.Tests.Visual +{ + /// + /// The most minimal implementation of a playfield with scrolling hit objects. + /// + public class TestCaseScrollingPlayfield : OsuTestCase + { + public TestCaseScrollingPlayfield() + { + Clock = new FramedClock(); + + var objects = new List(); + + int time = 1500; + for (int i = 0; i < 50; i++) + { + objects.Add(new TestHitObject { StartTime = time }); + + time += 500; + } + + Beatmap b = new Beatmap + { + HitObjects = objects, + BeatmapInfo = new BeatmapInfo + { + Difficulty = new BeatmapDifficulty(), + Metadata = new BeatmapMetadata() + } + }; + + WorkingBeatmap beatmap = new TestWorkingBeatmap(b); + + TestHitRenderer horizontalHitRenderer; + Add(horizontalHitRenderer = new TestHitRenderer(Axes.X, beatmap, true)); + + TestHitRenderer verticalHitRenderer; + Add(verticalHitRenderer = new TestHitRenderer(Axes.Y, beatmap, true)); + + AddStep("Reverse direction", () => + { + horizontalHitRenderer.Playfield.Reversed.Toggle(); + verticalHitRenderer.Playfield.Reversed.Toggle(); + }); + } + + private class TestHitRenderer : ScrollingHitRenderer + { + private readonly Axes scrollingAxes; + + public TestHitRenderer(Axes scrollingAxes, WorkingBeatmap beatmap, bool isForCurrentRuleset) + : base(beatmap, isForCurrentRuleset) + { + this.scrollingAxes = scrollingAxes; + } + + public new TestPlayfield Playfield => base.Playfield; + + public override ScoreProcessor CreateScoreProcessor() => new TestScoreProcessor(); + + protected override BeatmapConverter CreateBeatmapConverter() => new TestBeatmapConverter(); + + protected override Playfield CreatePlayfield() => new TestPlayfield(scrollingAxes); + + protected override DrawableHitObject GetVisualRepresentation(TestHitObject h) => new DrawableTestHitObject(scrollingAxes, h); + } + + private class TestScoreProcessor : ScoreProcessor + { + protected override void OnNewJudgement(TestJudgement judgement) + { + } + } + + private class TestBeatmapConverter : BeatmapConverter + { + protected override IEnumerable ValidConversionTypes => new[] { typeof(HitObject) }; + + protected override IEnumerable ConvertHitObject(HitObject original, Beatmap beatmap) + { + yield return original as TestHitObject; + } + } + + private class DrawableTestHitObject : DrawableScrollingHitObject + { + public DrawableTestHitObject(Axes scrollingAxes, TestHitObject hitObject) + : base(hitObject) + { + Anchor = scrollingAxes == Axes.Y ? Anchor.TopCentre : Anchor.CentreLeft; + Origin = Anchor.Centre; + + AutoSizeAxes = Axes.Both; + + Add(new Circle + { + Size = new Vector2(50) + }); + } + + protected override TestJudgement CreateJudgement() => new TestJudgement(); + + protected override void UpdateState(ArmedState state) + { + } + } + + private class TestPlayfield : ScrollingPlayfield + { + protected override Container Content => content; + private readonly Container content; + + public TestPlayfield(Axes scrollingAxes) + : base(scrollingAxes) + { + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.2f + }, + content = new Container { RelativeSizeAxes = Axes.Both } + }; + } + } + + + private class TestHitObject : HitObject + { + } + + private class TestJudgement : Judgement + { + public override string ResultString { get { throw new NotImplementedException(); } } + public override string MaxResultString { get { throw new NotImplementedException(); } } + } + } +} \ No newline at end of file diff --git a/osu.Desktop.Tests/osu.Desktop.Tests.csproj b/osu.Desktop.Tests/osu.Desktop.Tests.csproj index ba8ae06863..3111088ff6 100644 --- a/osu.Desktop.Tests/osu.Desktop.Tests.csproj +++ b/osu.Desktop.Tests/osu.Desktop.Tests.csproj @@ -99,7 +99,7 @@ - + diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index cb1352fc4a..10dc607ec3 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -4,7 +4,6 @@ using OpenTK.Graphics; using OpenTK.Input; using osu.Framework.Configuration; -using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Objects.Drawables; @@ -27,9 +26,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (key != null) Key.BindTo(key); - - RelativePositionAxes = Axes.Y; - Y = (float)HitObject.StartTime; } public override Color4 AccentColour diff --git a/osu.Game.Rulesets.Mania/Timing/BasicScrollingDrawableTimingSection.cs b/osu.Game.Rulesets.Mania/Timing/BasicScrollingDrawableTimingSection.cs deleted file mode 100644 index e485581d9f..0000000000 --- a/osu.Game.Rulesets.Mania/Timing/BasicScrollingDrawableTimingSection.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Game.Rulesets.Timing; - -namespace osu.Game.Rulesets.Mania.Timing -{ - /// - /// A which scrolls relative to the control point start time. - /// - internal class BasicScrollingDrawableTimingSection : DrawableTimingSection - { - private readonly MultiplierControlPoint controlPoint; - - public BasicScrollingDrawableTimingSection(MultiplierControlPoint controlPoint) - { - this.controlPoint = controlPoint; - } - - protected override void Update() - { - base.Update(); - - Y = (float)(controlPoint.StartTime - Time.Current); - } - } -} diff --git a/osu.Game.Rulesets.Mania/Timing/GravityScrollingDrawableTimingSection.cs b/osu.Game.Rulesets.Mania/Timing/GravityScrollingContainer.cs similarity index 80% rename from osu.Game.Rulesets.Mania/Timing/GravityScrollingDrawableTimingSection.cs rename to osu.Game.Rulesets.Mania/Timing/GravityScrollingContainer.cs index 730daa9ffd..699acc477b 100644 --- a/osu.Game.Rulesets.Mania/Timing/GravityScrollingDrawableTimingSection.cs +++ b/osu.Game.Rulesets.Mania/Timing/GravityScrollingContainer.cs @@ -6,13 +6,13 @@ using osu.Game.Rulesets.Timing; namespace osu.Game.Rulesets.Mania.Timing { /// - /// A that emulates a form of gravity where hit objects speed up over time. + /// A that emulates a form of gravity where hit objects speed up over time. /// - internal class GravityScrollingDrawableTimingSection : DrawableTimingSection + internal class GravityScrollingContainer : ScrollingContainer { private readonly MultiplierControlPoint controlPoint; - public GravityScrollingDrawableTimingSection(MultiplierControlPoint controlPoint) + public GravityScrollingContainer(MultiplierControlPoint controlPoint) { this.controlPoint = controlPoint; } @@ -51,10 +51,10 @@ namespace osu.Game.Rulesets.Mania.Timing private double acceleration => 1 / VisibleTimeRange; /// - /// Computes the current time relative to , accounting for . + /// Computes the current time relative to , accounting for . /// /// The non-offset time. - /// The current time relative to - . + /// The current time relative to - . private double relativeTimeAt(double time) => Time.Current - time + VisibleTimeRange; } } diff --git a/osu.Game.Rulesets.Mania/Timing/ManiaSpeedAdjustmentContainer.cs b/osu.Game.Rulesets.Mania/Timing/ManiaSpeedAdjustmentContainer.cs index ed22264d74..321b4ee92b 100644 --- a/osu.Game.Rulesets.Mania/Timing/ManiaSpeedAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Mania/Timing/ManiaSpeedAdjustmentContainer.cs @@ -15,15 +15,14 @@ namespace osu.Game.Rulesets.Mania.Timing this.scrollingAlgorithm = scrollingAlgorithm; } - protected override DrawableTimingSection CreateTimingSection() + protected override ScrollingContainer CreateScrollingContainer() { switch (scrollingAlgorithm) { default: - case ScrollingAlgorithm.Basic: - return new BasicScrollingDrawableTimingSection(ControlPoint); + return base.CreateScrollingContainer(); case ScrollingAlgorithm.Gravity: - return new GravityScrollingDrawableTimingSection(ControlPoint); + return new GravityScrollingContainer(ControlPoint); } } } diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 94cf6a4337..9fbc9ba5e7 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -14,11 +14,13 @@ using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using System; using osu.Framework.Configuration; -using osu.Game.Rulesets.Timing; +using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Judgements; namespace osu.Game.Rulesets.Mania.UI { - public class Column : Container, IHasAccentColour + public class Column : ScrollingPlayfield, IHasAccentColour { private const float key_icon_size = 10; private const float key_icon_corner_radius = 3; @@ -30,13 +32,6 @@ namespace osu.Game.Rulesets.Mania.UI private const float column_width = 45; private const float special_column_width = 70; - private readonly BindableDouble visibleTimeRange = new BindableDouble(); - public BindableDouble VisibleTimeRange - { - get { return visibleTimeRange; } - set { visibleTimeRange.BindTo(value); } - } - /// /// The key that will trigger input actions for this column and hit objects contained inside it. /// @@ -46,14 +41,15 @@ namespace osu.Game.Rulesets.Mania.UI private readonly Container hitTargetBar; private readonly Container keyIcon; - private readonly SpeedAdjustmentCollection speedAdjustments; + protected override Container Content => content; + private readonly Container content; public Column() + : base(Axes.Y) { - RelativeSizeAxes = Axes.Y; Width = column_width; - Children = new Drawable[] + InternalChildren = new Drawable[] { background = new Box { @@ -97,11 +93,10 @@ namespace osu.Game.Rulesets.Mania.UI } } }, - speedAdjustments = new SpeedAdjustmentCollection(Axes.Y) + content = new Container { Name = "Hit objects", RelativeSizeAxes = Axes.Both, - VisibleTimeRange = VisibleTimeRange }, // For column lighting, we need to capture input events before the notes new InputTarget @@ -150,6 +145,8 @@ namespace osu.Game.Rulesets.Mania.UI }; } + public override Axes RelativeSizeAxes => Axes.Y; + private bool isSpecial; public bool IsSpecial { @@ -192,11 +189,14 @@ namespace osu.Game.Rulesets.Mania.UI } } - public void Add(SpeedAdjustmentContainer speedAdjustment) => speedAdjustments.Add(speedAdjustment); - public void Add(DrawableHitObject hitObject) + /// + /// Adds a DrawableHitObject to this Playfield. + /// + /// The DrawableHitObject to add. + public override void Add(DrawableHitObject hitObject) { hitObject.AccentColour = AccentColour; - speedAdjustments.Add(hitObject); + HitObjects.Add(hitObject); } private bool onKeyDown(InputState state, KeyDownEventArgs args) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs b/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs index b92c0916b1..0b8ad10b16 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs @@ -17,7 +17,6 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Judgements; -using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Scoring; @@ -30,7 +29,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mania.UI { - public class ManiaHitRenderer : SpeedAdjustedHitRenderer + public class ManiaHitRenderer : ScrollingHitRenderer { /// /// Preferred column count. This will only have an effect during the initialization of the play field. @@ -39,24 +38,9 @@ namespace osu.Game.Rulesets.Mania.UI public IEnumerable BarLines; - /// - /// Per-column timing changes. - /// - private readonly List[] hitObjectSpeedAdjustments; - - /// - /// Bar line timing changes. - /// - private readonly List barLineSpeedAdjustments = new List(); - public ManiaHitRenderer(WorkingBeatmap beatmap, bool isForCurrentRuleset) : base(beatmap, isForCurrentRuleset) { - // Generate the speed adjustment container lists - hitObjectSpeedAdjustments = new List[PreferredColumns]; - for (int i = 0; i < PreferredColumns; i++) - hitObjectSpeedAdjustments[i] = new List(); - // Generate the bar lines double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue; @@ -83,30 +67,12 @@ namespace osu.Game.Rulesets.Mania.UI } BarLines = barLines; - - // Generate speed adjustments from mods first - bool useDefaultSpeedAdjustments = true; - - if (Mods != null) - { - foreach (var speedAdjustmentMod in Mods.OfType()) - { - useDefaultSpeedAdjustments = false; - speedAdjustmentMod.ApplyToHitRenderer(this, ref hitObjectSpeedAdjustments, ref barLineSpeedAdjustments); - } - } - - // Generate the default speed adjustments - if (useDefaultSpeedAdjustments) - generateDefaultSpeedAdjustments(); } [BackgroundDependencyLoader] private void load() { - var maniaPlayfield = (ManiaPlayfield)Playfield; - - BarLines.ForEach(maniaPlayfield.Add); + BarLines.ForEach(Playfield.Add); } protected override void ApplyBeatmap() @@ -116,28 +82,6 @@ namespace osu.Game.Rulesets.Mania.UI PreferredColumns = (int)Math.Max(1, Math.Round(Beatmap.BeatmapInfo.Difficulty.CircleSize)); } - protected override void ApplySpeedAdjustments() - { - var maniaPlayfield = (ManiaPlayfield)Playfield; - - for (int i = 0; i < PreferredColumns; i++) - foreach (var change in hitObjectSpeedAdjustments[i]) - maniaPlayfield.Columns.ElementAt(i).Add(change); - - foreach (var change in barLineSpeedAdjustments) - maniaPlayfield.Add(change); - } - - private void generateDefaultSpeedAdjustments() - { - DefaultControlPoints.ForEach(c => - { - foreach (List t in hitObjectSpeedAdjustments) - t.Add(new ManiaSpeedAdjustmentContainer(c, ScrollingAlgorithm.Basic)); - barLineSpeedAdjustments.Add(new ManiaSpeedAdjustmentContainer(c, ScrollingAlgorithm.Basic)); - }); - } - protected sealed override Playfield CreatePlayfield() => new ManiaPlayfield(PreferredColumns) { Anchor = Anchor.Centre, @@ -152,9 +96,7 @@ namespace osu.Game.Rulesets.Mania.UI protected override DrawableHitObject GetVisualRepresentation(ManiaHitObject h) { - var maniaPlayfield = (ManiaPlayfield)Playfield; - - Bindable key = maniaPlayfield.Columns.ElementAt(h.Column).Key; + Bindable key = Playfield.Columns.ElementAt(h.Column).Key; var holdNote = h as HoldNote; if (holdNote != null) @@ -168,5 +110,7 @@ namespace osu.Game.Rulesets.Mania.UI } protected override Vector2 GetPlayfieldAspectAdjust() => new Vector2(1, 0.8f); + + protected override SpeedAdjustmentContainer CreateSpeedAdjustmentContainer(MultiplierControlPoint controlPoint) => new ManiaSpeedAdjustmentContainer(controlPoint, ScrollingAlgorithm.Basic); } } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index 03b6e18856..b836b54fa8 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -15,23 +15,15 @@ using OpenTK.Input; using System.Linq; using System.Collections.Generic; using osu.Game.Rulesets.Objects.Drawables; -using osu.Framework.Input; using osu.Game.Rulesets.Mania.Objects.Drawables; -using osu.Game.Rulesets.Timing; -using osu.Framework.Configuration; using osu.Framework.Graphics.Shapes; namespace osu.Game.Rulesets.Mania.UI { - public class ManiaPlayfield : Playfield + public class ManiaPlayfield : ScrollingPlayfield { public const float HIT_TARGET_POSITION = 50; - private const double time_span_default = 1500; - private const double time_span_min = 50; - private const double time_span_max = 10000; - private const double time_span_step = 50; - /// /// Default column keys, expanding outwards from the middle as more column are added. /// E.g. 2 columns use FJ, 4 columns use DFJK, 6 use SDFJKL, etc... @@ -56,13 +48,8 @@ namespace osu.Game.Rulesets.Mania.UI private readonly FlowContainer columns; public IEnumerable Columns => columns.Children; - private readonly BindableDouble visibleTimeRange = new BindableDouble(time_span_default) - { - MinValue = time_span_min, - MaxValue = time_span_max - }; - - private readonly SpeedAdjustmentCollection barLineContainer; + protected override Container Content => content; + private readonly Container content; private List normalColumnColours = new List(); private Color4 specialColumnColour; @@ -70,13 +57,14 @@ namespace osu.Game.Rulesets.Mania.UI private readonly int columnCount; public ManiaPlayfield(int columnCount) + : base(Axes.Y) { this.columnCount = columnCount; if (columnCount <= 0) throw new ArgumentException("Can't have zero or fewer columns."); - Children = new Drawable[] + InternalChildren = new Drawable[] { new Container { @@ -120,13 +108,12 @@ namespace osu.Game.Rulesets.Mania.UI Padding = new MarginPadding { Top = HIT_TARGET_POSITION }, Children = new[] { - barLineContainer = new SpeedAdjustmentCollection(Axes.Y) + content = new Container { Name = "Bar lines", Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Y, - VisibleTimeRange = visibleTimeRange // Width is set in the Update method } } @@ -136,7 +123,13 @@ namespace osu.Game.Rulesets.Mania.UI }; for (int i = 0; i < columnCount; i++) - columns.Add(new Column { VisibleTimeRange = visibleTimeRange }); + { + var c = new Column(); + c.VisibleTimeRange.BindTo(VisibleTimeRange); + + columns.Add(c); + AddNested(c); + } } [BackgroundDependencyLoader] @@ -210,37 +203,13 @@ namespace osu.Game.Rulesets.Mania.UI } public override void Add(DrawableHitObject h) => Columns.ElementAt(h.HitObject.Column).Add(h); - public void Add(DrawableBarLine barline) => barLineContainer.Add(barline); - public void Add(SpeedAdjustmentContainer speedAdjustment) => barLineContainer.Add(speedAdjustment); - - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - if (state.Keyboard.ControlPressed) - { - switch (args.Key) - { - case Key.Minus: - transformVisibleTimeRangeTo(visibleTimeRange + time_span_step, 200, Easing.OutQuint); - break; - case Key.Plus: - transformVisibleTimeRangeTo(visibleTimeRange - time_span_step, 200, Easing.OutQuint); - break; - } - } - - return false; - } - - private void transformVisibleTimeRangeTo(double newTimeRange, double duration = 0, Easing easing = Easing.None) - { - this.TransformTo(nameof(visibleTimeRange), newTimeRange, duration, easing); - } + public void Add(DrawableBarLine barline) => HitObjects.Add(barline); protected override void Update() { // Due to masking differences, it is not possible to get the width of the columns container automatically // While masking on effectively only the Y-axis, so we need to set the width of the bar line container manually - barLineContainer.Width = columns.Width; + content.Width = columns.Width; } } } diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index 88d1ad7ad8..3832f9ace5 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -79,8 +79,7 @@ - - + diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index e80ac933c8..2604b1ee8a 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -80,9 +80,9 @@ namespace osu.Game.Rulesets.Osu.UI public override void PostProcess() { - connectionLayer.HitObjects = HitObjects.Children + connectionLayer.HitObjects = HitObjects.Objects .Select(d => d.HitObject) - .OrderBy(h => h.StartTime); + .OrderBy(h => h.StartTime).OfType(); } public override void OnJudgement(DrawableHitObject judgedObject) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableScrollingHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableScrollingHitObject.cs index 8dc17b9542..f67dd4dfea 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableScrollingHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableScrollingHitObject.cs @@ -1,8 +1,8 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; using osu.Framework.Configuration; +using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; @@ -10,6 +10,8 @@ namespace osu.Game.Rulesets.Objects.Drawables { /// /// A basic class that overrides and implements . + /// This object does not need to have its set to be able to scroll, as this will + /// will be set by the scrolling container that contains it. /// public abstract class DrawableScrollingHitObject : DrawableHitObject, IScrollingHitObject where TObject : HitObject @@ -17,25 +19,40 @@ namespace osu.Game.Rulesets.Objects.Drawables { public BindableDouble LifetimeOffset { get; } = new BindableDouble(); + Axes IScrollingHitObject.ScrollingAxes + { + set + { + RelativePositionAxes |= value; + + if ((value & Axes.X) > 0) + X = (float)HitObject.StartTime; + if ((value & Axes.Y) > 0) + Y = (float)HitObject.StartTime; + } + } + protected DrawableScrollingHitObject(TObject hitObject) : base(hitObject) { } + private double? lifetimeStart; public override double LifetimeStart { - get { return Math.Min(HitObject.StartTime - LifetimeOffset, base.LifetimeStart); } - set { base.LifetimeStart = value; } + get { return lifetimeStart ?? HitObject.StartTime - LifetimeOffset; } + set { lifetimeStart = value; } } + private double? lifetimeEnd; public override double LifetimeEnd { get { var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; - return Math.Max(endTime + LifetimeOffset, base.LifetimeEnd); + return lifetimeEnd ?? endTime + LifetimeOffset; } - set { base.LifetimeEnd = value; } + set { lifetimeEnd = value; } } protected override void AddNested(DrawableHitObject h) diff --git a/osu.Game/Rulesets/Objects/Drawables/IScrollingHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/IScrollingHitObject.cs index 560f15f133..30b65237a4 100644 --- a/osu.Game/Rulesets/Objects/Drawables/IScrollingHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/IScrollingHitObject.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// An interface that exposes properties required for scrolling hit objects to be properly displayed. /// - public interface IScrollingHitObject : IDrawable + internal interface IScrollingHitObject : IDrawable { /// /// Time offset before the hit object start time at which this becomes visible and the time offset @@ -21,5 +21,11 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// BindableDouble LifetimeOffset { get; } + + /// + /// Axes which this will scroll through. + /// This is set by the container which this scrolls through. + /// + Axes ScrollingAxes { set; } } } \ No newline at end of file diff --git a/osu.Game/Rulesets/Timing/DrawableTimingSection.cs b/osu.Game/Rulesets/Timing/DrawableTimingSection.cs deleted file mode 100644 index 6345a566c2..0000000000 --- a/osu.Game/Rulesets/Timing/DrawableTimingSection.cs +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.Linq; -using osu.Framework.Caching; -using osu.Framework.Configuration; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Objects.Drawables; -using OpenTK; -using osu.Game.Rulesets.Objects.Types; - -namespace osu.Game.Rulesets.Timing -{ - /// - /// A collection of hit objects which scrolls within a . - /// - /// - /// This container handles the conversion between time and position through and - /// such that hit objects added to this container should have time values set as their - /// positions/sizes to make proper use of this container. - /// - /// - /// - /// This container will auto-size to the total duration of the contained hit objects along the desired auto-sizing axes such that the resulting size - /// of this container will be a value representing the total duration of all contained hit objects. - /// - /// - /// - /// This container is and must always be relatively-sized and positioned to its such that the parent can utilise - /// and to apply further time offsets to this collection of hit objects. - /// - /// - public abstract class DrawableTimingSection : Container - { - private readonly BindableDouble visibleTimeRange = new BindableDouble(); - /// - /// Gets or sets the range of time that is visible by the length of this container. - /// - public BindableDouble VisibleTimeRange - { - get { return visibleTimeRange; } - set { visibleTimeRange.BindTo(value); } - } - - /// - /// Axes through which this timing section scrolls. This is set by the . - /// - internal Axes ScrollingAxes; - - /// - /// The control point that provides the speed adjustments for this container. This is set by the . - /// - internal MultiplierControlPoint ControlPoint; - - protected override int Compare(Drawable x, Drawable y) - { - var xHitObject = x as DrawableHitObject; - var yHitObject = y as DrawableHitObject; - - // If either of the two drawables are not hit objects, fall back to the base comparer - if (xHitObject?.HitObject == null || yHitObject?.HitObject == null) - return base.Compare(x, y); - - // Compare by start time - int i = yHitObject.HitObject.StartTime.CompareTo(xHitObject.HitObject.StartTime); - if (i != 0) - return i; - - return base.Compare(x, y); - } - - /// - /// Creates a new . - /// - protected DrawableTimingSection() - { - RelativeSizeAxes = Axes.Both; - RelativePositionAxes = Axes.Both; - } - - public override void InvalidateFromChild(Invalidation invalidation) - { - // We only want to re-compute our size when a child's size or position has changed - if ((invalidation & Invalidation.RequiredParentSizeToFit) == 0) - { - base.InvalidateFromChild(invalidation); - return; - } - - durationBacking.Invalidate(); - - base.InvalidateFromChild(invalidation); - } - - private Cached durationBacking; - - private double computeDuration() - { - if (!Children.Any()) - return 0; - - double baseDuration = Children.Max(c => (c.HitObject as IHasEndTime)?.EndTime ?? c.HitObject.StartTime) - ControlPoint.StartTime; - - // If we have a singular hit object at the timing section's start time, let's set a sane default duration - if (baseDuration == 0) - baseDuration = 1; - - // Scrolling ruleset hit objects typically have anchors+origins set to the hit object's start time, but if the hit object doesn't implement IHasEndTime and lies on the control point - // then the baseDuration above will be 0. This will cause problems with masking when it is further set as the value for Size in Update(). We _want_ the timing section bounds to - // completely enclose the hit object to avoid the masking optimisations. - // - // To do this we need to find a duration that corresponds to the absolute size of the element that extrudes beyond the timing section's bounds and add that to baseDuration. - // We can utilize the fact that the Size and RelativeChildSpace are 1:1, meaning that an change in duration for the timing section has no change to the hit object's positioning - // and simply find the largest absolutely-sized element in this timing section. This introduces a little bit of error, but will never under-estimate the duration. - - // Find the largest element that is absolutely-sized along ScrollingAxes - float maxAbsoluteSize = Children.Where(c => (c.RelativeSizeAxes & ScrollingAxes) == 0) - .Select(c => (ScrollingAxes & Axes.X) > 0 ? c.Width : c.Height) - .DefaultIfEmpty().Max(); - - float ourAbsoluteSize = (ScrollingAxes & Axes.X) > 0 ? DrawWidth : DrawHeight; - - // Add the extra duration to account for the absolute size - baseDuration *= 1 + maxAbsoluteSize / ourAbsoluteSize; - - return baseDuration; - } - - /// - /// The maximum duration of any one hit object inside this . This is calculated as the maximum - /// end time between all hit objects relative to this 's . - /// - public double Duration => durationBacking.IsValid ? durationBacking : (durationBacking.Value = computeDuration()); - - protected override void Update() - { - base.Update(); - - // We want our size and position-space along ScrollingAxes to span our duration to completely enclose all the hit objects - Size = new Vector2((ScrollingAxes & Axes.X) > 0 ? (float)Duration : Size.X, (ScrollingAxes & Axes.Y) > 0 ? (float)Duration : Size.Y); - // And we need to make sure the hit object's position-space doesn't change due to our resizing - RelativeChildSize = Size; - } - } -} diff --git a/osu.Game/Rulesets/Timing/LinearScrollingContainer.cs b/osu.Game/Rulesets/Timing/LinearScrollingContainer.cs new file mode 100644 index 0000000000..db497b7664 --- /dev/null +++ b/osu.Game/Rulesets/Timing/LinearScrollingContainer.cs @@ -0,0 +1,30 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; + +namespace osu.Game.Rulesets.Timing +{ + /// + /// A which scrolls linearly relative to the start time. + /// + internal class LinearScrollingContainer : ScrollingContainer + { + private readonly Axes scrollingAxes; + private readonly MultiplierControlPoint controlPoint; + + public LinearScrollingContainer(Axes scrollingAxes, MultiplierControlPoint controlPoint) + { + this.scrollingAxes = scrollingAxes; + this.controlPoint = controlPoint; + } + + protected override void Update() + { + base.Update(); + + if ((scrollingAxes & Axes.X) > 0) X = (float)(controlPoint.StartTime - Time.Current); + if ((scrollingAxes & Axes.Y) > 0) Y = (float)(controlPoint.StartTime - Time.Current); + } + } +} diff --git a/osu.Game/Rulesets/Timing/ScrollingContainer.cs b/osu.Game/Rulesets/Timing/ScrollingContainer.cs new file mode 100644 index 0000000000..843f307d0d --- /dev/null +++ b/osu.Game/Rulesets/Timing/ScrollingContainer.cs @@ -0,0 +1,103 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Linq; +using osu.Framework.Caching; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; +using OpenTK; +using osu.Game.Rulesets.Objects.Types; + +namespace osu.Game.Rulesets.Timing +{ + /// + /// A container that scrolls relative to the current time. Will autosize to the total duration of all contained hit objects along the scrolling axes. + /// + public abstract class ScrollingContainer : Container + { + /// + /// Gets or sets the range of time that is visible by the length of the scrolling axes. + /// + public readonly BindableDouble VisibleTimeRange = new BindableDouble { Default = 1000 }; + + /// + /// The axes through which this scrolls. This is set by the . + /// + internal Axes ScrollingAxes; + + /// + /// The control point that defines the speed adjustments for this container. This is set by the . + /// + internal MultiplierControlPoint ControlPoint; + + private Cached durationBacking; + + /// + /// Creates a new . + /// + protected ScrollingContainer() + { + RelativeSizeAxes = Axes.Both; + RelativePositionAxes = Axes.Both; + } + + public override void InvalidateFromChild(Invalidation invalidation) + { + // We only want to re-compute our size when a child's size or position has changed + if ((invalidation & Invalidation.RequiredParentSizeToFit) == 0) + { + base.InvalidateFromChild(invalidation); + return; + } + + durationBacking.Invalidate(); + + base.InvalidateFromChild(invalidation); + } + + private double computeDuration() + { + if (!Children.Any()) + return 0; + + double baseDuration = Children.Max(c => (c.HitObject as IHasEndTime)?.EndTime ?? c.HitObject.StartTime) - ControlPoint.StartTime; + + // If we have a singular hit object at the timing section's start time, let's set a sane default duration + if (baseDuration == 0) + baseDuration = 1; + + // This container needs to resize such that it completely encloses the hit objects to avoid masking optimisations. This is done by converting the largest + // absolutely-sized element along the scrolling axes and adding a corresponding duration value. This introduces a bit of error, but will never under-estimate.ion. + + // Find the largest element that is absolutely-sized along ScrollingAxes + float maxAbsoluteSize = Children.Where(c => (c.RelativeSizeAxes & ScrollingAxes) == 0) + .Select(c => (ScrollingAxes & Axes.X) > 0 ? c.Width : c.Height) + .DefaultIfEmpty().Max(); + + float ourAbsoluteSize = (ScrollingAxes & Axes.X) > 0 ? DrawWidth : DrawHeight; + + // Add the extra duration to account for the absolute size + baseDuration *= 1 + maxAbsoluteSize / ourAbsoluteSize; + + return baseDuration; + } + + /// + /// The maximum duration of any one hit object inside this . This is calculated as the maximum + /// duration of all hit objects relative to this 's . + /// + public double Duration => durationBacking.IsValid ? durationBacking : (durationBacking.Value = computeDuration()); + + protected override void Update() + { + base.Update(); + + // We want our size and position-space along the scrolling axes to span our duration to completely enclose all the hit objects + Size = new Vector2((ScrollingAxes & Axes.X) > 0 ? (float)Duration : Size.X, (ScrollingAxes & Axes.Y) > 0 ? (float)Duration : Size.Y); + // And we need to make sure the hit object's position-space doesn't change due to our resizing + RelativeChildSize = Size; + } + } +} diff --git a/osu.Game/Rulesets/Timing/SpeedAdjustmentCollection.cs b/osu.Game/Rulesets/Timing/SpeedAdjustmentCollection.cs deleted file mode 100644 index 22213be740..0000000000 --- a/osu.Game/Rulesets/Timing/SpeedAdjustmentCollection.cs +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Configuration; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Objects.Drawables; - -namespace osu.Game.Rulesets.Timing -{ - /// - /// A collection of s. - /// - /// - /// This container redirects any 's added to it to the - /// which provides the speed adjustment active at the start time of the hit object. Furthermore, this container provides the - /// necessary for the contained s. - /// - /// - public class SpeedAdjustmentCollection : Container - { - private readonly BindableDouble visibleTimeRange = new BindableDouble(); - /// - /// Gets or sets the range of time that is visible by the length of this container. - /// For example, only hit objects with start time less than or equal to 1000 will be visible with = 1000. - /// - public Bindable VisibleTimeRange - { - get { return visibleTimeRange; } - set { visibleTimeRange.BindTo(value); } - } - - protected override int Compare(Drawable x, Drawable y) - { - var xSpeedAdjust = x as SpeedAdjustmentContainer; - var ySpeedAdjust = y as SpeedAdjustmentContainer; - - // If either of the two drawables are not hit objects, fall back to the base comparer - if (xSpeedAdjust?.ControlPoint == null || ySpeedAdjust?.ControlPoint == null) - return CompareReverseChildID(x, y); - - // Compare by start time - int i = ySpeedAdjust.ControlPoint.StartTime.CompareTo(xSpeedAdjust.ControlPoint.StartTime); - - return i != 0 ? i : CompareReverseChildID(x, y); - } - - /// - /// Hit objects that are to be re-processed on the next update. - /// - private readonly Queue queuedHitObjects = new Queue(); - - private readonly Axes scrollingAxes; - - /// - /// Creates a new . - /// - /// The axes upon which hit objects should appear to scroll inside this container. - public SpeedAdjustmentCollection(Axes scrollingAxes) - { - this.scrollingAxes = scrollingAxes; - } - - public override void Add(SpeedAdjustmentContainer speedAdjustment) - { - speedAdjustment.VisibleTimeRange.BindTo(VisibleTimeRange); - speedAdjustment.ScrollingAxes = scrollingAxes; - base.Add(speedAdjustment); - } - - /// - /// Adds a hit object to this . The hit objects will be kept in a queue - /// and will be processed when new s are added to this . - /// - /// The hit object to add. - public void Add(DrawableHitObject hitObject) - { - if (!(hitObject is IScrollingHitObject)) - throw new InvalidOperationException($"Hit objects added to a {nameof(SpeedAdjustmentCollection)} must implement {nameof(IScrollingHitObject)}."); - - queuedHitObjects.Enqueue(hitObject); - } - - protected override void Update() - { - base.Update(); - - // Todo: At the moment this is going to re-process every single Update, however this will only be a null-op - // when there are no SpeedAdjustmentContainers available. This should probably error or something, but it's okay for now. - - // An external count is kept because hit objects that can't be added are re-queued - int count = queuedHitObjects.Count; - while (count-- > 0) - { - var hitObject = queuedHitObjects.Dequeue(); - - var target = adjustmentContainerFor(hitObject); - if (target == null) - { - // We can't add this hit object to a speed adjustment container yet, so re-queue it - // for re-processing when the layout next invalidated - queuedHitObjects.Enqueue(hitObject); - continue; - } - - if (hitObject.RelativePositionAxes != target.ScrollingAxes) - throw new InvalidOperationException($"Make sure to set all {nameof(DrawableHitObject)}'s {nameof(RelativePositionAxes)} are equal to the correct axes of scrolling ({target.ScrollingAxes})."); - - target.Add(hitObject); - } - } - - /// - /// Finds the which provides the speed adjustment active at the start time - /// of a hit object. If there is no active at the start time of the hit object, - /// then the first (time-wise) speed adjustment is returned. - /// - /// The hit object to find the active for. - /// The active at 's start time. Null if there are no speed adjustments. - private SpeedAdjustmentContainer adjustmentContainerFor(DrawableHitObject hitObject) => Children.FirstOrDefault(c => c.CanContain(hitObject)) ?? Children.LastOrDefault(); - - /// - /// Finds the which provides the speed adjustment active at a time. - /// If there is no active at the time, then the first (time-wise) speed adjustment is returned. - /// - /// The time to find the active at. - /// The active at . Null if there are no speed adjustments. - private SpeedAdjustmentContainer adjustmentContainerAt(double time) => Children.FirstOrDefault(c => c.CanContain(time)) ?? Children.LastOrDefault(); - } -} diff --git a/osu.Game/Rulesets/Timing/SpeedAdjustmentContainer.cs b/osu.Game/Rulesets/Timing/SpeedAdjustmentContainer.cs index f41ef21c1e..9d5f49e155 100644 --- a/osu.Game/Rulesets/Timing/SpeedAdjustmentContainer.cs +++ b/osu.Game/Rulesets/Timing/SpeedAdjustmentContainer.cs @@ -11,61 +11,57 @@ using OpenTK; namespace osu.Game.Rulesets.Timing { /// - /// A container for hit objects which applies applies the speed adjustments defined by the properties of a - /// to affect the scroll speed of the contained . - /// - /// - /// This container must always be relatively-sized to its parent to provide the speed adjustments. This container will provide the speed adjustments - /// by modifying its size while maintaining a constant for its children - /// + /// A container that provides the speed adjustments defined by s to affect the scroll speed + /// of container s. /// - public abstract class SpeedAdjustmentContainer : Container + public class SpeedAdjustmentContainer : Container { - private readonly Bindable visibleTimeRange = new Bindable(); /// - /// Gets or sets the range of time that is visible by the length of this container. + /// Gets or sets the range of time that is visible by the length of the scrolling axes. /// - public Bindable VisibleTimeRange - { - get { return visibleTimeRange; } - set { visibleTimeRange.BindTo(value); } - } + public readonly Bindable VisibleTimeRange = new Bindable { Default = 1000 }; + + /// + /// Whether to reverse the scrolling direction is reversed. + /// + public readonly BindableBool Reversed = new BindableBool(); protected override Container Content => content; private Container content; /// - /// Axes which the content of this container will scroll through. + /// The axes which the content of this container will scroll through. /// - /// public Axes ScrollingAxes { get; internal set; } + /// + /// The that defines the speed adjustments. + /// public readonly MultiplierControlPoint ControlPoint; - private DrawableTimingSection timingSection; + private ScrollingContainer scrollingContainer; /// /// Creates a new . /// - /// The which provides the speed adjustments for this container. - protected SpeedAdjustmentContainer(MultiplierControlPoint controlPoint) + /// The that defines the speed adjustments. + public SpeedAdjustmentContainer(MultiplierControlPoint controlPoint) { ControlPoint = controlPoint; - RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] private void load() { - timingSection = CreateTimingSection(); + scrollingContainer = CreateScrollingContainer(); - timingSection.ScrollingAxes = ScrollingAxes; - timingSection.ControlPoint = ControlPoint; - timingSection.VisibleTimeRange.BindTo(VisibleTimeRange); - timingSection.RelativeChildOffset = new Vector2((ScrollingAxes & Axes.X) > 0 ? (float)ControlPoint.StartTime : 0, (ScrollingAxes & Axes.Y) > 0 ? (float)ControlPoint.StartTime : 0); + scrollingContainer.ScrollingAxes = ScrollingAxes; + scrollingContainer.ControlPoint = ControlPoint; + scrollingContainer.VisibleTimeRange.BindTo(VisibleTimeRange); + scrollingContainer.RelativeChildOffset = new Vector2((ScrollingAxes & Axes.X) > 0 ? (float)ControlPoint.StartTime : 0, (ScrollingAxes & Axes.Y) > 0 ? (float)ControlPoint.StartTime : 0); - AddInternal(content = timingSection); + AddInternal(content = scrollingContainer); } protected override void Update() @@ -74,34 +70,51 @@ namespace osu.Game.Rulesets.Timing // The speed adjustment happens by modifying our size by the multiplier while maintaining the visible time range as the relatve size for our children Size = new Vector2((ScrollingAxes & Axes.X) > 0 ? multiplier : 1, (ScrollingAxes & Axes.Y) > 0 ? multiplier : 1); - RelativeChildSize = new Vector2((ScrollingAxes & Axes.X) > 0 ? (float)VisibleTimeRange : 1, (ScrollingAxes & Axes.Y) > 0 ? (float)VisibleTimeRange : 1); + + if (Reversed) + { + RelativeChildSize = new Vector2((ScrollingAxes & Axes.X) > 0 ? (float)-VisibleTimeRange : 1, (ScrollingAxes & Axes.Y) > 0 ? (float)-VisibleTimeRange : 1); + RelativeChildOffset = new Vector2((ScrollingAxes & Axes.X) > 0 ? (float)VisibleTimeRange : 0, (ScrollingAxes & Axes.Y) > 0 ? (float)VisibleTimeRange : 0); + Origin = Anchor = Anchor.BottomRight; + } + else + { + RelativeChildSize = new Vector2((ScrollingAxes & Axes.X) > 0 ? (float)VisibleTimeRange : 1, (ScrollingAxes & Axes.Y) > 0 ? (float)VisibleTimeRange : 1); + RelativeChildOffset = Vector2.Zero; + Origin = Anchor = Anchor.TopLeft; + } } public override double LifetimeStart => ControlPoint.StartTime - VisibleTimeRange; - public override double LifetimeEnd => ControlPoint.StartTime + timingSection.Duration + VisibleTimeRange; + public override double LifetimeEnd => ControlPoint.StartTime + scrollingContainer.Duration + VisibleTimeRange; public override void Add(DrawableHitObject drawable) { var scrollingHitObject = drawable as IScrollingHitObject; - scrollingHitObject?.LifetimeOffset.BindTo(VisibleTimeRange); + + if (scrollingHitObject != null) + { + scrollingHitObject.LifetimeOffset.BindTo(VisibleTimeRange); + scrollingHitObject.ScrollingAxes = ScrollingAxes; + } base.Add(drawable); } /// - /// Whether this speed adjustment can contain a hit object. This is true if the hit object occurs after this speed adjustment with respect to time. + /// Whether a falls within this s affecting timespan. /// public bool CanContain(DrawableHitObject hitObject) => CanContain(hitObject.HitObject.StartTime); /// - /// Whether this speed adjustment can contain an object placed at a time value. This is true if the time occurs after this speed adjustment. + /// Whether a point in time falls within this s affecting timespan. /// public bool CanContain(double startTime) => ControlPoint.StartTime <= startTime; /// - /// Creates the container which handles the movement of a collection of hit objects. + /// Creates the which contains the scrolling s of this container. /// - /// The . - protected abstract DrawableTimingSection CreateTimingSection(); + /// The . + protected virtual ScrollingContainer CreateScrollingContainer() => new LinearScrollingContainer(ScrollingAxes, ControlPoint); } } \ No newline at end of file diff --git a/osu.Game/Rulesets/UI/HitRenderer.cs b/osu.Game/Rulesets/UI/HitRenderer.cs index 2ce868b22e..13ea83dc48 100644 --- a/osu.Game/Rulesets/UI/HitRenderer.cs +++ b/osu.Game/Rulesets/UI/HitRenderer.cs @@ -225,7 +225,7 @@ namespace osu.Game.Rulesets.UI /// /// The playfield. /// - protected Playfield Playfield; + public Playfield Playfield { get; private set; } protected override Container Content => content; private readonly Container content; @@ -323,6 +323,33 @@ namespace osu.Game.Rulesets.UI protected abstract Playfield CreatePlayfield(); } + /// + /// A derivable HitRenderer that manages the Playfield and HitObjects. + /// + /// The type of Playfield contained by this HitRenderer. + /// The type of HitObject contained by this HitRenderer. + /// The type of Judgement of DrawableHitObjects contained by this HitRenderer. + public abstract class HitRenderer : HitRenderer + where TObject : HitObject + where TJudgement : Judgement + where TPlayfield : Playfield + { + /// + /// The playfield. + /// + protected new TPlayfield Playfield => (TPlayfield)base.Playfield; + + /// + /// Creates a hit renderer for a beatmap. + /// + /// The beatmap to create the hit renderer for. + /// Whether to assume the beatmap is for the current ruleset. + protected HitRenderer(WorkingBeatmap beatmap, bool isForCurrentRuleset) + : base(beatmap, isForCurrentRuleset) + { + } + } + public class BeatmapInvalidForRulesetException : ArgumentException { public BeatmapInvalidForRulesetException(string text) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index ff321a18a5..cc16eff6d6 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -9,6 +9,8 @@ using osu.Game.Rulesets.Objects.Drawables; using OpenTK; using osu.Game.Rulesets.Judgements; using osu.Framework.Allocation; +using System.Collections.Generic; +using System.Linq; namespace osu.Game.Rulesets.UI { @@ -19,7 +21,7 @@ namespace osu.Game.Rulesets.UI /// /// The HitObjects contained in this Playfield. /// - protected HitObjectContainer> HitObjects; + public HitObjectContainer HitObjects { get; protected set; } internal Container ScaledContent; @@ -53,7 +55,7 @@ namespace osu.Game.Rulesets.UI } }); - HitObjects = new HitObjectContainer> + HitObjects = new HitObjectContainer { RelativeSizeAxes = Axes.Both, }; @@ -82,12 +84,25 @@ namespace osu.Game.Rulesets.UI /// The DrawableHitObject to add. public virtual void Add(DrawableHitObject h) => HitObjects.Add(h); + /// + /// Remove a DrawableHitObject from this Playfield. + /// + /// The DrawableHitObject to remove. + public virtual void Remove(DrawableHitObject h) => HitObjects.Remove(h); + /// /// Triggered when an object's Judgement is updated. /// /// The object that Judgement has been updated for. public virtual void OnJudgement(DrawableHitObject judgedObject) { } + public class HitObjectContainer : CompositeDrawable + { + public virtual IEnumerable Objects => InternalChildren.OfType(); + public virtual void Add(DrawableHitObject hitObject) => AddInternal(hitObject); + public virtual bool Remove(DrawableHitObject hitObject) => RemoveInternal(hitObject); + } + private class ScaledContainer : Container { /// @@ -98,9 +113,5 @@ namespace osu.Game.Rulesets.UI //dividing by the customwidth will effectively scale our content to the required container size. protected override Vector2 DrawScale => CustomWidth.HasValue ? new Vector2(DrawSize.X / CustomWidth.Value) : base.DrawScale; } - - public class HitObjectContainer : Container where U : Drawable - { - } } } diff --git a/osu.Game/Rulesets/UI/SpeedAdjustedHitRenderer.cs b/osu.Game/Rulesets/UI/ScrollingHitRenderer.cs similarity index 60% rename from osu.Game/Rulesets/UI/SpeedAdjustedHitRenderer.cs rename to osu.Game/Rulesets/UI/ScrollingHitRenderer.cs index 42dadb047d..da4340ac7a 100644 --- a/osu.Game/Rulesets/UI/SpeedAdjustedHitRenderer.cs +++ b/osu.Game/Rulesets/UI/ScrollingHitRenderer.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Lists; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -16,15 +17,22 @@ using osu.Game.Rulesets.Timing; namespace osu.Game.Rulesets.UI { /// - /// A type of that supports speed adjustments in some capacity. + /// A type of that supports a . + /// s inside this will scroll within the playfield. /// - public abstract class SpeedAdjustedHitRenderer : HitRenderer + public abstract class ScrollingHitRenderer : HitRenderer where TObject : HitObject where TJudgement : Judgement + where TPlayfield : ScrollingPlayfield { + /// + /// Provides the default s that adjust the scrolling rate of s + /// inside this . + /// + /// protected readonly SortedList DefaultControlPoints = new SortedList(Comparer.Default); - protected SpeedAdjustedHitRenderer(WorkingBeatmap beatmap, bool isForCurrentRuleset) + protected ScrollingHitRenderer(WorkingBeatmap beatmap, bool isForCurrentRuleset) : base(beatmap, isForCurrentRuleset) { } @@ -32,7 +40,13 @@ namespace osu.Game.Rulesets.UI [BackgroundDependencyLoader] private void load() { - ApplySpeedAdjustments(); + DefaultControlPoints.ForEach(c => applySpeedAdjustment(c, Playfield)); + } + + private void applySpeedAdjustment(MultiplierControlPoint controlPoint, ScrollingPlayfield playfield) + { + playfield.HitObjects.AddSpeedAdjustment(CreateSpeedAdjustmentContainer(controlPoint)); + playfield.NestedPlayfields.ForEach(p => applySpeedAdjustment(controlPoint, p)); } protected override void ApplyBeatmap() @@ -77,13 +91,17 @@ namespace osu.Game.Rulesets.UI .GroupBy(s => s.TimingPoint.BeatLength * s.DifficultyPoint.SpeedMultiplier).Select(g => g.First()); DefaultControlPoints.AddRange(timingChanges); + + // If we have no control points, add a default one + if (DefaultControlPoints.Count == 0) + DefaultControlPoints.Add(new MultiplierControlPoint()); } /// - /// Generates a control point with the default timing change/difficulty change from the beatmap at a time. + /// Generates a with the default timing change/difficulty change from the beatmap at a time. /// /// The time to create the control point at. - /// The at . + /// The default at . public MultiplierControlPoint CreateControlPointAt(double time) { if (DefaultControlPoints.Count == 0) @@ -97,8 +115,10 @@ namespace osu.Game.Rulesets.UI } /// - /// Applies speed changes to the playfield. + /// Creates a that facilitates the movement of hit objects. /// - protected abstract void ApplySpeedAdjustments(); + /// The that provides the speed adjustments for the hitobjects. + /// The . + protected virtual SpeedAdjustmentContainer CreateSpeedAdjustmentContainer(MultiplierControlPoint controlPoint) => new SpeedAdjustmentContainer(controlPoint); } } diff --git a/osu.Game/Rulesets/UI/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/ScrollingPlayfield.cs new file mode 100644 index 0000000000..66ca2dd7c4 --- /dev/null +++ b/osu.Game/Rulesets/UI/ScrollingPlayfield.cs @@ -0,0 +1,239 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenTK.Input; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Transforms; +using osu.Framework.Input; +using osu.Framework.MathUtils; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Timing; + +namespace osu.Game.Rulesets.UI +{ + /// + /// A type of specialized towards scrolling s. + /// + public class ScrollingPlayfield : Playfield + where TObject : HitObject + where TJudgement : Judgement + { + /// + /// The default span of time visible by the length of the scrolling axes. + /// This is clamped between and . + /// + private const double time_span_default = 1500; + /// + /// The minimum span of time that may be visible by the length of the scrolling axes. + /// + private const double time_span_min = 50; + /// + /// The maximum span of time that may be visible by the length of the scrolling axes. + /// + private const double time_span_max = 10000; + /// + /// The step increase/decrease of the span of time visible by the length of the scrolling axes. + /// + private const double time_span_step = 50; + + /// + /// The span of time that is visible by the length of the scrolling axes. + /// For example, only hit objects with start time less than or equal to 1000 will be visible with = 1000. + /// + public readonly BindableDouble VisibleTimeRange = new BindableDouble(time_span_default) + { + Default = time_span_default, + MinValue = time_span_min, + MaxValue = time_span_max + }; + + /// + /// Whether to reverse the scrolling direction is reversed. + /// + public readonly BindableBool Reversed = new BindableBool(); + + /// + /// The container that contains the s and s. + /// + internal new readonly ScrollingHitObjectContainer HitObjects; + + /// + /// Creates a new . + /// + /// The axes on which s in this container should scroll. + /// Whether we want our internal coordinate system to be scaled to a specified width + protected ScrollingPlayfield(Axes scrollingAxes, float? customWidth = null) + : base(customWidth) + { + base.HitObjects = HitObjects = new ScrollingHitObjectContainer(scrollingAxes) { RelativeSizeAxes = Axes.Both }; + HitObjects.VisibleTimeRange.BindTo(VisibleTimeRange); + HitObjects.Reversed.BindTo(Reversed); + } + + private List> nestedPlayfields; + /// + /// All the s nested inside this playfield. + /// + public IEnumerable> NestedPlayfields => nestedPlayfields; + + /// + /// Adds a to this playfield. The nested + /// will be given all of the same speed adjustments as this playfield. + /// + /// The to add. + protected void AddNested(ScrollingPlayfield otherPlayfield) + { + if (nestedPlayfields == null) + nestedPlayfields = new List>(); + + nestedPlayfields.Add(otherPlayfield); + } + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (state.Keyboard.ControlPressed) + { + switch (args.Key) + { + case Key.Minus: + transformVisibleTimeRangeTo(VisibleTimeRange + time_span_step, 200, Easing.OutQuint); + break; + case Key.Plus: + transformVisibleTimeRangeTo(VisibleTimeRange - time_span_step, 200, Easing.OutQuint); + break; + } + } + + return false; + } + + private void transformVisibleTimeRangeTo(double newTimeRange, double duration = 0, Easing easing = Easing.None) + { + this.TransformTo(this.PopulateTransform(new TransformVisibleTimeRange(), newTimeRange, duration, easing)); + } + + private class TransformVisibleTimeRange : Transform> + { + private double valueAt(double time) + { + if (time < StartTime) return StartValue; + if (time >= EndTime) return EndValue; + + return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing); + } + + public override string TargetMember => "VisibleTimeRange.Value"; + + protected override void Apply(ScrollingPlayfield d, double time) => d.VisibleTimeRange.Value = valueAt(time); + protected override void ReadIntoStartValue(ScrollingPlayfield d) => StartValue = d.VisibleTimeRange.Value; + } + + /// + /// A container that provides the foundation for sorting s into s. + /// + internal class ScrollingHitObjectContainer : HitObjectContainer + { + /// + /// Gets or sets the range of time that is visible by the length of the scrolling axes. + /// For example, only hit objects with start time less than or equal to 1000 will be visible with = 1000. + /// + public readonly BindableDouble VisibleTimeRange = new BindableDouble { Default = 1000 }; + + /// + /// Whether to reverse the scrolling direction is reversed. + /// + public readonly BindableBool Reversed = new BindableBool(); + + /// + /// Hit objects that are to be re-processed on the next update. + /// + private readonly List queuedHitObjects = new List(); + private readonly List speedAdjustments = new List(); + + private readonly Axes scrollingAxes; + + /// + /// Creates a new . + /// + /// The axes upon which hit objects should appear to scroll inside this container. + public ScrollingHitObjectContainer(Axes scrollingAxes) + { + this.scrollingAxes = scrollingAxes; + } + + /// + /// Adds a to this container. + /// + /// The . + public void AddSpeedAdjustment(SpeedAdjustmentContainer speedAdjustment) + { + speedAdjustment.ScrollingAxes = scrollingAxes; + speedAdjustment.VisibleTimeRange.BindTo(VisibleTimeRange); + speedAdjustment.Reversed.BindTo(Reversed); + + speedAdjustments.Add(speedAdjustment); + AddInternal(speedAdjustment); + } + + public override IEnumerable Objects => speedAdjustments.SelectMany(s => s.Children); + + /// + /// Adds a hit object to this . The hit objects will be queued to be processed + /// new s are added to this . + /// + /// The hit object to add. + public override void Add(DrawableHitObject hitObject) + { + if (!(hitObject is IScrollingHitObject)) + throw new InvalidOperationException($"Hit objects added to a {nameof(ScrollingHitObjectContainer)} must implement {nameof(IScrollingHitObject)}."); + + queuedHitObjects.Add(hitObject); + } + + public override bool Remove(DrawableHitObject hitObject) => speedAdjustments.Any(s => s.Remove(hitObject)) || queuedHitObjects.Remove(hitObject); + + protected override void Update() + { + base.Update(); + + // Todo: At the moment this is going to re-process every single Update, however this will only be a null-op + // when there are no SpeedAdjustmentContainers available. This should probably error or something, but it's okay for now. + + for (int i = queuedHitObjects.Count - 1; i >= 0; i--) + { + var hitObject = queuedHitObjects[i]; + + var target = adjustmentContainerFor(hitObject); + if (target != null) + { + target.Add(hitObject); + queuedHitObjects.RemoveAt(i); + } + } + } + + /// + /// Finds the which provides the speed adjustment active at the start time + /// of a hit object. If there is no active at the start time of the hit object, + /// then the first (time-wise) speed adjustment is returned. + /// + /// The hit object to find the active for. + /// The active at 's start time. Null if there are no speed adjustments. + private SpeedAdjustmentContainer adjustmentContainerFor(DrawableHitObject hitObject) => speedAdjustments.FirstOrDefault(c => c.CanContain(hitObject)) ?? speedAdjustments.LastOrDefault(); + + /// + /// Finds the which provides the speed adjustment active at a time. + /// If there is no active at the time, then the first (time-wise) speed adjustment is returned. + /// + /// The time to find the active at. + /// The active at . Null if there are no speed adjustments. + private SpeedAdjustmentContainer adjustmentContainerAt(double time) => speedAdjustments.FirstOrDefault(c => c.CanContain(time)) ?? speedAdjustments.LastOrDefault(); + } + } +} \ No newline at end of file diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index d07203aa25..17596f0f49 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -228,9 +228,9 @@ - + + - @@ -340,7 +340,8 @@ - + +