Skip to content

Instantly share code, notes, and snippets.

@egonelbre
Last active August 21, 2023 15:15
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save egonelbre/eda3b78db47236a453f5ed257da7b2e2 to your computer and use it in GitHub Desktop.
Save egonelbre/eda3b78db47236a453f5ed257da7b2e2 to your computer and use it in GitHub Desktop.
Input handling in Go for games
// adjust the input/controller to your own liking
type Device interface {
Update(input *Controller, window *glfw.Window)
}
// this is the general
type Controller struct {
ID int
Device Device
Connected bool
Sticky bool
DPad DPad
Start, Back bool
A, B, X, Y bool
Inputs []Input
}
type Input struct {
Direction V2
Hold bool
Trigger bool
}
type DPad struct {
Up bool
Down bool
Left bool
Right bool
}
func (dpad DPad) Direction() (r V2) {
if dpad.Down {
r.Y -= 1
}
if dpad.Up {
r.Y += 1
}
if dpad.Left {
r.X -= 1
}
if dpad.Right {
r.X += 1
}
return
}
type Keyboard struct {
Connected bool
// here it allows remapping of keys
Up, Down, Left, Right glfw.Key
Start, Back glfw.Key
A, B, X, Y glfw.Key
}
func (key *Keyboard) Update(input *Controller, window *glfw.Window) {
input.DPad.Up = window.GetKey(key.Up) == glfw.Press
input.DPad.Down = window.GetKey(key.Down) == glfw.Press
input.DPad.Left = window.GetKey(key.Left) == glfw.Press
input.DPad.Right = window.GetKey(key.Right) == glfw.Press
input.Start = window.GetKey(key.Start) == glfw.Press
input.Back = window.GetKey(key.Back) == glfw.Press
input.A = window.GetKey(key.A) == glfw.Press
input.B = window.GetKey(key.B) == glfw.Press
input.X = window.GetKey(key.X) == glfw.Press
input.Y = window.GetKey(key.Y) == glfw.Press
input.Inputs = []Input{}
key.Connected = true
input.Connected = key.Connected
}
type Gamepad struct {
Id glfw.Joystick
DeadZone float32
}
func (gamepad Gamepad) Update(input *Controller, window *glfw.Window) {
// clear state
*input = Controller{ID: input.ID, Updater: input.Updater}
input.Inputs = []Input{{}, {}}
axes := glfw.GetJoystickAxes(gamepad.Id)
buttons := glfw.GetJoystickButtons(gamepad.Id)
input.Connected = len(axes) > 0 && len(buttons) > 0
if !input.Connected {
return
}
input.DPad.Up = buttons[10] == 1
input.DPad.Right = buttons[11] == 1
input.DPad.Down = buttons[12] == 1
input.DPad.Left = buttons[13] == 1
input.A = buttons[0] == 1
input.B = buttons[1] == 1
input.X = buttons[2] == 1
input.Y = buttons[3] == 1
input.Back = buttons[6] == 1
input.Start = buttons[7] == 1
input.Inputs[0].Direction = V2{ // left thumb
X: ApplyDeadZone(axes[0], gamepad.DeadZone),
Y: ApplyDeadZone(axes[1], gamepad.DeadZone),
}
input.Inputs[0].Hold = buttons[8] == 1 // left thumb pressed
input.Inputs[0].Trigger = buttons[4] == 1 // left trigger
input.Inputs[1].Direction = V2{ // right thumb
X: ApplyDeadZone(axes[4], gamepad.DeadZone),
Y: ApplyDeadZone(axes[3], gamepad.DeadZone),
}
input.Inputs[1].Hold = buttons[9] == 1 // right thumb pressed
input.Inputs[1].Trigger = buttons[5] == 1 // right trigger
}
func ApplyDeadZone(v float32, deadZone float32) float32 {
if v < -deadZone {
return (v + deadZone) / (1 - deadZone)
}
if v > deadZone {
return (v - deadZone) / (1 - deadZone)
}
return 0.0
}
package main
import (
"fmt"
"time"
"<your codebase>/gamepad"
)
func main() {
gamepads := gamepad.All{}
prev := int16(0)
for range time.Tick(1 * time.Millisecond) {
gamepads.Update()
for i := range gamepads {
pad := &gamepads[i]
if !pad.Connected {
continue
}
if pad.Raw.ThumbLX != prev {
fmt.Println(time.Now().Nanosecond(), pad.Raw.ThumbLX, pad.Raw.ThumbRX)
prev = pad.Raw.ThumbLX
}
}
}
}
package gamepad
import (
"math"
"syscall"
"unsafe"
)
type ID byte
type All [ControllerCount]State
func (all *All) Update() (firsterr error) {
for i := range all {
all[i].ID = ID(i)
err := all[i].Update()
if err != nil && firsterr == nil {
firsterr = err
}
}
return
}
type State struct {
ID ID
Connected bool
Packet uint32
Raw struct {
Buttons Button
LeftTrigger uint8
RightTrigger uint8
ThumbLX int16
ThumbLY int16
ThumbRX int16
ThumbRY int16
}
}
func (state *State) Pressed(button Button) bool { return state.Raw.Buttons&button != 0 }
func (state *State) Update() error { return Get(state.ID, state) }
type Thumb struct{ X, Y, Magnitude float32 }
func (state *State) RectDPad() (thumb Thumb) {
if state.Pressed(DPadUp) {
thumb.Y += 1
}
if state.Pressed(DPadDown) {
thumb.Y -= 1
}
if state.Pressed(DPadLeft) {
thumb.X -= 1
}
if state.Pressed(DPadRight) {
thumb.X += 1
}
if thumb.X != 0 || thumb.Y != 0 {
thumb.Magnitude = 1
}
return
}
func (state *State) RoundDPad() (thumb Thumb) {
thumb = state.RectDPad()
if thumb.X != 0 && thumb.Y != 0 {
thumb.X *= isqrt2
thumb.Y *= isqrt2
}
return
}
func round16(rx, ry, deadzone int16) (thumb Thumb) {
//TODO: use sqrt32
fx, fy := float64(rx), float64(ry)
thumb.Magnitude = float32(math.Sqrt(fx*fx + fy*fy))
thumb.X = float32(rx) / thumb.Magnitude
thumb.Y = float32(ry) / thumb.Magnitude
if thumb.Magnitude > float32(deadzone) {
if thumb.Magnitude > 32767 {
thumb.Magnitude = 32767
}
thumb.Magnitude = (thumb.Magnitude - float32(deadzone)) / float32(32767-deadzone)
} else {
thumb.Magnitude = 0
}
thumb.X *= thumb.Magnitude
thumb.Y *= thumb.Magnitude
return
}
func (state *State) RoundLeft() Thumb {
return round16(state.Raw.ThumbLX, state.Raw.ThumbLY, LeftThumbDeadZone)
}
func (state *State) RoundRight() Thumb {
return round16(state.Raw.ThumbRX, state.Raw.ThumbRY, RightThumbDeadZone)
}
func linear16(v, deadzone int16) float32 {
if v < -deadzone {
return float32(v+deadzone) / float32(32767-deadzone)
}
if v > deadzone {
return float32(v-deadzone) / float32(32767-deadzone)
}
return 0
}
func rect16(rx, ry, deadzone int16) (thumb Thumb) {
thumb.X = linear16(rx, deadzone)
thumb.Y = linear16(ry, deadzone)
if thumb.X != 0 && thumb.Y != 0 {
thumb.Magnitude = 1
}
return
}
func (state *State) RectLeft() Thumb {
return rect16(state.Raw.ThumbLX, state.Raw.ThumbLY, LeftThumbDeadZone)
}
func (state *State) RectRight() Thumb {
return rect16(state.Raw.ThumbRX, state.Raw.ThumbRY, RightThumbDeadZone)
}
func (state *State) Vibrate(left, right uint16) {
if !state.Connected {
return
}
Vibrate(state.ID, &Vibration{left, right})
}
type Vibration struct {
LeftMotor uint16
RightMotor uint16
}
const (
ControllerCount = ID(4)
TriggerThreshold = 30
LeftThumbDeadZone = 7849
RightThumbDeadZone = 8689
sqrt2 = 1.4142135623730950488
isqrt2 = 1 / sqrt2
)
type Button uint16
const (
DPadUp Button = 0x0001
DPadDown = 0x0002
DPadLeft = 0x0004
DPadRight = 0x0008
Start Button = 0x0010
Back = 0x0020
LeftThumb Button = 0x0040
RightThumb = 0x0080
LeftShoulder Button = 0x0100
RightShoulder = 0x0200
ButtonA Button = 0x1000
ButtonB = 0x2000
ButtonX = 0x4000
ButtonY = 0x8000
)
// Get retrieves the latest state of the controller.
func Get(id ID, state *State) error {
r, _, _ := procGetState.Call(uintptr(id), uintptr(unsafe.Pointer(&state.Packet)))
state.ID = id
state.Connected = r == 0
if r == 0 {
return nil
}
return syscall.Errno(r)
}
func Vibrate(id ID, vibration *Vibration) error {
r, _, _ := procSetState.Call(uintptr(id), uintptr(unsafe.Pointer(vibration)))
if r == 0 {
return nil
}
return syscall.Errno(r)
}
var (
procGetState *syscall.Proc
procSetState *syscall.Proc
)
func init() {
dll, err := syscall.LoadDLL("xinput1_4.dll")
defer func() {
if err != nil {
panic(err)
}
}()
if err != nil {
dll, err = syscall.LoadDLL("xinput1_3.dll")
if err != nil {
dll, err = syscall.LoadDLL("xinput9_1_0.dll")
return
}
}
procGetState = dll.MustFindProc("XInputGetState")
procSetState = dll.MustFindProc("XInputSetState")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment