Noah Petherbridge
4de0126b19
Adds support for Xbox and Nintendo style game controllers. The gamepad controls are documented on the README and in the game's Settings window. The buttons are not customizable yet, except that the player can choose between two button styles: * X Style (default): "A" button is on the bottom and "B" on the right. * N Style: swaps the A/B and the X/Y buttons to use a Nintendo-style layout instead of an Xbox-style.
395 lines
9.6 KiB
Go
395 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)
|
|
}
|
|
} 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 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)
|
|
}
|