Noah Petherbridge
0fc046250e
* 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
396 lines
9.6 KiB
Go
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)
|
|
}
|