Game Controller Support
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.
This commit is contained in:
parent
626fd53a84
commit
4de0126b19
37
README.md
37
README.md
|
@ -97,6 +97,43 @@ Ctrl-Y
|
||||||
Redo
|
Redo
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Gamepad Controls
|
||||||
|
|
||||||
|
The game supports Xbox and Nintendo style game controllers. The button
|
||||||
|
bindings are not yet customizable, except to choose between the
|
||||||
|
"X Style" or "N Style" for A/B and X/Y button mappings.
|
||||||
|
|
||||||
|
Gamepad controls very depending on two modes the game can be in:
|
||||||
|
|
||||||
|
## Mouse Mode
|
||||||
|
|
||||||
|
The Gamepad emulates a mouse cursor in this mode.
|
||||||
|
|
||||||
|
* The left analog stick moves a cursor around the screen.
|
||||||
|
* The right analog stick scrolls the level (title screen and editor)
|
||||||
|
* A or X button simulates a Left-click
|
||||||
|
* B or Y button simulates a Right-click
|
||||||
|
* L1 (left shoulder) emulates a Middle-click
|
||||||
|
* L2 (left trigger) closes the top-most window in the editor mode
|
||||||
|
(like the Backspace key).
|
||||||
|
|
||||||
|
## Gameplay Mode
|
||||||
|
|
||||||
|
When playing a level, the controls are as follows:
|
||||||
|
|
||||||
|
* The left analog stick and the D-Pad will move the player character.
|
||||||
|
* A or X button to "Use" objects such as Warp Doors.
|
||||||
|
* B or Y button to "Jump"
|
||||||
|
* R1 (right shoulder) toggles between Mouse Mode and Gameplay Mode.
|
||||||
|
|
||||||
|
You can use the R1 button to access Mouse Mode to interact with the
|
||||||
|
menus or click on the "Edit Level" button.
|
||||||
|
|
||||||
|
Note: characters with antigravity (such as the Bird) can move in all
|
||||||
|
four directions but characters with gravity only move left and right
|
||||||
|
and have the dedicated "Jump" button. This differs from regular
|
||||||
|
keyboard controls where the Up arrow is to Jump.
|
||||||
|
|
||||||
# Built-In Doodads
|
# Built-In Doodads
|
||||||
|
|
||||||
A brief introduction to the built-in doodads available so far:
|
A brief introduction to the built-in doodads available so far:
|
||||||
|
|
BIN
assets/sprites/pointer.png
Normal file
BIN
assets/sprites/pointer.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 729 B |
|
@ -16,6 +16,7 @@ import (
|
||||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||||
"git.kirsle.net/apps/doodle/pkg/branding"
|
"git.kirsle.net/apps/doodle/pkg/branding"
|
||||||
"git.kirsle.net/apps/doodle/pkg/chatbot"
|
"git.kirsle.net/apps/doodle/pkg/chatbot"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/gamepad"
|
||||||
"git.kirsle.net/apps/doodle/pkg/license"
|
"git.kirsle.net/apps/doodle/pkg/license"
|
||||||
"git.kirsle.net/apps/doodle/pkg/log"
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
"git.kirsle.net/apps/doodle/pkg/shmem"
|
"git.kirsle.net/apps/doodle/pkg/shmem"
|
||||||
|
@ -25,6 +26,7 @@ import (
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
"git.kirsle.net/go/render/sdl"
|
"git.kirsle.net/go/render/sdl"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
sdl2 "github.com/veandco/go-sdl2/sdl"
|
||||||
|
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
)
|
)
|
||||||
|
@ -70,6 +72,9 @@ func main() {
|
||||||
usercfg.Current.CrosshairColor = balance.DefaultCrosshairColor
|
usercfg.Current.CrosshairColor = balance.DefaultCrosshairColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set GameController style.
|
||||||
|
gamepad.SetStyle(gamepad.Style(usercfg.Current.ControllerStyle))
|
||||||
|
|
||||||
app.Version = fmt.Sprintf("%s build %s%s. Built on %s",
|
app.Version = fmt.Sprintf("%s build %s%s. Built on %s",
|
||||||
branding.Version,
|
branding.Version,
|
||||||
Build,
|
Build,
|
||||||
|
@ -149,6 +154,9 @@ func main() {
|
||||||
balance.Height,
|
balance.Height,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Activate game controller event support.
|
||||||
|
sdl2.GameControllerEventState(1)
|
||||||
|
|
||||||
// Load the SDL fonts in from bindata storage.
|
// Load the SDL fonts in from bindata storage.
|
||||||
if fonts, err := assets.AssetDir("assets/fonts"); err == nil {
|
if fonts, err := assets.AssetDir("assets/fonts"); err == nil {
|
||||||
for _, file := range fonts {
|
for _, file := range fonts {
|
||||||
|
|
|
@ -92,6 +92,10 @@ var (
|
||||||
// Invulnerability time in seconds at respawn from checkpoint, in case
|
// Invulnerability time in seconds at respawn from checkpoint, in case
|
||||||
// enemies are spawn camping.
|
// enemies are spawn camping.
|
||||||
RespawnGodModeTimer = 3 * time.Second
|
RespawnGodModeTimer = 3 * time.Second
|
||||||
|
|
||||||
|
// GameController thresholds.
|
||||||
|
GameControllerMouseMoveMax float64 = 20 // Max pixels per tick to simulate mouse movement.
|
||||||
|
GameControllerScrollMin float64 = 0.3 // Minimum threshold for a right-stick scroll event.
|
||||||
)
|
)
|
||||||
|
|
||||||
// Edit Mode Values
|
// Edit Mode Values
|
||||||
|
|
|
@ -13,6 +13,7 @@ var (
|
||||||
GoldCoin = "assets/sprites/gold.png"
|
GoldCoin = "assets/sprites/gold.png"
|
||||||
SilverCoin = "assets/sprites/silver.png"
|
SilverCoin = "assets/sprites/silver.png"
|
||||||
LockIcon = "assets/sprites/padlock.png"
|
LockIcon = "assets/sprites/padlock.png"
|
||||||
|
CursorIcon = "assets/sprites/pointer.png"
|
||||||
|
|
||||||
// Title Screen Font
|
// Title Screen Font
|
||||||
TitleScreenFont = render.Text{
|
TitleScreenFont = render.Text{
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||||
"git.kirsle.net/apps/doodle/pkg/branding"
|
"git.kirsle.net/apps/doodle/pkg/branding"
|
||||||
"git.kirsle.net/apps/doodle/pkg/enum"
|
"git.kirsle.net/apps/doodle/pkg/enum"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/gamepad"
|
||||||
"git.kirsle.net/apps/doodle/pkg/keybind"
|
"git.kirsle.net/apps/doodle/pkg/keybind"
|
||||||
"git.kirsle.net/apps/doodle/pkg/levelpack"
|
"git.kirsle.net/apps/doodle/pkg/levelpack"
|
||||||
"git.kirsle.net/apps/doodle/pkg/log"
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
|
@ -131,7 +132,6 @@ func (d *Doodle) Run() error {
|
||||||
|
|
||||||
// Poll for events.
|
// Poll for events.
|
||||||
ev, err := d.Engine.Poll()
|
ev, err := d.Engine.Poll()
|
||||||
shmem.Cursor = render.NewPoint(ev.CursorX, ev.CursorY)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("event poll error: %s", err)
|
log.Error("event poll error: %s", err)
|
||||||
d.running = false
|
d.running = false
|
||||||
|
@ -139,6 +139,13 @@ func (d *Doodle) Run() error {
|
||||||
}
|
}
|
||||||
d.event = ev
|
d.event = ev
|
||||||
|
|
||||||
|
// Let the gamepad controller check for events, if it's in MouseMode
|
||||||
|
// it will fake the mouse cursor.
|
||||||
|
gamepad.Loop(ev)
|
||||||
|
|
||||||
|
// Globally store the cursor position.
|
||||||
|
shmem.Cursor = render.NewPoint(ev.CursorX, ev.CursorY)
|
||||||
|
|
||||||
// Command line shell.
|
// Command line shell.
|
||||||
if d.shell.Open {
|
if d.shell.Open {
|
||||||
} else if keybind.ShellKey(ev) {
|
} else if keybind.ShellKey(ev) {
|
||||||
|
@ -196,6 +203,9 @@ func (d *Doodle) Run() error {
|
||||||
// Draw the debug overlay over all scenes.
|
// Draw the debug overlay over all scenes.
|
||||||
d.DrawDebugOverlay()
|
d.DrawDebugOverlay()
|
||||||
|
|
||||||
|
// Let the gamepad controller draw in case of MouseMode to show the cursor.
|
||||||
|
gamepad.Draw(d.Engine)
|
||||||
|
|
||||||
// Render the pixels to the screen.
|
// Render the pixels to the screen.
|
||||||
err = d.Engine.Present()
|
err = d.Engine.Present()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -246,6 +256,7 @@ func (d *Doodle) MakeSettingsWindow(supervisor *ui.Supervisor) *ui.Window {
|
||||||
CrosshairColor: &usercfg.Current.CrosshairColor,
|
CrosshairColor: &usercfg.Current.CrosshairColor,
|
||||||
HideTouchHints: &usercfg.Current.HideTouchHints,
|
HideTouchHints: &usercfg.Current.HideTouchHints,
|
||||||
DisableAutosave: &usercfg.Current.DisableAutosave,
|
DisableAutosave: &usercfg.Current.DisableAutosave,
|
||||||
|
ControllerStyle: &usercfg.Current.ControllerStyle,
|
||||||
}
|
}
|
||||||
return windows.MakeSettingsWindow(d.width, d.height, cfg)
|
return windows.MakeSettingsWindow(d.width, d.height, cfg)
|
||||||
}
|
}
|
||||||
|
|
29
pkg/gamepad/enum.go
Normal file
29
pkg/gamepad/enum.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package gamepad
|
||||||
|
|
||||||
|
// Enums and constants.
|
||||||
|
|
||||||
|
// Mode of controller behavior (Player One)
|
||||||
|
type Mode int
|
||||||
|
|
||||||
|
// Style of controller (Player One)
|
||||||
|
type Style int
|
||||||
|
|
||||||
|
// Controller mode options.
|
||||||
|
const (
|
||||||
|
// MouseMode: the joystick moves a mouse cursor around and
|
||||||
|
// the face buttons emulate mouse click events.
|
||||||
|
MouseMode Mode = iota
|
||||||
|
|
||||||
|
// GameplayMode: to control the player character during Play Mode.
|
||||||
|
GameplayMode
|
||||||
|
|
||||||
|
// EditorMode: to support the Level Editor.
|
||||||
|
EditorMode
|
||||||
|
)
|
||||||
|
|
||||||
|
// Controller style options.
|
||||||
|
const (
|
||||||
|
XStyle Style = iota // Xbox 360 layout (A button on bottom)
|
||||||
|
NStyle // Nintendo style (A button on right)
|
||||||
|
CustomStyle // Custom style (TODO)
|
||||||
|
)
|
394
pkg/gamepad/gamepad.go
Normal file
394
pkg/gamepad/gamepad.go
Normal file
|
@ -0,0 +1,394 @@
|
||||||
|
/*
|
||||||
|
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)
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||||
"git.kirsle.net/apps/doodle/pkg/collision"
|
"git.kirsle.net/apps/doodle/pkg/collision"
|
||||||
"git.kirsle.net/apps/doodle/pkg/doodads"
|
"git.kirsle.net/apps/doodle/pkg/doodads"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/gamepad"
|
||||||
"git.kirsle.net/apps/doodle/pkg/keybind"
|
"git.kirsle.net/apps/doodle/pkg/keybind"
|
||||||
"git.kirsle.net/apps/doodle/pkg/level"
|
"git.kirsle.net/apps/doodle/pkg/level"
|
||||||
"git.kirsle.net/apps/doodle/pkg/levelpack"
|
"git.kirsle.net/apps/doodle/pkg/levelpack"
|
||||||
|
@ -282,6 +283,9 @@ func (s *PlayScene) setupAsync(d *Doodle) error {
|
||||||
// runtime, + the bitmap generation is pretty wicked fast anyway.
|
// runtime, + the bitmap generation is pretty wicked fast anyway.
|
||||||
loadscreen.PreloadAllChunkBitmaps(s.Level.Chunker)
|
loadscreen.PreloadAllChunkBitmaps(s.Level.Chunker)
|
||||||
|
|
||||||
|
// Gamepad: put into GameplayMode.
|
||||||
|
gamepad.SetMode(gamepad.GameplayMode)
|
||||||
|
|
||||||
s.startTime = time.Now()
|
s.startTime = time.Now()
|
||||||
s.perfectRun = true
|
s.perfectRun = true
|
||||||
s.running = true
|
s.running = true
|
||||||
|
@ -416,6 +420,7 @@ func (s *PlayScene) installPlayerDoodad(filename string, spawn render.Point, cen
|
||||||
// EditLevel toggles out of Play Mode to edit the level.
|
// EditLevel toggles out of Play Mode to edit the level.
|
||||||
func (s *PlayScene) EditLevel() {
|
func (s *PlayScene) EditLevel() {
|
||||||
log.Info("Edit Mode, Go!")
|
log.Info("Edit Mode, Go!")
|
||||||
|
gamepad.SetMode(gamepad.MouseMode)
|
||||||
s.d.Goto(&EditorScene{
|
s.d.Goto(&EditorScene{
|
||||||
Filename: s.Filename,
|
Filename: s.Filename,
|
||||||
Level: s.Level,
|
Level: s.Level,
|
||||||
|
@ -518,6 +523,7 @@ func (s *PlayScene) ShowEndLevelModal(success bool, title, message string) {
|
||||||
OnRestartLevel: s.RestartLevel,
|
OnRestartLevel: s.RestartLevel,
|
||||||
OnRetryCheckpoint: s.RetryCheckpoint,
|
OnRetryCheckpoint: s.RetryCheckpoint,
|
||||||
OnExitToMenu: func() {
|
OnExitToMenu: func() {
|
||||||
|
gamepad.SetMode(gamepad.MouseMode)
|
||||||
s.d.Goto(&MainScene{})
|
s.d.Goto(&MainScene{})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -589,6 +595,9 @@ func (s *PlayScene) Loop(d *Doodle, ev *event.State) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inform the gamepad controller whether we have antigravity controls.
|
||||||
|
gamepad.PlayModeAntigravity = s.antigravity || !s.Player.HasGravity()
|
||||||
|
|
||||||
// Update debug overlay values.
|
// Update debug overlay values.
|
||||||
*s.debWorldIndex = s.drawing.WorldIndexAt(render.NewPoint(ev.CursorX, ev.CursorY)).String()
|
*s.debWorldIndex = s.drawing.WorldIndexAt(render.NewPoint(ev.CursorX, ev.CursorY)).String()
|
||||||
*s.debPosition = s.Player.Position().String() + " vel " + s.Player.Velocity().String()
|
*s.debPosition = s.Player.Position().String() + " vel " + s.Player.Velocity().String()
|
||||||
|
@ -614,6 +623,7 @@ func (s *PlayScene) Loop(d *Doodle, ev *event.State) error {
|
||||||
|
|
||||||
// Switching to Edit Mode?
|
// Switching to Edit Mode?
|
||||||
if s.CanEdit && keybind.GotoEdit(ev) {
|
if s.CanEdit && keybind.GotoEdit(ev) {
|
||||||
|
gamepad.SetMode(gamepad.MouseMode)
|
||||||
s.EditLevel()
|
s.EditLevel()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package doodle
|
package doodle
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/gamepad"
|
||||||
"git.kirsle.net/apps/doodle/pkg/log"
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
"git.kirsle.net/go/render/event"
|
"git.kirsle.net/go/render/event"
|
||||||
)
|
)
|
||||||
|
@ -23,6 +24,9 @@ type Scene interface {
|
||||||
|
|
||||||
// Goto a scene. First it unloads the current scene.
|
// Goto a scene. First it unloads the current scene.
|
||||||
func (d *Doodle) Goto(scene Scene) error {
|
func (d *Doodle) Goto(scene Scene) error {
|
||||||
|
// Inform the gamepad controller what scene.
|
||||||
|
gamepad.SceneName = scene.Name()
|
||||||
|
|
||||||
// Clear any debug labels.
|
// Clear any debug labels.
|
||||||
customDebugLabels = []debugLabel{}
|
customDebugLabels = []debugLabel{}
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,7 @@ type Field struct {
|
||||||
BoolVariable *bool // Checkbox
|
BoolVariable *bool // Checkbox
|
||||||
TextVariable *string // Textbox
|
TextVariable *string // Textbox
|
||||||
Options []Option // Selectbox
|
Options []Option // Selectbox
|
||||||
|
SelectValue interface{} // Selectbox default choice
|
||||||
|
|
||||||
// Tooltip to add to a form control.
|
// Tooltip to add to a form control.
|
||||||
// Checkbox only for now.
|
// Checkbox only for now.
|
||||||
|
@ -210,6 +211,7 @@ func (form Form) Create(into *ui.Frame, fields []Field) {
|
||||||
frame.Pack(btn, ui.Pack{
|
frame.Pack(btn, ui.Pack{
|
||||||
Side: ui.W,
|
Side: ui.W,
|
||||||
FillX: true,
|
FillX: true,
|
||||||
|
Expand: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
if row.Options != nil {
|
if row.Options != nil {
|
||||||
|
@ -218,7 +220,11 @@ func (form Form) Create(into *ui.Frame, fields []Field) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
btn.Handle(ui.Click, func(ed ui.EventData) error {
|
if row.SelectValue != nil {
|
||||||
|
btn.SetValue(row.SelectValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.Handle(ui.Change, func(ed ui.EventData) error {
|
||||||
if selection, ok := btn.GetValue(); ok {
|
if selection, ok := btn.GetValue(); ok {
|
||||||
if row.OnSelect != nil {
|
if row.OnSelect != nil {
|
||||||
row.OnSelect(selection.Value)
|
row.OnSelect(selection.Value)
|
||||||
|
@ -227,6 +233,7 @@ func (form Form) Create(into *ui.Frame, fields []Field) {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
btn.Supervise(form.Supervisor)
|
||||||
form.Supervisor.Add(btn)
|
form.Supervisor.Add(btn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ type Settings struct {
|
||||||
CrosshairColor render.Color
|
CrosshairColor render.Color
|
||||||
HideTouchHints bool `json:",omitempty"`
|
HideTouchHints bool `json:",omitempty"`
|
||||||
DisableAutosave bool `json:",omitempty"`
|
DisableAutosave bool `json:",omitempty"`
|
||||||
|
ControllerStyle int
|
||||||
|
|
||||||
// Secret boolprops from balance/boolprops.go
|
// Secret boolprops from balance/boolprops.go
|
||||||
ShowHiddenDoodads bool `json:",omitempty"`
|
ShowHiddenDoodads bool `json:",omitempty"`
|
||||||
|
|
|
@ -5,9 +5,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/gamepad"
|
||||||
"git.kirsle.net/apps/doodle/pkg/log"
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
"git.kirsle.net/apps/doodle/pkg/native"
|
"git.kirsle.net/apps/doodle/pkg/native"
|
||||||
"git.kirsle.net/apps/doodle/pkg/shmem"
|
"git.kirsle.net/apps/doodle/pkg/shmem"
|
||||||
|
magicform "git.kirsle.net/apps/doodle/pkg/uix/magic-form"
|
||||||
"git.kirsle.net/apps/doodle/pkg/usercfg"
|
"git.kirsle.net/apps/doodle/pkg/usercfg"
|
||||||
"git.kirsle.net/apps/doodle/pkg/userdir"
|
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
|
@ -30,6 +32,7 @@ type Settings struct {
|
||||||
CrosshairColor *render.Color
|
CrosshairColor *render.Color
|
||||||
HideTouchHints *bool
|
HideTouchHints *bool
|
||||||
DisableAutosave *bool
|
DisableAutosave *bool
|
||||||
|
ControllerStyle *int
|
||||||
|
|
||||||
// Configuration options.
|
// Configuration options.
|
||||||
SceneName string // name of scene which called this window
|
SceneName string // name of scene which called this window
|
||||||
|
@ -81,6 +84,7 @@ func NewSettingsWindow(cfg Settings) *ui.Window {
|
||||||
// Make the tabs
|
// Make the tabs
|
||||||
cfg.makeOptionsTab(tabFrame, Width, Height)
|
cfg.makeOptionsTab(tabFrame, Width, Height)
|
||||||
cfg.makeControlsTab(tabFrame, Width, Height)
|
cfg.makeControlsTab(tabFrame, Width, Height)
|
||||||
|
cfg.makeControllerTab(tabFrame, Width, Height)
|
||||||
cfg.makeExperimentalTab(tabFrame, Width, Height)
|
cfg.makeExperimentalTab(tabFrame, Width, Height)
|
||||||
|
|
||||||
tabFrame.Supervise(cfg.Supervisor)
|
tabFrame.Supervise(cfg.Supervisor)
|
||||||
|
@ -721,3 +725,82 @@ func (c Settings) makeExperimentalTab(tabFrame *ui.TabFrame, Width, Height int)
|
||||||
|
|
||||||
return tab
|
return tab
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Settings Window "Controller" Tab
|
||||||
|
func (c Settings) makeControllerTab(tabFrame *ui.TabFrame, Width, Height int) *ui.Frame {
|
||||||
|
tab := tabFrame.AddTab("Gamepad", ui.NewLabel(ui.Label{
|
||||||
|
Text: "Gamepad",
|
||||||
|
Font: balance.TabFont,
|
||||||
|
}))
|
||||||
|
tab.Resize(render.NewRect(Width-4, Height-tab.Size().H-46))
|
||||||
|
|
||||||
|
// Render the form.
|
||||||
|
form := magicform.Form{
|
||||||
|
Supervisor: c.Supervisor,
|
||||||
|
Engine: c.Engine,
|
||||||
|
Vertical: true,
|
||||||
|
LabelWidth: 150,
|
||||||
|
}
|
||||||
|
form.Create(tab, []magicform.Field{
|
||||||
|
{
|
||||||
|
Label: "About",
|
||||||
|
Font: balance.LabelFont,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Play Sketchy Maze with an Xbox or Nintendo controller!\n\n" +
|
||||||
|
"Full customization options aren't here yet, but you can\n" +
|
||||||
|
"choose between the 'X Style' or 'N Style' profile below.\n" +
|
||||||
|
"'N Style' will swap the A/B and X/Y buttons.",
|
||||||
|
Font: balance.UIFont,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Button Style:",
|
||||||
|
Font: balance.LabelFont,
|
||||||
|
Type: magicform.Selectbox,
|
||||||
|
Options: []magicform.Option{
|
||||||
|
{
|
||||||
|
Label: "X Style (default)",
|
||||||
|
Value: int(gamepad.XStyle),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "N Style",
|
||||||
|
Value: int(gamepad.NStyle),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SelectValue: *c.ControllerStyle,
|
||||||
|
OnSelect: func(v interface{}) {
|
||||||
|
style, _ := v.(int)
|
||||||
|
log.Error("style: %d", style)
|
||||||
|
gamepad.SetStyle(gamepad.Style(style))
|
||||||
|
saveGameSettings()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "\nThe gamepad controls vary between two modes:",
|
||||||
|
Font: balance.UIFont,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Mouse Mode (outside of gameplay)",
|
||||||
|
Font: balance.LabelFont,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "The left analog stick moves a mouse cursor around.\n" +
|
||||||
|
"The right analog stick scrolls the level around.\n" +
|
||||||
|
"A or X: Left-click B or Y: Right-click\n" +
|
||||||
|
"L1: Middle-click L2: Close window",
|
||||||
|
Font: balance.UIFont,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Gameplay Mode",
|
||||||
|
Font: balance.LabelFont,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Left stick or D-Pad to move the player around.\n" +
|
||||||
|
"A or X: 'Use' B or Y: 'Jump'\n" +
|
||||||
|
"R1: Toggle between Mouse and Gameplay controls.",
|
||||||
|
Font: balance.UIFont,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return tab
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user