diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 5776c64c86..d73ad888f4 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var result = HitObject.HitWindows.ResultFor(timeOffset);
- if (result == HitResult.None || CheckHittable?.Invoke(this) == false)
+ if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false)
{
Shake(Math.Abs(timeOffset) - HitObject.HitWindows.WindowFor(HitResult.Miss));
return;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
index 13829dc2f7..fe23e3729d 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// Whether this can be hit.
/// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false.
///
- public Func CheckHittable;
+ public Func CheckHittable;
protected DrawableOsuHitObject(OsuHitObject hitObject)
: base(hitObject)
diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs
new file mode 100644
index 0000000000..ddaf714e5b
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs
@@ -0,0 +1,104 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Osu.UI
+{
+ ///
+ /// Ensures that s are hit in-order.
+ /// If a is hit out of order:
+ ///
+ /// - The hit is blocked if it occurred earlier than the previous 's start time.
+ /// - The hit causes all previous s to missed otherwise.
+ ///
+ ///
+ public class OrderedHitPolicy
+ {
+ private readonly HitObjectContainer hitObjectContainer;
+
+ public OrderedHitPolicy(HitObjectContainer hitObjectContainer)
+ {
+ this.hitObjectContainer = hitObjectContainer;
+ }
+
+ ///
+ /// Determines whether a can be hit at a point in time.
+ ///
+ /// The to check.
+ /// The time to check.
+ /// Whether can be hit at the given .
+ public bool IsHittable(DrawableHitObject hitObject, double time)
+ {
+ DrawableHitObject lastObject = hitObject;
+
+ // Get the last hitobject that can block future hits
+ while ((lastObject = hitObjectContainer.AliveObjects.GetPrevious(lastObject)) != null)
+ {
+ if (canBlockFutureHits(lastObject.HitObject))
+ break;
+ }
+
+ // If there is no previous object alive, allow the hit.
+ if (lastObject == null)
+ return true;
+
+ // Ensure that either the last object has received a judgement or the hit time occurs at or after the last object's start time.
+ // Simultaneous hitobjects are allowed to be hit at the same time value to account for edge-cases such as Centipede.
+ if (lastObject.Judged || time >= lastObject.HitObject.StartTime)
+ return true;
+
+ return false;
+ }
+
+ ///
+ /// Handles a being hit to potentially miss all earlier s.
+ ///
+ /// The that was hit.
+ public void HandleHit(HitObject hitObject)
+ {
+ if (!canBlockFutureHits(hitObject))
+ return;
+
+ double minimumTime = hitObject.StartTime;
+
+ foreach (var obj in hitObjectContainer.AliveObjects)
+ {
+ if (obj.HitObject.StartTime >= minimumTime)
+ break;
+
+ switch (obj)
+ {
+ case DrawableHitCircle circle:
+ miss(circle);
+ break;
+
+ case DrawableSlider slider:
+ miss(slider.HeadCircle);
+ break;
+ }
+ }
+
+ static void miss(DrawableOsuHitObject obj)
+ {
+ // Hitobjects that have already been judged cannot be missed.
+ if (obj.Judged)
+ return;
+
+ obj.MissForcefully();
+ }
+ }
+
+ ///
+ /// Whether a blocks hits on future s until its start time is reached.
+ ///
+ /// The to test.
+ private bool canBlockFutureHits(HitObject hitObject)
+ => hitObject is HitCircle || hitObject is Slider;
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index f4009a281c..2f222f59b4 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Extensions.IEnumerableExtensions;
using osuTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -11,7 +10,6 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Skinning;
@@ -22,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly ApproachCircleProxyContainer approachCircles;
private readonly JudgementContainer judgementLayer;
private readonly FollowPointRenderer followPoints;
+ private readonly OrderedHitPolicy hitPolicy;
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
@@ -53,6 +52,8 @@ namespace osu.Game.Rulesets.Osu.UI
Depth = -1,
},
};
+
+ hitPolicy = new OrderedHitPolicy(HitObjectContainer);
}
public override void Add(DrawableHitObject h)
@@ -67,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.UI
base.Add(h);
DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h;
- osuHitObject.CheckHittable = checkHittable;
+ osuHitObject.CheckHittable = hitPolicy.IsHittable;
followPoints.AddFollowPoints(osuHitObject);
}
@@ -82,34 +83,10 @@ namespace osu.Game.Rulesets.Osu.UI
return result;
}
- private bool checkHittable(DrawableOsuHitObject osuHitObject)
- {
- DrawableHitObject lastObject = osuHitObject;
-
- // Get the last hitobject that can block future hits
- while ((lastObject = HitObjectContainer.AliveObjects.GetPrevious(lastObject)) != null)
- {
- if (canBlockFutureHits(lastObject.HitObject))
- break;
- }
-
- // If there is no previous object alive, allow the hit.
- if (lastObject == null)
- return true;
-
- // Ensure that either the last object has received a judgement or the hit time occurs at or after the last object's start time.
- // Simultaneous hitobjects are allowed to be hit at the same time value to account for edge-cases such as Centipede.
- if (lastObject.Judged || Time.Current >= lastObject.HitObject.StartTime)
- return true;
-
- return false;
- }
-
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
{
// Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order.
- if (canBlockFutureHits(result.HitObject))
- missAllEarlierObjects(result.HitObject);
+ hitPolicy.HandleHit(result.HitObject);
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
@@ -124,49 +101,6 @@ namespace osu.Game.Rulesets.Osu.UI
judgementLayer.Add(explosion);
}
- ///
- /// Misses all s occurring earlier than the start time of a judged .
- ///
- /// The marker , which all s earlier than will get missed.
- private void missAllEarlierObjects(HitObject hitObject)
- {
- double minimumTime = hitObject.StartTime;
-
- foreach (var obj in HitObjectContainer.AliveObjects)
- {
- if (obj.HitObject.StartTime >= minimumTime)
- break;
-
- switch (obj)
- {
- case DrawableHitCircle circle:
- miss(circle);
- break;
-
- case DrawableSlider slider:
- miss(slider.HeadCircle);
- break;
- }
- }
-
- static void miss(DrawableOsuHitObject obj)
- {
- // Hitobjects that have already been judged cannot be missed.
- if (obj.Judged)
- return;
-
- obj.MissForcefully();
- }
- }
-
- ///
- /// Whether a can block hits on future s until its start time is reached.
- ///
- /// The to test.
- /// Whether can block hits on future s.
- private bool canBlockFutureHits(HitObject hitObject)
- => hitObject is HitCircle || hitObject is Slider;
-
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos);
private class ApproachCircleProxyContainer : LifetimeManagementContainer