Last active
August 21, 2023 15:15
-
-
Save egonelbre/eda3b78db47236a453f5ed257da7b2e2 to your computer and use it in GitHub Desktop.
Input handling in Go for games
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
// 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 | |
} |
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
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 | |
} | |
} | |
} | |
} |
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
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