Skip to content
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

gamepad: add Gamepad package #8

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Comment on lines +87 to +93
Copy link
Contributor Author

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?


// 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, *)) {
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
Copy link
Contributor Author

@inkeliz inkeliz Dec 25, 2021

Choose a reason for hiding this comment

The 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 [...][2]uinptr (the first is NSString and the second item is the offset struct-item) and provide access directly to controller.Buttons). So, the C code will change the Go-Struct directly, so it's a single call to C, instead of multiple calls.


// 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