From dcf879687d30fba36c5e7a1f19213ed63e84de27 Mon Sep 17 00:00:00 2001 From: smoogipooo Date: Wed, 24 May 2017 20:45:01 +0900 Subject: [PATCH] Implement basic hold note + tick input. --- .../Tests/TestCaseManiaPlayfield.cs | 47 +++++ .../Legacy/DistanceObjectPatternGenerator.cs | 9 +- .../Legacy/EndTimeObjectPatternGenerator.cs | 9 +- .../Judgements/HoldNoteTailJudgement.cs | 13 ++ .../Judgements/HoldNoteTickJudgement.cs | 9 + .../Objects/Drawables/DrawableHoldNote.cs | 198 ++++++++++++++++-- .../Objects/Drawables/DrawableHoldNoteTick.cs | 75 +++++++ osu.Game.Rulesets.Mania/Objects/HoldNote.cs | 42 ++-- .../Objects/HoldNoteTail.cs | 24 +++ .../Objects/HoldNoteTick.cs | 9 + .../osu.Game.Rulesets.Mania.csproj | 5 + 11 files changed, 406 insertions(+), 34 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/Judgements/HoldNoteTailJudgement.cs create mode 100644 osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs create mode 100644 osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs create mode 100644 osu.Game.Rulesets.Mania/Objects/HoldNoteTail.cs create mode 100644 osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs diff --git a/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs b/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs index 9dcba02849..27ad39fb1e 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs @@ -11,6 +11,9 @@ 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; namespace osu.Desktop.VisualTests.Tests { @@ -59,6 +62,48 @@ namespace osu.Desktop.VisualTests.Tests } }; + Action createPlayfieldWithNotesAcceptingInput = () => + { + Clear(); + + var rateAdjustClock = new StopwatchClock(true) { Rate = 0.2 }; + + ManiaPlayfield playField; + Add(playField = new ManiaPlayfield(4, new List { new TimingChange { BeatLength = 200 } }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1, -1), + Clock = new FramedClock(rateAdjustClock) + }); + + playField.Add(new DrawableNote(new Note + { + StartTime = 1000, + Column = 0 + }, new Bindable(Key.D))); + + playField.Add(new DrawableHoldNote(new HoldNote + { + StartTime = 1000, + Duration = 2000, + Column = 1 + }, new Bindable(Key.F))); + + playField.Add(new DrawableHoldNote(new HoldNote + { + StartTime = 1000, + Duration = 2000, + Column = 2 + }, new Bindable(Key.J))); + + playField.Add(new DrawableNote(new Note + { + StartTime = 1000, + Column = 3 + }, new Bindable(Key.K))); + }; + AddStep("1 column", () => createPlayfield(1, SpecialColumnPosition.Normal)); AddStep("4 columns", () => createPlayfield(4, SpecialColumnPosition.Normal)); AddStep("Left special style", () => createPlayfield(4, SpecialColumnPosition.Left)); @@ -76,6 +121,8 @@ namespace osu.Desktop.VisualTests.Tests AddWaitStep(10); AddStep("Right special style", () => createPlayfieldWithNotes(4, SpecialColumnPosition.Right)); AddWaitStep(10); + + AddStep("Test", () => createPlayfieldWithNotesAcceptingInput()); } private void triggerKeyDown(Column column) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index 718e0967da..279ac868f1 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -471,14 +471,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy } else { - newObject = new HoldNote + var holdNote = new HoldNote { StartTime = startTime, - Samples = sampleInfoListAt(startTime), - EndSamples = sampleInfoListAt(endTime), Column = column, Duration = endTime - startTime }; + + holdNote.HeadNote.Samples = sampleInfoListAt(startTime); + holdNote.TailNote.Samples = sampleInfoListAt(endTime); + + newObject = holdNote; } pattern.Add(newObject); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs index 8f438f9ff4..ddc7d77c0c 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs @@ -69,18 +69,21 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (holdNote) { - newObject = new HoldNote + var hold = new HoldNote { StartTime = HitObject.StartTime, - EndSamples = HitObject.Samples, Column = column, Duration = endTime - HitObject.StartTime }; - newObject.Samples.Add(new SampleInfo + hold.HeadNote.Samples.Add(new SampleInfo { Name = SampleInfo.HIT_NORMAL }); + + hold.TailNote.Samples = HitObject.Samples; + + newObject = hold; } else { diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTailJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTailJudgement.cs new file mode 100644 index 0000000000..df2f7e9e63 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTailJudgement.cs @@ -0,0 +1,13 @@ +// 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.Judgements +{ + public class HoldNoteTailJudgement : ManiaJudgement + { + /// + /// Whether the hold note has been released too early and shouldn't give full score for the release. + /// + public bool HasBroken; + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs new file mode 100644 index 0000000000..bead455c13 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs @@ -0,0 +1,9 @@ +// 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.Judgements +{ + public class HoldNoteTickJudgement : ManiaJudgement + { + } +} \ 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 f9d027e7ce..a8efe5d2f8 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -7,14 +7,47 @@ using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using OpenTK.Graphics; using osu.Framework.Configuration; using OpenTK.Input; +using osu.Framework.Input; +using OpenTK; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.Judgements; namespace osu.Game.Rulesets.Mania.Objects.Drawables { public class DrawableHoldNote : DrawableManiaHitObject { - private readonly NotePiece headPiece; + private readonly DrawableNote headNote; + private readonly DrawableNote tailNote; private readonly BodyPiece bodyPiece; - private readonly NotePiece tailPiece; + private readonly Container tickContainer; + + /// + /// Relative time at which the user started holding this note. + /// + private double holdStartTime; + + /// + /// Whether the hold note has been released too early and shouldn't give full score for the release. + /// + private bool hasBroken; + + private bool _holding; + /// + /// Whether the user is holding the hold note. + /// + private bool holding + { + get { return _holding; } + set + { + if (_holding == value) + return; + _holding = value; + + if (holding) + holdStartTime = Time.Current; + } + } public DrawableHoldNote(HoldNote hitObject, Bindable key = null) : base(hitObject, key) @@ -32,17 +65,39 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, - headPiece = new NotePiece + tickContainer = new Container + { + RelativeSizeAxes = Axes.Both, + RelativeCoordinateSpace = new Vector2(1, (float)HitObject.Duration) + }, + headNote = new DrawableHoldNoteHead(this, key) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre }, - tailPiece = new NotePiece + tailNote = new DrawableHoldNoteTail(this, key) { Anchor = Anchor.BottomCentre, Origin = Anchor.TopCentre } }); + + foreach (var tick in HitObject.Ticks) + { + var drawableTick = new DrawableHoldNoteTick(tick) + { + IsHolding = () => holding, + HoldStartTime = () => holdStartTime + }; + + drawableTick.Y -= (float)HitObject.StartTime; + + tickContainer.Add(drawableTick); + AddNested(drawableTick); + } + + AddNested(headNote); + AddNested(tailNote); } public override Color4 AccentColour @@ -54,9 +109,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables return; base.AccentColour = value; - headPiece.AccentColour = value; bodyPiece.AccentColour = value; - tailPiece.AccentColour = value; + headNote.AccentColour = value; + tailNote.AccentColour = value; } } @@ -64,14 +119,133 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { } - protected override void Update() + /// + /// Handles key down events on the body of the hold note. + /// + /// The input state. + /// The key down args. + /// Whether the key press was handled. + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { - if (Time.Current > HitObject.StartTime) - headPiece.Colour = Color4.Green; - if (Time.Current > HitObject.EndTime) + // Make sure the keypress happened within reasonable bounds of the hold note + if (Time.Current < HitObject.StartTime || Time.Current > HitObject.EndTime) + return false; + + if (args.Key != Key) + return false; + + if (args.Repeat) + return false; + + holding = true; + + return true; + } + + /// + /// Handles key up events on the body of the hold note. + /// + /// The input state. + /// The key down args. + /// Whether the key press was handled. + protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) + { + // Make sure that the user started holding the key during the hold note + if (!holding) + return false; + + if (args.Key != Key) + return false; + + holding = false; + + // If the key has been released too early, they should not receive full score for the release + if (!tailNote.Judged) + hasBroken = true; + + return true; + } + + private class DrawableHoldNoteHead : DrawableNote + { + private readonly DrawableHoldNote holdNote; + + public DrawableHoldNoteHead(DrawableHoldNote holdNote, Bindable key = null) + : base(holdNote.HitObject.HeadNote, key) { - bodyPiece.Colour = Color4.Green; - tailPiece.Colour = Color4.Green; + this.holdNote = holdNote; + + RelativePositionAxes = Axes.None; + Y = 0; + } + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (!base.OnKeyDown(state, args)) + return false; + + // We only want to trigger a holding state from the head if the head has received a judgement + if (Judgement.Result == HitResult.None) + return false; + + // If the head has been missed, make sure the user also can't receive a full score for the release + if (Judgement.Result == HitResult.Miss) + holdNote.hasBroken = true; + + holdNote.holding = true; + + return true; + } + } + + private class DrawableHoldNoteTail : DrawableNote + { + private readonly DrawableHoldNote holdNote; + + public DrawableHoldNoteTail(DrawableHoldNote holdNote, Bindable key = null) + : base(holdNote.HitObject.TailNote, key) + { + this.holdNote = holdNote; + + RelativePositionAxes = Axes.None; + Y = 0; + } + + protected override ManiaJudgement CreateJudgement() => new HoldNoteTailJudgement(); + + protected override void CheckJudgement(bool userTriggered) + { + base.CheckJudgement(userTriggered); + + var tailJudgement = Judgement as HoldNoteTailJudgement; + if (tailJudgement == null) + return; + + tailJudgement.HasBroken = holdNote.hasBroken; + } + + protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) + { + // Make sure that the user started holding the key during the hold note + if (!holdNote.holding) + return false; + + if (Judgement.Result != HitResult.None) + return false; + + if (args.Key != Key) + return false; + + UpdateJudgement(true); + + // Handled by the hold note, which will set holding = false + return false; + } + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + // Tail doesn't handle key down + return false; } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs new file mode 100644 index 0000000000..13fde29bc2 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs @@ -0,0 +1,75 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using OpenTK.Graphics; +using OpenTK.Input; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using osu.Game.Rulesets.Mania.Judgements; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Mania.Objects.Drawables +{ + public class DrawableHoldNoteTick : DrawableManiaHitObject + { + public Func HoldStartTime; + public Func IsHolding; + + public DrawableHoldNoteTick(HoldNoteTick hitObject) + : base(hitObject, null) + { + RelativeSizeAxes = Axes.X; + + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.X, + Height = 1 + } + }; + } + + protected override ManiaJudgement CreateJudgement() => new HoldNoteTickJudgement(); + + protected override void CheckJudgement(bool userTriggered) + { + if (!userTriggered) + return; + + if (Time.Current < HitObject.StartTime) + return; + + + if (HoldStartTime?.Invoke() > HitObject.StartTime) + return; + + Judgement.ManiaResult = ManiaHitResult.Perfect; + Judgement.Result = HitResult.Hit; + } + + protected override void UpdateState(ArmedState state) + { + switch (State) + { + case ArmedState.Hit: + Colour = Color4.Green; + break; + } + } + + protected override void Update() + { + if (Judgement.Result != HitResult.None) + return; + + if (IsHolding?.Invoke() != true) + return; + + UpdateJudgement(true); + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 30e71aeb5d..926f434015 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -1,6 +1,7 @@ // 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.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Database; @@ -12,32 +13,41 @@ namespace osu.Game.Rulesets.Mania.Objects /// /// Represents a hit object which requires pressing, holding, and releasing a key. /// - public class HoldNote : Note, IHasEndTime + public class HoldNote : ManiaHitObject, IHasEndTime { - /// - /// Lenience of release hit windows. This is to make cases where the hold note release - /// is timed alongside presses of other hit objects less awkward. - /// - private const double release_window_lenience = 1.5; - public double Duration { get; set; } public double EndTime => StartTime + Duration; - /// - /// The samples to be played when this hold note is released. - /// - public SampleInfoList EndSamples = new SampleInfoList(); + private Note headNote; + public Note HeadNote => headNote ?? (headNote = new Note { StartTime = StartTime }); + + private Note tailNote; + public Note TailNote => tailNote ?? (tailNote = new HoldNoteTail { StartTime = EndTime }); /// - /// The key-release hit windows for this hold note. + /// The length (in milliseconds) between ticks of this hold. /// - public HitWindows ReleaseHitWindows { get; protected set; } = new HitWindows(); + private double tickSpacing = 50; - public override void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + public IEnumerable Ticks => ticks ?? (ticks = createTicks()); + private List ticks; + + private List createTicks() { - base.ApplyDefaults(controlPointInfo, difficulty); + var ret = new List(); - ReleaseHitWindows = HitWindows * release_window_lenience; + if (tickSpacing == 0) + return ret; + + for (double t = StartTime + HeadNote.HitWindows.Great / 2; t <= EndTime - TailNote.HitWindows.Great / 2; t+= tickSpacing) + { + ret.Add(new HoldNoteTick + { + StartTime = t + }); + } + + return ret; } } } diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/HoldNoteTail.cs new file mode 100644 index 0000000000..6399277a2b --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/HoldNoteTail.cs @@ -0,0 +1,24 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Database; + +namespace osu.Game.Rulesets.Mania.Objects +{ + public class HoldNoteTail : Note + { + /// + /// Lenience of release hit windows. This is to make cases where the hold note release + /// is timed alongside presses of other hit objects less awkward. + /// + private const double release_window_lenience = 1.5; + + public override void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + { + base.ApplyDefaults(controlPointInfo, difficulty); + + HitWindows *= release_window_lenience; + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs new file mode 100644 index 0000000000..805b2a0938 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs @@ -0,0 +1,9 @@ +// 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.Objects +{ + public class HoldNoteTick : ManiaHitObject + { + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index 9442d7cf8f..91cb551414 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -57,10 +57,13 @@ + + + @@ -68,6 +71,8 @@ + +