-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add slider head circle late hit lenience #25776
Conversation
if (!head.Judged || !head.Result.IsHit) | ||
return; | ||
|
||
if (!isMouseInFollowArea(true)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is arguable, but I think it might make more sense to move this check to after the foreach ?
(Shouldn't you hit the first few ticks that are within range even if the ball is no longer within range ?)
Taking your example videos, I would say it doesn't make sense for "Late hit outside tracking radius" (2nd) to not hit the first few ticks when in "Sliders with outside extremums" (4th) it does. Either it should hit the first ticks in both or miss them in both.
Edit: Actually it might be better to leave that line here but miss all ticks if any is not in range. (so changing the 4th example with the circle-shaped slider to miss all ticks) see #25776 (comment) bellow
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disagree
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Edit: Smoogi applied the suggestion from #25776 (comment) so you can ignore that point)
You disagree even with the fact that 2nd and 4th being treated differently is weird ? Not that it matters that much anyway, but I just felt like it was a tiny bit weird, and if you don't want to miss all ticks if any is not in range, you might as well move the isMouseInFollowArea
after the foreach as I initially suggested to be more consistent.
if (slider.Tracking.Value) | ||
{ | ||
// Attempt to preserve correct ordering of judgements as best we can by forcing an un-judged head to be missed when the user has clearly skipped it. | ||
slider.HeadCircle.MissForcefully(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What will happen if a player is still holding the key from the previous hit-object and is on his way to hit the slider-head late ?
Can it accidentally activate the slider-ball on the way and forcefully miss the slider-head because it detected a hit on a tick ?
The player might still have had the time to hit the head in time...
Not sure how realistic this scenario is though. (But I think it can happen a lot in slider-streams ?)
Simply returning might be better here. It would mean that the tick would be ignored until the head is hit or missed. (seems better to risk missing two elements instead of one in some cases than risk breaking combo when the player was about to hit the head in time in the other cases)
Or maybe a distinction can be made specifically for the case where the player didn't press any new key since the last object ? (might be better if doable)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems valid, although
Or maybe a distinction can be made specifically for the case where the player didn't press any new key since the last object ? (might be better if doable)
would be more complexity than I'm willing to include.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disagree with this in the way that's it's proposed. It would mean someone that a pixel-missed slider would never gain tracking unless they realised in time and repressed either on the slider ball or slider head. I feel like that behaviour would not be intuitive.
There might be a better way of handling it, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would mean someone that a pixel-missed slider would never gain tracking unless they realised in time and repressed either on the slider ball or slider head.
That's not fully correct, they would be able to gain tracking again once the head is missed. But the thing is they missed one element anyway (the head) so it's not that much cost to miss a few ticks along the way (while in the other scenario, it's breaking combo when it should not, because the player was on his way to the slider-head).
And if you really want to mitigate, then there's my suggestion about key-presses. It's a bit complex but very similar to what is done in the tracking code with timeToAcceptAnyKeyAfter
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I applied this change in the recent commits.
@@ -125,36 +121,7 @@ protected override void UpdateHitStateTransforms(ArmedState state) | |||
} | |||
} | |||
|
|||
protected override void CheckForResult(bool userTriggered, double timeOffset) | |||
{ | |||
if (userTriggered) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what this test was doing* but better be safe than sorry:
Is this test no longer needed ?
(*: I looked at the doc of UpdateResult but still didn't get what userTriggered means exactly)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
userTriggered = true
comes from user input, if somewhere UpdateResult(true)
was called.
It's irrelevant here because that's not called.
I was looking just by curiosity and was not planning to code-review but ended up making a pass on everything except the test scene. I hope those comments can help. Everything else seemed sane. (Edit: made some update after thinking more about my 1st comment) |
} | ||
else | ||
nested.HitForcefully(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, I asked what was preferred and it seems that they prefer always missing all ticks if any tick is not in range. Thus the suggested change would be something like this:
// Check if all past ticks are in range
foreach (var nested in slider.NestedHitObjects.OfType<DrawableOsuHitObject>())
{
// Skip nested objects that are already judged.
if (nested.Judged)
continue;
// Stop the process when a nested object is reached that can't be hit before the current time.
if (nested.HitObject.StartTime > Time.Current)
break;
float radius = getFollowRadius(true);
double objectProgress = Math.Clamp((nested.HitObject.StartTime - slider.HitObject.StartTime) / slider.HitObject.Duration, 0, 1);
Vector2 objectPosition = slider.HitObject.CurvePositionAt(objectProgress);
// If any nested object that is further outside the follow area is reached,
// forcefully miss all nested objects.
if ((objectPosition - mousePositionInSlider).LengthSquared > radius * radius)
{
forceMiss = true;
break;
}
}
// Validate or miss all past ticks
foreach (var nested in slider.NestedHitObjects.OfType<DrawableOsuHitObject>())
{
if (nested.Judged)
continue;
if (nested.HitObject.StartTime > Time.Current)
break;
if (forceMiss)
nested.MissForcefully();
else
nested.HitForcefully();
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the feedback, but strongly disagree with this one, we'll go with what we have.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think I mind much either way, but this is an extreme edge case imo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Edit: Sorry I just saw that you applied the change already)
Sorry if this was not clear, the reasoning for that was the same as here : #25776 (comment)
Basically, it doesn't make sense for the 2nd example above (Late hit outside tracking radius https://drive.google.com/file/d/10gdU9WV2ze0jRVaDiqov1I-jpgJMO3q1/view?usp=share_link) to be full misses but the 4th example above (Sliders with outside extremums https://drive.google.com/file/d/10hvIwpzszqceWP-5NI6-WMcFoJDZJ-gc/view) to be hitting the first ticks.
Because in both cases, the ball has left the follow-circle range at one point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I applied this change in the recent commits.
} | ||
|
||
// Enable tracking, since the mouse is within the follow area (if it were expanded). | ||
updateTracking(true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just realized that it might make more sense to not start tracking if forceMiss
is true and the cursor is not on the ball. (we missed some tick so the ball should no longer be active)
So it should be:
updateTracking(!forceMiss || isMouseInFollowArea(false));
Which is actually also equivalent to
updateTracking(isMouseInFollowArea(!forceMiss));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@smoogipoo this seems valid, please check whether you agree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a specific reason why this makes more sense to you?
When it comes to these extreme edge cases, I tend to prefer to follow the easier-to-explain mechanic. In this case, that if you're within the tracking radius at the point where the head is hit, you gain tracking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like we are simulating the movement of the ball as if the ball was instantly reproducing the movement from the start to where it should be at the time. This means we start tracking on the head hit, and as soon as the ball is further than follow-circle away, the ball deactivates (which justifies the miss all the other ticks). If the ball finishes it's movement under the cursor it should reactivate though (which is why we have isMouseInFollowArea(false)
in that case)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I applied this change in the recent commits.
Gonna block this one for a bit. Will apply some of the suggestions above and think I might've discovered an issue with the tail. |
This is now once again ready to go. I've updated the OP to -- hopefully -- explain the new mechanics. |
I'm going to merge this in as-is with the goal of getting it out for testing (and using the tests as a general guarantee that things are working). As discussed with smoogi IRL, there's going to need to be some refactoring on this code – for one I don't want |
What makes this not feasible? This is my main qualm with the implementation described, since its pretty common to have buzz sliders that leave the follow radius and re-enter. While following the slider it is checked every frame whether the cursor is in the follow radius, so why doesn't that checking just start at the head time, and following is tracked with a growing/shrinking follow radius as normal (but the followcircle is invisible and holding click is not required), each individual tick is marked as followed or not, and then when the head is clicked all marked followed hits are scored, or if the head is missed they are missed? This will give much more accurate results in the edge cases, including when you failed to follow just some ticks before clicking, the late hit will give score for the rest you would have followed. I am also a bit concerned about 2b scoring order, but I think it would be ok to reset 2b scores to fix mechanics edge cases later. |
It's something that can be worked on with a refactoring of this, sure. |
Order should be enforced. Please start by testing, then opening a discussion. We generally don't have discussions on merged PR branches, or deal with hypotheticals without evidence backing them up. |
Resolves the second part of #25129
Resolves #25651
There are two parts to this, that I could split out:
SliderInputManager
. The first commit (599fdb0) can be reviewed on its own, with the refactoring in the follow up commit being an almost 1:1 move of all existing logic to a new class. All other commits from there on can be reviewed on their own.This allows slider heads to be hit late. I've taken a little bit of liberty in the specifics here which may differ slightly from the discussion, but is hopefully amicable.
From a non-visual perspective, the intention here is as such:
The discussion thread wanted to check if the follow circle had "at any point" been too far from the mouse position, but this is not feasible. I believe this implementation will behave better with a slider that has a single tick towards the end, but a point on tbe path that is far from the head.
There are quite a few test cases, but here are some that players would find interest in:
Late hit inside tracking radius
https://drive.google.com/file/d/10fjm9VSZ2qRSwpJRuPQ60IEKztrwzhwE/view?usp=share_link
Late hit outside tracking radius
https://drive.google.com/file/d/10gdU9WV2ze0jRVaDiqov1I-jpgJMO3q1/view?usp=share_link
Miss hit inside tracking radius
https://drive.google.com/file/d/10lNfyaR8-8FLpH3msXbvYuBPy0-l4wWV/view?usp=share_link
Sliders with required outside tracking point
https://drive.google.com/file/d/10llkF0XoKG0yJ_1Qc_M2a_BGkit_yHu-/view?usp=sharing
Short slider with repeats and ticks
https://drive.google.com/file/d/10iC_ZAsP8exFxpwu5EZqiKnzSAPFkeXP/view?usp=share_link