diff --git a/osu-framework b/osu-framework index 1b0d7a584c..cea7e88cad 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit 1b0d7a584cad85efcdf25c5155e6e62a4ccb5758 +Subproject commit cea7e88cad687a6a6613655ddd74be3f15bf49db diff --git a/osu.Desktop.VisualTests/Tests/TestCaseGamefield.cs b/osu.Desktop.VisualTests/Tests/TestCaseGamefield.cs index 3841420dad..d77e2d135b 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseGamefield.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseGamefield.cs @@ -42,7 +42,8 @@ namespace osu.Desktop.VisualTests.Tests objects.Add(new HitCircle() { StartTime = time, - Position = new Vector2(RNG.Next(0, 512), RNG.Next(0, 384)) + Position = new Vector2(RNG.Next(0, 512), RNG.Next(0, 384)), + Scale = RNG.NextSingle(0.5f, 1.0f), }); time += RNG.Next(50, 500); diff --git a/osu.Game.Modes.Osu/Objects/BezierApproximator.cs b/osu.Game.Modes.Osu/Objects/BezierApproximator.cs index 9a4be51240..f03e1c0738 100644 --- a/osu.Game.Modes.Osu/Objects/BezierApproximator.cs +++ b/osu.Game.Modes.Osu/Objects/BezierApproximator.cs @@ -13,7 +13,7 @@ namespace osu.Game.Modes.Osu.Objects private Vector2[] subdivisionBuffer1; private Vector2[] subdivisionBuffer2; - private const float TOLERANCE = 0.5f; + private const float TOLERANCE = 0.25f; private const float TOLERANCE_SQ = TOLERANCE * TOLERANCE; public BezierApproximator(List controlPoints) @@ -36,7 +36,7 @@ namespace osu.Game.Modes.Osu.Objects private static bool IsFlatEnough(Vector2[] controlPoints) { for (int i = 1; i < controlPoints.Length - 1; i++) - if ((controlPoints[i - 1] - 2 * controlPoints[i] + controlPoints[i + 1]).LengthSquared > TOLERANCE_SQ) + if ((controlPoints[i - 1] - 2 * controlPoints[i] + controlPoints[i + 1]).LengthSquared > TOLERANCE_SQ * 4) return false; return true; @@ -96,7 +96,6 @@ namespace osu.Game.Modes.Osu.Objects /// Creates a piecewise-linear approximation of a bezier curve, by adaptively repeatedly subdividing /// the control points until their approximation error vanishes below a given threshold. /// - /// The control points describing the curve. /// A list of vectors representing the piecewise-linear approximation. public List CreateBezier() { diff --git a/osu.Game.Modes.Osu/Objects/CircularArcApproximator.cs b/osu.Game.Modes.Osu/Objects/CircularArcApproximator.cs new file mode 100644 index 0000000000..b8f84ed510 --- /dev/null +++ b/osu.Game.Modes.Osu/Objects/CircularArcApproximator.cs @@ -0,0 +1,102 @@ +//Copyright (c) 2007-2016 ppy Pty Ltd . +//Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using osu.Framework.MathUtils; +using System; +using System.Collections.Generic; + +namespace osu.Game.Modes.Osu.Objects +{ + public class CircularArcApproximator + { + private Vector2 A; + private Vector2 B; + private Vector2 C; + + private int amountPoints; + + private const float TOLERANCE = 0.1f; + + public CircularArcApproximator(Vector2 A, Vector2 B, Vector2 C) + { + this.A = A; + this.B = B; + this.C = C; + } + + /// + /// Creates a piecewise-linear approximation of a circular arc curve. + /// + /// A list of vectors representing the piecewise-linear approximation. + public List CreateArc() + { + float aSq = (B - C).LengthSquared; + float bSq = (A - C).LengthSquared; + float cSq = (A - B).LengthSquared; + + // If we have a degenerate triangle where a side-length is almost zero, then give up and fall + // back to a more numerically stable method. + if (Precision.AlmostEquals(aSq, 0) || Precision.AlmostEquals(bSq, 0) || Precision.AlmostEquals(cSq, 0)) + return new List(); + + float s = aSq * (bSq + cSq - aSq); + float t = bSq * (aSq + cSq - bSq); + float u = cSq * (aSq + bSq - cSq); + + float sum = s + t + u; + + // If we have a degenerate triangle with an almost-zero size, then give up and fall + // back to a more numerically stable method. + if (Precision.AlmostEquals(sum, 0)) + return new List(); + + Vector2 centre = (s * A + t * B + u * C) / sum; + Vector2 dA = A - centre; + Vector2 dC = C - centre; + + float r = dA.Length; + + double thetaStart = Math.Atan2(dA.Y, dA.X); + double thetaEnd = Math.Atan2(dC.Y, dC.X); + + while (thetaEnd < thetaStart) + thetaEnd += 2 * Math.PI; + + double dir = 1; + double thetaRange = thetaEnd - thetaStart; + + // Decide in which direction to draw the circle, depending on which side of + // AC B lies. + Vector2 orthoAC = C - A; + orthoAC = new Vector2(orthoAC.Y, -orthoAC.X); + if (Vector2.Dot(orthoAC, B - A) < 0) + { + dir = -dir; + thetaRange = 2 * Math.PI - thetaRange; + } + + // We select the amount of points for the approximation by requiring the discrete curvature + // to be smaller than the provided tolerance. The exact angle required to meet the tolerance + // is: 2 * Math.Acos(1 - TOLERANCE / r) + if (2 * r <= TOLERANCE) + // This special case is required for extremely short sliders where the radius is smaller than + // the tolerance. This is a pathological rather than a realistic case. + amountPoints = 2; + else + amountPoints = Math.Max(2, (int)Math.Ceiling(thetaRange / (2 * Math.Acos(1 - TOLERANCE / r)))); + + List output = new List(amountPoints); + + for (int i = 0; i < amountPoints; ++i) + { + double fract = (double)i / (amountPoints - 1); + double theta = thetaStart + dir * fract * thetaRange; + Vector2 o = new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta)) * r; + output.Add(centre + o); + } + + return output; + } + } +} diff --git a/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs index ebb5057a49..ccbfbb1db3 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -13,7 +13,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables { public class DrawableHitCircle : DrawableOsuHitObject { - private OsuHitObject osuObject; + private HitCircle osuObject; public ApproachCircle ApproachCircle; private CirclePiece circle; @@ -23,12 +23,13 @@ namespace osu.Game.Modes.Osu.Objects.Drawables private NumberPiece number; private GlowPiece glow; - public DrawableHitCircle(OsuHitObject h) : base(h) + public DrawableHitCircle(HitCircle h) : base(h) { osuObject = h; Origin = Anchor.Centre; Position = osuObject.Position; + Scale = new Vector2(osuObject.Scale); Children = new Drawable[] { @@ -104,7 +105,6 @@ namespace osu.Game.Modes.Osu.Objects.Drawables ApproachCircle.Alpha = 0; ApproachCircle.Scale = new Vector2(2); explode.Alpha = 0; - Scale = new Vector2(0.5f); //this will probably need to be moved to DrawableHitObject at some point. } protected override void UpdatePreemptState() diff --git a/osu.Game.Modes.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Modes.Osu/Objects/Drawables/DrawableOsuHitObject.cs index e6e948cf6f..9e1fb93cd5 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -1,12 +1,7 @@ //Copyright (c) 2007-2016 ppy Pty Ltd . //Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using osu.Game.Modes.Objects; using osu.Game.Modes.Objects.Drawables; diff --git a/osu.Game.Modes.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Modes.Osu/Objects/Drawables/DrawableSlider.cs index 9e0ad36dd7..38cee11f23 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/DrawableSlider.cs @@ -27,27 +27,35 @@ namespace osu.Game.Modes.Osu.Objects.Drawables { slider = s; - Origin = Anchor.TopLeft; - Position = Vector2.Zero; - RelativeSizeAxes = Axes.Both; - Children = new Drawable[] { body = new SliderBody(s) { Position = s.Position, - PathWidth = 36, + PathWidth = s.Scale * 72, + }, + bouncer1 = new SliderBouncer(s, false) + { + Position = s.Curve.PositionAt(1), + Scale = new Vector2(s.Scale), + }, + bouncer2 = new SliderBouncer(s, true) + { + Position = s.Position, + Scale = new Vector2(s.Scale), + }, + ball = new SliderBall(s) + { + Scale = new Vector2(s.Scale), }, - bouncer1 = new SliderBouncer(slider, false) { Position = slider.Curve.PositionAt(1) }, - bouncer2 = new SliderBouncer(slider, true) { Position = slider.Position }, initialCircle = new DrawableHitCircle(new HitCircle { StartTime = s.StartTime, Position = s.Position, + Scale = s.Scale, Colour = s.Colour, Sample = s.Sample, }), - ball = new SliderBall(slider), }; components.Add(body); @@ -56,6 +64,10 @@ namespace osu.Game.Modes.Osu.Objects.Drawables components.Add(bouncer2); } + // Since the DrawableSlider itself is just a container without a size we need to + // pass all input through. + public override bool Contains(Vector2 screenSpacePos) => true; + int currentRepeat; protected override void Update() diff --git a/osu.Game.Modes.Osu/Objects/Drawables/HitExplosion.cs b/osu.Game.Modes.Osu/Objects/Drawables/HitExplosion.cs index dd0738e6e5..cdb087f6c3 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/HitExplosion.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/HitExplosion.cs @@ -58,7 +58,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables ScaleTo(1.6f); ScaleTo(1, 100, EasingTypes.In); - MoveToRelative(new Vector2(0, 100), 800, EasingTypes.InQuint); + MoveToOffset(new Vector2(0, 100), 800, EasingTypes.InQuint); RotateTo(40, 800, EasingTypes.InQuint); Delay(600); diff --git a/osu.Game.Modes.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Modes.Osu/Objects/Drawables/Pieces/SliderBall.cs index 61ecc09301..89df67757c 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -16,7 +16,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables.Pieces private readonly Slider slider; private Box follow; - const float width = 70; + const float width = 140; public SliderBall(Slider slider) { @@ -25,7 +25,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables.Pieces AutoSizeAxes = Axes.Both; BlendingMode = BlendingMode.Additive; Origin = Anchor.Centre; - BorderThickness = 5; + BorderThickness = 10; BorderColour = Color4.Orange; Children = new Drawable[] @@ -45,7 +45,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables.Pieces AutoSizeAxes = Axes.Both, Origin = Anchor.Centre, Anchor = Anchor.Centre, - BorderThickness = 7, + BorderThickness = 14, BorderColour = Color4.White, Alpha = 1, CornerRadius = width / 2, diff --git a/osu.Game.Modes.Osu/Objects/Drawables/Pieces/SliderBouncer.cs b/osu.Game.Modes.Osu/Objects/Drawables/Pieces/SliderBouncer.cs index 39d09e6d66..47a345bdc3 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/Pieces/SliderBouncer.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/Pieces/SliderBouncer.cs @@ -35,7 +35,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables.Pieces Icon = FontAwesome.fa_eercast, Anchor = Anchor.Centre, Origin = Anchor.Centre, - TextSize = 24, + TextSize = 48, } }; } diff --git a/osu.Game.Modes.Osu/Objects/OsuHitObject.cs b/osu.Game.Modes.Osu/Objects/OsuHitObject.cs index 61932f80a3..12fdf1a344 100644 --- a/osu.Game.Modes.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Modes.Osu/Objects/OsuHitObject.cs @@ -5,6 +5,7 @@ using System; using osu.Game.Beatmaps.Samples; using osu.Game.Modes.Objects; using OpenTK; +using osu.Game.Beatmaps; namespace osu.Game.Modes.Osu.Objects { @@ -12,8 +13,17 @@ namespace osu.Game.Modes.Osu.Objects { public Vector2 Position { get; set; } + public float Scale { get; set; } = 1; + public virtual Vector2 EndPosition => Position; + public override void SetDefaultsFromBeatmap(Beatmap beatmap) + { + base.SetDefaultsFromBeatmap(beatmap); + + Scale = (1.0f - 0.7f * (beatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5) / 5) / 2; + } + [Flags] internal enum HitObjectType { diff --git a/osu.Game.Modes.Osu/Objects/OsuHitObjectParser.cs b/osu.Game.Modes.Osu/Objects/OsuHitObjectParser.cs index 143aaecb44..43755b3996 100644 --- a/osu.Game.Modes.Osu/Objects/OsuHitObjectParser.cs +++ b/osu.Game.Modes.Osu/Objects/OsuHitObjectParser.cs @@ -83,7 +83,7 @@ namespace osu.Game.Modes.Osu.Objects s.Curve = new SliderCurve { - Path = points, + ControlPoints = points, Length = length, CurveType = curveType }; diff --git a/osu.Game.Modes.Osu/Objects/Slider.cs b/osu.Game.Modes.Osu/Objects/Slider.cs index 3c11ead7e4..27210eec10 100644 --- a/osu.Game.Modes.Osu/Objects/Slider.cs +++ b/osu.Game.Modes.Osu/Objects/Slider.cs @@ -1,9 +1,7 @@ //Copyright (c) 2007-2016 ppy Pty Ltd . //Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Game.Database; using osu.Game.Beatmaps; -using System; using OpenTK; namespace osu.Game.Modes.Osu.Objects @@ -18,6 +16,8 @@ namespace osu.Game.Modes.Osu.Objects public override void SetDefaultsFromBeatmap(Beatmap beatmap) { + base.SetDefaultsFromBeatmap(beatmap); + Velocity = 100 / beatmap.BeatLengthAt(StartTime, true) * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier; } diff --git a/osu.Game.Modes.Osu/Objects/SliderCurve.cs b/osu.Game.Modes.Osu/Objects/SliderCurve.cs index e8df4049f5..961658112f 100644 --- a/osu.Game.Modes.Osu/Objects/SliderCurve.cs +++ b/osu.Game.Modes.Osu/Objects/SliderCurve.cs @@ -4,9 +4,8 @@ using System.Collections.Generic; using OpenTK; using System.Linq; -using System.Diagnostics; using osu.Framework.MathUtils; -using System; +using System.Diagnostics; namespace osu.Game.Modes.Osu.Objects { @@ -14,21 +13,39 @@ namespace osu.Game.Modes.Osu.Objects { public double Length; - public List Path; + public List ControlPoints; public CurveTypes CurveType; private List calculatedPath = new List(); private List cumulativeLength = new List(); - private List calculateSubpath(List subpath) + private List calculateSubpath(List subControlPoints) { switch (CurveType) { case CurveTypes.Linear: - return subpath; + return subControlPoints; + case CurveTypes.PerfectCurve: + // If we have a different amount than 3 control points, use bezier for perfect curves. + if (ControlPoints.Count != 3) + return new BezierApproximator(subControlPoints).CreateBezier(); + else + { + Debug.Assert(subControlPoints.Count == 3); + + // Here we have exactly 3 control points. Attempt to fit a circular arc. + List subpath = new CircularArcApproximator(subControlPoints[0], subControlPoints[1], subControlPoints[2]).CreateArc(); + + if (subpath.Count == 0) + // For some reason a circular arc could not be fit to the 3 given points. Fall back + // to a numerically stable bezier approximation. + subpath = new BezierApproximator(subControlPoints).CreateBezier(); + + return subpath; + } default: - return new BezierApproximator(subpath).CreateBezier(); + return new BezierApproximator(subControlPoints).CreateBezier(); } } @@ -39,21 +56,19 @@ namespace osu.Game.Modes.Osu.Objects // Sliders may consist of various subpaths separated by two consecutive vertices // with the same position. The following loop parses these subpaths and computes // their shape independently, consecutively appending them to calculatedPath. - List subpath = new List(); - for (int i = 0; i < Path.Count; ++i) + List subControlPoints = new List(); + for (int i = 0; i < ControlPoints.Count; ++i) { - subpath.Add(Path[i]); - if (i == Path.Count - 1 || Path[i] == Path[i + 1]) + subControlPoints.Add(ControlPoints[i]); + if (i == ControlPoints.Count - 1 || ControlPoints[i] == ControlPoints[i + 1]) { - // If we already constructed a subpath previously, then the new subpath - // will have as starting position the end position of the previous subpath. - // Hence we can and should remove the previous endpoint to avoid a segment - // with 0 length. - if (calculatedPath.Count > 0) - calculatedPath.RemoveAt(calculatedPath.Count - 1); + List subpath = calculateSubpath(subControlPoints); + for (int j = 0; j < subpath.Count; ++j) + // Only add those vertices that add a new segment to the path. + if (calculatedPath.Count == 0 || calculatedPath.Last() != subpath[j]) + calculatedPath.Add(subpath[j]); - calculatedPath.AddRange(calculateSubpath(subpath)); - subpath.Clear(); + subControlPoints.Clear(); } } } diff --git a/osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj b/osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj index 503fabd28d..a9a346f563 100644 --- a/osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj +++ b/osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj @@ -42,6 +42,7 @@ + diff --git a/osu.Game/Modes/UI/Playfield.cs b/osu.Game/Modes/UI/Playfield.cs index ec91536f29..fc042d72a0 100644 --- a/osu.Game/Modes/UI/Playfield.cs +++ b/osu.Game/Modes/UI/Playfield.cs @@ -35,6 +35,8 @@ namespace osu.Game.Modes.UI public class ScaledContainer : Container { protected override Vector2 DrawScale => new Vector2(DrawSize.X / 512); + + public override bool Contains(Vector2 screenSpacePos) => true; } public class HitObjectContainer : Container diff --git a/osu.Game/Screens/Select/CarouselContainer.cs b/osu.Game/Screens/Select/CarouselContainer.cs index 069c8d8d37..2c59639ce7 100644 --- a/osu.Game/Screens/Select/CarouselContainer.cs +++ b/osu.Game/Screens/Select/CarouselContainer.cs @@ -190,18 +190,45 @@ namespace osu.Game.Screens.Select return 125 + x; } + /// + /// Update a panel's x position and multiplicative alpha based on its y position and + /// the current scroll position. + /// + /// The panel to be updated. + /// Half the draw height of the carousel container. + private void updatePanel(Panel p, float halfHeight) + { + float panelDrawY = p.Position.Y - Current + p.DrawHeight / 2; + float dist = Math.Abs(1f - panelDrawY / halfHeight); + + // Setting the origin position serves as an additive position on top of potential + // local transformation we may want to apply (e.g. when a panel gets selected, we + // may want to smoothly transform it leftwards.) + p.OriginPosition = new Vector2(-offsetX(dist, halfHeight), 0); + + // We are applying a multiplicative alpha (which is internally done by nesting an + // additional container and setting that container's alpha) such that we can + // layer transformations on top, with a similar reasoning to the previous comment. + p.SetMultiplicativeAlpha(MathHelper.Clamp(1.75f - 1.5f * dist, 0, 1)); + } + protected override void Update() { base.Update(); + // Determine which items stopped being on screen for future removal from the lifetimelist. float drawHeight = DrawHeight; + float halfHeight = drawHeight / 2; - Lifetime.AliveItems.ForEach(delegate (Panel p) + foreach (Panel p in Lifetime.AliveItems) { float panelPosY = p.Position.Y; p.IsOnScreen = panelPosY >= Current - p.DrawHeight && panelPosY <= Current + drawHeight; - }); + updatePanel(p, halfHeight); + } + // Determine range of indices for items that are now definitely on screen to be added + // to the lifetimelist in the future. int firstIndex = yPositions.BinarySearch(Current - Panel.MAX_HEIGHT); if (firstIndex < 0) firstIndex = ~firstIndex; int lastIndex = yPositions.BinarySearch(Current + drawHeight); @@ -210,26 +237,11 @@ namespace osu.Game.Screens.Select Lifetime.StartIndex = firstIndex; Lifetime.EndIndex = lastIndex; - float halfHeight = drawHeight / 2; - for (int i = firstIndex; i < lastIndex; ++i) { - var panel = Lifetime[i]; - - panel.IsOnScreen = true; - - float panelDrawY = panel.Position.Y - Current + panel.DrawHeight / 2; - float dist = Math.Abs(1f - panelDrawY / halfHeight); - - // Setting the origin position serves as an additive position on top of potential - // local transformation we may want to apply (e.g. when a panel gets selected, we - // may want to smoothly transform it leftwards.) - panel.OriginPosition = new Vector2(-offsetX(dist, halfHeight), 0); - - // We are applying a multiplicative alpha (which is internally done by nesting an - // additional container and setting that container's alpha) such that we can - // layer transformations on top, with a similar reasoning to the previous comment. - panel.SetMultiplicativeAlpha(MathHelper.Clamp(1.75f - 1.5f * dist, 0, 1)); + Panel p = Lifetime[i]; + p.IsOnScreen = true; + updatePanel(p, halfHeight); } } }