Skip to content

Commit

Permalink
gamepad: add Gamepad package
Browse files Browse the repository at this point in the history
Support gamepad for Windows, macOS, iOS and JS.

Signed-off-by: Inkeliz <inkeliz@inkeliz.com>
  • Loading branch information
inkeliz committed Dec 25, 2021
1 parent cf2b1f0 commit 409eb02
Show file tree
Hide file tree
Showing 7 changed files with 974 additions and 0 deletions.
134 changes: 134 additions & 0 deletions gamepad/gamepad.go
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))
)
155 changes: 155 additions & 0 deletions gamepad/gamepad_darwin.go
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),
)
}
93 changes: 93 additions & 0 deletions gamepad/gamepad_js.go
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())
}
Loading

0 comments on commit 409eb02

Please sign in to comment.