doodle/pkg/gamepad/gamepad.go
Noah Petherbridge 0fc046250e Window Focus Bugfixes
* Fix the Doodad Dropper and Registration windows not stealing the focus
  when they are opened via menu bars.
* Bugfixes in gamepad support: stop at the first controller found,
  Draw() to handle controllers going away and hide the mouse cursor
2022-02-19 20:20:58 -08:00

396 lines
9.6 KiB
Go

/*
Package gamepad provides game controller logic for the game.
Controls
The gamepad controls are currently hard-coded for Xbox 360 style controllers, and the
controller mappings vary depending on the "mode" of control.
N Style and X Style
If the gamepad control is set to "NStyle" then the A/B and X/Y buttons will be swapped
to match the labels of a Nintendo style controller. Since the game has relatively few
inputs needed now, this module maps them to a PrimaryButton and a SecondaryButton,
defined as:
PrimaryButton: A or X button
SecondaryButton: B or Y button
Mouse Mode
- Left stick moves the mouse cursor (a cursor sprite is drawn on screen)
- Right stick scrolls the level (title screen or level editor)
- PrimaryButton emulates a left click.
- SecondaryButton emulates a right click.
- Left Shoulder emulates a middle click.
- Left Trigger (L2) closes the top-most window in the Editor (Backspace key)
- Right Shoulder toggles between Mouse Mode and other scene-specific mode.
Gameplay Mode
- Left stick moves the player character (left/right only).
- D-Pad also moves the player character (left/right only).
- PrimaryButton is to "Use"
- SecondaryButton is to "Jump"
- If the player has antigravity, up/down controls on left stick or D-Pad work too.
- Right Shoulder toggles between GameplayMode and MouseMode.
*/
package gamepad
import (
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/shmem"
"git.kirsle.net/apps/doodle/pkg/sprites"
"git.kirsle.net/go/render"
"git.kirsle.net/go/render/event"
"git.kirsle.net/go/ui"
)
// Global state variables for gamepad support.
var (
// PlayScene tells us whether antigravity is on, so directional controls work all directions.
PlayModeAntigravity bool
SceneName string // Set by doodle.Goto() so we know what scene name we're on.
playerOne *int // controller index for Player 1 (main).
p1style Style
p1mode Mode
// Mouse cursor
cursorVisible bool
cursorSprite *ui.Image
cursorLast render.Point // detect if mouse cursor took over from gamepad
// MouseMode button state history to emulate mouse-ups.
leftClickLast bool
rightClickLast bool
middleClickLast bool
leftTriggerLast bool
// MouseMode right stick last position, for arrow key (level scroll) emulation.
rightStickLast event.Vector
// Gameplay mode last left-stick position.
leftStickLast event.Vector
dpLeftLast bool // D-Pad last positions.
dpRightLast bool
dpDownLast bool
dpUpLast bool
// Right Shoulder last state (mode switch)
rightShoulderLast bool
)
// SetControllerIndex sets which gamepad will be "Player One"
func SetControllerIndex(index int) {
playerOne = &index
}
// UnsetController detaches the Player One controller.
func UnsetController() {
playerOne = nil
}
// SetStyle sets the controller button style.
func SetStyle(s Style) {
p1style = s
}
// SetMode sets the controller mode.
func SetMode(m Mode) {
p1mode = m
}
// PrimaryButton returns whether the A or X button is pressed.
func PrimaryButton(ctrl event.GameController) bool {
if p1style == NStyle {
return ctrl.ButtonB() || ctrl.ButtonY()
}
return ctrl.ButtonA() || ctrl.ButtonX()
}
// SecondaryButton returns whether the B or Y button is pressed.
func SecondaryButton(ctrl event.GameController) bool {
if p1style == NStyle {
return ctrl.ButtonA() || ctrl.ButtonX()
}
return ctrl.ButtonB() || ctrl.ButtonY()
}
// Loop hooks the render events on each game tick.
func Loop(ev *event.State) {
// If we don't have a controller registered, watch out for one until we do.
if playerOne == nil {
if len(ev.Controllers) > 0 {
for idx, ctrl := range ev.Controllers {
SetControllerIndex(idx)
log.Info("gamepad: using controller #%d (%s) as Player 1", idx, ctrl)
break
}
} else {
return
}
}
// Get our SDL2 controller.
ctrl, ok := ev.GetController(*playerOne)
if !ok {
log.Error("gamepad: controller #%d has gone away! Detaching as Player 1", playerOne)
playerOne = nil
return
}
// Right Shoulder = toggle controller mode, handle this first.
if ctrl.ButtonR1() {
if !rightShoulderLast {
if SceneName == "Play" {
// Toggle between GameplayMode and MouseMode.
if p1mode == GameplayMode {
p1mode = MouseMode
} else {
p1mode = GameplayMode
}
// Reset all button states.
ev.Left = false
ev.Right = false
ev.Up = false
ev.Down = false
ev.Enter = false
ev.Space = false
}
rightShoulderLast = true
return
}
} else if rightShoulderLast {
rightShoulderLast = false
}
// If we are in Play Mode, translate gamepad events into key events.
if p1mode == GameplayMode {
// D-Pad controls to move the player character.
if ctrl.ButtonLeft() {
ev.Left = true
dpLeftLast = true
} else if dpLeftLast {
ev.Left = false
dpLeftLast = false
}
if ctrl.ButtonRight() {
ev.Right = true
dpRightLast = true
} else if dpRightLast {
ev.Right = false
dpRightLast = false
}
// Antigravity on? Up/Down arrow emulation.
if PlayModeAntigravity {
if ctrl.ButtonUp() {
ev.Up = true
dpUpLast = true
} else if dpUpLast {
ev.Up = false
dpUpLast = false
}
if ctrl.ButtonDown() {
ev.Down = true
dpDownLast = true
} else if dpDownLast {
ev.Down = false
dpDownLast = false
}
}
// "Use" button.
if PrimaryButton(ctrl) {
ev.Space = true
ev.Enter = true // to click thru modals
leftClickLast = true
} else if leftClickLast {
ev.Space = false
ev.Enter = false
leftClickLast = false
}
// Jump button.
if SecondaryButton(ctrl) {
ev.Up = true
rightClickLast = true
} else if rightClickLast {
ev.Up = false
rightClickLast = false
}
// Left control stick to move the player character.
// TODO: analog movements.
leftStick := ctrl.LeftStick()
if leftStick.X != 0 {
if leftStick.X < -balance.GameControllerScrollMin {
ev.Left = true
ev.Right = false
} else if leftStick.X > balance.GameControllerScrollMin {
ev.Right = true
ev.Left = false
} else {
ev.Right = false
ev.Left = false
}
} else if leftStickLast.X != 0 {
ev.Left = false
ev.Right = false
}
// Antigravity on?
if PlayModeAntigravity {
if leftStick.Y != 0 {
if leftStick.Y < -balance.GameControllerScrollMin {
ev.Up = true
ev.Down = false
} else if leftStick.Y > balance.GameControllerScrollMin {
ev.Down = true
ev.Up = false
} else {
ev.Down = false
ev.Up = false
}
} else if leftStickLast.Y != 0 {
ev.Up = false
ev.Down = false
}
}
leftStickLast = leftStick
}
// If we are emulating a mouse, handle that now.
if p1mode == MouseMode {
// Move the cursor.
leftStick := ctrl.LeftStick()
if leftStick.X != 0 || leftStick.Y != 0 {
cursorVisible = true
} else if cursorVisible {
// If the mouse cursor has moved behind our back (e.g., real mouse moved), turn off
// the MouseMode cursor sprite.
if cursorLast.X != ev.CursorX || cursorLast.Y != ev.CursorY {
cursorVisible = false
}
}
ev.CursorX += int(leftStick.X * balance.GameControllerMouseMoveMax)
ev.CursorY += int(leftStick.Y * balance.GameControllerMouseMoveMax)
// Constrain the cursor inside window boundaries.
w, h := shmem.CurrentRenderEngine.WindowSize()
if ev.CursorX < 0 {
ev.CursorX = 0
} else if ev.CursorX > w {
ev.CursorX = w
}
if ev.CursorY < 0 {
ev.CursorY = 0
} else if ev.CursorY > h {
ev.CursorY = h
}
// Store last cursor point so we can detect mouse movement outside the gamepad.
cursorLast = render.NewPoint(ev.CursorX, ev.CursorY)
// Are we clicking?
if PrimaryButton(ctrl) {
ev.Button1 = true // left-click
leftClickLast = true
} else if leftClickLast {
ev.Button1 = false
leftClickLast = false
}
// Right-click
if SecondaryButton(ctrl) {
ev.Button3 = true // right-click
rightClickLast = true
} else if rightClickLast {
ev.Button3 = false
rightClickLast = false
}
// Middle-click
if ctrl.ButtonL1() {
ev.Button2 = true // middle click
middleClickLast = true
} else if middleClickLast {
ev.Button2 = false
middleClickLast = false
}
// Left Trigger = Backspace (close active window)
if ctrl.ButtonL2() {
if !leftTriggerLast {
ev.SetKeyDown(`\b`, true)
}
leftTriggerLast = true
} else if leftTriggerLast {
ev.SetKeyDown(`\b`, false)
leftTriggerLast = false
}
// Arrow Key emulation on the right control stick, e.g. for Level Editor.
rightStick := ctrl.RightStick()
if rightStick.X != 0 {
if rightStick.X < -balance.GameControllerScrollMin {
ev.Left = true
ev.Right = false
} else if rightStick.X > balance.GameControllerScrollMin {
ev.Right = true
ev.Left = false
} else {
ev.Right = false
ev.Left = false
}
} else if rightStickLast.X != 0 {
ev.Left = false
ev.Right = false
}
if rightStick.Y != 0 {
if rightStick.Y < -balance.GameControllerScrollMin {
ev.Up = true
ev.Down = false
} else if rightStick.Y > balance.GameControllerScrollMin {
ev.Down = true
ev.Up = false
} else {
ev.Down = false
ev.Up = false
}
} else if rightStickLast.Y != 0 {
ev.Up = false
ev.Down = false
}
rightStickLast = rightStick
}
}
// Draw the cursor on screen if the game controller is emulating a mouse.
func Draw(e render.Engine) {
if playerOne == nil || p1mode != MouseMode || !cursorVisible {
return
}
if cursorSprite == nil {
img, err := sprites.LoadImage(e, balance.CursorIcon)
if err != nil {
log.Error("gamepad: couldn't load cursor sprite (%s): %s", balance.CursorIcon, err)
return
}
cursorSprite = img
}
cursorSprite.Present(e, shmem.Cursor)
}