From 1f4af682e11f47cbf7160b373dc9172ce727f003 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sat, 19 Feb 2022 18:22:55 -0800 Subject: [PATCH] SDL2 GameController Support --- event/event.go | 35 ++++++- event/game_controller.go | 41 ++++++++ event/vector.go | 7 ++ sdl/events.go | 97 ++++++++++++++++--- sdl/game_controller.go | 199 +++++++++++++++++++++++++++++++++++++++ sdl/sdl.go | 10 +- sdl/text.go | 2 + 7 files changed, 372 insertions(+), 19 deletions(-) create mode 100644 event/game_controller.go create mode 100644 event/vector.go create mode 100644 sdl/game_controller.go diff --git a/event/event.go b/event/event.go index 82b2622..4b8c82a 100644 --- a/event/event.go +++ b/event/event.go @@ -40,12 +40,18 @@ type State struct { TouchCenterY int GestureRotated float64 GesturePinched float64 + + // Game controller events. + // NOTE: for SDL2 you will need to call GameControllerEventState(1) + // from veandco/go-sdl2/sdl for events to be read by SDL2. + Controllers map[int]GameController } // NewState creates a new event.State. func NewState() *State { return &State{ - keydown: map[string]interface{}{}, + keydown: map[string]interface{}{}, + Controllers: map[int]GameController{}, } } @@ -118,3 +124,30 @@ var shiftMap = map[string]string{ ".": ">", "/": "?", } + +// AddController adds a new controller to the event state. This is typically called +// automatically by the render engine, e.g. on an SDL ControllerDeviceEvent. +func (s *State) AddController(index int, v GameController) bool { + if _, ok := s.Controllers[index]; ok { + return false + } + s.Controllers[index] = v + return true +} + +// RemoveController removes the available controller. +func (s *State) RemoveController(index int) bool { + if _, ok := s.Controllers[index]; ok { + delete(s.Controllers, index) + return true + } + return false +} + +// GetController gets a registered controller by index. +func (s *State) GetController(index int) (GameController, bool) { + if c, ok := s.Controllers[index]; ok { + return c, true + } + return nil, false +} diff --git a/event/game_controller.go b/event/game_controller.go new file mode 100644 index 0000000..b7dc2cc --- /dev/null +++ b/event/game_controller.go @@ -0,0 +1,41 @@ +package event + +// GameController holds event state for one or more (Xbox-style) controllers. +type GameController interface { + ID() int // Usually the controller index number + Name() string // Friendly name of the controller + + // State setters, to be called by the engine. + // Note: button names are implementation-specific, use Button*() methods in your code. + SetButtonState(name string, pressed bool) + GetButtonState(name string) bool + SetAxisState(name string, value int) // value maybe -32768 to 32767 + + // State getters. + ButtonA() bool + ButtonB() bool + ButtonX() bool + ButtonY() bool + ButtonL1() bool // Left shoulder + ButtonR1() bool // Right shoulder + ButtonL2() bool // Left trigger (digital) + ButtonR2() bool // Right trigger (digital) + ButtonLStick() bool + ButtonRStick() bool + ButtonStart() bool + ButtonSelect() bool // Back button + ButtonHome() bool // Guide button + + // D-Pad buttons. + ButtonUp() bool + ButtonLeft() bool + ButtonRight() bool + ButtonDown() bool + + // Axis getters. Returns Vectors ranging from -1.0 to 1.0 being a + // percentage of the axis between neutral and maxed out. + LeftStick() Vector + RightStick() Vector + LeftTrigger() float64 + RightTrigger() float64 +} diff --git a/event/vector.go b/event/vector.go new file mode 100644 index 0000000..27993fb --- /dev/null +++ b/event/vector.go @@ -0,0 +1,7 @@ +package event + +// Vector holds a floating point vector along an X and Y axis. +type Vector struct { + X float64 + Y float64 +} diff --git a/sdl/events.go b/sdl/events.go index e88fa20..4a8dd54 100644 --- a/sdl/events.go +++ b/sdl/events.go @@ -10,11 +10,12 @@ import ( // Debug certain SDL events var ( - DebugWindowEvents = false - DebugTouchEvents = false - DebugMouseEvents = false - DebugClickEvents = false - DebugKeyEvents = false + DebugWindowEvents = false + DebugTouchEvents = false + DebugMouseEvents = false + DebugClickEvents = false + DebugKeyEvents = false + DebugControllerEvents = false ) // Poll for events. @@ -39,7 +40,7 @@ func (r *Renderer) Poll() (*event.State, error) { if t.Event == sdl.WINDOWEVENT_RESIZED { fmt.Printf("[%d ms] tick:%d Window Resized to %dx%d\n", t.Timestamp, - r.ticks, + sdl.GetTicks(), t.Data1, t.Data2, ) @@ -52,7 +53,7 @@ func (r *Renderer) Poll() (*event.State, error) { case *sdl.MouseMotionEvent: if DebugMouseEvents { fmt.Printf("[%d ms] tick:%d MouseMotion type:%d id:%d x:%d y:%d xrel:%d yrel:%d\n", - t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y, t.XRel, t.YRel, + t.Timestamp, sdl.GetTicks(), t.Type, t.Which, t.X, t.Y, t.XRel, t.YRel, ) } @@ -62,7 +63,7 @@ func (r *Renderer) Poll() (*event.State, error) { case *sdl.MouseButtonEvent: if DebugClickEvents { fmt.Printf("[%d ms] tick:%d MouseButton type:%d id:%d x:%d y:%d button:%d state:%d\n", - t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y, t.Button, t.State, + t.Timestamp, sdl.GetTicks(), t.Type, t.Which, t.X, t.Y, t.Button, t.State, ) } @@ -104,13 +105,13 @@ func (r *Renderer) Poll() (*event.State, error) { case *sdl.MouseWheelEvent: if DebugMouseEvents { fmt.Printf("[%d ms] tick:%d MouseWheel type:%d id:%d x:%d y:%d\n", - t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y, + t.Timestamp, sdl.GetTicks(), t.Type, t.Which, t.X, t.Y, ) } case *sdl.MultiGestureEvent: if DebugTouchEvents { fmt.Printf("[%d ms] tick:%d MultiGesture type:%d Num=%d TouchID=%+v Dt=%f Dd=%f XY=%f,%f\n", - t.Timestamp, r.ticks, t.Type, t.NumFingers, t.TouchID, t.DTheta, t.DDist, t.X, t.Y, + t.Timestamp, sdl.GetTicks(), t.Type, t.NumFingers, t.TouchID, t.DTheta, t.DDist, t.X, t.Y, ) } s.Touching = true @@ -122,7 +123,7 @@ func (r *Renderer) Poll() (*event.State, error) { case *sdl.KeyboardEvent: if DebugKeyEvents { fmt.Printf("[%d ms] tick:%d Keyboard type:%d sym:%c modifiers:%d state:%d repeat:%d\n", - t.Timestamp, r.ticks, t.Type, t.Keysym.Sym, t.Keysym.Mod, t.State, t.Repeat, + t.Timestamp, sdl.GetTicks(), t.Type, t.Keysym.Sym, t.Keysym.Mod, t.State, t.Repeat, ) } @@ -190,6 +191,80 @@ func (r *Renderer) Poll() (*event.State, error) { // Push the string value of the key. s.SetKeyDown(string(t.Keysym.Sym), t.State == 1 || t.Repeat == 1) } + case *sdl.ControllerDeviceEvent: + if DebugControllerEvents { + fmt.Printf("[%d ms] tick:%d ControllerDevice type:%d timestamp:%d which:%d\n", + t.Timestamp, sdl.GetTicks(), t.Type, t.Timestamp, t.Which, + ) + } + + var index = int(t.Which) + + // Update the catalog of available controllers. + switch t.Type { + case sdl.CONTROLLERDEVICEADDED: + if DebugControllerEvents { + fmt.Printf("[%d ms] tick:%d AddController: %s\n", + t.Timestamp, sdl.GetTicks(), sdl.GameControllerNameForIndex(index), + ) + } + + // Add and open the GameController. + var ( + name = sdl.GameControllerNameForIndex(index) + ctrlImpl = sdl.GameControllerOpen(index) + ctrl = NewGameController(index, name, ctrlImpl) + ) + s.AddController(index, ctrl) + case sdl.CONTROLLERDEVICEREMOVED: + if DebugControllerEvents { + fmt.Printf("[%d ms] tick:%d RemoveController: %s\n", + t.Timestamp, sdl.GetTicks(), sdl.GameControllerNameForIndex(index), + ) + } + s.RemoveController(index) + } + case *sdl.ControllerButtonEvent: + if DebugControllerEvents { + fmt.Printf("[%d ms] tick:%d ControllerButton type:%d timestamp:%d which:%d btn:%d state:%d\n", + t.Timestamp, sdl.GetTicks(), t.Type, t.Timestamp, t.Which, t.Button, t.State, + ) + } + + var ( + index = int(t.Which) + buttonName = sdl.GameControllerGetStringForButton(sdl.GameControllerButton(t.Button)) + ) + if DebugControllerEvents { + fmt.Printf("[%d ms] tick:%d ControllerButton: index:%d name:%s pressed:%d\n", + t.Timestamp, sdl.GetTicks(), index, buttonName, t.State, + ) + } + + if ctrl, ok := s.GetController(index); ok { + ctrl.SetButtonState(buttonName, t.State == sdl.PRESSED) + } + case *sdl.ControllerAxisEvent: + if DebugControllerEvents { + fmt.Printf("[%d ms] tick:%d ControllerAxis type:%d timestamp:%d which:%d axis:%d value:%d\n", + t.Timestamp, sdl.GetTicks(), t.Type, t.Timestamp, t.Which, t.Axis, t.Value, + ) + } + + var ( + index = int(t.Which) + axisName = sdl.GameControllerGetStringForAxis(sdl.GameControllerAxis(t.Axis)) + value = int(t.Value) + ) + if DebugControllerEvents { + fmt.Printf("[%d ms] tick:%d ControllerAxis: index:%d name:%s value:%d\n", + t.Timestamp, sdl.GetTicks(), index, axisName, value, + ) + } + + if ctrl, ok := s.GetController(index); ok { + ctrl.SetAxisState(axisName, value) + } } } diff --git a/sdl/game_controller.go b/sdl/game_controller.go new file mode 100644 index 0000000..b2a3ebf --- /dev/null +++ b/sdl/game_controller.go @@ -0,0 +1,199 @@ +package sdl + +import ( + "git.kirsle.net/go/render/event" + "github.com/veandco/go-sdl2/sdl" +) + +// User tuneable properties. +var ( + // For controllers having a digital (non-analog) Left/Right Trigger, the press percentage + // for which to consider it a boolean press. + TriggerAxisBooleanThreshold float64 = 0.5 +) + +// GameController holds an abstraction around SDL2 GameControllers. +type GameController struct { + id int + name string + active bool + + // Underlying SDL2 GameController. + ctrl *sdl.GameController + + // Button states. + buttons map[string]bool + axes map[string]int +} + +// NewGameController creates a GameController from an SDL2 controller. +func NewGameController(index int, name string, ctrl *sdl.GameController) *GameController { + return &GameController{ + id: index, + name: name, + ctrl: ctrl, + buttons: map[string]bool{}, + axes: map[string]int{}, + } +} + +// ID returns the controller index as SDL2 knows it. +func (gc *GameController) ID() int { + return gc.id +} + +// Name returns the controller name. +func (gc *GameController) Name() string { + return gc.name +} + +// SetButtonState sets the state using the SDL2 button names. +func (gc *GameController) SetButtonState(name string, pressed bool) { + gc.buttons[name] = pressed +} + +// GetButtonState returns the button state by SDL2 button name. +func (gc *GameController) GetButtonState(name string) bool { + if v, ok := gc.buttons[name]; ok { + return v + } + return false +} + +// SetAxisState sets the axis state. +func (gc *GameController) SetAxisState(name string, value int) { + gc.axes[name] = value +} + +// GetAxisState returns the underlying SDL2 axis state. +func (gc *GameController) GetAxisState(name string) int { + if v, ok := gc.axes[name]; ok { + return v + } + return 0 +} + +// ButtonA returns whether the logical Xbox button is pressed. +func (gc *GameController) ButtonA() bool { + return gc.GetButtonState("a") +} + +// ButtonB returns whether the logical Xbox button is pressed. +func (gc *GameController) ButtonB() bool { + return gc.GetButtonState("b") +} + +// ButtonX returns whether the logical Xbox button is pressed. +func (gc *GameController) ButtonX() bool { + return gc.GetButtonState("x") +} + +// ButtonY returns whether the logical Xbox button is pressed. +func (gc *GameController) ButtonY() bool { + return gc.GetButtonState("y") +} + +// ButtonL1 returns whether the Left Shoulder button is pressed. +func (gc *GameController) ButtonL1() bool { + return gc.GetButtonState("leftshoulder") +} + +// ButtonR1 returns whether the Right Shoulder button is pressed. +func (gc *GameController) ButtonR1() bool { + return gc.GetButtonState("rightshoulder") +} + +// ButtonL2 returns whether the Left Trigger (digital) button is pressed. +// Returns true if the LeftTrigger is 50% pressed or TriggerAxisBooleanThreshold. +func (gc *GameController) ButtonL2() bool { + return gc.axisToFloat("lefttrigger") > TriggerAxisBooleanThreshold +} + +// ButtonR2 returns whether the Left Trigger (digital) button is pressed. +// Returns true if the LeftTrigger is 50% pressed or TriggerAxisBooleanThreshold. +func (gc *GameController) ButtonR2() bool { + return gc.axisToFloat("righttrigger") > TriggerAxisBooleanThreshold +} + +// ButtonLStick returns whether the Left Stick button is pressed. +func (gc *GameController) ButtonLStick() bool { + return gc.GetButtonState("leftstick") +} + +// ButtonRStick returns whether the Right Stick button is pressed. +func (gc *GameController) ButtonRStick() bool { + return gc.GetButtonState("rightstick") +} + +// ButtonStart returns whether the logical Xbox button is pressed. +func (gc *GameController) ButtonStart() bool { + return gc.GetButtonState("start") +} + +// ButtonSelect returns whether the Xbox "back" button is pressed. +func (gc *GameController) ButtonSelect() bool { + return gc.GetButtonState("back") +} + +// ButtonUp returns whether the Xbox D-Pad button is pressed. +func (gc *GameController) ButtonUp() bool { + return gc.GetButtonState("dpup") +} + +// ButtonDown returns whether the Xbox D-Pad button is pressed. +func (gc *GameController) ButtonDown() bool { + return gc.GetButtonState("dpdown") +} + +// ButtonLeft returns whether the Xbox D-Pad button is pressed. +func (gc *GameController) ButtonLeft() bool { + return gc.GetButtonState("dpleft") +} + +// ButtonRight returns whether the Xbox D-Pad button is pressed. +func (gc *GameController) ButtonRight() bool { + return gc.GetButtonState("dpright") +} + +// ButtonHome returns whether the Xbox "guide" button is pressed. +func (gc *GameController) ButtonHome() bool { + return gc.GetButtonState("guide") +} + +// LeftStick returns the vector of X and Y of the left analog stick. +func (gc *GameController) LeftStick() event.Vector { + return event.Vector{ + X: gc.axisToFloat("leftx"), + Y: gc.axisToFloat("lefty"), + } +} + +// RightStick returns the vector of X and Y of the right analog stick. +func (gc *GameController) RightStick() event.Vector { + return event.Vector{ + X: gc.axisToFloat("rightx"), + Y: gc.axisToFloat("righty"), + } +} + +// LeftTrigger returns the vector of the left analog trigger. +func (gc *GameController) LeftTrigger() float64 { + return gc.axisToFloat("lefttrigger") +} + +// RightTrigger returns the vector of the left analog trigger. +func (gc *GameController) RightTrigger() float64 { + return gc.axisToFloat("righttrigger") +} + +// axisToFloat converts an SDL2 Axis value to a float between -1.0..1.0 +func (gc *GameController) axisToFloat(name string) float64 { + // SDL2 Axis is an int16 ranging from -32768 to 32767, + // convert this into a percentage +- 0 to 1. + axis := gc.GetAxisState(name) + if axis < 0 { + return float64(axis) / 32768 + } else { + return float64(axis) / 32767 + } +} diff --git a/sdl/sdl.go b/sdl/sdl.go index 9cca831..a17c845 100644 --- a/sdl/sdl.go +++ b/sdl/sdl.go @@ -5,7 +5,6 @@ import ( "bytes" "fmt" "image" - "time" "git.kirsle.net/go/render" "git.kirsle.net/go/render/event" @@ -17,17 +16,14 @@ import ( // Renderer manages the SDL state. type Renderer struct { // Configurable fields. - title string - width int32 - height int32 - startTime time.Time + title string + width int32 + height int32 // Private fields. events *event.State window *sdl.Window renderer *sdl.Renderer - running bool - ticks uint64 textures map[string]*Texture // cached textures // Optimizations to minimize SDL calls. diff --git a/sdl/text.go b/sdl/text.go index 34d49bc..d9545bc 100644 --- a/sdl/text.go +++ b/sdl/text.go @@ -52,6 +52,8 @@ func LoadFont(filename string, size int) (*ttf.Font, error) { err error ) + fontsMu.Lock() + defer fontsMu.Unlock() if binary, ok := installedFont[filename]; ok { var RWops *sdl.RWops RWops, err = sdl.RWFromMem(binary)