-
Notifications
You must be signed in to change notification settings - Fork 15
gamepad: add Gamepad package #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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)) | ||
) |
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, *)) { | ||
inkeliz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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))) | ||
} | ||
Comment on lines
+134
to
+136
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It sounds very inefficient, but it's easier to understand. Maybe change the map to |
||
|
||
// 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), | ||
) | ||
} |
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()) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe replace to
map[ButtonID]float32
?