Skip to content

Commit d57dc4b

Browse files
committed
feat: add xbox controller support
1 parent 2d3760b commit d57dc4b

File tree

6 files changed

+153
-3
lines changed

6 files changed

+153
-3
lines changed

CrossLaunch/CrossLaunch.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@
4444
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
4545
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
4646
</PackageReference>
47+
<PackageReference Include="InputSimulator" Version="1.0.4" />
4748
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0"/>
49+
<PackageReference Include="SharpDX.XInput" Version="4.2.0" />
4850
</ItemGroup>
4951

5052
<ItemGroup>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using System.Threading;
2+
using System.Threading.Tasks;
3+
using WindowsInput;
4+
using WindowsInput.Native;
5+
6+
namespace CrossLaunch.Features.Gamepad;
7+
8+
public class GamepadHandler
9+
{
10+
private static readonly KeyboardSimulator KeyboardSimulator = new(new InputSimulator());
11+
12+
public static void HandleGamepadInput(CancellationToken ct)
13+
{
14+
var gamepadInput = new GamepadInput();
15+
16+
gamepadInput.ButtonPressed += direction =>
17+
{
18+
SimulateArrowKeyPress(direction);
19+
};
20+
21+
gamepadInput.StartPollingAsync(ct);
22+
}
23+
24+
private static void SimulateArrowKeyPress(string direction)
25+
{
26+
switch (direction)
27+
{
28+
case "Up":
29+
KeyboardSimulator.KeyPress(VirtualKeyCode.UP);
30+
break;
31+
case "Down":
32+
KeyboardSimulator.KeyPress(VirtualKeyCode.DOWN);
33+
break;
34+
case "Left":
35+
KeyboardSimulator.KeyPress(VirtualKeyCode.LEFT);
36+
break;
37+
case "Right":
38+
KeyboardSimulator.KeyPress(VirtualKeyCode.RIGHT);
39+
break;
40+
case "A":
41+
KeyboardSimulator.KeyPress(VirtualKeyCode.RETURN);
42+
break;
43+
}
44+
}
45+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using SharpDX.XInput;
5+
6+
namespace CrossLaunch.Features.Gamepad;
7+
8+
public class GamepadInput
9+
{
10+
private readonly Controller _controller = new(UserIndex.One);
11+
private GamepadButtonFlags _previousButtons; //For fixing holding the button down
12+
private bool _isRunning;
13+
private readonly int _pollingInterval;
14+
15+
public GamepadInput(int pollingInterval = 50)
16+
{
17+
_pollingInterval = pollingInterval;
18+
}
19+
20+
public event Action<string> ButtonPressed = null!;
21+
22+
public void StartPollingAsync(CancellationToken ct)
23+
{
24+
_isRunning = true;
25+
26+
Task.Run(
27+
async () =>
28+
{
29+
while (_isRunning && !ct.IsCancellationRequested)
30+
{
31+
UpdateState();
32+
await Task.Delay(_pollingInterval, ct);
33+
}
34+
35+
StopPolling();
36+
},
37+
ct
38+
);
39+
}
40+
41+
public void StopPolling()
42+
{
43+
_isRunning = false;
44+
}
45+
46+
private void UpdateState()
47+
{
48+
if (!_controller.IsConnected)
49+
return;
50+
51+
var state = _controller.GetState();
52+
var buttons = state.Gamepad.Buttons;
53+
54+
if (buttons == _previousButtons)
55+
return;
56+
57+
if (buttons.HasFlag(GamepadButtonFlags.DPadUp))
58+
ButtonPressed("Up");
59+
else if (buttons.HasFlag(GamepadButtonFlags.DPadDown))
60+
ButtonPressed("Down");
61+
else if (buttons.HasFlag(GamepadButtonFlags.DPadLeft))
62+
ButtonPressed("Left");
63+
else if (buttons.HasFlag(GamepadButtonFlags.DPadRight))
64+
ButtonPressed("Right");
65+
else if (buttons.HasFlag(GamepadButtonFlags.A) && !_previousButtons.HasFlag(GamepadButtonFlags.A))
66+
ButtonPressed("A");
67+
68+
_previousButtons = buttons;
69+
}
70+
}

CrossLaunch/GameSelection.axaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<ItemsControl.ItemTemplate>
2323
<DataTemplate DataType="crossLaunch:GameItemVm">
2424
<Border CornerRadius="10" Background="#444444" BorderThickness="4" Padding="10" Margin="20"
25-
PointerPressed="StartGame" Tag="{Binding Identifier}" IsHitTestVisible="True" Width="450"
25+
KeyDown="OnKeyDown" PointerPressed="OnPointerPressed" Tag="{Binding Identifier}" IsHitTestVisible="True" Width="450"
2626
Height="300" Focusable="True" TabIndex="0" Name="{Binding Identifier}">
2727
<Border.Styles>
2828
<Style Selector="Border:pointerover, Border:focus-visible">

CrossLaunch/GameSelection.axaml.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ private async void Foo()
4242
Items.Add(game);
4343
}
4444

45-
private async void StartGame(object? sender, PointerPressedEventArgs e)
45+
private async void OnPointerPressed(object? sender, PointerPressedEventArgs e)
4646
{
4747
var element = sender as Border;
4848
var gameId = element?.Tag?.ToString();
@@ -51,4 +51,17 @@ private async void StartGame(object? sender, PointerPressedEventArgs e)
5151

5252
await _gameService.StartGame(game);
5353
}
54+
55+
private async void OnKeyDown(object? sender, KeyEventArgs e)
56+
{
57+
if (e.Key != Key.Enter && e.Key != Key.Space)
58+
return;
59+
60+
var element = sender as Border;
61+
var gameId = element?.Tag?.ToString();
62+
63+
var game = Items.First(x => x.Identifier == gameId);
64+
65+
await _gameService.StartGame(game);
66+
}
5467
}

CrossLaunch/Program.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
24
using Avalonia;
5+
using Avalonia.Controls.ApplicationLifetimes;
6+
using CrossLaunch.Features.Gamepad;
37

48
namespace CrossLaunch;
59

@@ -11,7 +15,23 @@ internal class Program
1115
[STAThread]
1216
public static void Main(string[] args)
1317
{
14-
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
18+
var cts = new CancellationTokenSource();
19+
GamepadHandler.HandleGamepadInput(cts.Token);
20+
21+
BuildAvaloniaApp()
22+
.StartWithClassicDesktopLifetime(
23+
args,
24+
lifetime =>
25+
{
26+
lifetime.ShutdownRequested += OnShutdownRequested;
27+
return;
28+
29+
void OnShutdownRequested(object? sender, ShutdownRequestedEventArgs e)
30+
{
31+
cts.Cancel();
32+
}
33+
}
34+
);
1535
}
1636

1737
// Avalonia configuration, don't remove; also used by visual designer.

0 commit comments

Comments
 (0)