SDL2 GameController Support

This commit is contained in:
Noah 2022-02-19 18:22:55 -08:00
parent 9e640ab5c3
commit 1f4af682e1
7 changed files with 372 additions and 19 deletions

View File

@ -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
}

41
event/game_controller.go Normal file
View File

@ -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
}

7
event/vector.go Normal file
View File

@ -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
}

View File

@ -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)
}
}
}

199
sdl/game_controller.go Normal file
View File

@ -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
}
}

View File

@ -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.

View File

@ -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)