diff --git a/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs b/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs index 7ca51f8af3..1e3d8df6c6 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs @@ -1,18 +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.Framework; using osu.Framework.GameModes.Testing; using osu.Framework.Graphics; using osu.Framework.Timing; using OpenTK; using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Game.Modes.Objects; using osu.Game.Modes.Objects.Drawables; using osu.Game.Modes.Osu.Objects; using osu.Game.Modes.Osu.Objects.Drawables; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Game.Modes; +using OpenTK.Graphics; namespace osu.Desktop.VisualTests.Tests { @@ -20,44 +25,124 @@ namespace osu.Desktop.VisualTests.Tests { public override string Name => @"Hit Objects"; + private StopwatchClock rateAdjustClock; + private FramedClock framedClock; + + bool auto = false; + public TestCaseHitObjects() { - var swClock = new StopwatchClock(true) { Rate = 0.2f }; - Clock = new FramedClock(swClock); + rateAdjustClock = new StopwatchClock(true); + framedClock = new FramedClock(rateAdjustClock); + playbackSpeed.ValueChanged += delegate { rateAdjustClock.Rate = playbackSpeed.Value; }; + } + + HitObjectType mode = HitObjectType.Spinner; + + BindableNumber playbackSpeed = new BindableDouble(0.5) { MinValue = 0, MaxValue = 1 }; + private Container playfieldContainer; + private Container approachContainer; + + private void load(HitObjectType mode) + { + this.mode = mode; + + switch (mode) + { + case HitObjectType.Circle: + const int count = 10; + + for (int i = 0; i < count; i++) + { + var h = new HitCircle + { + StartTime = framedClock.CurrentTime + 600 + i * 80, + Position = new Vector2((i - count / 2) * 14), + }; + + add(new DrawableHitCircle(h)); + } + break; + case HitObjectType.Slider: + add(new DrawableSlider(new Slider + { + StartTime = framedClock.CurrentTime + 600, + ControlPoints = new List() + { + new Vector2(-200, 0), + new Vector2(400, 0), + }, + Length = 400, + Position = new Vector2(-200, 0), + Velocity = 1, + })); + break; + case HitObjectType.Spinner: + add(new DrawableSpinner(new Spinner + { + StartTime = framedClock.CurrentTime + 600, + Length = 1000, + Position = new Vector2(0, 0), + })); + break; + } } public override void Reset() { base.Reset(); - Clock.ProcessFrame(); + playbackSpeed.TriggerChange(); - Container approachContainer = new Container { Depth = float.MinValue, }; + AddButton(@"circles", () => load(HitObjectType.Circle)); + AddButton(@"slider", () => load(HitObjectType.Slider)); + AddButton(@"spinner", () => load(HitObjectType.Spinner)); - Add(approachContainer); + AddToggle(@"auto", () => { auto = !auto; load(mode); }); - const int count = 10; - - for (int i = 0; i < count; i++) + ButtonsContainer.Add(new SpriteText { Text = "Playback Speed" }); + ButtonsContainer.Add(new BasicSliderBar { - var h = new HitCircle + Width = 150, + Height = 10, + SelectionColor = Color4.Orange, + Bindable = playbackSpeed + }); + + framedClock.ProcessFrame(); + + var clockAdjustContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Clock = framedClock, + Children = new[] { - StartTime = Clock.CurrentTime + 600 + i * 80, - Position = new Vector2((i - count / 2) * 14), - }; + playfieldContainer = new Container { RelativeSizeAxes = Axes.Both }, + approachContainer = new Container { RelativeSizeAxes = Axes.Both } + } + }; - DrawableHitCircle d = new DrawableHitCircle(h) - { - Anchor = Anchor.Centre, - Depth = i, - State = ArmedState.Hit, - Judgement = new OsuJudgementInfo { Result = HitResult.Hit } - }; + Add(clockAdjustContainer); + load(mode); + } - approachContainer.Add(d.ApproachCircle.CreateProxy()); - Add(d); + int depth; + void add(DrawableHitObject h) + { + h.Anchor = Anchor.Centre; + h.Depth = depth++; + + if (auto) + { + h.State = ArmedState.Hit; + h.Judgement = new OsuJudgementInfo { Result = HitResult.Hit }; } + + playfieldContainer.Add(h); + var proxyable = h as IDrawableHitObjectWithProxiedApproach; + if (proxyable != null) + approachContainer.Add(proxyable.ProxiedLayer.CreateProxy()); } } } diff --git a/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs index 091366b88c..5c4401455c 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.ComponentModel; using osu.Framework.Graphics; using osu.Framework.Graphics.Transformations; using osu.Game.Modes.Objects.Drawables; @@ -13,7 +12,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables { public class DrawableHitCircle : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach { - private HitCircle osuObject; + private OsuHitObject osuObject; public ApproachCircle ApproachCircle; private CirclePiece circle; @@ -23,11 +22,12 @@ namespace osu.Game.Modes.Osu.Objects.Drawables private NumberPiece number; private GlowPiece glow; - public DrawableHitCircle(HitCircle h) : base(h) + public DrawableHitCircle(OsuHitObject h) : base(h) { + Origin = Anchor.Centre; + osuObject = h; - Origin = Anchor.Centre; Position = osuObject.StackedPosition; Scale = new Vector2(osuObject.Scale); diff --git a/osu.Game.Modes.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Modes.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 0c7ca11672..ef153848d4 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using osu.Game.Modes.Objects; using osu.Game.Modes.Objects.Drawables; +using osu.Framework.Graphics; namespace osu.Game.Modes.Osu.Objects.Drawables { diff --git a/osu.Game.Modes.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Modes.Osu/Objects/Drawables/DrawableSpinner.cs new file mode 100644 index 0000000000..94bcade2dc --- /dev/null +++ b/osu.Game.Modes.Osu/Objects/Drawables/DrawableSpinner.cs @@ -0,0 +1,140 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Transformations; +using osu.Framework.MathUtils; +using osu.Game.Modes.Objects.Drawables; +using osu.Game.Modes.Osu.Objects.Drawables.Pieces; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Modes.Osu.Objects.Drawables +{ + public class DrawableSpinner : DrawableOsuHitObject + { + private Spinner spinner; + + private SpinnerDisc disc; + private SpinnerBackground background; + private DrawableHitCircle circle; + private NumberPiece number; + + public DrawableSpinner(Spinner s) : base(s) + { + Origin = Anchor.Centre; + Position = s.Position; + + //take up full playfield. + Size = new Vector2(512); + + spinner = s; + + Children = new Drawable[] + { + background = new SpinnerBackground + { + Alpha = 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + DiscColour = Color4.Black + }, + disc = new SpinnerDisc + { + Alpha = 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + DiscColour = s.Colour + }, + circle = new DrawableHitCircle(s) + { + Interactive = false, + Position = Vector2.Zero, + Anchor = Anchor.Centre, + } + }; + + circle.ApproachCircle.Colour = Color4.Transparent; + + background.Scale = scaleToCircle; + disc.Scale = scaleToCircle; + } + + public override bool Contains(Vector2 screenSpacePos) => true; + + protected override void CheckJudgement(bool userTriggered) + { + if (Time.Current < HitObject.StartTime) return; + + var j = Judgement as OsuJudgementInfo; + + disc.ScaleTo(Interpolation.ValueAt(Math.Sqrt(Progress), scaleToCircle, Vector2.One, 0, 1), 100); + + if (!userTriggered && Time.Current >= HitObject.EndTime) + { + if (Progress >= 1) + { + j.Score = OsuScoreResult.Hit300; + j.Result = HitResult.Hit; + } + else if (Progress > .9) + { + j.Score = OsuScoreResult.Hit100; + j.Result = HitResult.Hit; + } + else if (Progress > .75) + { + j.Score = OsuScoreResult.Hit50; + j.Result = HitResult.Hit; + } + else + { + j.Score = OsuScoreResult.Miss; + j.Result = HitResult.Miss; + } + } + } + + private Vector2 scaleToCircle => new Vector2(circle.Scale * circle.DrawWidth / DrawWidth) * 0.95f; + + private float spinsPerMinuteNeeded = 100 + (5 * 15); //TODO: read per-map OD and place it on the 5 + + private float rotationsNeeded => (float)(spinsPerMinuteNeeded * (spinner.EndTime - spinner.StartTime) / 60000f); + + public float Progress => MathHelper.Clamp(disc.RotationAbsolute / 360 / rotationsNeeded, 0, 1); + + protected override void UpdatePreemptState() + { + base.UpdatePreemptState(); + + FadeIn(200); + + background.Delay(TIME_PREEMPT - 100); + background.FadeIn(200); + background.ScaleTo(1, 200, EasingTypes.OutQuint); + + disc.Delay(TIME_PREEMPT - 50); + disc.FadeIn(200); + } + + protected override void UpdateState(ArmedState state) + { + base.UpdateState(state); + + Delay(HitObject.Duration, true); + + FadeOut(160); + + switch (state) + { + case ArmedState.Hit: + ScaleTo(Scale * 1.2f, 320, EasingTypes.Out); + break; + case ArmedState.Miss: + ScaleTo(Scale * 0.8f, 320, EasingTypes.In); + break; + } + } + } +} diff --git a/osu.Game.Modes.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs b/osu.Game.Modes.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs new file mode 100644 index 0000000000..50dab933b0 --- /dev/null +++ b/osu.Game.Modes.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs @@ -0,0 +1,10 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Modes.Osu.Objects.Drawables.Pieces +{ + public class SpinnerBackground : SpinnerDisc + { + public override bool HandleInput => false; + } +} diff --git a/osu.Game.Modes.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Modes.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs new file mode 100644 index 0000000000..76fd360818 --- /dev/null +++ b/osu.Game.Modes.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -0,0 +1,168 @@ +// 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 osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Transformations; +using osu.Framework.Input; +using osu.Framework.Logging; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Modes.Osu.Objects.Drawables.Pieces +{ + public class SpinnerDisc : CircularContainer + { + public override bool Contains(Vector2 screenSpacePos) => true; + + protected Sprite Disc; + + public SRGBColour DiscColour + { + get { return Disc.Colour; } + set { Disc.Colour = value; } + } + + class SpinnerBorder : Container + { + public SpinnerBorder() + { + Origin = Anchor.Centre; + Anchor = Anchor.Centre; + RelativeSizeAxes = Axes.Both; + + layout(); + } + + private int lastLayoutDotCount; + private void layout() + { + int count = (int)(MathHelper.Pi * ScreenSpaceDrawQuad.Width / 9); + + if (count == lastLayoutDotCount) return; + + lastLayoutDotCount = count; + + while (Children.Count() < count) + { + Add(new CircularContainer + { + Colour = Color4.White, + RelativePositionAxes = Axes.Both, + Origin = Anchor.Centre, + Size = new Vector2(1 / ScreenSpaceDrawQuad.Width * 2000), + Children = new[] + { + new Box + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + } + } + }); + } + + var size = new Vector2(1 / ScreenSpaceDrawQuad.Width * 2000); + + int i = 0; + foreach (var d in Children) + { + d.Size = size; + d.Position = new Vector2( + 0.5f + (float)Math.Sin((float)i / count * 2 * MathHelper.Pi) / 2, + 0.5f + (float)Math.Cos((float)i / count * 2 * MathHelper.Pi) / 2 + ); + + i++; + } + } + + protected override void Update() + { + base.Update(); + layout(); + } + } + + public SpinnerDisc() + { + RelativeSizeAxes = Axes.Both; + + Children = new Drawable[] + { + Disc = new Box + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Alpha = 0.2f, + }, + new SpinnerBorder() + }; + } + + bool tracking; + public bool Tracking + { + get { return tracking; } + set + { + if (value == tracking) return; + + tracking = value; + + Disc.FadeTo(tracking ? 0.5f : 0.2f, 100); + } + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + Tracking = true; + return base.OnMouseDown(state, args); + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + Tracking = false; + return base.OnMouseUp(state, args); + } + + protected override bool OnMouseMove(InputState state) + { + Tracking |= state.Mouse.HasMainButtonPressed; + mousePosition = state.Mouse.Position; + return base.OnMouseMove(state); + } + + private Vector2 mousePosition; + + private float lastAngle; + private float currentRotation; + public float RotationAbsolute; + + protected override void Update() + { + base.Update(); + + var thisAngle = -(float)MathHelper.RadiansToDegrees(Math.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2)); + if (tracking) + { + if (thisAngle - lastAngle > 180) + lastAngle += 360; + else if (lastAngle - thisAngle > 180) + lastAngle -= 360; + + currentRotation += thisAngle - lastAngle; + RotationAbsolute += Math.Abs(thisAngle - lastAngle); + } + lastAngle = thisAngle; + + RotateTo(currentRotation, 100, EasingTypes.OutExpo); + } + } +} diff --git a/osu.Game.Modes.Osu/Objects/OsuHitObjectParser.cs b/osu.Game.Modes.Osu/Objects/OsuHitObjectParser.cs index 822e9a9b7c..52329a2766 100644 --- a/osu.Game.Modes.Osu/Objects/OsuHitObjectParser.cs +++ b/osu.Game.Modes.Osu/Objects/OsuHitObjectParser.cs @@ -26,7 +26,10 @@ namespace osu.Game.Modes.Osu.Objects switch (type) { case HitObjectType.Circle: - result = new HitCircle(); + result = new HitCircle + { + Position = new Vector2(int.Parse(split[0]), int.Parse(split[1])) + }; break; case HitObjectType.Slider: CurveTypes curveType = CurveTypes.Catmull; @@ -82,18 +85,22 @@ namespace osu.Game.Modes.Osu.Objects ControlPoints = points, Length = length, CurveType = curveType, - RepeatCount = repeatCount + RepeatCount = repeatCount, + Position = new Vector2(int.Parse(split[0]), int.Parse(split[1])) }; break; case HitObjectType.Spinner: - result = new Spinner(); + result = new Spinner + { + Length = Convert.ToDouble(split[5], CultureInfo.InvariantCulture) - Convert.ToDouble(split[2], CultureInfo.InvariantCulture), + Position = new Vector2(512, 384) / 2, + }; break; default: //throw new InvalidOperationException($@"Unknown hit object type {type}"); return null; } - result.Position = new Vector2(int.Parse(split[0]), int.Parse(split[1])); - result.StartTime = double.Parse(split[2]); + result.StartTime = Convert.ToDouble(split[2], CultureInfo.InvariantCulture); result.Sample = new HitSampleInfo { Type = (SampleType)int.Parse(split[4]), diff --git a/osu.Game.Modes.Osu/Objects/Spinner.cs b/osu.Game.Modes.Osu/Objects/Spinner.cs index ea6f1b53f2..fa1bddf760 100644 --- a/osu.Game.Modes.Osu/Objects/Spinner.cs +++ b/osu.Game.Modes.Osu/Objects/Spinner.cs @@ -1,9 +1,14 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Beatmaps; + namespace osu.Game.Modes.Osu.Objects { public class Spinner : OsuHitObject { + public double Length; + + public override double EndTime => StartTime + Length; } } diff --git a/osu.Game.Modes.Osu/UI/OsuHitRenderer.cs b/osu.Game.Modes.Osu/UI/OsuHitRenderer.cs index fdaab87d55..fa222aafd7 100644 --- a/osu.Game.Modes.Osu/UI/OsuHitRenderer.cs +++ b/osu.Game.Modes.Osu/UI/OsuHitRenderer.cs @@ -21,7 +21,8 @@ namespace osu.Game.Modes.Osu.UI return new DrawableHitCircle(h as HitCircle); if (h is Slider) return new DrawableSlider(h as Slider); - + if (h is Spinner) + return new DrawableSpinner(h as Spinner); return null; } } diff --git a/osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj b/osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj index e0b9f5c904..4845deb167 100644 --- a/osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj +++ b/osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj @@ -49,9 +49,11 @@ + + @@ -59,6 +61,7 @@ +