-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support gamepad for Windows, macOS, iOS and JS. Signed-off-by: Inkeliz <inkeliz@inkeliz.com>
- Loading branch information
Showing
7 changed files
with
974 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
// Package gamepad package allows any Gio application to listen for gamepad input, | ||
// it's supported on Windows 10+, JS, iOS 15+, macOS 12+. | ||
// | ||
// That package was inspired by WebGamepad API (see https://w3c.github.io/gamepad/#gamepad-interface). | ||
// | ||
// You must include `op.InvalidateOp` in your main game loop, otherwise the state of the gamepad will | ||
// not be updated. | ||
package gamepad | ||
|
||
import ( | ||
"gioui.org/app" | ||
"gioui.org/f32" | ||
"gioui.org/io/event" | ||
"unsafe" | ||
) | ||
|
||
// Gamepad is the main struct and holds information about the state of all Controllers currently available. | ||
// You must use Gamepad.ListenEvents to keep the state up-to-date. | ||
type Gamepad struct { | ||
Controllers [4]*Controller | ||
|
||
// gamepad varies accordingly with the current OS. | ||
*gamepad | ||
} | ||
|
||
// NewGamepad creates a new Share for the given *app.Window. | ||
// The given app.Window must be unique, and you should call NewGamepad | ||
// once per new app.Window. | ||
// | ||
// It's mandatory to use Gamepad.ListenEvents on the same *app.Window. | ||
func NewGamepad(w *app.Window) *Gamepad { | ||
return &Gamepad{ | ||
Controllers: [4]*Controller{ | ||
new(Controller), | ||
new(Controller), | ||
new(Controller), | ||
new(Controller), | ||
}, | ||
gamepad: newGamepad(w), | ||
} | ||
} | ||
|
||
// ListenEvents must get all the events from Gio, in order to get the GioView. You must | ||
// include that function where you listen for Gio events. | ||
// | ||
// Similar as: | ||
// | ||
// select { | ||
// case e := <-window.Events(): | ||
// gamepad.ListenEvents(e) | ||
// switch e := e.(type) { | ||
// (( ... your code ... )) | ||
// } | ||
// } | ||
func (g *Gamepad) ListenEvents(evt event.Event) { | ||
g.listenEvents(evt) | ||
} | ||
|
||
// Controller is used to report what Buttons are currently pressed, and where is the position of the Joysticks | ||
// and how much the Triggers are pressed. | ||
type Controller struct { | ||
Joysticks Joysticks | ||
Buttons Buttons | ||
|
||
Connected bool | ||
Changed bool | ||
packet float64 | ||
} | ||
|
||
// Joysticks hold the information about the position of the joystick, the position are from -1.0 to 1.0, and | ||
// 0.0 represents the center. | ||
// The maximum and minimum values are: | ||
// [Y:-1.0] | ||
// [X:-1.0] [X:+1.0] | ||
// [Y:+1.0] | ||
type Joysticks struct { | ||
LeftThumb, RightThumb f32.Point | ||
} | ||
|
||
// Buttons hold the information about the state of the buttons, it's based on XBOX Controller scheme. | ||
// The buttons will be informed based on their physical position. Clicking "B" on Nintendo | ||
// gamepad will be "A" since it correspond to same key-position. | ||
// | ||
// That struct must NOT change, or those change must reflect on all maps, which varies per each OS. | ||
// | ||
// Internally, Buttons will be interpreted as [...]Button. | ||
type Buttons struct { | ||
A, B, Y, X Button | ||
Left, Right, Up, Down Button | ||
LT, RT, LB, RB Button | ||
LeftThumb, RightThumb Button | ||
Start, Back Button | ||
} | ||
|
||
// Button reports if the button is pressed or not, and how much it's pressed (from 0.0 to 1.0 when fully pressed). | ||
type Button struct { | ||
Pressed bool | ||
Force float32 | ||
} | ||
|
||
func (b *Buttons) setButtonPressed(button int, v bool) { | ||
bp := (*Button)(unsafe.Add(unsafe.Pointer(b), button)) | ||
bp.Pressed = v | ||
if v { | ||
bp.Force = 1.0 | ||
} else { | ||
bp.Force = 0.0 | ||
} | ||
} | ||
|
||
func (b *Buttons) setButtonForce(button int, v float32) { | ||
bp := (*Button)(unsafe.Add(unsafe.Pointer(b), button)) | ||
bp.Force = v | ||
bp.Pressed = v > 0 | ||
} | ||
|
||
const ( | ||
buttonA = int(unsafe.Offsetof(Buttons{}.A)) | ||
buttonB = int(unsafe.Offsetof(Buttons{}.B)) | ||
buttonY = int(unsafe.Offsetof(Buttons{}.Y)) | ||
buttonX = int(unsafe.Offsetof(Buttons{}.X)) | ||
buttonLeft = int(unsafe.Offsetof(Buttons{}.Left)) | ||
buttonRight = int(unsafe.Offsetof(Buttons{}.Right)) | ||
buttonUp = int(unsafe.Offsetof(Buttons{}.Up)) | ||
buttonDown = int(unsafe.Offsetof(Buttons{}.Down)) | ||
buttonLT = int(unsafe.Offsetof(Buttons{}.LT)) | ||
buttonRT = int(unsafe.Offsetof(Buttons{}.RT)) | ||
buttonLB = int(unsafe.Offsetof(Buttons{}.LB)) | ||
buttonRB = int(unsafe.Offsetof(Buttons{}.RB)) | ||
buttonLeftThumb = int(unsafe.Offsetof(Buttons{}.LeftThumb)) | ||
buttonRightThumb = int(unsafe.Offsetof(Buttons{}.RightThumb)) | ||
buttonStart = int(unsafe.Offsetof(Buttons{}.Start)) | ||
buttonBack = int(unsafe.Offsetof(Buttons{}.Back)) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
package gamepad | ||
|
||
/* | ||
#cgo CFLAGS: -Werror -xobjective-c -fmodules -fobjc-arc | ||
#import <Foundation/Foundation.h> | ||
#import <GameController/GameController.h> | ||
static CFTypeRef getGamepads() { | ||
if (@available(iOS 15, macOS 12, *)) { | ||
NSArray<GCController *> * Controllers = [GCController controllers]; | ||
return (CFTypeRef)CFBridgingRetain(Controllers); | ||
} | ||
return 0; | ||
} | ||
static CFTypeRef getState(CFTypeRef gamepads, int64_t player) { | ||
if (@available(iOS 15, macOS 12, *)) { | ||
NSArray<GCController *> * Controllers = (__bridge NSArray<GCController *> *)gamepads; | ||
if ([Controllers count] <= player) { | ||
return 0; | ||
} | ||
GCExtendedGamepad * Gamepad = [[Controllers objectAtIndex:player] extendedGamepad]; | ||
if (Gamepad == nil) { | ||
return 0; | ||
} | ||
GCPhysicalInputProfile* Inputs = (GCPhysicalInputProfile*)Gamepad; | ||
return (CFTypeRef)CFBridgingRetain(Inputs); | ||
} | ||
return 0; | ||
} | ||
static double getLastEventFrom(CFTypeRef inputs) { | ||
if (@available(iOS 15, macOS 12, *)) { | ||
return (double)(((__bridge GCPhysicalInputProfile*)(inputs)).lastEventTimestamp); | ||
} | ||
return 0; | ||
} | ||
static NSString * getKeyName(GCPhysicalInputProfile * Inputs, void * button) { | ||
if (@available(iOS 15, macOS 12, *)) { | ||
NSString * name = *((__unsafe_unretained NSString **)(button)); | ||
if ([Inputs hasRemappedElements] == false) { | ||
return name; | ||
} | ||
return [Inputs mappedElementAliasForPhysicalInputName:name]; | ||
} | ||
return nil; | ||
} | ||
static float getButtonFrom(CFTypeRef inputs, void * button) { | ||
if (@available(iOS 15, macOS 12, *)) { | ||
GCPhysicalInputProfile * Inputs = ((__bridge GCPhysicalInputProfile*)(inputs)); | ||
return Inputs.buttons[getKeyName(Inputs, button)].value; | ||
} | ||
return 0; | ||
} | ||
static void getAxesFrom(CFTypeRef inputs, void * button, void * x, void * y) { | ||
if (@available(iOS 15, macOS 12, *)) { | ||
GCPhysicalInputProfile * Inputs = ((__bridge GCPhysicalInputProfile*)(inputs)); | ||
GCControllerDirectionPad * Pad = Inputs.dpads[getKeyName(Inputs, button)]; | ||
*((float *)(x)) = Pad.xAxis.value; | ||
*((float *)(y)) = -Pad.yAxis.value; | ||
} | ||
} | ||
*/ | ||
import "C" | ||
import ( | ||
"gioui.org/app" | ||
"gioui.org/io/event" | ||
"gioui.org/io/system" | ||
"unsafe" | ||
) | ||
|
||
var mappingButton = map[unsafe.Pointer]int{ | ||
unsafe.Pointer(&C.GCInputButtonA): buttonA, | ||
unsafe.Pointer(&C.GCInputButtonB): buttonB, | ||
unsafe.Pointer(&C.GCInputButtonX): buttonX, | ||
unsafe.Pointer(&C.GCInputButtonY): buttonY, | ||
unsafe.Pointer(&C.GCInputLeftThumbstickButton): buttonLeftThumb, | ||
unsafe.Pointer(&C.GCInputRightThumbstickButton): buttonRightThumb, | ||
unsafe.Pointer(&C.GCInputLeftShoulder): buttonLB, | ||
unsafe.Pointer(&C.GCInputRightShoulder): buttonRB, | ||
unsafe.Pointer(&C.GCInputLeftTrigger): buttonLT, | ||
unsafe.Pointer(&C.GCInputRightTrigger): buttonRT, | ||
unsafe.Pointer(&C.GCInputButtonMenu): buttonStart, | ||
unsafe.Pointer(&C.GCInputButtonOptions): buttonBack, | ||
} | ||
|
||
type gamepad struct{} | ||
|
||
func newGamepad(_ *app.Window) *gamepad { | ||
return &gamepad{} | ||
} | ||
|
||
func (g *Gamepad) listenEvents(evt event.Event) { | ||
switch evt.(type) { | ||
case system.FrameEvent: | ||
g.getState() | ||
} | ||
} | ||
|
||
func (g *Gamepad) getState() { | ||
gamepads := C.getGamepads() | ||
defer C.CFRelease(gamepads) | ||
for player, controller := range g.Controllers { | ||
controller.updateState(C.getState(gamepads, C.int64_t(player))) | ||
} | ||
} | ||
|
||
func (controller *Controller) updateState(state C.CFTypeRef) { | ||
if state == 0 { | ||
controller.Connected = false | ||
controller.Changed = false | ||
return | ||
} | ||
defer C.CFRelease(state) | ||
|
||
packet := float64(C.getLastEventFrom(state)) | ||
if controller.packet == packet { | ||
controller.Changed = false | ||
return | ||
} | ||
|
||
controller.packet = packet | ||
controller.Connected = true | ||
controller.Changed = true | ||
|
||
// Buttons | ||
for name, button := range mappingButton { | ||
controller.Buttons.setButtonForce(button, float32(C.getButtonFrom(state, name))) | ||
} | ||
|
||
// D-Pads | ||
var x, y float32 | ||
C.getAxesFrom(state, unsafe.Pointer(&C.GCInputDirectionPad), unsafe.Pointer(&x), unsafe.Pointer(&y)) | ||
controller.Buttons.setButtonPressed(buttonLeft, x < 0) | ||
controller.Buttons.setButtonPressed(buttonRight, x > 0) | ||
controller.Buttons.setButtonPressed(buttonUp, y < 0) | ||
controller.Buttons.setButtonPressed(buttonDown, y > 0) | ||
|
||
// Joysticks | ||
C.getAxesFrom(state, unsafe.Pointer(&C.GCInputLeftThumbstick), | ||
unsafe.Pointer(&controller.Joysticks.LeftThumb.X), | ||
unsafe.Pointer(&controller.Joysticks.LeftThumb.Y), | ||
) | ||
C.getAxesFrom(state, unsafe.Pointer(&C.GCInputRightThumbstick), | ||
unsafe.Pointer(&controller.Joysticks.RightThumb.X), | ||
unsafe.Pointer(&controller.Joysticks.RightThumb.Y), | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package gamepad | ||
|
||
import ( | ||
"gioui.org/app" | ||
"gioui.org/io/event" | ||
"gioui.org/io/system" | ||
"syscall/js" | ||
) | ||
|
||
// mappingButton corresponds to https://w3c.github.io/gamepad/#dom-gamepad-mapping: | ||
var mappingButton = [...]int{ | ||
buttonA, | ||
buttonB, | ||
buttonX, | ||
buttonY, | ||
buttonLB, | ||
buttonRB, | ||
buttonLT, | ||
buttonRT, | ||
buttonBack, | ||
buttonStart, | ||
buttonLeftThumb, | ||
buttonRightThumb, | ||
buttonUp, | ||
buttonDown, | ||
buttonLeft, | ||
buttonRight, | ||
} | ||
|
||
type gamepad struct{} | ||
|
||
func newGamepad(_ *app.Window) *gamepad { | ||
return &gamepad{} | ||
} | ||
|
||
func (g *Gamepad) listenEvents(evt event.Event) { | ||
switch evt.(type) { | ||
case system.FrameEvent: | ||
g.getState() | ||
} | ||
} | ||
|
||
var ( | ||
_Navigator = js.Global().Get("navigator") | ||
) | ||
|
||
func (g *Gamepad) getState() { | ||
gamepads := _Navigator.Get("getGamepads") | ||
if !gamepads.Truthy() { | ||
return | ||
} | ||
|
||
gamepads = _Navigator.Call("getGamepads") | ||
for player, controller := range g.Controllers { | ||
controller.updateState(gamepads.Index(player)) | ||
} | ||
} | ||
|
||
func (controller *Controller) updateState(state js.Value) { | ||
if !state.Truthy() { | ||
controller.Connected = false | ||
controller.Changed = false | ||
return | ||
} | ||
|
||
packet := state.Get("timestamp").Float() | ||
if packet == controller.packet { | ||
controller.Changed = false | ||
return | ||
} | ||
|
||
controller.packet = packet | ||
controller.Connected = true | ||
controller.Changed = true | ||
|
||
// Buttons | ||
buttons := state.Get("buttons") | ||
for index, button := range mappingButton { | ||
btn := buttons.Index(index) | ||
force := 0.0 | ||
if btn.Truthy() { | ||
force = btn.Get("value").Float() | ||
} | ||
controller.Buttons.setButtonForce(button, float32(force)) | ||
} | ||
|
||
// Joysticks | ||
axes := state.Get("axes") | ||
controller.Joysticks.LeftThumb.X = float32(axes.Index(0).Float()) | ||
controller.Joysticks.LeftThumb.Y = float32(axes.Index(1).Float()) | ||
controller.Joysticks.RightThumb.X = float32(axes.Index(2).Float()) | ||
controller.Joysticks.RightThumb.Y = float32(axes.Index(3).Float()) | ||
} |
Oops, something went wrong.