diff --git a/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs b/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs index 95287c3199..20e94e0dde 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs @@ -14,6 +14,7 @@ using osu.Framework.Configuration; using OpenTK.Input; using osu.Framework.Timing; +using osu.Framework.Extensions.IEnumerableExtensions; namespace osu.Desktop.VisualTests.Tests { @@ -30,7 +31,7 @@ public override void Reset() Action createPlayfield = (cols, pos) => { Clear(); - Add(new ManiaPlayfield(cols, new List()) + Add(new ManiaPlayfield(cols) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -44,7 +45,7 @@ public override void Reset() Clear(); ManiaPlayfield playField; - Add(playField = new ManiaPlayfield(cols, new List { new TimingChange { BeatLength = 200 } }) + Add(playField = new ManiaPlayfield(cols) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -52,6 +53,8 @@ public override void Reset() Scale = new Vector2(1, -1) }); + playField.Columns.ForEach(c => c.Add(new DrawableScrollingTimingChange(new TimingChange { BeatLength = 200 }))); + for (int i = 0; i < cols; i++) { playField.Add(new DrawableNote(new Note @@ -69,7 +72,7 @@ public override void Reset() var rateAdjustClock = new StopwatchClock(true) { Rate = 0.5 }; 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,6 +80,8 @@ public override void Reset() Clock = new FramedClock(rateAdjustClock) }); + playField.Columns.ForEach(c => c.Add(new DrawableScrollingTimingChange(new TimingChange { BeatLength = 200 }))); + for (int t = 1000; t <= 2000; t += 100) { playField.Add(new DrawableNote(new Note diff --git a/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs b/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs deleted file mode 100644 index 0a8bc2d44a..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); - RelativeCoordinateSpace = 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.Geometry) == 0) - { - base.InvalidateFromChild(invalidation); - return; - } - - if (!Children.Any()) - return; - - float height = Children.Select(child => child.Y + child.Height).Max(); - - Height = height; - RelativeCoordinateSpace = new Vector2(1, height); - - base.InvalidateFromChild(invalidation); - } - } - } - } -} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Timing/DrawableTimingChange.cs b/osu.Game.Rulesets.Mania/Timing/DrawableTimingChange.cs new file mode 100644 index 0000000000..38b28f16a8 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Timing/DrawableTimingChange.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using System.Linq; +using OpenTK; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Mania.Timing +{ + public abstract class DrawableTimingChange : Container + { + protected readonly TimingChange TimingChange; + + protected override Container Content => content; + private readonly Container content; + + public DrawableTimingChange(TimingChange timingChange) + { + TimingChange = timingChange; + + RelativeSizeAxes = Axes.Both; + + AddInternal(content = new RelativeCoordinateAutoSizingContainer + { + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + Y = (float)timingChange.Time + }); + } + + protected override void Update() + { + var parent = (TimingChangeContainer)Parent; + + // Adjust our height to account for the speed changes + Height = (float)(1000 / TimingChange.BeatLength / TimingChange.SpeedMultiplier); + RelativeCoordinateSpace = new Vector2(1, (float)parent.TimeSpan); + } + + public override void Add(DrawableHitObject 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 timing change can contain a drawable. This is true if the drawable occurs "after" after this timing change. + /// + public bool CanContain(DrawableHitObject hitObject) => TimingChange.Time <= hitObject.HitObject.StartTime; + + private class RelativeCoordinateAutoSizingContainer : 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.Geometry) == 0) + { + base.InvalidateFromChild(invalidation); + return; + } + + if (!Children.Any()) + return; + + float height = Children.Select(child => child.Y + child.Height).Max(); + + Height = height; + RelativeCoordinateSpace = new Vector2(1, height); + + base.InvalidateFromChild(invalidation); + } + } + } + + public class DrawableScrollingTimingChange : DrawableTimingChange + { + public DrawableScrollingTimingChange(TimingChange timingChange) + : base(timingChange) + { + } + + protected override void Update() + { + base.Update(); + + Content.Y = (float)(TimingChange.Time - Time.Current); + } + } + + public class DrawableGravityTimingChange : DrawableTimingChange + { + public DrawableGravityTimingChange(TimingChange timingChange) + : base(timingChange) + { + } + + protected override void Update() + { + base.Update(); + + // Todo: Gravity calculations here + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Timing/TimingChangeContainer.cs b/osu.Game.Rulesets.Mania/Timing/TimingChangeContainer.cs new file mode 100644 index 0000000000..2fdd365ab7 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Timing/TimingChangeContainer.cs @@ -0,0 +1,44 @@ +// 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; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Mania.Timing +{ + public class TimingChangeContainer : Container + { + /// + /// The amount of time which this container spans. + /// + public double TimeSpan { get; set; } + + /// + /// Adds a hit object to the most applicable timing change in this container. + /// + /// The hit object to add. + public void Add(DrawableHitObject hitObject) + { + var target = timingChangeFor(hitObject); + + if (target == null) + throw new ArgumentException("No timing change could be found that can contain the hit object.", nameof(hitObject)); + + target.Add(hitObject); + } + + /// + /// Finds the most applicable timing change that can contain a hit object. + /// + /// The hit object to contain. + /// The last timing change which can contain . + private DrawableTimingChange timingChangeFor(DrawableHitObject hitObject) => Children.LastOrDefault(c => c.CanContain(hitObject)); + } +} \ 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 6dfd5000d4..edb396a1e6 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -42,9 +42,9 @@ public class Column : Container, IHasAccentColour private readonly Container hitTargetBar; private readonly Container keyIcon; - public readonly ControlPointContainer ControlPointContainer; + private readonly TimingChangeContainer timingChanges; - public Column(IEnumerable timingChanges) + public Column() { RelativeSizeAxes = Axes.Y; Width = column_width; @@ -93,7 +93,7 @@ public Column(IEnumerable timingChanges) } } }, - ControlPointContainer = new ControlPointContainer(timingChanges) + timingChanges = new TimingChangeContainer { Name = "Hit objects", RelativeSizeAxes = Axes.Both, @@ -187,10 +187,17 @@ public Color4 AccentColour } } - public void Add(DrawableHitObject hitObject) + public double TimeSpan + { + get { return timingChanges.TimeSpan; } + set { timingChanges.TimeSpan = value; } + } + + public void Add(DrawableTimingChange timingChange) => timingChanges.Add(timingChange); + public void Add(DrawableHitObject hitObject) { hitObject.AccentColour = AccentColour; - ControlPointContainer.Add(hitObject); + timingChanges.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..a250c56944 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs @@ -8,6 +8,7 @@ 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; @@ -78,13 +79,17 @@ protected override Playfield CreatePlayfield() .GroupBy(s => s.BeatLength * s.SpeedMultiplier).Select(g => g.First()) .ToList(); - return new ManiaPlayfield(Columns ?? (int)Math.Round(Beatmap.BeatmapInfo.Difficulty.CircleSize), timingChanges) + var playfield = new ManiaPlayfield(Columns ?? (int)Math.Round(Beatmap.BeatmapInfo.Difficulty.CircleSize)) { Anchor = Anchor.Centre, Origin = Anchor.Centre, // Invert by default for now (should be moved to config/skin later) Scale = new Vector2(1, -1) }; + + timingChanges.ForEach(t => playfield.Columns.ForEach(c => c.Add(new DrawableScrollingTimingChange(t)))); + + return playfield; } [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index 2e6b63579e..a50673d117 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -58,14 +58,14 @@ public SpecialColumnPosition SpecialColumnPosition private readonly FlowContainer columns; public IEnumerable Columns => columns.Children; - private readonly ControlPointContainer barLineContainer; + private readonly TimingChangeContainer 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,7 +116,7 @@ public ManiaPlayfield(int columnCount, IEnumerable timingChanges) Padding = new MarginPadding { Top = HIT_TARGET_POSITION }, Children = new[] { - barLineContainer = new ControlPointContainer(timingChanges) + barLineContainer = new TimingChangeContainer { Name = "Bar lines", Anchor = Anchor.TopCentre, @@ -131,7 +131,7 @@ public ManiaPlayfield(int columnCount, IEnumerable timingChanges) }; for (int i = 0; i < columnCount; i++) - columns.Add(new Column(timingChanges)); + columns.Add(new Column()); TimeSpan = time_span_default; } @@ -207,6 +207,8 @@ private bool isSpecialColumn(int column) } public override void Add(DrawableHitObject h) => Columns.ElementAt(h.HitObject.Column).Add(h); + + public void Add(DrawableTimingChange timingChange) => barLineContainer.Add(timingChange); public void Add(DrawableBarLine barline) => barLineContainer.Add(barline); protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) @@ -243,7 +245,7 @@ public double TimeSpan timeSpan = MathHelper.Clamp(timeSpan, time_span_min, time_span_max); barLineContainer.TimeSpan = value; - Columns.ForEach(c => c.ControlPointContainer.TimeSpan = value); + Columns.ForEach(c => c.TimeSpan = value); } } diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index 3d5614bd90..c437847a2b 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -83,8 +83,9 @@ - + +