diff --git a/osu-framework b/osu-framework index c80d5f53e7..e256557def 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit c80d5f53e740ffe63d9deca41749c5ba0573e744 +Subproject commit e256557defe595032cbc3a9896797fa41d6ee8b6 diff --git a/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs b/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs index 95287c3199..352b6cdc81 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs @@ -6,14 +6,16 @@ using osu.Framework.Testing; using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.UI; using System; -using System.Collections.Generic; using OpenTK; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Mania.Timing; using osu.Framework.Configuration; using OpenTK.Input; using osu.Framework.Timing; +using osu.Framework.Extensions.IEnumerableExtensions; +using System.Linq; +using osu.Game.Rulesets.Mania.Timing; +using osu.Game.Rulesets.Timing; namespace osu.Desktop.VisualTests.Tests { @@ -30,7 +32,7 @@ namespace osu.Desktop.VisualTests.Tests Action createPlayfield = (cols, pos) => { Clear(); - Add(new ManiaPlayfield(cols, new List()) + Add(new ManiaPlayfield(cols) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -39,37 +41,22 @@ namespace osu.Desktop.VisualTests.Tests }); }; - Action createPlayfieldWithNotes = (cols, pos) => + const double start_time = 500; + const double duration = 500; + + Func createTimingChange = (time, gravity) => new ManiaSpeedAdjustmentContainer(new MultiplierControlPoint(time) + { + TimingPoint = { BeatLength = 1000 } + }, gravity ? ScrollingAlgorithm.Gravity : ScrollingAlgorithm.Basic); + + Action createPlayfieldWithNotes = gravity => { Clear(); - ManiaPlayfield playField; - Add(playField = new ManiaPlayfield(cols, new List { new TimingChange { BeatLength = 200 } }) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - SpecialColumnPosition = pos, - Scale = new Vector2(1, -1) - }); - - for (int i = 0; i < cols; i++) - { - playField.Add(new DrawableNote(new Note - { - StartTime = Time.Current + 1000, - Column = i - })); - } - }; - - Action createPlayfieldWithNotesAcceptingInput = () => - { - Clear(); - - var rateAdjustClock = new StopwatchClock(true) { Rate = 0.5 }; + var rateAdjustClock = new StopwatchClock(true) { Rate = 1 }; ManiaPlayfield playField; - Add(playField = new ManiaPlayfield(4, new List { new TimingChange { BeatLength = 200 } }) + Add(playField = new ManiaPlayfield(4) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -77,14 +64,23 @@ namespace osu.Desktop.VisualTests.Tests Clock = new FramedClock(rateAdjustClock) }); - for (int t = 1000; t <= 2000; t += 100) + if (!gravity) + playField.Columns.ForEach(c => c.Add(createTimingChange(0, false))); + + for (double t = start_time; t <= start_time + duration; t += 100) { + if (gravity) + playField.Columns.ElementAt(0).Add(createTimingChange(t, true)); + playField.Add(new DrawableNote(new Note { StartTime = t, Column = 0 }, new Bindable(Key.D))); + if (gravity) + playField.Columns.ElementAt(3).Add(createTimingChange(t, true)); + playField.Add(new DrawableNote(new Note { StartTime = t, @@ -92,17 +88,23 @@ namespace osu.Desktop.VisualTests.Tests }, new Bindable(Key.K))); } - playField.Add(new DrawableHoldNote(new HoldNote - { - StartTime = 1000, - Duration = 1000, - Column = 1 - }, new Bindable(Key.F))); + if (gravity) + playField.Columns.ElementAt(1).Add(createTimingChange(start_time, true)); playField.Add(new DrawableHoldNote(new HoldNote { - StartTime = 1000, - Duration = 1000, + StartTime = start_time, + Duration = duration, + Column = 1 + }, new Bindable(Key.F))); + + if (gravity) + playField.Columns.ElementAt(2).Add(createTimingChange(start_time, true)); + + playField.Add(new DrawableHoldNote(new HoldNote + { + StartTime = start_time, + Duration = duration, Column = 2 }, new Bindable(Key.J))); }; @@ -116,16 +118,11 @@ namespace osu.Desktop.VisualTests.Tests AddStep("Left special style", () => createPlayfield(8, SpecialColumnPosition.Left)); AddStep("Right special style", () => createPlayfield(8, SpecialColumnPosition.Right)); - AddStep("Normal special style", () => createPlayfield(4, SpecialColumnPosition.Normal)); + AddStep("Notes with input", () => createPlayfieldWithNotes(false)); + AddWaitStep((int)Math.Ceiling((start_time + duration) / TimePerAction)); - AddStep("Notes", () => createPlayfieldWithNotes(4, SpecialColumnPosition.Normal)); - AddWaitStep(10); - AddStep("Left special style", () => createPlayfieldWithNotes(4, SpecialColumnPosition.Left)); - AddWaitStep(10); - AddStep("Right special style", () => createPlayfieldWithNotes(4, SpecialColumnPosition.Right)); - AddWaitStep(10); - - AddStep("Notes with input", () => createPlayfieldWithNotesAcceptingInput()); + AddStep("Notes with gravity", () => createPlayfieldWithNotes(true)); + AddWaitStep((int)Math.Ceiling((start_time + duration) / TimePerAction)); } private void triggerKeyDown(Column column) diff --git a/osu.Desktop.VisualTests/Tests/TestCaseScrollingHitObjects.cs b/osu.Desktop.VisualTests/Tests/TestCaseScrollingHitObjects.cs new file mode 100644 index 0000000000..5a4b3deb05 --- /dev/null +++ b/osu.Desktop.VisualTests/Tests/TestCaseScrollingHitObjects.cs @@ -0,0 +1,220 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Timing; + +namespace osu.Desktop.VisualTests.Tests +{ + public class TestCaseScrollingHitObjects : TestCase + { + public override string Description => "SpeedAdjustmentContainer/DrawableTimingSection"; + + private SpeedAdjustmentCollection adjustmentCollection; + + private BindableDouble timeRangeBindable; + private OsuSpriteText timeRangeText; + private OsuSpriteText bottomLabel; + private SpriteText topTime, bottomTime; + + public override void Reset() + { + base.Reset(); + + 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:#,#}"; + + Add(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 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 + { + private readonly Box background; + private const float height = 14; + + 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(); + FadeInFromZero(250, EasingTypes.OutQuint); + } + + private bool hasExpired; + protected override void Update() + { + base.Update(); + if (Time.Current >= HitObject.StartTime) + { + background.Colour = Color4.Red; + + if (!hasExpired) + { + using (BeginDelayedSequence(200)) + { + FadeOut(200); + Expire(); + } + + hasExpired = true; + } + } + } + } + } +} diff --git a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj index fd0179a3dd..03f531c8aa 100644 --- a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj +++ b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj @@ -207,6 +207,7 @@ + diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 0af8825208..8be3870ebe 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -96,6 +96,7 @@ namespace osu.Game.Rulesets.Mania new ModCinema(), }, }, + new ManiaModGravity() }; default: diff --git a/osu.Game.Rulesets.Mania/Mods/IGenerateSpeedAdjustments.cs b/osu.Game.Rulesets.Mania/Mods/IGenerateSpeedAdjustments.cs new file mode 100644 index 0000000000..f179aa2ff8 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/IGenerateSpeedAdjustments.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Timing; + +namespace osu.Game.Rulesets.Mania.Mods +{ + /// + /// A type of mod which generates speed adjustments that scroll the hit objects and bar lines. + /// + internal interface IGenerateSpeedAdjustments + { + /// + /// Applies this mod to a hit renderer. + /// + /// The hit renderer to apply to. + /// The per-column list of speed adjustments for hit objects. + /// The list of speed adjustments for bar lines. + void ApplyToHitRenderer(ManiaHitRenderer hitRenderer, ref List[] hitObjectTimingChanges, ref List barlineTimingChanges); + } +} diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModGravity.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModGravity.cs new file mode 100644 index 0000000000..1ba8ac4710 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModGravity.cs @@ -0,0 +1,46 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Mods; +using osu.Game.Graphics; +using osu.Game.Rulesets.Mania.Timing; +using osu.Game.Rulesets.Timing; +using osu.Game.Rulesets.Mania.Objects.Drawables; + +namespace osu.Game.Rulesets.Mania.Mods +{ + public class ManiaModGravity : Mod, IGenerateSpeedAdjustments + { + public override string Name => "Gravity"; + + public override double ScoreMultiplier => 0; + + public override FontAwesome Icon => FontAwesome.fa_sort_desc; + + public void ApplyToHitRenderer(ManiaHitRenderer hitRenderer, ref List[] hitObjectTimingChanges, ref List barlineTimingChanges) + { + // We have to generate one speed adjustment per hit object for gravity + foreach (ManiaHitObject obj in hitRenderer.Objects) + { + MultiplierControlPoint controlPoint = hitRenderer.CreateControlPointAt(obj.StartTime); + // Beat length has too large of an effect for gravity, so we'll force it to a constant value for now + controlPoint.TimingPoint.BeatLength = 1000; + + hitObjectTimingChanges[obj.Column].Add(new ManiaSpeedAdjustmentContainer(controlPoint, ScrollingAlgorithm.Gravity)); + } + + // Like with hit objects, we need to generate one speed adjustment per bar line + foreach (DrawableBarLine barLine in hitRenderer.BarLines) + { + var controlPoint = hitRenderer.CreateControlPointAt(barLine.HitObject.StartTime); + // Beat length has too large of an effect for gravity, so we'll force it to a constant value for now + controlPoint.TimingPoint.BeatLength = 1000; + + barlineTimingChanges.Add(new ManiaSpeedAdjustmentContainer(controlPoint, ScrollingAlgorithm.Gravity)); + } + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 1d751b0293..e52fb1362f 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -55,6 +55,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables tickContainer = new Container { RelativeSizeAxes = Axes.Both, + RelativeChildOffset = new Vector2(0, (float)HitObject.StartTime), RelativeChildSize = new Vector2(1, (float)HitObject.Duration) }, head = new DrawableHeadNote(this, key) @@ -76,9 +77,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables HoldStartTime = () => holdStartTime }; - // To make the ticks relative to ourselves we need to offset them backwards - drawableTick.Y -= (float)HitObject.StartTime; - tickContainer.Add(drawableTick); AddNested(drawableTick); } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 4e276fddb7..e32e953404 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -6,6 +6,7 @@ using OpenTK.Input; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Judgements; +using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Mania.Objects.Drawables @@ -32,6 +33,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables Y = (float)HitObject.StartTime; } + protected override void LoadComplete() + { + base.LoadComplete(); + + LifetimeStart = HitObject.StartTime - ManiaPlayfield.TIME_SPAN_MAX; + } + public override Color4 AccentColour { get { return base.AccentColour; } diff --git a/osu.Game.Rulesets.Mania/Timing/BasicScrollingDrawableTimingSection.cs b/osu.Game.Rulesets.Mania/Timing/BasicScrollingDrawableTimingSection.cs new file mode 100644 index 0000000000..e485581d9f --- /dev/null +++ b/osu.Game.Rulesets.Mania/Timing/BasicScrollingDrawableTimingSection.cs @@ -0,0 +1,27 @@ +// 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/ControlPointContainer.cs b/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs deleted file mode 100644 index 26d5146aae..0000000000 --- a/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs +++ /dev/null @@ -1,156 +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.Graphics; -using osu.Framework.Graphics.Containers; -using OpenTK; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Objects; - -namespace osu.Game.Rulesets.Mania.Timing -{ - /// - /// A container in which added drawables are put into a relative coordinate space spanned by a length of time. - /// - /// This container contains s which scroll inside this container. - /// Drawables added to this container are moved inside the relevant , - /// and as such, will scroll along with the s. - /// - /// - public class ControlPointContainer : Container - { - /// - /// The amount of time which this container spans. - /// - public double TimeSpan { get; set; } - - private readonly List drawableControlPoints; - - public ControlPointContainer(IEnumerable timingChanges) - { - drawableControlPoints = timingChanges.Select(t => new DrawableControlPoint(t)).ToList(); - Children = drawableControlPoints; - } - - /// - /// Adds a drawable to this container. Note that the drawable added must have its Y-position be - /// an absolute unit of time that is _not_ relative to . - /// - /// The drawable to add. - public override void Add(Drawable drawable) - { - // Always add timing sections to ourselves - if (drawable is DrawableControlPoint) - { - base.Add(drawable); - return; - } - - var controlPoint = drawableControlPoints.LastOrDefault(t => t.CanContain(drawable)) ?? drawableControlPoints.FirstOrDefault(); - - if (controlPoint == null) - throw new InvalidOperationException("Could not find suitable timing section to add object to."); - - controlPoint.Add(drawable); - } - - /// - /// A container that contains drawables within the time span of a timing section. - /// - /// The content of this container will scroll relative to the current time. - /// - /// - private class DrawableControlPoint : Container - { - private readonly TimingChange timingChange; - - protected override Container Content => content; - private readonly Container content; - - /// - /// Creates a drawable control point. The height of this container will be proportional - /// to the beat length of the control point it is initialized with such that, e.g. a beat length - /// of 500ms results in this container being twice as high as its parent, which further means that - /// the content container will scroll at twice the normal rate. - /// - /// The control point to create the drawable control point for. - public DrawableControlPoint(TimingChange timingChange) - { - this.timingChange = timingChange; - - RelativeSizeAxes = Axes.Both; - - AddInternal(content = new AutoTimeRelativeContainer - { - RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Both, - Y = (float)timingChange.Time - }); - } - - protected override void Update() - { - var parent = (ControlPointContainer)Parent; - - // Adjust our height to account for the speed changes - Height = (float)(1000 / timingChange.BeatLength / timingChange.SpeedMultiplier); - RelativeChildSize = new Vector2(1, (float)parent.TimeSpan); - - // Scroll the content - content.Y = (float)(timingChange.Time - Time.Current); - } - - public override void Add(Drawable drawable) - { - // The previously relatively-positioned drawable will now become relative to content, but since the drawable has no knowledge of content, - // we need to offset it back by content's position position so that it becomes correctly relatively-positioned to content - // This can be removed if hit objects were stored such that either their StartTime or their "beat offset" was relative to the timing change - // they belonged to, but this requires a radical change to the beatmap format which we're not ready to do just yet - drawable.Y -= (float)timingChange.Time; - - base.Add(drawable); - } - - /// - /// Whether this control point can contain a drawable. This control point can contain a drawable if the drawable is positioned "after" this control point. - /// - /// The drawable to check. - public bool CanContain(Drawable drawable) => content.Y <= drawable.Y; - - /// - /// A container which always keeps its height and relative coordinate space "auto-sized" to its children. - /// - /// This is used in the case where children are relatively positioned/sized to time values (e.g. notes/bar lines) to keep - /// such children wrapped inside a container, otherwise they would disappear due to container flattening. - /// - /// - private class AutoTimeRelativeContainer : Container - { - protected override IComparer DepthComparer => new HitObjectReverseStartTimeComparer(); - - 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; - } - - if (!Children.Any()) - return; - - float height = Children.Select(child => child.Y + child.Height).Max(); - - Height = height; - RelativeChildSize = new Vector2(1, height); - - base.InvalidateFromChild(invalidation); - } - } - } - } -} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Timing/GravityScrollingDrawableTimingSection.cs b/osu.Game.Rulesets.Mania/Timing/GravityScrollingDrawableTimingSection.cs new file mode 100644 index 0000000000..730daa9ffd --- /dev/null +++ b/osu.Game.Rulesets.Mania/Timing/GravityScrollingDrawableTimingSection.cs @@ -0,0 +1,60 @@ +// 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 that emulates a form of gravity where hit objects speed up over time. + /// + internal class GravityScrollingDrawableTimingSection : DrawableTimingSection + { + private readonly MultiplierControlPoint controlPoint; + + public GravityScrollingDrawableTimingSection(MultiplierControlPoint controlPoint) + { + this.controlPoint = controlPoint; + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + // The gravity-adjusted start position + float startPos = (float)computeGravityTime(controlPoint.StartTime); + // The gravity-adjusted end position + float endPos = (float)computeGravityTime(controlPoint.StartTime + RelativeChildSize.Y); + + Y = startPos; + Height = endPos - startPos; + } + + /// + /// Applies gravity to a time value based on the current time. + /// + /// The time value gravity should be applied to. + /// The time after gravity is applied to . + private double computeGravityTime(double time) + { + double relativeTime = relativeTimeAt(time); + + // The sign of the relative time, this is used to apply backwards acceleration leading into startTime + double sign = relativeTime < 0 ? -1 : 1; + + return VisibleTimeRange - acceleration * relativeTime * relativeTime * sign; + } + + /// + /// The acceleration due to "gravity" of the content of this container. + /// + private double acceleration => 1 / VisibleTimeRange; + + /// + /// Computes the current time relative to , accounting for . + /// + /// The non-offset time. + /// 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 new file mode 100644 index 0000000000..ed22264d74 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Timing/ManiaSpeedAdjustmentContainer.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.Game.Rulesets.Timing; + +namespace osu.Game.Rulesets.Mania.Timing +{ + public class ManiaSpeedAdjustmentContainer : SpeedAdjustmentContainer + { + private readonly ScrollingAlgorithm scrollingAlgorithm; + + public ManiaSpeedAdjustmentContainer(MultiplierControlPoint timingSection, ScrollingAlgorithm scrollingAlgorithm) + : base(timingSection) + { + this.scrollingAlgorithm = scrollingAlgorithm; + } + + protected override DrawableTimingSection CreateTimingSection() + { + switch (scrollingAlgorithm) + { + default: + case ScrollingAlgorithm.Basic: + return new BasicScrollingDrawableTimingSection(ControlPoint); + case ScrollingAlgorithm.Gravity: + return new GravityScrollingDrawableTimingSection(ControlPoint); + } + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Timing/ScrollingAlgorithm.cs b/osu.Game.Rulesets.Mania/Timing/ScrollingAlgorithm.cs new file mode 100644 index 0000000000..72e096f5aa --- /dev/null +++ b/osu.Game.Rulesets.Mania/Timing/ScrollingAlgorithm.cs @@ -0,0 +1,17 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.Mania.Timing +{ + public enum ScrollingAlgorithm + { + /// + /// Basic scrolling algorithm based on the timing section time. This is the default algorithm. + /// + Basic, + /// + /// Emulating a form of gravity where hit objects speed up over time. + /// + Gravity + } +} diff --git a/osu.Game.Rulesets.Mania/Timing/TimingChange.cs b/osu.Game.Rulesets.Mania/Timing/TimingChange.cs deleted file mode 100644 index 9153ba6991..0000000000 --- a/osu.Game.Rulesets.Mania/Timing/TimingChange.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -namespace osu.Game.Rulesets.Mania.Timing -{ - public class TimingChange - { - /// - /// The time at which this timing change happened. - /// - public double Time; - - /// - /// The beat length. - /// - public double BeatLength = 500; - - /// - /// The speed multiplier. - /// - public double SpeedMultiplier = 1; - } -} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 41c693a5d9..dea4dc1a3a 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -11,13 +11,10 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Colour; using osu.Framework.Input; using osu.Game.Graphics; -using osu.Game.Rulesets.Mania.Timing; -using System.Collections.Generic; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Mania.Judgements; using System; using osu.Framework.Configuration; +using osu.Game.Rulesets.Timing; namespace osu.Game.Rulesets.Mania.UI { @@ -33,6 +30,13 @@ 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. /// @@ -42,9 +46,9 @@ namespace osu.Game.Rulesets.Mania.UI private readonly Container hitTargetBar; private readonly Container keyIcon; - public readonly ControlPointContainer ControlPointContainer; + private readonly SpeedAdjustmentCollection speedAdjustments; - public Column(IEnumerable timingChanges) + public Column() { RelativeSizeAxes = Axes.Y; Width = column_width; @@ -93,10 +97,11 @@ namespace osu.Game.Rulesets.Mania.UI } } }, - ControlPointContainer = new ControlPointContainer(timingChanges) + speedAdjustments = new SpeedAdjustmentCollection(Axes.Y) { Name = "Hit objects", RelativeSizeAxes = Axes.Both, + VisibleTimeRange = VisibleTimeRange }, // For column lighting, we need to capture input events before the notes new InputTarget @@ -187,10 +192,11 @@ namespace osu.Game.Rulesets.Mania.UI } } - public void Add(DrawableHitObject hitObject) + public void Add(SpeedAdjustmentContainer speedAdjustment) => speedAdjustments.Add(speedAdjustment); + public void Add(DrawableHitObject hitObject) { hitObject.AccentColour = AccentColour; - ControlPointContainer.Add(hitObject); + speedAdjustments.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 57477147d5..dcb2a29556 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs @@ -8,6 +8,7 @@ using OpenTK; using OpenTK.Input; using osu.Framework.Allocation; using osu.Framework.Configuration; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Lists; using osu.Framework.MathUtils; @@ -16,6 +17,7 @@ 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; @@ -23,78 +25,44 @@ using osu.Game.Rulesets.Mania.Timing; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Timing; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mania.UI { - public class ManiaHitRenderer : HitRenderer + public class ManiaHitRenderer : SpeedAdjustedHitRenderer { - public int? Columns; + /// + /// Preferred column count. This will only have an effect during the initialization of the play field. + /// + public int PreferredColumns; + + 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) { - } - - protected override Playfield CreatePlayfield() - { - double lastSpeedMultiplier = 1; - double lastBeatLength = 500; - - // Merge timing + difficulty points - var allPoints = new SortedList(Comparer.Default); - allPoints.AddRange(Beatmap.ControlPointInfo.TimingPoints); - allPoints.AddRange(Beatmap.ControlPointInfo.DifficultyPoints); - - // Generate the timing points, making non-timing changes use the previous timing change - var timingChanges = allPoints.Select(c => - { - var timingPoint = c as TimingControlPoint; - var difficultyPoint = c as DifficultyControlPoint; - - if (timingPoint != null) - lastBeatLength = timingPoint.BeatLength; - - if (difficultyPoint != null) - lastSpeedMultiplier = difficultyPoint.SpeedMultiplier; - - return new TimingChange - { - Time = c.Time, - BeatLength = lastBeatLength, - SpeedMultiplier = lastSpeedMultiplier - }; - }); - - double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue; - - // Perform some post processing of the timing changes - timingChanges = timingChanges - // Collapse sections after the last hit object - .Where(s => s.Time <= lastObjectTime) - // Collapse sections with the same start time - .GroupBy(s => s.Time).Select(g => g.Last()).OrderBy(s => s.Time) - // Collapse sections with the same beat length - .GroupBy(s => s.BeatLength * s.SpeedMultiplier).Select(g => g.First()) - .ToList(); - - return new ManiaPlayfield(Columns ?? (int)Math.Round(Beatmap.BeatmapInfo.Difficulty.CircleSize), timingChanges) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - // Invert by default for now (should be moved to config/skin later) - Scale = new Vector2(1, -1) - }; - } - - [BackgroundDependencyLoader] - private void load() - { - var maniaPlayfield = (ManiaPlayfield)Playfield; + // 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; SortedList timingPoints = Beatmap.ControlPointInfo.TimingPoints; + var barLines = new List(); + for (int i = 0; i < timingPoints.Count; i++) { TimingControlPoint point = timingPoints[i]; @@ -105,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.UI int index = 0; for (double t = timingPoints[i].Time; Precision.DefinitelyBigger(endTime, t); t += point.BeatLength, index++) { - maniaPlayfield.Add(new DrawableBarLine(new BarLine + barLines.Add(new DrawableBarLine(new BarLine { StartTime = t, ControlPoint = point, @@ -113,17 +81,78 @@ 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); + } + + protected override void ApplyBeatmap() + { + base.ApplyBeatmap(); + + PreferredColumns = (int)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, + Origin = Anchor.Centre, + // Invert by default for now (should be moved to config/skin later) + Scale = new Vector2(1, -1) + }; + public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this); protected override BeatmapConverter CreateBeatmapConverter() => new ManiaBeatmapConverter(); protected override DrawableHitObject GetVisualRepresentation(ManiaHitObject h) { - var maniaPlayfield = Playfield as ManiaPlayfield; - if (maniaPlayfield == null) - return null; + var maniaPlayfield = (ManiaPlayfield)Playfield; Bindable key = maniaPlayfield.Columns.ElementAt(h.Column).Key; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index 2e6b63579e..5f33ac2cf1 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -15,13 +15,13 @@ using osu.Framework.Allocation; using OpenTK.Input; using System.Linq; using System.Collections.Generic; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Mania.Timing; using osu.Framework.Input; using osu.Framework.Graphics.Transforms; using osu.Framework.MathUtils; using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Timing; +using osu.Framework.Configuration; namespace osu.Game.Rulesets.Mania.UI { @@ -29,10 +29,10 @@ namespace osu.Game.Rulesets.Mania.UI { public const float HIT_TARGET_POSITION = 50; - private const float time_span_default = 5000; - private const float time_span_min = 10; - private const float time_span_max = 50000; - private const float time_span_step = 200; + private const double time_span_default = 1500; + public const double TIME_SPAN_MIN = 50; + public 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. @@ -58,14 +58,20 @@ namespace osu.Game.Rulesets.Mania.UI private readonly FlowContainer columns; public IEnumerable Columns => columns.Children; - private readonly ControlPointContainer barLineContainer; + private readonly BindableDouble visibleTimeRange = new BindableDouble(time_span_default) + { + MinValue = TIME_SPAN_MIN, + MaxValue = TIME_SPAN_MAX + }; + + private readonly SpeedAdjustmentCollection barLineContainer; private List normalColumnColours = new List(); private Color4 specialColumnColour; private readonly int columnCount; - public ManiaPlayfield(int columnCount, IEnumerable timingChanges) + public ManiaPlayfield(int columnCount) { this.columnCount = columnCount; @@ -116,12 +122,13 @@ namespace osu.Game.Rulesets.Mania.UI Padding = new MarginPadding { Top = HIT_TARGET_POSITION }, Children = new[] { - barLineContainer = new ControlPointContainer(timingChanges) + barLineContainer = new SpeedAdjustmentCollection(Axes.Y) { Name = "Bar lines", Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Y + RelativeSizeAxes = Axes.Y, + VisibleTimeRange = visibleTimeRange // Width is set in the Update method } } @@ -131,9 +138,7 @@ namespace osu.Game.Rulesets.Mania.UI }; for (int i = 0; i < columnCount; i++) - columns.Add(new Column(timingChanges)); - - TimeSpan = time_span_default; + columns.Add(new Column { VisibleTimeRange = visibleTimeRange }); } [BackgroundDependencyLoader] @@ -208,6 +213,7 @@ 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) { @@ -216,10 +222,10 @@ namespace osu.Game.Rulesets.Mania.UI switch (args.Key) { case Key.Minus: - transformTimeSpanTo(TimeSpan + time_span_step, 200, EasingTypes.OutQuint); + transformVisibleTimeRangeTo(visibleTimeRange + time_span_step, 200, EasingTypes.OutQuint); break; case Key.Plus: - transformTimeSpanTo(TimeSpan - time_span_step, 200, EasingTypes.OutQuint); + transformVisibleTimeRangeTo(visibleTimeRange - time_span_step, 200, EasingTypes.OutQuint); break; } } @@ -227,29 +233,9 @@ namespace osu.Game.Rulesets.Mania.UI return false; } - private double timeSpan; - /// - /// The amount of time which the length of the playfield spans. - /// - public double TimeSpan + private void transformVisibleTimeRangeTo(double newTimeRange, double duration = 0, EasingTypes easing = EasingTypes.None) { - get { return timeSpan; } - set - { - if (timeSpan == value) - return; - timeSpan = value; - - timeSpan = MathHelper.Clamp(timeSpan, time_span_min, time_span_max); - - barLineContainer.TimeSpan = value; - Columns.ForEach(c => c.ControlPointContainer.TimeSpan = value); - } - } - - private void transformTimeSpanTo(double newTimeSpan, double duration = 0, EasingTypes easing = EasingTypes.None) - { - TransformTo(() => TimeSpan, newTimeSpan, duration, easing, new TransformTimeSpan()); + TransformTo(() => visibleTimeRange.Value, newTimeRange, duration, easing, new TransformTimeSpan()); } protected override void Update() @@ -278,7 +264,7 @@ namespace osu.Game.Rulesets.Mania.UI base.Apply(d); var p = (ManiaPlayfield)d; - p.TimeSpan = CurrentValue; + p.visibleTimeRange.Value = (float)CurrentValue; } } } diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index 154e3d9b3e..88d1ad7ad8 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -63,6 +63,7 @@ + @@ -78,14 +79,17 @@ + + + + - - + diff --git a/osu.Game.Rulesets.Mania/packages.config b/osu.Game.Rulesets.Mania/packages.config index fa6edb9c8f..8add43d5d5 100644 --- a/osu.Game.Rulesets.Mania/packages.config +++ b/osu.Game.Rulesets.Mania/packages.config @@ -1,5 +1,4 @@  -