1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 12:57:36 +08:00

Add empty space tap-streaming support for osu! ruleset on touchscreen devices

This commit is contained in:
Dean Herbert 2023-01-23 17:07:27 +09:00
parent e3932c077b
commit c4d5957ac3
3 changed files with 186 additions and 6 deletions

View File

@ -13,7 +13,12 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Input.States; using osu.Framework.Input.States;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
@ -33,6 +38,8 @@ namespace osu.Game.Rulesets.Osu.Tests
private OsuInputManager osuInputManager = null!; private OsuInputManager osuInputManager = null!;
private Container mainContent = null!;
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
@ -44,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
osuInputManager = new OsuInputManager(new OsuRuleset().RulesetInfo) osuInputManager = new OsuInputManager(new OsuRuleset().RulesetInfo)
{ {
Child = new Container Child = mainContent = new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -54,12 +61,14 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
Depth = float.MinValue,
X = -100, X = -100,
}, },
rightKeyCounter = new TestActionKeyCounter(OsuAction.RightButton) rightKeyCounter = new TestActionKeyCounter(OsuAction.RightButton)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Depth = float.MinValue,
X = 100, X = 100,
} }
}, },
@ -116,9 +125,127 @@ namespace osu.Game.Rulesets.Osu.Tests
endTouch(TouchSource.Touch2); endTouch(TouchSource.Touch2);
checkPosition(TouchSource.Touch2); checkPosition(TouchSource.Touch2);
// note that touch1 was never ended, but becomes active for tracking again. // note that touch1 was never ended, but is no longer valid for touch input due to touch 2 occurring.
beginTouch(TouchSource.Touch1); beginTouch(TouchSource.Touch1);
checkPosition(TouchSource.Touch2);
}
[Test]
public void TestStreamInput()
{
// In this scenario, the user is tapping on the first object in a stream,
// then using one or two fingers in empty space to continue the stream.
addHitCircleAt(TouchSource.Touch1);
beginTouch(TouchSource.Touch1);
// The first touch is handled as normal.
assertKeyCounter(1, 0);
checkPressed(OsuAction.LeftButton);
checkPosition(TouchSource.Touch1); checkPosition(TouchSource.Touch1);
// The second touch should release the first, and also act as a right button.
beginTouch(TouchSource.Touch2);
assertKeyCounter(1, 1);
// Importantly, this is different from the simple case because an object was interacted with
// in the first touch, but not the second touch.
checkNotPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
// Also importantly, the positional part of the second touch is ignored.
checkPosition(TouchSource.Touch1);
// In this scenario, a third touch should be allowed, and handled similarly to the second.
beginTouch(TouchSource.Touch3);
assertKeyCounter(2, 1);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
// Position is still ignored.
checkPosition(TouchSource.Touch1);
endTouch(TouchSource.Touch2);
checkPressed(OsuAction.LeftButton);
checkNotPressed(OsuAction.RightButton);
// Position is still ignored.
checkPosition(TouchSource.Touch1);
// User continues streaming
beginTouch(TouchSource.Touch2);
assertKeyCounter(2, 2);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
// Position is still ignored.
checkPosition(TouchSource.Touch1);
// In this mode a maximum of three touches should be supported.
// A fourth touch should result in no changes anywhere.
beginTouch(TouchSource.Touch4);
assertKeyCounter(2, 2);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
checkPosition(TouchSource.Touch1);
endTouch(TouchSource.Touch4);
}
[Test]
public void TestNonStreamOverlappingDirectTouchesWithRelease()
{
// In this scenario, the user is tapping on three circles directly while correctly releasing the first touch.
// All three should be recognised.
addHitCircleAt(TouchSource.Touch1);
addHitCircleAt(TouchSource.Touch2);
addHitCircleAt(TouchSource.Touch3);
beginTouch(TouchSource.Touch1);
assertKeyCounter(1, 0);
checkPressed(OsuAction.LeftButton);
checkPosition(TouchSource.Touch1);
beginTouch(TouchSource.Touch2);
assertKeyCounter(1, 1);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
checkPosition(TouchSource.Touch2);
endTouch(TouchSource.Touch1);
beginTouch(TouchSource.Touch3);
assertKeyCounter(2, 1);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
checkPosition(TouchSource.Touch3);
}
[Test]
public void TestNonStreamOverlappingDirectTouchesWithoutRelease()
{
// In this scenario, the user is tapping on three circles directly without releasing any touches.
// The first two should be recognised, but a third should not (as the user already has two fingers down).
addHitCircleAt(TouchSource.Touch1);
addHitCircleAt(TouchSource.Touch2);
addHitCircleAt(TouchSource.Touch3);
beginTouch(TouchSource.Touch1);
assertKeyCounter(1, 0);
checkPressed(OsuAction.LeftButton);
checkPosition(TouchSource.Touch1);
beginTouch(TouchSource.Touch2);
assertKeyCounter(1, 1);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
checkPosition(TouchSource.Touch2);
beginTouch(TouchSource.Touch3);
assertKeyCounter(1, 1);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
checkPosition(TouchSource.Touch3);
} }
[Test] [Test]
@ -263,6 +390,22 @@ namespace osu.Game.Rulesets.Osu.Tests
assertKeyCounter(1, 1); assertKeyCounter(1, 1);
} }
private void addHitCircleAt(TouchSource source)
{
AddStep($"Add circle at {source}", () =>
{
var hitCircle = new HitCircle();
hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
mainContent.Add(new DrawableHitCircle(hitCircle)
{
Clock = new FramedClock(new ManualClock()),
Position = mainContent.ToLocalSpace(getSanePositionForSource(source)),
});
});
}
private void beginTouch(TouchSource source, Vector2? screenSpacePosition = null) => private void beginTouch(TouchSource source, Vector2? screenSpacePosition = null) =>
AddStep($"Begin touch for {source}", () => InputManager.BeginTouch(new Touch(source, screenSpacePosition ??= getSanePositionForSource(source)))); AddStep($"Begin touch for {source}", () => InputManager.BeginTouch(new Touch(source, screenSpacePosition ??= getSanePositionForSource(source))));

View File

@ -9,8 +9,10 @@ using osu.Framework.Graphics;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu namespace osu.Game.Rulesets.Osu
{ {
@ -40,6 +42,9 @@ namespace osu.Game.Rulesets.Osu
protected override KeyBindingContainer<OsuAction> CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) protected override KeyBindingContainer<OsuAction> CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
=> new OsuKeyBindingContainer(ruleset, variant, unique); => new OsuKeyBindingContainer(ruleset, variant, unique);
public bool CheckScreenSpaceActionPressJudgeable(Vector2 screenSpacePosition) =>
NonPositionalInputQueue.OfType<DrawableHitCircle.HitReceptor>().Any(c => c.ReceivePositionalInputAt(screenSpacePosition));
public OsuInputManager(RulesetInfo ruleset) public OsuInputManager(RulesetInfo ruleset)
: base(ruleset, 0, SimultaneousBindingMode.Unique) : base(ruleset, 0, SimultaneousBindingMode.Unique)
{ {

View File

@ -44,6 +44,8 @@ namespace osu.Game.Rulesets.Osu.UI
handleTouchMovement(e); handleTouchMovement(e);
} }
private TrackedTouch? positionTrackingTouch;
protected override bool OnTouchDown(TouchDownEvent e) protected override bool OnTouchDown(TouchDownEvent e)
{ {
OsuAction action = trackedTouches.Any(t => t.Action == OsuAction.LeftButton) OsuAction action = trackedTouches.Any(t => t.Action == OsuAction.LeftButton)
@ -53,7 +55,31 @@ namespace osu.Game.Rulesets.Osu.UI
// Ignore any taps which trigger an action which is already handled. But track them for potential positional input in the future. // Ignore any taps which trigger an action which is already handled. But track them for potential positional input in the future.
bool shouldResultInAction = osuInputManager.AllowGameplayInputs && !mouseDisabled.Value && trackedTouches.All(t => t.Action != action); bool shouldResultInAction = osuInputManager.AllowGameplayInputs && !mouseDisabled.Value && trackedTouches.All(t => t.Action != action);
trackedTouches.Add(new TrackedTouch(e.Touch.Source, shouldResultInAction ? action : null)); // If we can actually accept as an action, check whether this tap was on a circle's receptor.
// This case gets special handling to allow for empty-space stream tapping.
bool isDirectCircleTouch = osuInputManager.CheckScreenSpaceActionPressJudgeable(e.ScreenSpaceTouchDownPosition);
var trackedTouch = new TrackedTouch(e.Touch.Source, shouldResultInAction ? action : null, isDirectCircleTouch);
if (isDirectCircleTouch)
positionTrackingTouch = trackedTouch;
else
{
// If no direct touch is registered, we always use the new (latest) touch for positional tracking.
if (positionTrackingTouch?.DirectTouch != true)
positionTrackingTouch = trackedTouch;
else
{
// If not a direct circle touch, consider whether to release the an original direct touch.
if (positionTrackingTouch.DirectTouch && positionTrackingTouch.Action is OsuAction directTouchAction)
{
osuInputManager.KeyBindingContainer.TriggerReleased(directTouchAction);
positionTrackingTouch.Action = null;
}
}
}
trackedTouches.Add(trackedTouch);
// Important to update position before triggering the pressed action. // Important to update position before triggering the pressed action.
handleTouchMovement(e); handleTouchMovement(e);
@ -67,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.UI
private void handleTouchMovement(TouchEvent touchEvent) private void handleTouchMovement(TouchEvent touchEvent)
{ {
// Movement should only be tracked for the most recent touch. // Movement should only be tracked for the most recent touch.
if (touchEvent.Touch.Source != trackedTouches.Last().Source) if (touchEvent.Touch.Source != positionTrackingTouch?.Source)
return; return;
if (!osuInputManager.AllowUserCursorMovement) if (!osuInputManager.AllowUserCursorMovement)
@ -83,6 +109,9 @@ namespace osu.Game.Rulesets.Osu.UI
if (tracked.Action is OsuAction action) if (tracked.Action is OsuAction action)
osuInputManager.KeyBindingContainer.TriggerReleased(action); osuInputManager.KeyBindingContainer.TriggerReleased(action);
if (positionTrackingTouch == tracked)
positionTrackingTouch = null;
trackedTouches.Remove(tracked); trackedTouches.Remove(tracked);
base.OnTouchUp(e); base.OnTouchUp(e);
@ -92,12 +121,15 @@ namespace osu.Game.Rulesets.Osu.UI
{ {
public readonly TouchSource Source; public readonly TouchSource Source;
public readonly OsuAction? Action; public OsuAction? Action;
public TrackedTouch(TouchSource source, OsuAction? action) public readonly bool DirectTouch;
public TrackedTouch(TouchSource source, OsuAction? action, bool directTouch)
{ {
Source = source; Source = source;
Action = action; Action = action;
DirectTouch = directTouch;
} }
} }
} }