Skip to content

Commit 18b2f18

Browse files
authored
Merge pull request #164 from LumpBloom7/touch-input
Implement touch input support
2 parents 2238a58 + a579988 commit 18b2f18

24 files changed

+472
-77
lines changed

osu.Game.Rulesets.Rush.Tests/Replay/RushReplayFrameTest.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using NUnit.Framework;
55
using osu.Game.Beatmaps;
6+
using osu.Game.Rulesets.Rush.Input;
67
using osu.Game.Rulesets.Rush.Replays;
78

89
namespace osu.Game.Rulesets.Rush.Tests.Replay
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Copyright (c) Shane Woolcock. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System;
5+
using System.Linq;
6+
using NUnit.Framework;
7+
using osu.Framework.Allocation;
8+
using osu.Framework.Graphics;
9+
using osu.Framework.Graphics.Containers;
10+
using osu.Framework.Graphics.Shapes;
11+
using osu.Framework.Input;
12+
using osu.Game.Rulesets.Rush.Input;
13+
using osu.Game.Screens.Play;
14+
using osu.Game.Tests.Visual;
15+
using osuTK;
16+
using osuTK.Graphics;
17+
18+
namespace osu.Game.Rulesets.Rush.Tests.Visual
19+
{
20+
public class TestSceneTouchInputHandling : OsuManualInputManagerTestScene
21+
{
22+
protected override Ruleset CreateRuleset() => new RushRuleset();
23+
24+
private static readonly TouchSource[] touch_sources = (TouchSource[])Enum.GetValues(typeof(TouchSource));
25+
26+
private TouchRegion airRegion;
27+
private TouchRegion groundRegion;
28+
private TouchRegion feverRegion;
29+
30+
private KeyCounterDisplay keyCounters;
31+
private RushInputManager rushInputManager;
32+
33+
[BackgroundDependencyLoader]
34+
private void load()
35+
{
36+
Children = new Drawable[]
37+
{
38+
airRegion = new TouchRegion
39+
{
40+
RelativeSizeAxes = Axes.Both,
41+
Anchor = Anchor.TopCentre,
42+
Origin = Anchor.TopCentre,
43+
Size = new Vector2(1,0.5f),
44+
45+
Action = RushActionTarget.Air,
46+
Colour = Color4.Aqua,
47+
Alpha = 0.8f,
48+
},
49+
groundRegion = new TouchRegion
50+
{
51+
RelativeSizeAxes = Axes.Both,
52+
Anchor = Anchor.BottomCentre,
53+
Origin = Anchor.BottomCentre,
54+
Size = new Vector2(1,0.5f),
55+
56+
Action = RushActionTarget.Ground,
57+
Colour = Color4.Red,
58+
Alpha = 0.8f,
59+
},
60+
feverRegion = new TouchRegion
61+
{
62+
RelativeSizeAxes = Axes.Both,
63+
Anchor = Anchor.Centre,
64+
Origin = Anchor.Centre,
65+
Size = new Vector2(0.25f, 0.25f),
66+
67+
Action = RushActionTarget.Fever,
68+
Colour = Color4.Purple,
69+
Alpha = 0.8f,
70+
},
71+
keyCounters = new KeyCounterDisplay
72+
{
73+
Origin = Anchor.BottomRight,
74+
Anchor = Anchor.BottomRight,
75+
}
76+
};
77+
78+
rushInputManager = new RushInputManager(Ruleset.Value);
79+
rushInputManager.Attach(keyCounters);
80+
81+
var tmpChild = InputManager.Child;
82+
InputManager.Clear(false);
83+
84+
InputManager.Child = rushInputManager.WithChild(tmpChild);
85+
}
86+
87+
[Test]
88+
public void TestPreferEarliestFreeAction()
89+
{
90+
91+
AddStep("Touch ground area (1)", () => touchDrawable(TouchSource.Touch1, groundRegion));
92+
AddStep("Touch ground area (2)", () => touchDrawable(TouchSource.Touch2, groundRegion));
93+
AddAssert("B1 on, B2 on", () => inputStateOnlyContains(RushAction.GroundPrimary, RushAction.GroundSecondary));
94+
95+
AddStep("Release ground area (1)", () => endTouch(TouchSource.Touch1));
96+
AddAssert("B1 off, B2 on", () => inputStateOnlyContains(RushAction.GroundSecondary));
97+
98+
AddStep("Touch ground area (3)", () => touchDrawable(TouchSource.Touch3, groundRegion));
99+
AddAssert("B1 on, B2 on", () => inputStateOnlyContains(RushAction.GroundPrimary, RushAction.GroundSecondary));
100+
101+
AddStep("Touch ground area (4)", () => touchDrawable(TouchSource.Touch4, groundRegion));
102+
AddAssert("B1 on, B2 on, B3 on", () => inputStateOnlyContains(RushAction.GroundPrimary, RushAction.GroundSecondary, RushAction.GroundTertiary));
103+
104+
AddStep("Release ground area (2)", () => endTouch(TouchSource.Touch2));
105+
AddAssert("B1 on, B2 off, B3 on", () => inputStateOnlyContains(RushAction.GroundPrimary, RushAction.GroundTertiary));
106+
107+
AddStep("Touch ground area (5)", () => touchDrawable(TouchSource.Touch5, groundRegion));
108+
AddAssert("B1 on, B2 on, B3 on", () => inputStateOnlyContains(RushAction.GroundPrimary, RushAction.GroundSecondary, RushAction.GroundTertiary));
109+
}
110+
111+
[Test]
112+
public void TestIgnoreExcessiveTouches()
113+
{
114+
AddStep("Touch ground area (1)", () => touchDrawable(TouchSource.Touch1, groundRegion));
115+
AddStep("Touch ground area (2)", () => touchDrawable(TouchSource.Touch2, groundRegion));
116+
AddStep("Touch ground area (3)", () => touchDrawable(TouchSource.Touch3, groundRegion));
117+
AddStep("Touch ground area (4)", () => touchDrawable(TouchSource.Touch4, groundRegion));
118+
AddAssert("All ground actions on", () => inputStateOnlyContains(RushAction.GroundPrimary, RushAction.GroundSecondary, RushAction.GroundTertiary, RushAction.GroundQuaternary));
119+
120+
AddStep("Touch ground area (5)", () => touchDrawable(TouchSource.Touch5, groundRegion));
121+
AddAssert("Only ground actions on", () => inputStateOnlyContains(RushAction.GroundPrimary, RushAction.GroundSecondary, RushAction.GroundTertiary, RushAction.GroundQuaternary));
122+
}
123+
124+
[Test]
125+
public void TestTouchConversionBlocking()
126+
{
127+
AddStep("Touch Fever region", () => touchDrawableWithOffset(TouchSource.Touch1, feverRegion, new Vector2(0, 10)));
128+
129+
AddAssert("Only fever action on", () => inputStateOnlyContains(RushAction.Fever));
130+
}
131+
132+
[Test]
133+
public void TestReleaseAfterSwitchingRegions()
134+
{
135+
AddStep("Touch ground region", () => touchDrawable(TouchSource.Touch1, groundRegion));
136+
AddStep("Move to air region", () => moveTouchToDrawable(TouchSource.Touch1, airRegion));
137+
AddAssert("Ground action still held", () => inputStateOnlyContains(RushAction.GroundPrimary));
138+
AddStep("Release touch", () => endTouch(TouchSource.Touch1));
139+
AddAssert("Ground action released", () => inputStateOnlyContains());
140+
}
141+
142+
[SetUp]
143+
public void ReleaseAllTouches()
144+
{
145+
foreach (var source in touch_sources)
146+
InputManager.EndTouch(new Touch(source, Vector2.Zero));
147+
}
148+
149+
private bool inputStateOnlyContains(params RushAction[] actions) => rushInputManager.PressedActions.ToHashSet().SetEquals(actions);
150+
151+
private void touchDrawable(TouchSource source, Drawable drawable) => InputManager.BeginTouch(new Touch(source, drawable.ScreenSpaceDrawQuad.Centre));
152+
private void touchDrawableWithOffset(TouchSource source, Drawable drawable, Vector2 offset) => InputManager.BeginTouch(new Touch(source, drawable.ScreenSpaceDrawQuad.Centre + offset));
153+
private void moveTouchToDrawable(TouchSource source, Drawable drawable) => InputManager.MoveTouchTo(new Touch(source, drawable.ScreenSpaceDrawQuad.Centre));
154+
private void endTouch(TouchSource source) => InputManager.EndTouch(new Touch(source, Vector2.Zero));
155+
156+
private class TouchRegion : Box, IKeyBindingTouchHandler
157+
{
158+
public override bool HandlePositionalInput => true;
159+
160+
public RushActionTarget Action;
161+
162+
public RushActionTarget ActionTargetForTouchPosition(Vector2 screenSpaceTouchPos) => Action;
163+
}
164+
}
165+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) Shane Woolcock. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using osuTK;
5+
6+
namespace osu.Game.Rulesets.Rush.Input
7+
{
8+
public interface IKeyBindingTouchHandler
9+
{
10+
RushActionTarget ActionTargetForTouchPosition(Vector2 screenSpaceTouchPos) => RushActionTarget.None;
11+
}
12+
13+
public enum RushActionTarget
14+
{
15+
None,
16+
Ground,
17+
Air,
18+
Fever,
19+
}
20+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright (c) Shane Woolcock. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System.Collections.Generic;
5+
using System.ComponentModel;
6+
using osu.Framework.Extensions.ListExtensions;
7+
using osu.Framework.Input;
8+
using osu.Framework.Input.Bindings;
9+
using osu.Framework.Lists;
10+
using osu.Game.Rulesets.Rush.Objects;
11+
using osu.Game.Rulesets.Rush.Replays;
12+
using osu.Game.Rulesets.UI;
13+
using osuTK.Input;
14+
15+
namespace osu.Game.Rulesets.Rush.Input
16+
{
17+
public class RushInputManager : RulesetInputManager<RushAction>
18+
{
19+
protected override bool MapMouseToLatestTouch => false;
20+
public new RushFramedReplayInputHandler ReplayInputHandler => (RushFramedReplayInputHandler)base.ReplayInputHandler;
21+
22+
/// <summary>
23+
/// Retrieves all actions in a currenty pressed states.
24+
/// </summary>
25+
public SlimReadOnlyListWrapper<RushAction> PressedActions => ((List<RushAction>)KeyBindingContainer.PressedActions).AsSlimReadOnly();
26+
27+
public RushInputManager(RulesetInfo ruleset)
28+
: base(ruleset, 0, SimultaneousBindingMode.Unique)
29+
{
30+
}
31+
32+
protected override MouseButtonEventManager CreateButtonEventManagerFor(MouseButton button)
33+
=> new RushMouseEventManager(button, this);
34+
35+
protected override TouchEventManager CreateButtonEventManagerFor(TouchSource source)
36+
=> new RushTouchEventManager(source, this);
37+
38+
39+
private readonly Dictionary<TouchSource, RushAction> touchActionMap = new Dictionary<TouchSource, RushAction>();
40+
41+
private readonly Dictionary<RushAction, bool> actionsInUse = new Dictionary<RushAction, bool>();
42+
43+
private void updateActionsCache()
44+
{
45+
for (RushAction action = RushAction.GroundPrimary; action <= RushAction.Fever; ++action)
46+
actionsInUse[action] = false;
47+
48+
foreach (var pressedAction in PressedActions)
49+
actionsInUse[pressedAction] = true;
50+
}
51+
52+
private RushAction? tryGetGroundAction() => tryGetActionInRange(RushAction.GroundPrimary, RushAction.AirPrimary);
53+
private RushAction? tryGetAirAction() => tryGetActionInRange(RushAction.AirPrimary, RushAction.Fever);
54+
private RushAction? tryGetFeverAction()
55+
{
56+
actionsInUse.TryGetValue(RushAction.Fever, out var inUse);
57+
58+
if (!inUse) return RushAction.Fever;
59+
60+
return null;
61+
}
62+
private RushAction? tryGetActionInRange(RushAction lowerBound, RushAction upperBound)
63+
{
64+
for (RushAction a = lowerBound; a < upperBound; ++a)
65+
{
66+
actionsInUse.TryGetValue(a, out var inUse);
67+
68+
if (!inUse) return a;
69+
}
70+
71+
return null;
72+
}
73+
74+
public bool TryPressTouchAction(TouchSource source, RushActionTarget action)
75+
{
76+
updateActionsCache();
77+
RushAction? convertedAction = action switch
78+
{
79+
RushActionTarget.Ground => tryGetGroundAction(),
80+
RushActionTarget.Air => tryGetAirAction(),
81+
RushActionTarget.Fever => tryGetFeverAction(),
82+
_ => null
83+
};
84+
85+
if (convertedAction is null) return false;
86+
87+
touchActionMap[source] = convertedAction.Value;
88+
KeyBindingContainer.TriggerPressed(convertedAction.Value);
89+
90+
return true;
91+
}
92+
93+
public void ReleaseTouchAction(TouchSource source)
94+
{
95+
if (!touchActionMap.TryGetValue(source, out var action)) return;
96+
// The action has already been released, maybe due to the same keyboard key being released before the touch releases.
97+
if (!PressedActions.Contains(action)) return;
98+
99+
KeyBindingContainer.TriggerReleased(action);
100+
}
101+
}
102+
103+
public enum RushAction
104+
{
105+
[Description("Ground (Primary)")]
106+
GroundPrimary = 0,
107+
108+
[Description("Ground (Secondary)")]
109+
GroundSecondary = 1,
110+
111+
[Description("Ground (Tertiary)")]
112+
GroundTertiary = 2,
113+
114+
[Description("Ground (Quaternary)")]
115+
GroundQuaternary = 3,
116+
117+
[Description("Air (Primary)")]
118+
AirPrimary = 4,
119+
120+
[Description("Air (Secondary)")]
121+
AirSecondary = 5,
122+
123+
[Description("Air (Tertiary)")]
124+
AirTertiary = 6,
125+
126+
[Description("Air (Quaternary)")]
127+
AirQuaternary = 7,
128+
129+
[Description("Activate fever")]
130+
Fever = 8,
131+
}
132+
133+
public static class RushActionExtensions
134+
{
135+
public static bool IsLaneAction(this RushAction action) => action < RushAction.Fever;
136+
137+
public static LanedHitLane Lane(this RushAction action) => action switch
138+
{
139+
RushAction.GroundPrimary => LanedHitLane.Ground,
140+
RushAction.GroundSecondary => LanedHitLane.Ground,
141+
RushAction.GroundTertiary => LanedHitLane.Ground,
142+
RushAction.GroundQuaternary => LanedHitLane.Ground,
143+
RushAction.AirPrimary => LanedHitLane.Air,
144+
RushAction.AirSecondary => LanedHitLane.Air,
145+
RushAction.AirTertiary => LanedHitLane.Air,
146+
RushAction.AirQuaternary => LanedHitLane.Air,
147+
_ => LanedHitLane.Ground
148+
};
149+
}
150+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) Shane Woolcock. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using osu.Framework.Graphics;
7+
using osu.Framework.Input;
8+
using osu.Framework.Input.States;
9+
using osuTK;
10+
using osuTK.Input;
11+
12+
namespace osu.Game.Rulesets.Rush.Input
13+
{
14+
// Temporarily exists to be as a testing convenience, will remove be PR merge
15+
public class RushMouseEventManager : MouseButtonEventManager
16+
{
17+
public override bool EnableClick => true;
18+
public override bool EnableDrag => false;
19+
public override bool ChangeFocusOnClick => false;
20+
21+
private readonly RushInputManager rushInputManager;
22+
23+
public RushMouseEventManager(MouseButton source, RushInputManager inputManager)
24+
: base(source)
25+
{
26+
rushInputManager = inputManager;
27+
}
28+
29+
private IKeyBindingTouchHandler touchHandler;
30+
31+
protected override Drawable HandleButtonDown(InputState state, List<Drawable> targets)
32+
{
33+
var result = base.HandleButtonDown(state, targets);
34+
touchHandler = targets.FirstOrDefault(d => d is IKeyBindingTouchHandler) as IKeyBindingTouchHandler;
35+
36+
if (touchHandler != null)
37+
rushInputManager.TryPressTouchAction((TouchSource)Button, touchHandler.ActionTargetForTouchPosition(MouseDownPosition ?? Vector2.Zero));
38+
39+
return result;
40+
}
41+
42+
protected override void HandleButtonUp(InputState state, List<Drawable> targets)
43+
{
44+
if (touchHandler != null)
45+
rushInputManager.ReleaseTouchAction((TouchSource)Button);
46+
47+
touchHandler = null;
48+
49+
base.HandleButtonUp(state, targets);
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)