diff --git a/osu.Desktop.VisualTests/Tests/TestCaseGamefield.cs b/osu.Desktop.VisualTests/Tests/TestCaseGamefield.cs index cb15558ec3..1049a8818a 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseGamefield.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseGamefield.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Taiko.UI; using System.Collections.Generic; using osu.Desktop.VisualTests.Beatmaps; using osu.Framework.Allocation; +using osu.Game.Beatmaps.Timing; namespace osu.Desktop.VisualTests.Tests { @@ -52,6 +53,12 @@ namespace osu.Desktop.VisualTests.Tests time += RNG.Next(50, 500); } + TimingInfo timing = new TimingInfo(); + timing.ControlPoints.Add(new ControlPoint + { + BeatLength = 200 + }); + WorkingBeatmap beatmap = new TestWorkingBeatmap(new Beatmap { HitObjects = objects, @@ -64,8 +71,9 @@ namespace osu.Desktop.VisualTests.Tests Artist = @"Unknown", Title = @"Sample Beatmap", Author = @"peppy", - } - } + }, + }, + TimingInfo = timing }); Add(new Drawable[] diff --git a/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs b/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs index 2d4414d19f..04fcd8e94a 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs @@ -1,13 +1,16 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Input; using osu.Framework.Testing; using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.UI; -using System.Linq; using System; +using System.Collections.Generic; +using osu.Game.Beatmaps.Timing; +using OpenTK; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.Objects; namespace osu.Desktop.VisualTests.Tests { @@ -21,33 +24,58 @@ namespace osu.Desktop.VisualTests.Tests { base.Reset(); - const int max_columns = 10; - Action createPlayfield = (cols, pos) => { Clear(); - Add(new ManiaPlayfield(cols) + Add(new ManiaPlayfield(cols, new List()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - SpecialColumnPosition = pos + SpecialColumnPosition = pos, + Scale = new Vector2(1, -1) }); }; - for (int i = 1; i <= max_columns; i++) + Action createPlayfieldWithNotes = (cols, pos) => { - int tempI = i; + Clear(); - AddStep($"{i} column" + (i > 1 ? "s" : ""), () => createPlayfield(tempI, SpecialColumnPosition.Normal)); + ManiaPlayfield playField; + Add(playField = new ManiaPlayfield(cols, new List { new ControlPoint { BeatLength = 200 } }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + SpecialColumnPosition = pos, + Scale = new Vector2(1, -1) + }); - AddStep("Trigger keys down", () => ((ManiaPlayfield)Children.First()).Columns.Children.ForEach(triggerKeyDown)); - AddStep("Trigger keys up", () => ((ManiaPlayfield)Children.First()).Columns.Children.ForEach(triggerKeyUp)); + for (int i = 0; i < cols; i++) + { + playField.Add(new DrawableNote(new Note + { + StartTime = Time.Current + 1000, + Column = i + })); + } + }; - AddStep("Left special style", () => createPlayfield(tempI, SpecialColumnPosition.Left)); - AddStep("Right special style", () => createPlayfield(tempI, SpecialColumnPosition.Right)); - } + AddStep("1 column", () => createPlayfield(1, SpecialColumnPosition.Normal)); + AddStep("4 columns", () => createPlayfield(4, SpecialColumnPosition.Normal)); + AddStep("Left special style", () => createPlayfield(4, SpecialColumnPosition.Left)); + AddStep("Right special style", () => createPlayfield(4, SpecialColumnPosition.Right)); + AddStep("5 columns", () => createPlayfield(5, SpecialColumnPosition.Normal)); + AddStep("8 columns", () => createPlayfield(8, SpecialColumnPosition.Normal)); + AddStep("Left special style", () => createPlayfield(8, SpecialColumnPosition.Left)); + AddStep("Right special style", () => createPlayfield(8, SpecialColumnPosition.Right)); - AddStep("Normal special style", () => createPlayfield(max_columns, SpecialColumnPosition.Normal)); + AddStep("Normal special style", () => createPlayfield(4, SpecialColumnPosition.Normal)); + + 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); } private void triggerKeyDown(Column column) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 10bebbdba7..e51bbcdc13 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -8,6 +8,7 @@ using System; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Beatmaps; using osu.Game.Rulesets.Objects; +using OpenTK; namespace osu.Game.Rulesets.Mania.Beatmaps { @@ -17,7 +18,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps protected override IEnumerable ConvertHitObject(HitObject original, Beatmap beatmap) { - yield return null; + int availableColumns = (int)Math.Round(beatmap.BeatmapInfo.Difficulty.CircleSize); + + var positionData = original as IHasXPosition; + + float localWDivisor = 512.0f / availableColumns; + int column = MathHelper.Clamp((int)Math.Floor((positionData?.X ?? 1) / localWDivisor), 0, availableColumns - 1); + + yield return new Note + { + StartTime = original.StartTime, + Column = column, + }; } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 61dc2638a6..767a2b3458 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -27,24 +27,20 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables // This will be fixed when new designs are given or the current design is finalized. bodyPiece = new BodyPiece { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, }, headPiece = new NotePiece { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre }, tailPiece = new NotePiece { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre } }); - - // The "length" of the hold note stops at the "base" of the tail piece - // but we want to contain the tail piece within our bounds - Height += (float)HitObject.Duration / headPiece.Height; } public override Color4 AccentColour diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 29e4137bba..0307e9162a 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -22,9 +22,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { HitObject = hitObject; - Anchor = Anchor.TopCentre; - Origin = Anchor.BottomCentre; - RelativePositionAxes = Axes.Y; Y = (float)HitObject.StartTime; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index bc194b7703..b216c362f5 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public DrawableNote(Note hitObject) : base(hitObject) { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.Both; + Height = 100; Add(headPiece = new NotePiece { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre }); } @@ -38,6 +38,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables } } + protected override void Update() + { + if (Time.Current > HitObject.StartTime) + Colour = Color4.Green; + } + protected override void UpdateState(ArmedState state) { } diff --git a/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs b/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs new file mode 100644 index 0000000000..6c39ba40f9 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs @@ -0,0 +1,153 @@ +// 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.Timing; + +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 Exception("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 ControlPoint 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(ControlPoint 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 + { + 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/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index d2fccb2c58..dea00433e6 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -12,13 +12,17 @@ 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 osu.Game.Beatmaps.Timing; namespace osu.Game.Rulesets.Mania.UI { public class Column : Container, IHasAccentColour { - private const float key_size = 50; - private const float key_icon_size = 10; private const float key_icon_corner_radius = 3; private const float key_icon_border_radius = 2; @@ -35,7 +39,9 @@ namespace osu.Game.Rulesets.Mania.UI private readonly Container hitTargetBar; private readonly Container keyIcon; - public Column() + public readonly ControlPointContainer ControlPointContainer; + + public Column(IEnumerable timingChanges) { RelativeSizeAxes = Axes.Y; Width = column_width; @@ -48,59 +54,16 @@ namespace osu.Game.Rulesets.Mania.UI RelativeSizeAxes = Axes.Both, Alpha = 0.2f }, - new FillFlowContainer + new Container { - Name = "Key + hit target", - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new[] + Name = "Hit target + hit objects", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = ManiaPlayfield.HIT_TARGET_POSITION}, + Children = new Drawable[] { - new Container - { - Name = "Key", - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.X, - Height = key_size, - Children = new Drawable[] - { - new Box - { - Name = "Key gradient", - RelativeSizeAxes = Axes.Both, - ColourInfo = ColourInfo.GradientVertical(Color4.Black, Color4.Black.Opacity(0)), - Alpha = 0.5f - }, - keyIcon = new Container - { - Name = "Key icon", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(key_icon_size), - Masking = true, - CornerRadius = key_icon_corner_radius, - BorderThickness = 2, - BorderColour = Color4.White, // Not true - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - } - } - } - } - }, new Container { Name = "Hit target", - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, RelativeSizeAxes = Axes.X, Height = hit_target_height, Children = new Drawable[] @@ -114,8 +77,6 @@ namespace osu.Game.Rulesets.Mania.UI hitTargetBar = new Container { Name = "Bar", - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, RelativeSizeAxes = Axes.X, Height = hit_target_bar_height, Masking = true, @@ -128,6 +89,47 @@ namespace osu.Game.Rulesets.Mania.UI } } } + }, + ControlPointContainer = new ControlPointContainer(timingChanges) + { + Name = "Hit objects", + RelativeSizeAxes = Axes.Both, + }, + } + }, + new Container + { + Name = "Key", + RelativeSizeAxes = Axes.X, + Height = ManiaPlayfield.HIT_TARGET_POSITION, + Children = new Drawable[] + { + new Box + { + Name = "Key gradient", + RelativeSizeAxes = Axes.Both, + ColourInfo = ColourInfo.GradientVertical(Color4.Black, Color4.Black.Opacity(0)), + Alpha = 0.5f + }, + keyIcon = new Container + { + Name = "Key icon", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(key_icon_size), + Masking = true, + CornerRadius = key_icon_corner_radius, + BorderThickness = 2, + BorderColour = Color4.White, // Not true + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + } } } } @@ -176,9 +178,17 @@ namespace osu.Game.Rulesets.Mania.UI } } + public void Add(DrawableHitObject hitObject) + { + ControlPointContainer.Add(hitObject); + } + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { - if (args.Key == Key && !args.Repeat) + if (args.Repeat) + return false; + + if (args.Key == Key) { background.FadeTo(background.Alpha + 0.2f, 50, EasingTypes.OutQuint); keyIcon.ScaleTo(1.4f, 50, EasingTypes.OutQuint); @@ -198,5 +208,4 @@ namespace osu.Game.Rulesets.Mania.UI return false; } } - } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs b/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs index c007cdc80e..986aefb2bd 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs @@ -1,13 +1,20 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; +using System.Linq; +using OpenTK; +using osu.Framework.Graphics; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -15,20 +22,67 @@ namespace osu.Game.Rulesets.Mania.UI { public class ManiaHitRenderer : HitRenderer { - private readonly int columns; + public int? Columns; - public ManiaHitRenderer(WorkingBeatmap beatmap, int columns = 5) + public ManiaHitRenderer(WorkingBeatmap beatmap) : base(beatmap) { - this.columns = columns; + } + + protected override Playfield CreatePlayfield() + { + ControlPoint firstTimingChange = Beatmap.TimingInfo.ControlPoints.FirstOrDefault(t => t.TimingChange); + + if (firstTimingChange == null) + throw new Exception("The Beatmap contains no timing points!"); + + // Generate the timing points, making non-timing changes use the previous timing change + var timingChanges = Beatmap.TimingInfo.ControlPoints.Select(c => + { + ControlPoint t = c.Clone(); + + if (c.TimingChange) + firstTimingChange = c; + else + t.BeatLength = firstTimingChange.BeatLength; + + return t; + }); + + double lastObjectTime = (Objects.Last() as IHasEndTime)?.EndTime ?? Objects.Last().StartTime; + + // 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) + }; } public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this); protected override BeatmapConverter CreateBeatmapConverter() => new ManiaBeatmapConverter(); - protected override Playfield CreatePlayfield() => new ManiaPlayfield(columns); + protected override DrawableHitObject GetVisualRepresentation(ManiaHitObject h) + { + var note = h as Note; + if (note != null) + return new DrawableNote(note); - protected override DrawableHitObject GetVisualRepresentation(ManiaHitObject h) => null; + return null; + } + + protected override Vector2 GetPlayfieldAspectAdjust() => new Vector2(1, 0.8f); } } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index 487304d7f3..56a86873e9 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -15,11 +15,25 @@ 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.Game.Beatmaps.Timing; +using osu.Framework.Graphics.Transforms; +using osu.Framework.MathUtils; namespace osu.Game.Rulesets.Mania.UI { public class ManiaPlayfield : Playfield { + 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; + /// /// 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... @@ -43,12 +57,14 @@ namespace osu.Game.Rulesets.Mania.UI public readonly FlowContainer Columns; + private readonly ControlPointContainer barlineContainer; + private List normalColumnColours = new List(); private Color4 specialColumnColour; private readonly int columnCount; - public ManiaPlayfield(int columnCount) + public ManiaPlayfield(int columnCount, IEnumerable timingChanges) { this.columnCount = columnCount; @@ -59,10 +75,11 @@ namespace osu.Game.Rulesets.Mania.UI { new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, + Masking = true, Children = new Drawable[] { new Box @@ -72,18 +89,34 @@ namespace osu.Game.Rulesets.Mania.UI }, Columns = new FillFlowContainer { + Name = "Columns", RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Direction = FillDirection.Horizontal, Padding = new MarginPadding { Left = 1, Right = 1 }, Spacing = new Vector2(1, 0) + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = HIT_TARGET_POSITION }, + Children = new[] + { + barlineContainer = new ControlPointContainer(timingChanges) + { + Name = "Bar lines", + RelativeSizeAxes = Axes.Both, + } + } } } } }; for (int i = 0; i < columnCount; i++) - Columns.Add(new Column()); + Columns.Add(new Column(timingChanges)); + + TimeSpan = time_span_default; } [BackgroundDependencyLoader] @@ -155,5 +188,73 @@ namespace osu.Game.Rulesets.Mania.UI return column == columnCount - 1; } } + + public override void Add(DrawableHitObject h) => Columns.Children.ElementAt(h.HitObject.Column).Add(h); + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (state.Keyboard.ControlPressed) + { + switch (args.Key) + { + case Key.Minus: + transformTimeSpanTo(TimeSpan + time_span_step, 200, EasingTypes.OutQuint); + break; + case Key.Plus: + transformTimeSpanTo(TimeSpan - time_span_step, 200, EasingTypes.OutQuint); + break; + } + } + + return false; + } + + private double timeSpan; + /// + /// The amount of time which the length of the playfield spans. + /// + public double TimeSpan + { + get { return timeSpan; } + set + { + if (timeSpan == value) + return; + timeSpan = value; + + timeSpan = MathHelper.Clamp(timeSpan, time_span_min, time_span_max); + + barlineContainer.TimeSpan = value; + Columns.Children.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()); + } + + private class TransformTimeSpan : Transform + { + public override double CurrentValue + { + get + { + double time = Time?.Current ?? 0; + if (time < StartTime) return StartValue; + if (time >= EndTime) return EndValue; + + return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing); + } + } + + public override void Apply(Drawable d) + { + base.Apply(d); + + var p = (ManiaPlayfield)d; + p.TimeSpan = CurrentValue; + } + } } } diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index 0f2d7e7c1c..00deaba85d 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -68,6 +68,7 @@ + diff --git a/osu.Game/Beatmaps/Timing/ControlPoint.cs b/osu.Game/Beatmaps/Timing/ControlPoint.cs index ea152ccb39..fbae7d9614 100644 --- a/osu.Game/Beatmaps/Timing/ControlPoint.cs +++ b/osu.Game/Beatmaps/Timing/ControlPoint.cs @@ -7,7 +7,7 @@ namespace osu.Game.Beatmaps.Timing { public string SampleBank; public int SampleVolume; - public TimeSignatures TimeSignature; + public TimeSignatures TimeSignature = TimeSignatures.SimpleQuadruple; public double Time; public double BeatLength = 500; public double SpeedMultiplier = 1;