Skip to content

Commit 409eb02

Browse files
committed
gamepad: add Gamepad package
Support gamepad for Windows, macOS, iOS and JS. Signed-off-by: Inkeliz <[email protected]>
1 parent cf2b1f0 commit 409eb02

File tree

7 files changed

+974
-0
lines changed

7 files changed

+974
-0
lines changed

gamepad/gamepad.go

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Package gamepad package allows any Gio application to listen for gamepad input,
2+
// it's supported on Windows 10+, JS, iOS 15+, macOS 12+.
3+
//
4+
// That package was inspired by WebGamepad API (see https://w3c.github.io/gamepad/#gamepad-interface).
5+
//
6+
// You must include `op.InvalidateOp` in your main game loop, otherwise the state of the gamepad will
7+
// not be updated.
8+
package gamepad
9+
10+
import (
11+
"gioui.org/app"
12+
"gioui.org/f32"
13+
"gioui.org/io/event"
14+
"unsafe"
15+
)
16+
17+
// Gamepad is the main struct and holds information about the state of all Controllers currently available.
18+
// You must use Gamepad.ListenEvents to keep the state up-to-date.
19+
type Gamepad struct {
20+
Controllers [4]*Controller
21+
22+
// gamepad varies accordingly with the current OS.
23+
*gamepad
24+
}
25+
26+
// NewGamepad creates a new Share for the given *app.Window.
27+
// The given app.Window must be unique, and you should call NewGamepad
28+
// once per new app.Window.
29+
//
30+
// It's mandatory to use Gamepad.ListenEvents on the same *app.Window.
31+
func NewGamepad(w *app.Window) *Gamepad {
32+
return &Gamepad{
33+
Controllers: [4]*Controller{
34+
new(Controller),
35+
new(Controller),
36+
new(Controller),
37+
new(Controller),
38+
},
39+
gamepad: newGamepad(w),
40+
}
41+
}
42+
43+
// ListenEvents must get all the events from Gio, in order to get the GioView. You must
44+
// include that function where you listen for Gio events.
45+
//
46+
// Similar as:
47+
//
48+
// select {
49+
// case e := <-window.Events():
50+
// gamepad.ListenEvents(e)
51+
// switch e := e.(type) {
52+
// (( ... your code ... ))
53+
// }
54+
// }
55+
func (g *Gamepad) ListenEvents(evt event.Event) {
56+
g.listenEvents(evt)
57+
}
58+
59+
// Controller is used to report what Buttons are currently pressed, and where is the position of the Joysticks
60+
// and how much the Triggers are pressed.
61+
type Controller struct {
62+
Joysticks Joysticks
63+
Buttons Buttons
64+
65+
Connected bool
66+
Changed bool
67+
packet float64
68+
}
69+
70+
// Joysticks hold the information about the position of the joystick, the position are from -1.0 to 1.0, and
71+
// 0.0 represents the center.
72+
// The maximum and minimum values are:
73+
// [Y:-1.0]
74+
// [X:-1.0] [X:+1.0]
75+
// [Y:+1.0]
76+
type Joysticks struct {
77+
LeftThumb, RightThumb f32.Point
78+
}
79+
80+
// Buttons hold the information about the state of the buttons, it's based on XBOX Controller scheme.
81+
// The buttons will be informed based on their physical position. Clicking "B" on Nintendo
82+
// gamepad will be "A" since it correspond to same key-position.
83+
//
84+
// That struct must NOT change, or those change must reflect on all maps, which varies per each OS.
85+
//
86+
// Internally, Buttons will be interpreted as [...]Button.
87+
type Buttons struct {
88+
A, B, Y, X Button
89+
Left, Right, Up, Down Button
90+
LT, RT, LB, RB Button
91+
LeftThumb, RightThumb Button
92+
Start, Back Button
93+
}
94+
95+
// Button reports if the button is pressed or not, and how much it's pressed (from 0.0 to 1.0 when fully pressed).
96+
type Button struct {
97+
Pressed bool
98+
Force float32
99+
}
100+
101+
func (b *Buttons) setButtonPressed(button int, v bool) {
102+
bp := (*Button)(unsafe.Add(unsafe.Pointer(b), button))
103+
bp.Pressed = v
104+
if v {
105+
bp.Force = 1.0
106+
} else {
107+
bp.Force = 0.0
108+
}
109+
}
110+
111+
func (b *Buttons) setButtonForce(button int, v float32) {
112+
bp := (*Button)(unsafe.Add(unsafe.Pointer(b), button))
113+
bp.Force = v
114+
bp.Pressed = v > 0
115+
}
116+
117+
const (
118+
buttonA = int(unsafe.Offsetof(Buttons{}.A))
119+
buttonB = int(unsafe.Offsetof(Buttons{}.B))
120+
buttonY = int(unsafe.Offsetof(Buttons{}.Y))
121+
buttonX = int(unsafe.Offsetof(Buttons{}.X))
122+
buttonLeft = int(unsafe.Offsetof(Buttons{}.Left))
123+
buttonRight = int(unsafe.Offsetof(Buttons{}.Right))
124+
buttonUp = int(unsafe.Offsetof(Buttons{}.Up))
125+
buttonDown = int(unsafe.Offsetof(Buttons{}.Down))
126+
buttonLT = int(unsafe.Offsetof(Buttons{}.LT))
127+
buttonRT = int(unsafe.Offsetof(Buttons{}.RT))
128+
buttonLB = int(unsafe.Offsetof(Buttons{}.LB))
129+
buttonRB = int(unsafe.Offsetof(Buttons{}.RB))
130+
buttonLeftThumb = int(unsafe.Offsetof(Buttons{}.LeftThumb))
131+
buttonRightThumb = int(unsafe.Offsetof(Buttons{}.RightThumb))
132+
buttonStart = int(unsafe.Offsetof(Buttons{}.Start))
133+
buttonBack = int(unsafe.Offsetof(Buttons{}.Back))
134+
)

gamepad/gamepad_darwin.go

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package gamepad
2+
3+
/*
4+
#cgo CFLAGS: -Werror -xobjective-c -fmodules -fobjc-arc
5+
6+
#import <Foundation/Foundation.h>
7+
#import <GameController/GameController.h>
8+
9+
static CFTypeRef getGamepads() {
10+
if (@available(iOS 15, macOS 12, *)) {
11+
NSArray<GCController *> * Controllers = [GCController controllers];
12+
return (CFTypeRef)CFBridgingRetain(Controllers);
13+
}
14+
return 0;
15+
}
16+
17+
static CFTypeRef getState(CFTypeRef gamepads, int64_t player) {
18+
if (@available(iOS 15, macOS 12, *)) {
19+
NSArray<GCController *> * Controllers = (__bridge NSArray<GCController *> *)gamepads;
20+
if ([Controllers count] <= player) {
21+
return 0;
22+
}
23+
24+
GCExtendedGamepad * Gamepad = [[Controllers objectAtIndex:player] extendedGamepad];
25+
if (Gamepad == nil) {
26+
return 0;
27+
}
28+
29+
GCPhysicalInputProfile* Inputs = (GCPhysicalInputProfile*)Gamepad;
30+
return (CFTypeRef)CFBridgingRetain(Inputs);
31+
}
32+
return 0;
33+
}
34+
35+
static double getLastEventFrom(CFTypeRef inputs) {
36+
if (@available(iOS 15, macOS 12, *)) {
37+
return (double)(((__bridge GCPhysicalInputProfile*)(inputs)).lastEventTimestamp);
38+
}
39+
return 0;
40+
}
41+
42+
static NSString * getKeyName(GCPhysicalInputProfile * Inputs, void * button) {
43+
if (@available(iOS 15, macOS 12, *)) {
44+
NSString * name = *((__unsafe_unretained NSString **)(button));
45+
if ([Inputs hasRemappedElements] == false) {
46+
return name;
47+
}
48+
return [Inputs mappedElementAliasForPhysicalInputName:name];
49+
}
50+
return nil;
51+
}
52+
53+
static float getButtonFrom(CFTypeRef inputs, void * button) {
54+
if (@available(iOS 15, macOS 12, *)) {
55+
GCPhysicalInputProfile * Inputs = ((__bridge GCPhysicalInputProfile*)(inputs));
56+
return Inputs.buttons[getKeyName(Inputs, button)].value;
57+
}
58+
return 0;
59+
}
60+
61+
static void getAxesFrom(CFTypeRef inputs, void * button, void * x, void * y) {
62+
if (@available(iOS 15, macOS 12, *)) {
63+
GCPhysicalInputProfile * Inputs = ((__bridge GCPhysicalInputProfile*)(inputs));
64+
GCControllerDirectionPad * Pad = Inputs.dpads[getKeyName(Inputs, button)];
65+
66+
*((float *)(x)) = Pad.xAxis.value;
67+
*((float *)(y)) = -Pad.yAxis.value;
68+
}
69+
}
70+
*/
71+
import "C"
72+
import (
73+
"gioui.org/app"
74+
"gioui.org/io/event"
75+
"gioui.org/io/system"
76+
"unsafe"
77+
)
78+
79+
var mappingButton = map[unsafe.Pointer]int{
80+
unsafe.Pointer(&C.GCInputButtonA): buttonA,
81+
unsafe.Pointer(&C.GCInputButtonB): buttonB,
82+
unsafe.Pointer(&C.GCInputButtonX): buttonX,
83+
unsafe.Pointer(&C.GCInputButtonY): buttonY,
84+
unsafe.Pointer(&C.GCInputLeftThumbstickButton): buttonLeftThumb,
85+
unsafe.Pointer(&C.GCInputRightThumbstickButton): buttonRightThumb,
86+
unsafe.Pointer(&C.GCInputLeftShoulder): buttonLB,
87+
unsafe.Pointer(&C.GCInputRightShoulder): buttonRB,
88+
unsafe.Pointer(&C.GCInputLeftTrigger): buttonLT,
89+
unsafe.Pointer(&C.GCInputRightTrigger): buttonRT,
90+
unsafe.Pointer(&C.GCInputButtonMenu): buttonStart,
91+
unsafe.Pointer(&C.GCInputButtonOptions): buttonBack,
92+
}
93+
94+
type gamepad struct{}
95+
96+
func newGamepad(_ *app.Window) *gamepad {
97+
return &gamepad{}
98+
}
99+
100+
func (g *Gamepad) listenEvents(evt event.Event) {
101+
switch evt.(type) {
102+
case system.FrameEvent:
103+
g.getState()
104+
}
105+
}
106+
107+
func (g *Gamepad) getState() {
108+
gamepads := C.getGamepads()
109+
defer C.CFRelease(gamepads)
110+
for player, controller := range g.Controllers {
111+
controller.updateState(C.getState(gamepads, C.int64_t(player)))
112+
}
113+
}
114+
115+
func (controller *Controller) updateState(state C.CFTypeRef) {
116+
if state == 0 {
117+
controller.Connected = false
118+
controller.Changed = false
119+
return
120+
}
121+
defer C.CFRelease(state)
122+
123+
packet := float64(C.getLastEventFrom(state))
124+
if controller.packet == packet {
125+
controller.Changed = false
126+
return
127+
}
128+
129+
controller.packet = packet
130+
controller.Connected = true
131+
controller.Changed = true
132+
133+
// Buttons
134+
for name, button := range mappingButton {
135+
controller.Buttons.setButtonForce(button, float32(C.getButtonFrom(state, name)))
136+
}
137+
138+
// D-Pads
139+
var x, y float32
140+
C.getAxesFrom(state, unsafe.Pointer(&C.GCInputDirectionPad), unsafe.Pointer(&x), unsafe.Pointer(&y))
141+
controller.Buttons.setButtonPressed(buttonLeft, x < 0)
142+
controller.Buttons.setButtonPressed(buttonRight, x > 0)
143+
controller.Buttons.setButtonPressed(buttonUp, y < 0)
144+
controller.Buttons.setButtonPressed(buttonDown, y > 0)
145+
146+
// Joysticks
147+
C.getAxesFrom(state, unsafe.Pointer(&C.GCInputLeftThumbstick),
148+
unsafe.Pointer(&controller.Joysticks.LeftThumb.X),
149+
unsafe.Pointer(&controller.Joysticks.LeftThumb.Y),
150+
)
151+
C.getAxesFrom(state, unsafe.Pointer(&C.GCInputRightThumbstick),
152+
unsafe.Pointer(&controller.Joysticks.RightThumb.X),
153+
unsafe.Pointer(&controller.Joysticks.RightThumb.Y),
154+
)
155+
}

gamepad/gamepad_js.go

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package gamepad
2+
3+
import (
4+
"gioui.org/app"
5+
"gioui.org/io/event"
6+
"gioui.org/io/system"
7+
"syscall/js"
8+
)
9+
10+
// mappingButton corresponds to https://w3c.github.io/gamepad/#dom-gamepad-mapping:
11+
var mappingButton = [...]int{
12+
buttonA,
13+
buttonB,
14+
buttonX,
15+
buttonY,
16+
buttonLB,
17+
buttonRB,
18+
buttonLT,
19+
buttonRT,
20+
buttonBack,
21+
buttonStart,
22+
buttonLeftThumb,
23+
buttonRightThumb,
24+
buttonUp,
25+
buttonDown,
26+
buttonLeft,
27+
buttonRight,
28+
}
29+
30+
type gamepad struct{}
31+
32+
func newGamepad(_ *app.Window) *gamepad {
33+
return &gamepad{}
34+
}
35+
36+
func (g *Gamepad) listenEvents(evt event.Event) {
37+
switch evt.(type) {
38+
case system.FrameEvent:
39+
g.getState()
40+
}
41+
}
42+
43+
var (
44+
_Navigator = js.Global().Get("navigator")
45+
)
46+
47+
func (g *Gamepad) getState() {
48+
gamepads := _Navigator.Get("getGamepads")
49+
if !gamepads.Truthy() {
50+
return
51+
}
52+
53+
gamepads = _Navigator.Call("getGamepads")
54+
for player, controller := range g.Controllers {
55+
controller.updateState(gamepads.Index(player))
56+
}
57+
}
58+
59+
func (controller *Controller) updateState(state js.Value) {
60+
if !state.Truthy() {
61+
controller.Connected = false
62+
controller.Changed = false
63+
return
64+
}
65+
66+
packet := state.Get("timestamp").Float()
67+
if packet == controller.packet {
68+
controller.Changed = false
69+
return
70+
}
71+
72+
controller.packet = packet
73+
controller.Connected = true
74+
controller.Changed = true
75+
76+
// Buttons
77+
buttons := state.Get("buttons")
78+
for index, button := range mappingButton {
79+
btn := buttons.Index(index)
80+
force := 0.0
81+
if btn.Truthy() {
82+
force = btn.Get("value").Float()
83+
}
84+
controller.Buttons.setButtonForce(button, float32(force))
85+
}
86+
87+
// Joysticks
88+
axes := state.Get("axes")
89+
controller.Joysticks.LeftThumb.X = float32(axes.Index(0).Float())
90+
controller.Joysticks.LeftThumb.Y = float32(axes.Index(1).Float())
91+
controller.Joysticks.RightThumb.X = float32(axes.Index(2).Float())
92+
controller.Joysticks.RightThumb.Y = float32(axes.Index(3).Float())
93+
}

0 commit comments

Comments
 (0)