├── .github └── workflows │ └── go.yml ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── _examples ├── action_released │ └── main.go ├── basic │ └── main.go ├── configfile │ ├── keymap.json │ └── main.go ├── gamepad_in_browser │ └── main.go ├── modkeys │ └── main.go ├── mousedrag │ └── main.go ├── press_any_key │ └── main.go ├── remap │ └── main.go ├── scroll │ └── main.go ├── simulateinput │ └── main.go ├── smooth_movement │ └── main.go └── touchscroll │ └── main.go ├── _scripts └── gen_key_list │ └── main.go ├── event.go ├── go.mod ├── go.sum ├── handler.go ├── input.go ├── input_test.go ├── internal_bool3.go ├── internal_firefox.go ├── internal_firefox_js.go ├── internal_gamepad_utils.go ├── internal_key.go ├── internal_key_list.go ├── internal_min.go ├── key.go ├── math_gmath.go ├── math_nodeps.go ├── parse_key.go ├── scan_key.go ├── scan_key_test.go └── system.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This file adheres to the YAML5 style. 2 | { 3 | "name": "Go", 4 | "on": ["push", "pull_request"], 5 | "jobs": { 6 | "build": { 7 | "name": "Build", 8 | "runs-on": "ubuntu-latest", 9 | "env": { 10 | "DISPLAY": ":99.0", 11 | }, 12 | "steps": [ 13 | { 14 | "name": "Set up Go 1.19", 15 | "uses": "actions/setup-go@v1", 16 | "with": {"go-version": 1.19}, 17 | "id": "go", 18 | }, 19 | { 20 | "name": "Set up dependencies", 21 | "run": "sudo -E apt-add-repository -y 'ppa:ubuntu-toolchain-r' && sudo apt-get install libasound2-dev libglew-dev libgles2-mesa-dev libalut-dev libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev libxxf86vm-dev", 22 | }, 23 | {"name": "Xvfb", "run": "Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &"}, 24 | {"name": "Check out code into the Go module directory", "uses": "actions/checkout@v1"}, 25 | {"name": "Test", "run": "make test"}, 26 | {"name": "Linter", "run": "make lint"}, 27 | ], 28 | }, 29 | }, 30 | } -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | { 2 | "run": { 3 | # timeout for analysis, e.g. 30s, 5m, default is 1m 4 | "deadline": "3m", 5 | }, 6 | "fast": false, 7 | "linters": { 8 | "enable": [ 9 | "errcheck", 10 | "gas", 11 | "gocritic", 12 | "gofmt", 13 | "goimports", 14 | "revive", 15 | "govet", 16 | "gosimple", 17 | "ineffassign", 18 | "megacheck", 19 | "misspell", 20 | "nakedret", 21 | "staticcheck", 22 | "unconvert", 23 | "unparam", 24 | "unused", 25 | ], 26 | }, 27 | "disable": [ 28 | "depguard", 29 | "dupl", 30 | "gocyclo", 31 | "interfacer", 32 | "lll", 33 | "maligned", 34 | "prealloc", 35 | ], 36 | "linters-settings": { 37 | "gocritic": { 38 | "disable-all": true, 39 | "enabled-checks": [ 40 | "commentedOutCode", 41 | ], 42 | }, 43 | }, 44 | } 45 | 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Iskander (Alex) Sharipov / quasilyte 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOPATH_DIR=`go env GOPATH` 2 | 3 | .PHONY: test 4 | test: 5 | go test -count 2 -v -race ./... 6 | 7 | .PHONY: lint 8 | lint: 9 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH_DIR)/bin v1.57.2 10 | $(GOPATH_DIR)/bin/golangci-lint run ./... 11 | $(GOPATH_DIR)/bin/golangci-lint run --build-tags example ./_examples/basic 12 | $(GOPATH_DIR)/bin/golangci-lint run --build-tags example ./_examples/configfile 13 | $(GOPATH_DIR)/bin/golangci-lint run --build-tags example ./_examples/gamepad_in_browser 14 | $(GOPATH_DIR)/bin/golangci-lint run --build-tags example ./_examples/modkeys 15 | $(GOPATH_DIR)/bin/golangci-lint run --build-tags example ./_examples/scroll 16 | $(GOPATH_DIR)/bin/golangci-lint run --build-tags example ./_examples/simulateinput 17 | $(GOPATH_DIR)/bin/golangci-lint run --build-tags example ./_examples/action_released 18 | @echo "everything is OK" 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Ebitengine input library 2 | 3 | ![Build Status](https://github.com/quasilyte/ebitengine-input/workflows/Go/badge.svg) 4 | [![PkgGoDev](https://pkg.go.dev/badge/mod/github.com/quasilyte/ebitengine-input)](https://pkg.go.dev/mod/github.com/quasilyte/ebitengine-input) 5 | 6 | ### Overview 7 | 8 | A [Godot](https://godotengine.org/)-inspired action input handling system for [Ebitengine](https://github.com/hajimehoshi/ebiten). 9 | 10 | **Key features:** 11 | 12 | * [Actions](https://docs.godotengine.org/en/stable/tutorials/inputs/inputevent.html#actions) paradigm instead of the raw input events 13 | * Configurable keymaps 14 | * Bind more than one key to a single action 15 | * Bind keys with modifiers to a single action (like `ctrl+c`) 16 | * Simplified multi-input handling (like multiple gamepads) 17 | * Implements keybind scanning (see [remap](_examples/remap/main.go) example) 18 | * Simplified keymap loading from a file (see [configfile](_examples/configfile/main.go) example) 19 | * Implements simulated/virtual input events (see [simulateinput](_examples/simulateinput/main.go) example) 20 | * No extra dependencies (apart from the [Ebitengine](https://github.com/hajimehoshi/ebiten) of course) 21 | * Solves some issues related to gamepads in browsers 22 | * Wheel/scroll as action events 23 | * Motion-style events, like "gamepad stick just moved" (see [smooth_movement](_examples/smooth_movement/main.go) example) 24 | * Can be used without extra deps or with [gmath](https://github.com/quasilyte/gmath) integration 25 | 26 | This library may require some extra docs, code comments and examples. You can significantly help me by providing those. Pointing out what is currently missing is helpful too! 27 | 28 | Some games that were built with this library: 29 | 30 | * [Roboden](https://quasilyte.itch.io/roboden) 31 | * [Cavebots](https://quasilyte.itch.io/cavebots) 32 | * [Assemblox](https://quasilyte.itch.io/assemblox) 33 | * [Decipherism](https://quasilyte.itch.io/decipherism) 34 | * [Retrowave City](https://quasilyte.itch.io/retrowave-city) 35 | * [Autotanks](https://quasilyte.itch.io/autotanks) 36 | * [Sinecord](https://quasilyte.itch.io/sinecord) 37 | * [Learn Georgian](https://quasilyte.itch.io/georgian-trainer) 38 | 39 | ### Installation 40 | 41 | ```bash 42 | go get github.com/quasilyte/ebitengine-input 43 | ``` 44 | 45 | A runnable [example](_examples/basic/main.go) is available: 46 | 47 | ```bash 48 | git clone https://github.com/quasilyte/ebitengine-input.git 49 | cd ebitengine-input 50 | go run ./_examples/basic/main.go 51 | ``` 52 | 53 | ### Quick Start 54 | 55 | ```go 56 | package main 57 | 58 | import ( 59 | "image" 60 | "log" 61 | 62 | "github.com/hajimehoshi/ebiten/v2" 63 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 64 | input "github.com/quasilyte/ebitengine-input" 65 | ) 66 | 67 | const ( 68 | ActionMoveLeft input.Action = iota 69 | ActionMoveRight 70 | ) 71 | 72 | func main() { 73 | ebiten.SetWindowSize(640, 480) 74 | if err := ebiten.RunGame(newExampleGame()); err != nil { 75 | log.Fatal(err) 76 | } 77 | } 78 | 79 | type exampleGame struct { 80 | p *player 81 | inputSystem input.System 82 | } 83 | 84 | func newExampleGame() *exampleGame { 85 | g := &exampleGame{} 86 | g.inputSystem.Init(input.SystemConfig{ 87 | DevicesEnabled: input.AnyDevice, 88 | }) 89 | keymap := input.Keymap{ 90 | ActionMoveLeft: {input.KeyGamepadLeft, input.KeyLeft, input.KeyA}, 91 | ActionMoveRight: {input.KeyGamepadRight, input.KeyRight, input.KeyD}, 92 | } 93 | g.p = &player{ 94 | input: g.inputSystem.NewHandler(0, keymap), 95 | pos: image.Point{X: 96, Y: 96}, 96 | } 97 | return g 98 | } 99 | 100 | func (g *exampleGame) Layout(outsideWidth, outsideHeight int) (int, int) { 101 | return 640, 480 102 | } 103 | 104 | func (g *exampleGame) Draw(screen *ebiten.Image) { 105 | g.p.Draw(screen) 106 | } 107 | 108 | func (g *exampleGame) Update() error { 109 | g.inputSystem.Update() 110 | g.p.Update() 111 | return nil 112 | } 113 | 114 | type player struct { 115 | input *input.Handler 116 | pos image.Point 117 | } 118 | 119 | func (p *player) Update() { 120 | if p.input.ActionIsPressed(ActionMoveLeft) { 121 | p.pos.X -= 4 122 | } 123 | if p.input.ActionIsPressed(ActionMoveRight) { 124 | p.pos.X += 4 125 | } 126 | } 127 | 128 | func (p *player) Draw(screen *ebiten.Image) { 129 | ebitenutil.DebugPrintAt(screen, "player", p.pos.X, p.pos.Y) 130 | } 131 | ``` 132 | 133 | ### Introduction 134 | 135 | Let's assume that we have a simple game where you can move a character left or right. 136 | 137 | You might end up checking the specific key events in your code like this: 138 | 139 | ```go 140 | if ebiten.IsKeyPressed(ebiten.KeyLeft) { 141 | // Move left 142 | } 143 | ``` 144 | 145 | But there are a few issues here: 146 | 147 | 1. This approach doesn't allow a key rebinding for the user 148 | 2. There is no clean way to add a gamepad support without making things messy 149 | 3. And even if you add a gamepad support, how would you handle multiple gamepads? 150 | 151 | All of these issues can be solved by our little library. First, we need to declare our abstract actions as enum-like constants: 152 | 153 | ```go 154 | const ( 155 | ActionUnknown input.Action = iota 156 | ActionMoveLeft 157 | ActionMoveRight 158 | ) 159 | ``` 160 | 161 | Then we change the keypress handling code to this: 162 | 163 | ```go 164 | if h.ActionIsPressed(ActionMoveLeft) { 165 | // Move left 166 | } 167 | ``` 168 | 169 | Now, what is `h`? It's an [`input.Handler`](https://pkg.go.dev/github.com/quasilyte/ebitengine-input#Handler). 170 | 171 | The input handler is bound to some keymap and device ID (only useful for the multi-devices setup with multiple gamepads being connected to the computer). 172 | 173 | Having a keymap solves the first issue. The keymap associates an [`input.Action`](https://pkg.go.dev/github.com/quasilyte/ebitengine-input#Action) with a list of [`input.Key`](https://pkg.go.dev/github.com/quasilyte/ebitengine-input#Key). This means that the second issue is resolved too. The third issue is covered by the bound device ID. 174 | 175 | So how do we create an input handler? We use a constructor provided by the [`input.System`](https://pkg.go.dev/github.com/quasilyte/ebitengine-input#System). 176 | 177 | ```go 178 | // The ID argument is important for devices like gamepads. 179 | // The input handlers can have the same keymaps. 180 | player1input := inputSystem.NewHandler(0, keymap) 181 | player2input := inputSystem.NewHandler(1, keymap) 182 | ``` 183 | 184 | The input system is an object that you integrate into your game `Update()` loop. 185 | 186 | ```go 187 | func (g *myGame) Update() { 188 | g.inputSystem.Update() // called every Update() 189 | 190 | // ...rest of the function 191 | } 192 | ``` 193 | 194 | You usually put this object into the game state. It could be either a global state (which I don't recommend) or a part of the state-like object that you pass through your game explicitely. 195 | 196 | ```go 197 | type myGame struct { 198 | inputSystem input.System 199 | 200 | // ...rest of the fields 201 | } 202 | ``` 203 | 204 | You'll need to call the `input.System.Init()` once before calling its `Update()` method. This `Init()` can be called **before** Ebitengine game is executed. 205 | 206 | ```go 207 | func newMyGame() *myGame { 208 | g := &myGame{} 209 | g.inputSystem.Init(input.SystemConfig{ 210 | DevicesEnabled: input.AnyDevice, 211 | }) 212 | // ... rest of the game object initialization 213 | return g 214 | } 215 | ``` 216 | 217 | The keymaps are quite straightforward. We're hardcoding the keymap here, but it could be read from the config file. 218 | 219 | ```go 220 | keymap := input.Keymap{ 221 | ActionMoveLeft: {input.KeyGamepadLeft, input.KeyLeft, input.KeyA}, 222 | ActionMoveRight: {input.KeyGamepadRight, input.KeyRight, input.KeyD}, 223 | } 224 | ``` 225 | 226 | With the keymap above, when we check for the `ActionMoveLeft`, it doesn't matter if it was activated by a gamepad left button on a D-pad or by a keyboard left/A key. 227 | 228 | Another benefit of this system is that we can get a list of relevant key events that can activate a given action. This is useful when you want to prompt player to press some button. 229 | 230 | ```go 231 | // If gamepad is connected, show only gamepad-related keys. 232 | // Otherwise show only keyboard-related keys. 233 | inputDeviceMask := input.KeyboardInput 234 | if h.GamepadConnected() { 235 | inputDeviceMask = input.GamepadDevice 236 | } 237 | keyNames := h.ActionKeyNames(ActionMoveLeft, inputDeviceMask) 238 | ``` 239 | 240 | Since the pattern above is quite common, there is a shorthand for that: 241 | 242 | ```go 243 | keyNames := h.ActionKeyNames(ActionMoveLeft, h.DefaultInputMask()) 244 | ``` 245 | 246 | If the gamepad is connected, the `keyNames` will be `["gamepad_left"]`. Otherwise it will contain two entries for our example: `["left", "a"]`. 247 | 248 | To build a combined key like `ctrl+c`, use `KeyWithModifier` function: 249 | 250 | ```go 251 | // trigger an action when c is pressed while ctrl is down 252 | input.KeyWithModifier(input.KeyC, input.ModControl) 253 | ``` 254 | 255 | See an [example](_examples/basic/main.go) for a complete source code. 256 | 257 | ### Enabling gmath 258 | 259 | This library can be used with and without [gmath](https://github.com/quasilyte/gmath) dependency. 260 | 261 | By default, it uses its own `Vec` implementation which is really just an `{X, Y float64}` wrapper without any methods. It's expected that you convert that simple type into your app's native vector2D type. 262 | 263 | But if you happen to use `gmath` package, you don't have to do this conversion as ebitengine-input can make `Vec` an alias to `gmath.Vec` type. 264 | 265 | To enable this alias declaration, you need to specify the `gmath` build tag like so: 266 | 267 | ```bash 268 | go run --tags=gmath ./mygame 269 | ``` 270 | 271 | You may also want to configure your IDE/editor to understand that `input.Vec` is an alias to `gmath.Vec`. The best way to do that is to declare the default build tags via something like `GOFLAGS`. 272 | 273 | For VSCode and VSCodium you can do the following: 274 | 275 | ```json 276 | "go.toolsEnvVars": { 277 | "GOFLAGS": "-tags=gmath", 278 | } 279 | ``` 280 | 281 | ### Thread Safety Notice 282 | 283 | This library never does any synchronization on its own. It's implied that you don't do a concurrent access to the input devices. 284 | 285 | Therefore, keep in mind: 286 | 287 | * Emitting a simulated input event from several goroutines is a data race 288 | * Using any `Handler` APIs while `System.Update` is in process is a data race 289 | -------------------------------------------------------------------------------- /_examples/action_released/main.go: -------------------------------------------------------------------------------- 1 | //go:build example 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | "strings" 9 | 10 | "github.com/hajimehoshi/ebiten/v2" 11 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 12 | input "github.com/quasilyte/ebitengine-input" 13 | ) 14 | 15 | const ( 16 | ActionUnknown input.Action = iota 17 | ActionAdd 18 | ActionSub 19 | ) 20 | 21 | func main() { 22 | ebiten.SetWindowSize(640, 480) 23 | 24 | if err := ebiten.RunGame(newExampleGame()); err != nil { 25 | log.Fatal(err) 26 | } 27 | } 28 | 29 | type exampleGame struct { 30 | started bool 31 | 32 | score int 33 | 34 | inputHandler *input.Handler 35 | inputSystem input.System 36 | } 37 | 38 | func newExampleGame() *exampleGame { 39 | g := &exampleGame{} 40 | 41 | g.inputSystem.Init(input.SystemConfig{ 42 | DevicesEnabled: input.AnyDevice, 43 | }) 44 | 45 | return g 46 | } 47 | 48 | func (g *exampleGame) Layout(_, _ int) (int, int) { 49 | return 640, 480 50 | } 51 | 52 | func (g *exampleGame) Draw(screen *ebiten.Image) { 53 | ebitenutil.DebugPrint(screen, strings.Join([]string{ 54 | "mouse controls:", 55 | " released [left mouse button]: increase score", 56 | " released [ctrl]+[left mouse button]: decrease score", 57 | "keyboard controls:", 58 | " released [enter]: increase score", 59 | " released [ctrl]+[enter]: decrease score", 60 | "gamepad controls:", 61 | " released [R1]: increase score", 62 | " released [L1]: increase score", 63 | "", 64 | fmt.Sprintf("score: %d", g.score), 65 | }, "\n")) 66 | } 67 | 68 | func (g *exampleGame) Update() error { 69 | g.inputSystem.Update() 70 | 71 | if !g.started { 72 | g.Init() 73 | g.started = true 74 | } 75 | 76 | g.handleInput() 77 | 78 | return nil 79 | } 80 | 81 | func (g *exampleGame) handleInput() { 82 | // Due to the fact that ActionAdd requires just an LMB while 83 | // ActionSub is ctrl+LMB, the ActionAdd can be confused with 84 | // ActionSub if it's checked first. 85 | // This is due to the fact that ebitengine-input does no 86 | // conflict resolution of any kind. 87 | // It may start preferring the "longest" key combination 88 | // in the future, but for now, you might want to order 89 | // the key checks carefully if your keymap might have these conflicts. 90 | // See #36 for more details. 91 | if g.inputHandler.ActionIsJustReleased(ActionSub) { 92 | g.score-- 93 | } else if g.inputHandler.ActionIsJustReleased(ActionAdd) { 94 | g.score++ 95 | } 96 | } 97 | 98 | func (g *exampleGame) Init() { 99 | keymap := input.Keymap{ 100 | ActionAdd: {input.KeyMouseLeft, input.KeyEnter, input.KeyGamepadR1}, 101 | ActionSub: { 102 | input.KeyWithModifier(input.KeyMouseLeft, input.ModControl), 103 | input.KeyWithModifier(input.KeyEnter, input.ModControl), 104 | input.KeyGamepadL1, 105 | }, 106 | } 107 | 108 | g.inputHandler = g.inputSystem.NewHandler(0, keymap) 109 | } 110 | -------------------------------------------------------------------------------- /_examples/basic/main.go: -------------------------------------------------------------------------------- 1 | //go:build example 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "image" 8 | "log" 9 | "os" 10 | "strings" 11 | 12 | "github.com/hajimehoshi/ebiten/v2" 13 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 14 | input "github.com/quasilyte/ebitengine-input" 15 | ) 16 | 17 | // Note: this example won't work well in browsers (wasm builds) 18 | // due to the fact that browsers don't "connect" the gamepads until 19 | // the user presses any button. 20 | // See _examples/gamepad_in_browser for an example that works around it. 21 | 22 | // Define our list of actions as enum-like constants. 23 | // 24 | // Actions usually have more than one key associated with them. 25 | // A key could be a keyboard key, a gamepad key, a mouse button, etc. 26 | // 27 | // When you want to check whether the player is pressing the "fire" key, 28 | // instead of checking the left mouse button directly, you check whether ActionFire is active. 29 | const ( 30 | ActionUnknown input.Action = iota 31 | ActionDebug 32 | ActionMoveLeft 33 | ActionMoveUp 34 | ActionMoveRight 35 | ActionMoveDown 36 | ActionExit 37 | ActionTeleport 38 | ) 39 | 40 | func main() { 41 | ebiten.SetWindowSize(640, 480) 42 | 43 | if err := ebiten.RunGame(newExampleGame()); err != nil { 44 | log.Fatal(err) 45 | } 46 | } 47 | 48 | type exampleGame struct { 49 | started bool 50 | 51 | players []*player 52 | 53 | message string 54 | 55 | inputHandlers []*input.Handler 56 | inputSystem input.System 57 | } 58 | 59 | func newExampleGame() *exampleGame { 60 | g := &exampleGame{} 61 | 62 | // The System.Init() should be called exactly once. 63 | g.inputSystem.Init(input.SystemConfig{ 64 | DevicesEnabled: input.AnyDevice, 65 | }) 66 | 67 | return g 68 | } 69 | 70 | func (g *exampleGame) Layout(_, _ int) (int, int) { 71 | return 640, 480 72 | } 73 | 74 | func (g *exampleGame) Draw(screen *ebiten.Image) { 75 | ebitenutil.DebugPrint(screen, g.message) 76 | for _, p := range g.players { 77 | p.Draw(screen) 78 | } 79 | } 80 | 81 | func (g *exampleGame) Update() error { 82 | g.inputSystem.Update() 83 | 84 | if !g.started { 85 | g.Init() 86 | g.started = true 87 | } 88 | 89 | // Treat the first input handler as the main one. 90 | // Only the first player can exit the game. 91 | if g.inputHandlers[0].ActionIsJustPressed(ActionExit) { 92 | os.Exit(0) 93 | } 94 | if g.inputHandlers[0].ActionIsJustPressed(ActionDebug) { 95 | fmt.Println("debug action is pressed") 96 | } 97 | 98 | for _, p := range g.players { 99 | p.Update() 100 | } 101 | 102 | return nil 103 | } 104 | 105 | func (g *exampleGame) Init() { 106 | // We're hardcoding the keymap here, but it could be read from the config file. 107 | keymap := input.Keymap{ 108 | // Every action can have a list of keys that can activate it. 109 | // KeyGamepadLStick implements a D-pad like events for L/R sticks. 110 | ActionMoveLeft: {input.KeyGamepadLeft, input.KeyGamepadLStickLeft, input.KeyLeft, input.KeyA}, 111 | ActionMoveUp: {input.KeyGamepadUp, input.KeyGamepadLStickUp, input.KeyUp, input.KeyW}, 112 | ActionMoveRight: {input.KeyGamepadRight, input.KeyGamepadLStickRight, input.KeyRight, input.KeyD}, 113 | ActionMoveDown: {input.KeyGamepadDown, input.KeyGamepadLStickDown, input.KeyDown, input.KeyS}, 114 | ActionExit: { 115 | input.KeyGamepadStart, 116 | input.KeyEscape, 117 | input.KeyWithModifier(input.KeyC, input.ModControl), 118 | }, 119 | ActionDebug: {input.KeyControlLeft, input.KeyGamepadLStick, input.KeyGamepadRStick}, 120 | } 121 | 122 | // Player 1 will have a teleport ability activated by a mouse click or a touch screen tap. 123 | keymap0 := keymap.Clone() 124 | keymap0[ActionTeleport] = []input.Key{input.KeyMouseLeft, input.KeyTouchTap} 125 | 126 | // Prepare the input handlers for all possible player slots. 127 | numGamepads := 0 128 | g.inputHandlers = make([]*input.Handler, 4) 129 | for i := range g.inputHandlers { 130 | m := keymap 131 | if i == 0 { 132 | m = keymap0 133 | } 134 | h := g.inputSystem.NewHandler(uint8(i), m) 135 | if h.GamepadConnected() { 136 | numGamepads++ 137 | } 138 | g.inputHandlers[i] = h 139 | } 140 | 141 | // There can be only one player with keyboard. 142 | // There can be up to 4 players with gamepads. 143 | numPlayers := 1 144 | inputDevice := input.KeyboardDevice 145 | if numGamepads != 0 { 146 | inputDevice = input.GamepadDevice 147 | numPlayers = numGamepads 148 | } 149 | 150 | // Depending on the actual number of players, create 151 | // player objects and give them associated input handlers. 152 | g.players = make([]*player, numPlayers) 153 | pos := image.Point{X: 256, Y: 128} 154 | for i := range g.players { 155 | g.players[i] = &player{ 156 | input: g.inputHandlers[i], 157 | pos: pos, 158 | label: fmt.Sprintf("[player%d]", i+1), 159 | } 160 | pos.Y += 64 161 | } 162 | 163 | // For the real-world games you will want to map these action key names to 164 | // something more human-readable (you may also want to translate them). 165 | // For simplicity, we'll use them here as is. 166 | messageLines := []string{ 167 | "preferred input: " + inputDevice.String(), 168 | "move left: " + strings.Join(g.inputHandlers[0].ActionKeyNames(ActionMoveLeft, inputDevice), " or "), 169 | "move right: " + strings.Join(g.inputHandlers[0].ActionKeyNames(ActionMoveRight, inputDevice), " or "), 170 | } 171 | g.message = strings.Join(messageLines, "\n") 172 | } 173 | 174 | type player struct { 175 | label string 176 | input *input.Handler 177 | pos image.Point 178 | } 179 | 180 | func (p *player) Update() { 181 | if p.input.ActionIsPressed(ActionMoveLeft) { 182 | p.pos.X -= 4 183 | } 184 | if p.input.ActionIsPressed(ActionMoveUp) { 185 | p.pos.Y -= 4 186 | } 187 | if p.input.ActionIsPressed(ActionMoveRight) { 188 | p.pos.X += 4 189 | } 190 | if p.input.ActionIsPressed(ActionMoveDown) { 191 | p.pos.Y += 4 192 | } 193 | if info, ok := p.input.JustPressedActionInfo(ActionTeleport); ok { 194 | p.pos.X = int(info.Pos.X) 195 | p.pos.Y = int(info.Pos.Y) 196 | } 197 | } 198 | 199 | func (p *player) Draw(screen *ebiten.Image) { 200 | ebitenutil.DebugPrintAt(screen, p.label, p.pos.X, p.pos.Y) 201 | } 202 | -------------------------------------------------------------------------------- /_examples/configfile/keymap.json: -------------------------------------------------------------------------------- 1 | { 2 | "Left": ["left", "a", "gamepad_left"], 3 | "Right": ["right", "d", "gamepad_right"], 4 | "Pause": ["space", "gamepad_start"], 5 | "Restart": ["ctrl+r", "gamepad_back"], 6 | "Secret": ["ctrl+shift+q"] 7 | } 8 | -------------------------------------------------------------------------------- /_examples/configfile/main.go: -------------------------------------------------------------------------------- 1 | //go:build example 2 | 3 | package main 4 | 5 | import ( 6 | _ "embed" 7 | "encoding/json" 8 | "fmt" 9 | "log" 10 | 11 | "github.com/hajimehoshi/ebiten/v2" 12 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 13 | input "github.com/quasilyte/ebitengine-input" 14 | ) 15 | 16 | // Here is a basic algorithm to load a keymap from a file: 17 | // 18 | // 1. Store an action=>[]keyname mapping somewhere in a file; 19 | // 20 | // 2. When initializing an input.Keymap, you need to associate 21 | // an action string key with an actual input.Action constant; 22 | // 23 | // 3. Map keyname to input.Key, this can be done by using input.ParseKey function. 24 | // 25 | // Only the 2nd step requires some extra efforts. 26 | // Since input.Action is an external type, you can't use a stringer tool to 27 | // generate the string mappings. You can try using some other tool to do that. 28 | // Or you can write the mapping manually (see actionString). 29 | // 30 | // When you have an action=>actionname mapping, it's easy to build a 31 | // reverse index for the second step. A special sentinel value like actionLast 32 | // can be useful (see the code below). 33 | 34 | const ( 35 | ActionUnknown input.Action = iota 36 | ActionLeft 37 | ActionRight 38 | ActionPause 39 | ActionRestart 40 | ActionSecret 41 | 42 | actionLast 43 | ) 44 | 45 | func actionString(a input.Action) string { 46 | // This is the only function we have to implement. 47 | // Write it manually or use the tools to generate it (not stringer though). 48 | switch a { 49 | case ActionLeft: 50 | return "Left" 51 | case ActionRight: 52 | return "Right" 53 | case ActionPause: 54 | return "Pause" 55 | case ActionRestart: 56 | return "Restart" 57 | case ActionSecret: 58 | return "Secret" 59 | default: 60 | return "?" 61 | } 62 | } 63 | 64 | //go:embed keymap.json 65 | var keymapConfigData []byte 66 | 67 | func main() { 68 | ebiten.SetWindowSize(640, 480) 69 | 70 | if err := ebiten.RunGame(newExampleGame()); err != nil { 71 | log.Fatal(err) 72 | } 73 | } 74 | 75 | type exampleGame struct { 76 | started bool 77 | 78 | lastActionPressed string 79 | 80 | inputHandler *input.Handler 81 | inputSystem input.System 82 | } 83 | 84 | func newExampleGame() *exampleGame { 85 | g := &exampleGame{} 86 | 87 | g.inputSystem.Init(input.SystemConfig{ 88 | DevicesEnabled: input.AnyDevice, 89 | }) 90 | 91 | return g 92 | } 93 | 94 | func (g *exampleGame) Layout(_, _ int) (int, int) { 95 | return 640, 480 96 | } 97 | 98 | func (g *exampleGame) Draw(screen *ebiten.Image) { 99 | ebitenutil.DebugPrint(screen, "last pressed action: "+g.lastActionPressed) 100 | } 101 | 102 | func (g *exampleGame) Update() error { 103 | g.inputSystem.Update() 104 | 105 | if !g.started { 106 | g.Init() 107 | g.started = true 108 | } 109 | 110 | actions := [...]input.Action{ 111 | ActionLeft, 112 | ActionRight, 113 | ActionPause, 114 | ActionRestart, 115 | ActionSecret, 116 | } 117 | for _, a := range actions { 118 | if g.inputHandler.ActionIsJustPressed(a) { 119 | g.lastActionPressed = actionString(a) 120 | break 121 | } 122 | } 123 | 124 | return nil 125 | } 126 | 127 | func (g *exampleGame) Init() { 128 | var keymapConfig map[string][]string 129 | if err := json.Unmarshal(keymapConfigData, &keymapConfig); err != nil { 130 | panic(err) 131 | } 132 | 133 | // Build a reverse index to get an action ID by its name. 134 | actionNameToID := map[string]input.Action{} 135 | for a := ActionUnknown; a < actionLast; a++ { 136 | actionNameToID[actionString(a)] = a 137 | } 138 | 139 | // Parse our config file into a keymap object. 140 | keymap := input.Keymap{} 141 | for actionName, keyNames := range keymapConfig { 142 | a, ok := actionNameToID[actionName] 143 | if !ok { 144 | panic(fmt.Sprintf("unexpected action name: %s", actionName)) 145 | } 146 | keys := make([]input.Key, len(keyNames)) 147 | for i, keyString := range keyNames { 148 | k, err := input.ParseKey(keyString) 149 | if err != nil { 150 | panic(err) 151 | } 152 | keys[i] = k 153 | } 154 | keymap[a] = keys 155 | } 156 | 157 | g.inputHandler = g.inputSystem.NewHandler(0, keymap) 158 | } 159 | -------------------------------------------------------------------------------- /_examples/gamepad_in_browser/main.go: -------------------------------------------------------------------------------- 1 | //go:build example 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "image" 8 | "log" 9 | "time" 10 | 11 | "github.com/hajimehoshi/ebiten/v2" 12 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 13 | input "github.com/quasilyte/ebitengine-input" 14 | ) 15 | 16 | // For the basics, see "_examples/basic"; 17 | // this example omits some explanations for brevity. 18 | 19 | const ( 20 | ActionUnknown input.Action = iota 21 | ActionMoveLeft 22 | ActionMoveRight 23 | ActionDebug 24 | ) 25 | 26 | func main() { 27 | ebiten.SetWindowSize(640, 480) 28 | 29 | if err := ebiten.RunGame(newExampleGame()); err != nil { 30 | log.Fatal(err) 31 | } 32 | } 33 | 34 | type exampleGame struct { 35 | started bool 36 | 37 | state gameState 38 | 39 | currentScene scene 40 | 41 | inputSystem input.System 42 | } 43 | 44 | type gameState struct { 45 | inputHandlers []*input.Handler 46 | } 47 | 48 | type scene interface { 49 | Update() scene 50 | Draw(screen *ebiten.Image) 51 | } 52 | 53 | func newExampleGame() *exampleGame { 54 | g := &exampleGame{} 55 | 56 | g.inputSystem.Init(input.SystemConfig{ 57 | DevicesEnabled: input.AnyDevice, 58 | }) 59 | 60 | return g 61 | } 62 | 63 | func (g *exampleGame) Layout(_, _ int) (int, int) { 64 | return 640, 480 65 | } 66 | 67 | func (g *exampleGame) Update() error { 68 | g.inputSystem.Update() 69 | if !g.started { 70 | g.init() 71 | g.started = true 72 | } 73 | g.currentScene = g.currentScene.Update() 74 | return nil 75 | } 76 | 77 | func (g *exampleGame) Draw(screen *ebiten.Image) { 78 | g.currentScene.Draw(screen) 79 | } 80 | 81 | func (g *exampleGame) init() { 82 | keymap := input.Keymap{ 83 | ActionMoveLeft: {input.KeyGamepadLeft, input.KeyGamepadLStickLeft}, 84 | ActionMoveRight: {input.KeyGamepadRight, input.KeyGamepadLStickRight}, 85 | ActionDebug: {input.KeyGamepadLeft}, 86 | } 87 | 88 | g.state.inputHandlers = make([]*input.Handler, 4) 89 | for i := range g.state.inputHandlers { 90 | g.state.inputHandlers[i] = g.inputSystem.NewHandler(uint8(i), keymap) 91 | } 92 | 93 | g.currentScene = &lobbyScene{ 94 | state: &g.state, 95 | timeout: time.Now().Add(9 * time.Second), 96 | } 97 | } 98 | 99 | type lobbyScene struct { 100 | state *gameState 101 | timeout time.Time 102 | secondsLeft float64 103 | gamepads int 104 | } 105 | 106 | func (s *lobbyScene) Draw(screen *ebiten.Image) { 107 | ebitenutil.DebugPrintAt(screen, "connect gamepads, press buttons", 200, 160) 108 | ebitenutil.DebugPrintAt(screen, fmt.Sprintf("the scene changes in %.1f seconds", s.secondsLeft), 200, 200) 109 | ebitenutil.DebugPrintAt(screen, fmt.Sprintf("gamepads detected: %d", s.gamepads), 240, 240) 110 | } 111 | 112 | func (s *lobbyScene) Update() scene { 113 | s.secondsLeft = time.Until(s.timeout).Seconds() 114 | 115 | numGamepads := 0 116 | for _, h := range s.state.inputHandlers { 117 | if h.GamepadConnected() { 118 | numGamepads++ 119 | } 120 | } 121 | s.gamepads = numGamepads 122 | 123 | if s.secondsLeft <= 0 { 124 | return newMainScene(s.state, s.gamepads) 125 | } 126 | return s 127 | } 128 | 129 | type mainScene struct { 130 | state *gameState 131 | players []*player 132 | } 133 | 134 | func newMainScene(state *gameState, gamepads int) *mainScene { 135 | s := &mainScene{state: state} 136 | 137 | // Depending on the actual number of players, create 138 | // player objects and give them associated input handlers. 139 | s.players = make([]*player, gamepads) 140 | pos := image.Point{X: 256, Y: 128} 141 | for i := range s.players { 142 | s.players[i] = &player{ 143 | input: state.inputHandlers[i], 144 | pos: pos, 145 | label: fmt.Sprintf("[player%d]", i+1), 146 | } 147 | pos.Y += 64 148 | } 149 | 150 | return s 151 | } 152 | 153 | func (s *mainScene) Draw(screen *ebiten.Image) { 154 | for _, p := range s.players { 155 | p.Draw(screen) 156 | } 157 | } 158 | 159 | func (s *mainScene) Update() scene { 160 | for _, p := range s.players { 161 | p.Update() 162 | } 163 | return s 164 | } 165 | 166 | type player struct { 167 | label string 168 | input *input.Handler 169 | pos image.Point 170 | } 171 | 172 | func (p *player) Update() { 173 | if p.input.ActionIsPressed(ActionMoveLeft) { 174 | p.pos.X -= 4 175 | } 176 | if p.input.ActionIsPressed(ActionMoveRight) { 177 | p.pos.X += 4 178 | } 179 | if p.input.ActionIsJustPressed(ActionDebug) { 180 | fmt.Printf("%s: debug action is pressed\n", p.label) 181 | } 182 | } 183 | 184 | func (p *player) Draw(screen *ebiten.Image) { 185 | ebitenutil.DebugPrintAt(screen, p.label, p.pos.X, p.pos.Y) 186 | } 187 | -------------------------------------------------------------------------------- /_examples/modkeys/main.go: -------------------------------------------------------------------------------- 1 | //go:build example 2 | 3 | package main 4 | 5 | import ( 6 | "image" 7 | "image/color" 8 | "log" 9 | "math" 10 | 11 | "github.com/hajimehoshi/ebiten/v2" 12 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 13 | input "github.com/quasilyte/ebitengine-input" 14 | ) 15 | 16 | const ( 17 | ActionUnknown input.Action = iota 18 | ActionAddGreenLine 19 | ActionAddBlueLine 20 | ActionResetGreenLine 21 | ActionResetBlueLine 22 | ) 23 | 24 | func main() { 25 | ebiten.SetWindowSize(640, 480) 26 | 27 | if err := ebiten.RunGame(newExampleGame()); err != nil { 28 | log.Fatal(err) 29 | } 30 | } 31 | 32 | type exampleGame struct { 33 | started bool 34 | 35 | points []drawPoint 36 | 37 | inputHandler *input.Handler 38 | inputSystem input.System 39 | } 40 | 41 | type drawPoint struct { 42 | pos input.Vec 43 | color color.RGBA 44 | } 45 | 46 | func newExampleGame() *exampleGame { 47 | g := &exampleGame{} 48 | 49 | g.inputSystem.Init(input.SystemConfig{ 50 | DevicesEnabled: input.AnyDevice, 51 | }) 52 | 53 | return g 54 | } 55 | 56 | func (g *exampleGame) Layout(_, _ int) (int, int) { 57 | return 640, 480 58 | } 59 | 60 | func (g *exampleGame) Draw(screen *ebiten.Image) { 61 | ebitenutil.DebugPrint(screen, "press lmb with ctrl/shift (any combo)") 62 | 63 | pos := input.Vec{X: 320, Y: 240} 64 | for _, pt := range g.points { 65 | drawLine(screen, pos, pt.pos, pt.color) 66 | pos = pt.pos 67 | } 68 | } 69 | 70 | func (g *exampleGame) Update() error { 71 | g.inputSystem.Update() 72 | 73 | if !g.started { 74 | g.Init() 75 | g.started = true 76 | } 77 | 78 | g.handleInput() 79 | 80 | return nil 81 | } 82 | 83 | func (g *exampleGame) handleInput() { 84 | blue := color.RGBA{B: 0xf0, A: 0xff} 85 | green := color.RGBA{G: 0xf0, A: 0xff} 86 | if info, ok := g.inputHandler.JustPressedActionInfo(ActionAddGreenLine); ok { 87 | g.points = append(g.points, drawPoint{color: green, pos: info.Pos}) 88 | return 89 | } 90 | if info, ok := g.inputHandler.JustPressedActionInfo(ActionResetGreenLine); ok { 91 | g.points = append(g.points[:0], drawPoint{color: green, pos: info.Pos}) 92 | return 93 | } 94 | if info, ok := g.inputHandler.JustPressedActionInfo(ActionAddBlueLine); ok { 95 | g.points = append(g.points, drawPoint{color: blue, pos: info.Pos}) 96 | return 97 | } 98 | if info, ok := g.inputHandler.JustPressedActionInfo(ActionResetBlueLine); ok { 99 | g.points = append(g.points[:0], drawPoint{color: blue, pos: info.Pos}) 100 | return 101 | } 102 | } 103 | 104 | func (g *exampleGame) Init() { 105 | keymap := input.Keymap{ 106 | ActionResetBlueLine: {input.KeyMouseLeft}, 107 | ActionAddBlueLine: {input.KeyWithModifier(input.KeyMouseLeft, input.ModControl)}, 108 | 109 | ActionResetGreenLine: {input.KeyWithModifier(input.KeyMouseLeft, input.ModShift)}, 110 | ActionAddGreenLine: {input.KeyWithModifier(input.KeyMouseLeft, input.ModControlShift)}, 111 | } 112 | 113 | g.inputHandler = g.inputSystem.NewHandler(0, keymap) 114 | } 115 | 116 | func drawLine(dst *ebiten.Image, pos1, pos2 input.Vec, c color.RGBA) { 117 | x1 := pos1.X 118 | y1 := pos1.Y 119 | x2 := pos2.X 120 | y2 := pos2.Y 121 | 122 | length := math.Hypot(x2-x1, y2-y1) 123 | 124 | var drawOptions ebiten.DrawImageOptions 125 | drawOptions.GeoM.Scale(length, 2) 126 | drawOptions.GeoM.Rotate(math.Atan2(y2-y1, x2-x1)) 127 | drawOptions.GeoM.Translate(x1, y1) 128 | drawOptions.ColorScale.Scale(float32(c.R), float32(c.G), float32(c.B), float32(c.A)) 129 | 130 | dst.DrawImage(whitePixel, &drawOptions) 131 | } 132 | 133 | var whitePixel *ebiten.Image 134 | 135 | func init() { 136 | emptyImage := ebiten.NewImage(3, 3) 137 | emptyImage.Fill(color.White) 138 | whitePixel = emptyImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image) 139 | } 140 | -------------------------------------------------------------------------------- /_examples/mousedrag/main.go: -------------------------------------------------------------------------------- 1 | //go:build example 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | 9 | "github.com/hajimehoshi/ebiten/v2" 10 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 11 | input "github.com/quasilyte/ebitengine-input" 12 | ) 13 | 14 | const ( 15 | ActionUnknown input.Action = iota 16 | ActionDrag 17 | ActionClick 18 | ) 19 | 20 | func main() { 21 | ebiten.SetWindowSize(640, 480) 22 | 23 | if err := ebiten.RunGame(newExampleGame()); err != nil { 24 | log.Fatal(err) 25 | } 26 | } 27 | 28 | type exampleGame struct { 29 | started bool 30 | 31 | pos input.Vec 32 | fallbackPos input.Vec 33 | 34 | numDrags int 35 | numClicks int 36 | 37 | inputHandler *input.Handler 38 | inputSystem input.System 39 | } 40 | 41 | func newExampleGame() *exampleGame { 42 | g := &exampleGame{} 43 | 44 | g.inputSystem.Init(input.SystemConfig{ 45 | DevicesEnabled: input.AnyDevice, 46 | }) 47 | 48 | return g 49 | } 50 | 51 | func (g *exampleGame) Layout(_, _ int) (int, int) { 52 | return 640, 480 53 | } 54 | 55 | func (g *exampleGame) Draw(screen *ebiten.Image) { 56 | ebitenutil.DebugPrint(screen, 57 | fmt.Sprintf("use drag to move the star\nnum drags: %d\nnum clicks: %d", g.numDrags, g.numClicks)) 58 | ebitenutil.DebugPrintAt(screen, "*", int(g.pos.X), int(g.pos.Y)) 59 | } 60 | 61 | func (g *exampleGame) Update() error { 62 | g.inputSystem.Update() 63 | 64 | if !g.started { 65 | g.Init() 66 | g.started = true 67 | } 68 | 69 | if g.inputHandler.ActionIsJustPressed(ActionClick) { 70 | g.numClicks++ 71 | } 72 | 73 | if info, ok := g.inputHandler.JustPressedActionInfo(ActionDrag); ok { 74 | // Start dragging. 75 | g.numDrags++ 76 | g.fallbackPos = info.StartPos 77 | } else if info, ok := g.inputHandler.PressedActionInfo(ActionDrag); ok { 78 | // Continue dragging. 79 | g.pos = info.Pos 80 | } else { 81 | // Not being dragged. 82 | g.pos = g.fallbackPos 83 | } 84 | 85 | return nil 86 | } 87 | 88 | func (g *exampleGame) Init() { 89 | g.pos = input.Vec{X: 200, Y: 200} 90 | g.fallbackPos = g.pos 91 | 92 | keymap := input.Keymap{ 93 | ActionDrag: {input.KeyMouseLeftDrag}, 94 | ActionClick: {input.KeyMouseLeft}, 95 | } 96 | 97 | g.inputHandler = g.inputSystem.NewHandler(0, keymap) 98 | } 99 | -------------------------------------------------------------------------------- /_examples/press_any_key/main.go: -------------------------------------------------------------------------------- 1 | //go:build example 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | 9 | "github.com/hajimehoshi/ebiten/v2" 10 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 11 | input "github.com/quasilyte/ebitengine-input" 12 | ) 13 | 14 | func main() { 15 | ebiten.SetWindowSize(640, 480) 16 | 17 | if err := ebiten.RunGame(newExampleGame()); err != nil { 18 | log.Fatal(err) 19 | } 20 | } 21 | 22 | type exampleGame struct { 23 | started bool 24 | 25 | inputHandler *input.Handler 26 | inputSystem input.System 27 | } 28 | 29 | func newExampleGame() *exampleGame { 30 | g := &exampleGame{} 31 | 32 | g.inputSystem.Init(input.SystemConfig{ 33 | DevicesEnabled: input.AnyDevice, 34 | }) 35 | 36 | return g 37 | } 38 | 39 | func (g *exampleGame) Layout(_, _ int) (int, int) { 40 | return 640, 480 41 | } 42 | 43 | func (g *exampleGame) Draw(screen *ebiten.Image) { 44 | ebitenutil.DebugPrint(screen, "Press any key and see game logs") 45 | } 46 | 47 | func (g *exampleGame) Update() error { 48 | g.inputSystem.Update() 49 | 50 | if !g.started { 51 | g.Init() 52 | g.started = true 53 | } 54 | 55 | if g.inputHandler.AnyKeyJustPressed() { 56 | fmt.Printf("A key is pressed!\n") 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func (g *exampleGame) Init() { 63 | g.inputHandler = g.inputSystem.NewHandler(0, input.Keymap{}) 64 | } 65 | -------------------------------------------------------------------------------- /_examples/remap/main.go: -------------------------------------------------------------------------------- 1 | //go:build example 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | 9 | "github.com/hajimehoshi/ebiten/v2" 10 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 11 | input "github.com/quasilyte/ebitengine-input" 12 | ) 13 | 14 | const ( 15 | ActionUnknown input.Action = iota 16 | ActionPing 17 | ActionRemap 18 | ) 19 | 20 | func main() { 21 | ebiten.SetWindowSize(640, 480) 22 | 23 | if err := ebiten.RunGame(newExampleGame()); err != nil { 24 | log.Fatal(err) 25 | } 26 | } 27 | 28 | type exampleGame struct { 29 | started bool 30 | 31 | k input.Key 32 | prevK input.Key 33 | scanning bool 34 | 35 | keyScanner input.KeyScanner 36 | 37 | inputHandler *input.Handler 38 | inputSystem input.System 39 | } 40 | 41 | func newExampleGame() *exampleGame { 42 | g := &exampleGame{} 43 | 44 | g.inputSystem.Init(input.SystemConfig{ 45 | DevicesEnabled: input.AnyDevice, 46 | }) 47 | 48 | return g 49 | } 50 | 51 | func (g *exampleGame) Layout(_, _ int) (int, int) { 52 | return 640, 480 53 | } 54 | 55 | func (g *exampleGame) Draw(screen *ebiten.Image) { 56 | if g.scanning { 57 | ebitenutil.DebugPrint(screen, fmt.Sprintf("keybind: %s\n", g.k)) 58 | } else { 59 | ebitenutil.DebugPrint(screen, fmt.Sprintf("keybind: %s\npress ctrl+enter to remap", g.k)) 60 | } 61 | } 62 | 63 | func (g *exampleGame) Update() error { 64 | g.inputSystem.Update() 65 | 66 | if !g.started { 67 | g.Init() 68 | g.started = true 69 | } 70 | 71 | g.handleRemap() 72 | 73 | if !g.scanning && g.inputHandler.ActionIsJustPressed(ActionPing) { 74 | fmt.Printf("ping! (activated with %s keybind)\n", g.k) 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func (g *exampleGame) handleRemap() { 81 | if !g.scanning { 82 | if g.inputHandler.ActionIsJustPressed(ActionRemap) { 83 | g.prevK = g.k // Save it for an easier fallback 84 | g.scanning = true 85 | } 86 | return 87 | } 88 | 89 | k, status := g.keyScanner.Scan() 90 | if status != input.KeyScanUnchanged { 91 | g.k = k 92 | } 93 | if status == input.KeyScanCompleted { 94 | g.scanning = false 95 | // Check for the new key to be available. 96 | // Resolve the conflicts here; I'll just reject 97 | // the combination that is already in use. 98 | if g.k == input.KeyWithModifier(input.KeyEnter, input.ModControl) { 99 | g.k = g.prevK 100 | } else { 101 | g.inputHandler.Remap(g.makeKeymap()) 102 | } 103 | } 104 | } 105 | 106 | func (g *exampleGame) makeKeymap() input.Keymap { 107 | return input.Keymap{ 108 | ActionPing: {g.k}, 109 | ActionRemap: {input.KeyWithModifier(input.KeyEnter, input.ModControl)}, 110 | } 111 | } 112 | 113 | func (g *exampleGame) Init() { 114 | g.k = input.KeyQ 115 | g.inputHandler = g.inputSystem.NewHandler(0, g.makeKeymap()) 116 | } 117 | -------------------------------------------------------------------------------- /_examples/scroll/main.go: -------------------------------------------------------------------------------- 1 | //go:build example 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | 8 | "github.com/hajimehoshi/ebiten/v2" 9 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 10 | input "github.com/quasilyte/ebitengine-input" 11 | ) 12 | 13 | const ( 14 | ActionUnknown input.Action = iota 15 | ActionScrollVertical 16 | ActionFastScrollVertical 17 | ) 18 | 19 | func main() { 20 | ebiten.SetWindowSize(640, 480) 21 | 22 | if err := ebiten.RunGame(newExampleGame()); err != nil { 23 | log.Fatal(err) 24 | } 25 | } 26 | 27 | type exampleGame struct { 28 | started bool 29 | 30 | pos input.Vec 31 | 32 | inputHandler *input.Handler 33 | inputSystem input.System 34 | } 35 | 36 | func newExampleGame() *exampleGame { 37 | g := &exampleGame{} 38 | 39 | g.inputSystem.Init(input.SystemConfig{ 40 | DevicesEnabled: input.AnyDevice, 41 | }) 42 | 43 | return g 44 | } 45 | 46 | func (g *exampleGame) Layout(_, _ int) (int, int) { 47 | return 640, 480 48 | } 49 | 50 | func (g *exampleGame) Draw(screen *ebiten.Image) { 51 | ebitenutil.DebugPrint(screen, "scroll up/down") 52 | ebitenutil.DebugPrintAt(screen, "*", int(g.pos.X), int(g.pos.Y)) 53 | } 54 | 55 | func (g *exampleGame) Update() error { 56 | g.inputSystem.Update() 57 | 58 | if !g.started { 59 | g.Init() 60 | g.started = true 61 | } 62 | 63 | g.handleScroll() 64 | 65 | return nil 66 | } 67 | 68 | func (g *exampleGame) handleScroll() { 69 | if info, ok := g.inputHandler.JustPressedActionInfo(ActionFastScrollVertical); ok { 70 | g.pos.Y += info.Pos.Y * 7 71 | return 72 | } 73 | if info, ok := g.inputHandler.JustPressedActionInfo(ActionScrollVertical); ok { 74 | g.pos.Y += info.Pos.Y 75 | return 76 | } 77 | } 78 | 79 | func (g *exampleGame) Init() { 80 | g.pos = input.Vec{X: 200, Y: 200} 81 | 82 | keymap := input.Keymap{ 83 | ActionFastScrollVertical: {input.KeyWithModifier(input.KeyWheelVertical, input.ModControl)}, 84 | ActionScrollVertical: {input.KeyWheelVertical}, 85 | } 86 | 87 | g.inputHandler = g.inputSystem.NewHandler(0, keymap) 88 | } 89 | -------------------------------------------------------------------------------- /_examples/simulateinput/main.go: -------------------------------------------------------------------------------- 1 | //go:build example 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | 9 | "github.com/hajimehoshi/ebiten/v2" 10 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 11 | input "github.com/quasilyte/ebitengine-input" 12 | ) 13 | 14 | // The virtual input API are most useful when you want to: 15 | // 16 | // * Simulate the user input (specific device input event) 17 | // * Test the game by triggering bound actions programmatically 18 | // * Implement a remote controller object 19 | // 20 | // There are two main ways to emit such an event: 21 | // 22 | // 1. Handler.EmitKeyEvent(...) 23 | // 2. Handler.EmitEvent(...) 24 | // 25 | // The first option requires a Key object to be specified. 26 | // It makes the system believe that this key was actually in its pressed 27 | // state during the frame. All device-related info is preserved. 28 | // This is really good for the user input emulation or 29 | // for the games that want to know which input device was used to 30 | // emit an event. All device-related behavior will be preserved too. 31 | // So, a gamepad button press will be controller-local, but 32 | // keyboard events like KeyEnter will be visible to all handlers. 33 | // 34 | // The second option just triggers an Action directly. 35 | // There will be no input device associated with that event. 36 | // This means that the info object methods like IsGamepadEvent() and alike 37 | // will always return false. It's possible to trigger an action that has 38 | // none keys associated with it. 39 | // All artificial actions triggered this way are only visible to 40 | // the handlers of the same player ID. So they're always handler-local 41 | // (like the gamepad button press would be). 42 | 43 | const ( 44 | ActionUnknown input.Action = iota 45 | ActionSpace 46 | ActionEnter 47 | ActionClick 48 | ActionGamepadButton 49 | ActionUnbound 50 | ) 51 | 52 | func main() { 53 | ebiten.SetWindowSize(640, 480) 54 | 55 | if err := ebiten.RunGame(newExampleGame()); err != nil { 56 | log.Fatal(err) 57 | } 58 | } 59 | 60 | type exampleGame struct { 61 | started bool 62 | 63 | frame int 64 | pressingEnter bool 65 | 66 | firstHandler *input.Handler 67 | secondHandler *input.Handler 68 | inputSystem input.System 69 | } 70 | 71 | func newExampleGame() *exampleGame { 72 | g := &exampleGame{} 73 | 74 | g.inputSystem.Init(input.SystemConfig{ 75 | DevicesEnabled: input.AnyDevice, 76 | }) 77 | 78 | return g 79 | } 80 | 81 | func (g *exampleGame) Layout(_, _ int) (int, int) { 82 | return 640, 480 83 | } 84 | 85 | func (g *exampleGame) Draw(screen *ebiten.Image) { 86 | ebitenutil.DebugPrint(screen, "check out the stdout logs\ntry clicking, pressing space/enter") 87 | } 88 | 89 | func (g *exampleGame) Update() error { 90 | g.inputSystem.Update() 91 | 92 | if !g.started { 93 | g.Init() 94 | g.started = true 95 | } 96 | 97 | g.frame++ 98 | // Every 90 frames, emit some events and toggle the enter pressing mode. 99 | // Note: the simulated input events won't be detected until the next frame. 100 | if g.frame%90 == 0 { 101 | g.firstHandler.EmitKeyEvent(input.SimulatedKeyEvent{Key: input.KeySpace}) 102 | g.firstHandler.EmitKeyEvent(input.SimulatedKeyEvent{Key: input.KeyGamepadA}) 103 | g.firstHandler.EmitKeyEvent(input.SimulatedKeyEvent{ 104 | Key: input.KeyMouseLeft, 105 | Pos: input.Vec{X: 100, Y: 100}, 106 | }) 107 | g.pressingEnter = !g.pressingEnter 108 | fmt.Printf(">> frame %d: switch 'pressing enter' (now %v)\n", g.frame, g.pressingEnter) 109 | fmt.Printf(">> frame %d: simulate space key press\n", g.frame) 110 | fmt.Printf(">> frame %d: simulate gamepad A key press\n", g.frame) 111 | fmt.Printf(">> frame %d: simulate a mouse click at (100,100)\n", g.frame) 112 | } 113 | if g.pressingEnter { 114 | // It's possible to trigger an action directly. 115 | // This would create a special 116 | g.firstHandler.EmitEvent(input.SimulatedAction{Action: ActionEnter}) 117 | } 118 | if g.frame%100 == 0 { 119 | g.firstHandler.EmitEvent(input.SimulatedAction{ 120 | Action: ActionUnbound, 121 | Pos: input.Vec{X: 1, Y: 2}, 122 | }) 123 | fmt.Printf(">> frame %d: simulate an unbound action\n", g.frame) 124 | } 125 | 126 | if info, ok := g.firstHandler.JustPressedActionInfo(ActionClick); ok { 127 | fmt.Printf("<< frame %d: click action is just pressed (pos: %f,%f)\n", g.frame, info.Pos.X, info.Pos.Y) 128 | } 129 | if g.firstHandler.ActionIsPressed(ActionSpace) { 130 | fmt.Printf("<< frame %d: space action is pressed\n", g.frame) 131 | } 132 | if g.firstHandler.ActionIsJustPressed(ActionEnter) { 133 | fmt.Printf("<< frame %d: enter action is just pressed\n", g.frame) 134 | } 135 | if g.firstHandler.ActionIsJustPressed(ActionUnbound) { 136 | info, _ := g.firstHandler.JustPressedActionInfo(ActionUnbound) 137 | fmt.Printf("<< frame %d: unbound action is just pressed (pos: %f,%f)\n", 138 | g.frame, info.Pos.X, info.Pos.Y) 139 | } 140 | if g.firstHandler.ActionIsPressed(ActionGamepadButton) { 141 | fmt.Printf("<< frame %d: gamepad button action is pressed\n", g.frame) 142 | } 143 | 144 | // Gamepad actions are bound to the device ID (same as player ID). 145 | // Therefore, the second handler doesn't get a gamepad button event. 146 | // But it's possible to trigger this action from the second gamepad! 147 | if g.secondHandler.ActionIsJustPressed(ActionGamepadButton) { 148 | fmt.Printf("<<<< frame %d: gamepad button action is pressed\n", g.frame) 149 | } 150 | // Artificial input events are id-bound too. 151 | if g.secondHandler.ActionIsJustPressed(ActionUnbound) { 152 | panic("should never happen") 153 | } 154 | // None of the simulated actions are broadcasted. 155 | // Even things that are bound to keys like "enter". 156 | // But it's possible to trigger this action by pressing the enter manually! 157 | if g.secondHandler.ActionIsJustPressed(ActionEnter) { 158 | fmt.Printf("<<<< frame %d: enter button action is just pressed\n", g.frame) 159 | } 160 | 161 | return nil 162 | } 163 | 164 | func (g *exampleGame) Init() { 165 | keymap := input.Keymap{ 166 | ActionClick: {input.KeyMouseLeft}, 167 | ActionSpace: {input.KeySpace}, 168 | ActionEnter: {input.KeyEnter}, 169 | ActionGamepadButton: {input.KeyGamepadA}, 170 | ActionUnbound: {}, // Empty by choice 171 | } 172 | 173 | g.pressingEnter = true 174 | g.firstHandler = g.inputSystem.NewHandler(0, keymap) 175 | g.secondHandler = g.inputSystem.NewHandler(1, keymap) 176 | } 177 | -------------------------------------------------------------------------------- /_examples/smooth_movement/main.go: -------------------------------------------------------------------------------- 1 | //go:build example 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | 8 | "github.com/hajimehoshi/ebiten/v2" 9 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 10 | input "github.com/quasilyte/ebitengine-input" 11 | ) 12 | 13 | const ( 14 | ActionUnknown input.Action = iota 15 | ActionMove 16 | ActionAlternativeMove 17 | ) 18 | 19 | func main() { 20 | ebiten.SetWindowSize(640, 480) 21 | 22 | if err := ebiten.RunGame(newExampleGame()); err != nil { 23 | log.Fatal(err) 24 | } 25 | } 26 | 27 | type exampleGame struct { 28 | started bool 29 | 30 | status unitStatus 31 | pos input.Vec 32 | startPos input.Vec 33 | 34 | inputHandler *input.Handler 35 | inputSystem input.System 36 | } 37 | 38 | func newExampleGame() *exampleGame { 39 | g := &exampleGame{} 40 | 41 | g.inputSystem.Init(input.SystemConfig{ 42 | DevicesEnabled: input.AnyDevice, 43 | }) 44 | 45 | return g 46 | } 47 | 48 | func (g *exampleGame) Layout(_, _ int) (int, int) { 49 | return 640, 480 50 | } 51 | 52 | func (g *exampleGame) Draw(screen *ebiten.Image) { 53 | ebitenutil.DebugPrint(screen, "move by using the gamepad left stick") 54 | 55 | // We'll use some ASCII art instead of the real graphics. 56 | var sprite string 57 | offsetX := -4 58 | offsetY := -6 59 | switch g.status { 60 | case statusIdle: 61 | sprite = "o" 62 | offsetX = 0 63 | offsetY = 0 64 | case statusMoving, statusStartMovement: 65 | sprite = "@@\n@@" 66 | } 67 | if g.startPos != (input.Vec{}) { 68 | ebitenutil.DebugPrintAt(screen, "O", int(g.startPos.X), int(g.startPos.Y)) 69 | } 70 | ebitenutil.DebugPrintAt(screen, sprite, int(g.pos.X)+offsetX, int(g.pos.Y)+offsetY) 71 | } 72 | 73 | func (g *exampleGame) Update() error { 74 | g.inputSystem.Update() 75 | 76 | if !g.started { 77 | g.Init() 78 | g.started = true 79 | } 80 | 81 | // We simulate the left stick events using our right controller stick. 82 | // It's better just to bind both L+R sticks to the action, but this example 83 | // demonstrates the power of simulated events. 84 | if info, ok := g.inputHandler.PressedActionInfo(ActionAlternativeMove); ok { 85 | g.inputHandler.EmitKeyEvent(input.SimulatedKeyEvent{ 86 | Key: input.KeyGamepadLStickMotion, 87 | Pos: info.Pos, 88 | }) 89 | } 90 | 91 | // You can control all movement phases: 92 | // - its first frame (started to move) 93 | // - its end (just stopped to move) 94 | // - its active phase (on the move) 95 | // - the movement absence (an idle state) 96 | if g.inputHandler.ActionIsJustPressed(ActionMove) { 97 | // The movement is just started. 98 | g.startPos = g.pos 99 | g.status = statusStartMovement 100 | } else if info, ok := g.inputHandler.PressedActionInfo(ActionMove); ok { 101 | // We're in the middle of the movement. 102 | // The info.Pos value is like a direction vector of the stick. 103 | g.pos.X += info.Pos.X * 2 104 | g.pos.Y += info.Pos.Y * 2 105 | g.status = statusMoving 106 | } else if g.status != statusIdle { 107 | // The movement has just finished. 108 | g.startPos = input.Vec{} 109 | g.status = statusIdle 110 | } else { 111 | // No movement is being executed. 112 | g.status = statusIdle 113 | } 114 | 115 | return nil 116 | } 117 | 118 | func (g *exampleGame) Init() { 119 | g.pos = input.Vec{X: 200, Y: 200} 120 | 121 | keymap := input.Keymap{ 122 | ActionMove: {input.KeyGamepadLStickMotion}, 123 | ActionAlternativeMove: {input.KeyGamepadRStickMotion}, 124 | } 125 | 126 | g.inputHandler = g.inputSystem.NewHandler(0, keymap) 127 | } 128 | 129 | type unitStatus int 130 | 131 | const ( 132 | statusIdle unitStatus = iota 133 | statusStartMovement 134 | statusMoving 135 | ) 136 | -------------------------------------------------------------------------------- /_examples/touchscroll/main.go: -------------------------------------------------------------------------------- 1 | //go:build example 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | 9 | "github.com/hajimehoshi/ebiten/v2" 10 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 11 | input "github.com/quasilyte/ebitengine-input" 12 | ) 13 | 14 | const ( 15 | ActionUnknown input.Action = iota 16 | ActionDrag 17 | ActionClick 18 | ActionLongClick 19 | ) 20 | 21 | func main() { 22 | ebiten.SetWindowSize(640, 480) 23 | 24 | if err := ebiten.RunGame(newExampleGame()); err != nil { 25 | log.Fatal(err) 26 | } 27 | } 28 | 29 | type exampleGame struct { 30 | started bool 31 | 32 | pos input.Vec 33 | fallbackPos input.Vec 34 | 35 | numDrags int 36 | numTaps int 37 | numLongTaps int 38 | 39 | inputHandler *input.Handler 40 | inputSystem input.System 41 | } 42 | 43 | func newExampleGame() *exampleGame { 44 | g := &exampleGame{} 45 | 46 | g.inputSystem.Init(input.SystemConfig{ 47 | DevicesEnabled: input.AnyDevice, 48 | }) 49 | 50 | return g 51 | } 52 | 53 | func (g *exampleGame) Layout(_, _ int) (int, int) { 54 | return 640, 480 55 | } 56 | 57 | func (g *exampleGame) Draw(screen *ebiten.Image) { 58 | ebitenutil.DebugPrint(screen, 59 | fmt.Sprintf("use drag gesture to move the star\nnum drags: %d\nnum taps: %d\nnum long taps: %d", g.numDrags, g.numTaps, g.numLongTaps)) 60 | ebitenutil.DebugPrintAt(screen, "*", int(g.pos.X), int(g.pos.Y)) 61 | } 62 | 63 | func (g *exampleGame) Update() error { 64 | g.inputSystem.Update() 65 | 66 | if !g.started { 67 | g.Init() 68 | g.started = true 69 | } 70 | 71 | if g.inputHandler.ActionIsJustPressed(ActionClick) { 72 | g.numTaps++ 73 | } 74 | if g.inputHandler.ActionIsJustPressed(ActionLongClick) { 75 | g.numLongTaps++ 76 | } 77 | 78 | if info, ok := g.inputHandler.JustPressedActionInfo(ActionDrag); ok { 79 | // Start dragging. 80 | g.numDrags++ 81 | g.fallbackPos = info.StartPos 82 | } else if info, ok := g.inputHandler.PressedActionInfo(ActionDrag); ok { 83 | // Continue dragging. 84 | g.pos = info.Pos 85 | } else { 86 | // Not being dragged. 87 | g.pos = g.fallbackPos 88 | } 89 | 90 | return nil 91 | } 92 | 93 | func (g *exampleGame) Init() { 94 | g.pos = input.Vec{X: 200, Y: 200} 95 | g.fallbackPos = g.pos 96 | 97 | keymap := input.Keymap{ 98 | ActionDrag: {input.KeyTouchDrag}, 99 | ActionClick: {input.KeyTouchTap}, 100 | ActionLongClick: {input.KeyTouchLongTap}, 101 | } 102 | 103 | g.inputHandler = g.inputSystem.NewHandler(0, keymap) 104 | } 105 | -------------------------------------------------------------------------------- /_scripts/gen_key_list/main.go: -------------------------------------------------------------------------------- 1 | //go:build generate 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "flag" 8 | "fmt" 9 | "go/ast" 10 | "go/format" 11 | "go/parser" 12 | "go/token" 13 | "html/template" 14 | "os" 15 | "sort" 16 | "strconv" 17 | "strings" 18 | ) 19 | 20 | type keyInfo struct { 21 | VarName string 22 | KeyName string 23 | } 24 | 25 | func main() { 26 | outputFile := flag.String("o", "", "output file name") 27 | flag.Parse() 28 | if len(flag.Args()) != 1 { 29 | panic("expected example 1 positional argument: keys file path") 30 | } 31 | keysFile := flag.Args()[0] 32 | 33 | fset := token.NewFileSet() 34 | astFile, err := parser.ParseFile(fset, keysFile, nil, 0) 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | var keyVariables []keyInfo 40 | for _, decl := range astFile.Decls { 41 | decl, ok := decl.(*ast.GenDecl) 42 | if !ok || decl.Tok != token.VAR { 43 | continue 44 | } 45 | for _, spec := range decl.Specs { 46 | spec := spec.(*ast.ValueSpec) 47 | if len(spec.Names) != 1 || len(spec.Values) != 1 { 48 | continue 49 | } 50 | id := spec.Names[0] 51 | if !strings.HasPrefix(id.Name, "Key") { 52 | continue 53 | } 54 | lit, ok := spec.Values[0].(*ast.CompositeLit) 55 | if !ok { 56 | continue 57 | } 58 | nameValue := getCompositeLitField(lit, "name") 59 | if nameValue == nil { 60 | panic(fmt.Sprintf("missing name field init value for %s", id)) 61 | } 62 | nameString, ok := nameValue.(*ast.BasicLit) 63 | if !ok || nameString.Kind != token.STRING { 64 | panic(fmt.Sprintf("unexpected name field init for %s", id)) 65 | } 66 | keyName, err := strconv.Unquote(nameString.Value) 67 | if err != nil { 68 | panic(err) // should never happen 69 | } 70 | if keyName == "" { 71 | panic(fmt.Sprintf("empty key name for %s", id)) 72 | } 73 | keyVariables = append(keyVariables, keyInfo{ 74 | VarName: id.Name, 75 | KeyName: keyName, 76 | }) 77 | } 78 | } 79 | 80 | sort.SliceStable(keyVariables, func(i, j int) bool { 81 | return keyVariables[i].KeyName < keyVariables[j].KeyName 82 | }) 83 | 84 | templateData := map[string]interface{}{ 85 | "Keys": keyVariables, 86 | } 87 | 88 | var buf bytes.Buffer 89 | if err := outputTemplate.Execute(&buf, templateData); err != nil { 90 | panic(err) 91 | } 92 | 93 | prettyOutput, err := format.Source(buf.Bytes()) 94 | if err != nil { 95 | panic(err) 96 | } 97 | 98 | if *outputFile == "" { 99 | fmt.Print(string(prettyOutput)) 100 | } else { 101 | if err := os.WriteFile(*outputFile, prettyOutput, 0o0664); err != nil { 102 | panic(err) 103 | } 104 | } 105 | } 106 | 107 | func getCompositeLitField(lit *ast.CompositeLit, fieldName string) ast.Expr { 108 | for _, elem := range lit.Elts { 109 | kv, ok := elem.(*ast.KeyValueExpr) 110 | if !ok { 111 | continue 112 | } 113 | id, ok := kv.Key.(*ast.Ident) 114 | if ok && id.Name == fieldName { 115 | return kv.Value 116 | } 117 | } 118 | return nil 119 | } 120 | 121 | var outputTemplate = template.Must(template.New("output").Parse(`// Code generated by "_scripts/gen_key_list"; DO NOT EDIT. 122 | 123 | package input 124 | 125 | // allKeys contains all basic keys provided by a package. 126 | // This slice is sorted by key names (see ActionKeyNames). 127 | var allKeys = []Key{ 128 | {{- range $.Keys }} 129 | {{.VarName}}, 130 | {{- end }} 131 | } 132 | `)) 133 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | // SimulatedKeyEvent represents a virtual input that can be send down the stream. 4 | // 5 | // The data carried by this event will be used to construct an EventInfo object. 6 | // 7 | // Experimental: this is a part of virtual input API, which is not stable yet. 8 | type SimulatedKeyEvent struct { 9 | Key Key 10 | 11 | Pos Vec 12 | } 13 | 14 | // SimulatedAction represents an artificially triggered action. 15 | // 16 | // It shares many properties with SimulatedKeyEvent, but 17 | // the event consumers will have no way of knowing which input 18 | // device was used to emit this event, because SimulatedAction 19 | // has no device associated with it. 20 | // 21 | // As a consequence, all event info methods like IsGamepadeEvent() will report false. 22 | // It's possible to trigger an action that has no keys associated with it. 23 | // All actions triggered using this method will be only visible to the handler 24 | // of the same player ID (like gamepad button events). 25 | // 26 | // Experimental: this is a part of virtual input API, which is not stable yet. 27 | type SimulatedAction struct { 28 | Action Action 29 | 30 | Pos Vec 31 | 32 | StartPos Vec 33 | } 34 | 35 | // EventInfo holds extra information about the input device event. 36 | // 37 | // Pos carries the event location, if available. 38 | // Pos is a click location for mouse events. 39 | // Pos is a tap location for screen touch events. 40 | // Use HasPos() predicate to know whether there is a pos associated 41 | // with the event to distinguish between (0, 0) pos and lack of pos info. 42 | // 43 | // StartPos is only set for a few events where it makes sense. 44 | // A drag event, for instance, will store the "dragging from" location there. 45 | // 46 | // Duration carries the key press duration if available. 47 | // Duration specifies how long the key has been pressed in ticks same as inpututil.KeyPressDuration. 48 | // Duration for key press with modifiers it will return the lowest duration of all key presses. 49 | // Use HasDuration() predicate to know whether there is a duration associated 50 | // with the event to distinguish between 0 duration and lack of duration info. 51 | type EventInfo struct { 52 | kind keyKind 53 | hasPos bool 54 | hasDuration bool 55 | 56 | Duration int 57 | Pos Vec 58 | StartPos Vec 59 | } 60 | 61 | // HasPos reports whether this event has a position associated with it. 62 | // Use Pos field to get the pos value. 63 | func (e EventInfo) HasPos() bool { return e.hasPos } 64 | 65 | // HasDuration reports whether this event has a press duration associated with it. 66 | // Use Duration field to get the press duration value. 67 | func (e EventInfo) HasDuration() bool { return e.hasDuration } 68 | 69 | // IsTouchEvent reports whether this event was triggered by a screen touch device. 70 | // 71 | // Deprecated: Use Source().IsTouch() instead. 72 | func (e EventInfo) IsTouchEvent() bool { 73 | return e.Source().IsTouch() 74 | } 75 | 76 | // IsKeyboardEvent reports whether this event was triggered by a keyboard device. 77 | // 78 | // Deprecated: Use Source().IsKeyboard() instead. 79 | func (e EventInfo) IsKeyboardEvent() bool { 80 | return e.Source().IsKeyboard() 81 | } 82 | 83 | // IsMouseEvent reports whether this event was triggered by a mouse device. 84 | // 85 | // Deprecated: Use Source().IsMouse() instead. 86 | func (e EventInfo) IsMouseEvent() bool { 87 | return e.Source().IsMouse() 88 | } 89 | 90 | // IsGamepadEvent reports whether this event was triggered by a gamepad device. 91 | // 92 | // Deprecated: Use Source().IsGamepad() instead. 93 | func (e EventInfo) IsGamepadEvent() bool { 94 | return e.Source().IsGamepad() 95 | } 96 | 97 | // Source returns the set of devices that were used to trigger the event. 98 | // 99 | // Usually, it returns a single device mask, but sometimes an action 100 | // can be triggered by several devices. 101 | // For example, a ctrl+click key would involve both keyboard and mouse devices, 102 | // resulting in a KeyboardDevice|KeyboardMouse value. 103 | // 104 | // A simulated event would have a zero mask returned, meaning 105 | // no real device was involved. 106 | func (e EventInfo) Source() DeviceKind { 107 | return e.kind.device() 108 | } 109 | 110 | type simulatedEvent struct { 111 | code int 112 | keyKind keyKind 113 | playerID uint8 114 | 115 | pos Vec 116 | startPos Vec 117 | } 118 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/quasilyte/ebitengine-input 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.5.9 7 | github.com/quasilyte/gmath v0.0.0-20221217210116-fba37a2e15c7 8 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d 9 | ) 10 | 11 | require ( 12 | github.com/ebitengine/purego v0.4.0 // indirect 13 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20231223183121-56fa3ac82ce7 // indirect 14 | github.com/jezek/xgb v1.1.1 // indirect 15 | golang.org/x/exp/shiny v0.0.0-20240416160154-fe59bbe5cc7f // indirect 16 | golang.org/x/image v0.14.0 // indirect 17 | golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect 18 | golang.org/x/sync v0.5.0 // indirect 19 | golang.org/x/sys v0.15.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/ebitengine/purego v0.4.0 h1:RQVuMIxQPQ5iCGEJvjQ17YOK+1tMKjVau2FUMvXH4HE= 2 | github.com/ebitengine/purego v0.4.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= 3 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20231223183121-56fa3ac82ce7 h1:7tf/0aw5DxRQjr7WaNqgtjidub6v21L2cogKIbMcTYw= 4 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20231223183121-56fa3ac82ce7/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 5 | github.com/hajimehoshi/ebiten/v2 v2.5.9 h1:xwPrSr4rgB7LgdAKBH9bW7YT8EBBpiruAzykf6QFCv8= 6 | github.com/hajimehoshi/ebiten/v2 v2.5.9/go.mod h1:PrOaLXiRkqAtImDIx2x/7jQdZHHuTcrcQZx5WFQtnK0= 7 | github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= 8 | github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= 9 | github.com/quasilyte/gmath v0.0.0-20221217210116-fba37a2e15c7 h1:mvIS9aGirkzuYmHHNWAP6sLRoA5VFaIzMoFdPmqgPdY= 10 | github.com/quasilyte/gmath v0.0.0-20221217210116-fba37a2e15c7/go.mod h1:EbI+KMbALSVE2s0YFOQpR4uj66zBh9ter5P4CBMSuvA= 11 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= 12 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 13 | golang.org/x/exp/shiny v0.0.0-20240416160154-fe59bbe5cc7f h1:W11kcexeK9nBV2PQVuWu7jFf0rIyI9jb+5AH1IxP7Xc= 14 | golang.org/x/exp/shiny v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o= 15 | golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= 16 | golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= 17 | golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a h1:sYbmY3FwUWCBTodZL1S3JUuOvaW6kM2o+clDzzDNBWg= 18 | golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a/go.mod h1:Ede7gF0KGoHlj822RtphAHK1jLdrcuRBZg0sF1Q+SPc= 19 | golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= 20 | golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 21 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 22 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 23 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/hajimehoshi/ebiten/v2" 7 | "github.com/hajimehoshi/ebiten/v2/inpututil" 8 | ) 9 | 10 | // Handler is used to associate a keymap with an abstract input consumer. 11 | // 12 | // The ID bound to the handler is used to distinguish which gamepad is 13 | // related to this handler. 14 | // 15 | // You usually need to create the input handlers only once and carry 16 | // them through the game using your preferred method. 17 | // 18 | // If any game object needs to handle the input, they need an input handler object. 19 | type Handler struct { 20 | id uint8 21 | keymap Keymap 22 | sys *System 23 | last DeviceKind 24 | 25 | // GamepadDeadzone is the magnitude of a controller stick movements 26 | // the handler can receive before registering it as an input. 27 | // 28 | // The default value is 0.055, meaning the slight movements are ignored. 29 | // A value of 0.5 means about half the axis is ignored. 30 | // 31 | // The default value is good for a new/responsive controller. 32 | // For more worn out controllers or flaky sticks, a higher value may be required. 33 | // 34 | // This parameter can be adjusted on the fly, so you're encouraged to 35 | // give a player a way to configure a deadzone that will fit their controller. 36 | // 37 | // Note that this is a per-handler option. 38 | // Different gamepads/devices can have different deadzone values. 39 | GamepadDeadzone float64 40 | } 41 | 42 | // Remap changes the handler keymap while keeping all other settings the same. 43 | func (h *Handler) Remap(keymap Keymap) { 44 | h.keymap = keymap 45 | } 46 | 47 | // GamepadConnected reports whether the gamepad associated with this handler is connected. 48 | // The gamepad ID is the handler ID used during the handler creation. 49 | // 50 | // There should be at least one call to the System.Update() before this function 51 | // can return the correct results. 52 | func (h *Handler) GamepadConnected() bool { 53 | for _, id := range h.sys.gamepadIDs { 54 | if id == ebiten.GamepadID(h.id) { 55 | return true 56 | } 57 | } 58 | return false 59 | } 60 | 61 | // TouchEventsEnabled reports whether this handler can receive screen touch events. 62 | func (h *Handler) TouchEventsEnabled() bool { 63 | return h.sys.touchEnabled 64 | } 65 | 66 | // CursorPos returns the current mouse cursor position on the screen. 67 | func (h *Handler) CursorPos() Vec { 68 | return h.sys.cursorPos 69 | } 70 | 71 | // DefaultInputMask returns the input mask suitable for functions like ActionKeyNames. 72 | // 73 | // If gamepad is connected, it returns GamepadDevice mask. 74 | // Otherwise it returns KeyboardInput+MouseInput mask. 75 | // This is good enough for the simplest games, but you may to implement this 76 | // logic inside your game if you need something more complicated. 77 | func (h *Handler) DefaultInputMask() DeviceKind { 78 | if h.GamepadConnected() { 79 | return GamepadDevice 80 | } 81 | return KeyboardDevice | MouseDevice 82 | } 83 | 84 | // EmitKeyEvent sends given key event into the input system. 85 | // 86 | // The event is emitted from the perspective of this handler, 87 | // so the gamepad events will be handled properly in the multi-device context. 88 | // 89 | // Note: simulated events are only visible after the next System.Update() call. 90 | // 91 | // Note: key release events can't be simulated yet (see #35). 92 | // 93 | // See SimulatedKeyEvent documentation for more info. 94 | // 95 | // Experimental: this is a part of virtual input API, which is not stable yet. 96 | func (h *Handler) EmitKeyEvent(e SimulatedKeyEvent) { 97 | h.sys.pendingEvents = append(h.sys.pendingEvents, simulatedEvent{ 98 | code: e.Key.code, 99 | keyKind: e.Key.kind, 100 | playerID: h.id, 101 | pos: e.Pos, 102 | }) 103 | } 104 | 105 | // EmitEvent activates the given action for the player. 106 | // Only the handlers with the same player ID will discover this action. 107 | // 108 | // Note: simulated events are only visible after the next System.Update() call. 109 | // 110 | // Note: action release events can't be simulated yet (see #35). 111 | // 112 | // See SimulatedAction documentation for more info. 113 | // 114 | // Experimental: this is a part of virtual input API, which is not stable yet. 115 | func (h *Handler) EmitEvent(e SimulatedAction) { 116 | h.sys.pendingEvents = append(h.sys.pendingEvents, simulatedEvent{ 117 | code: int(e.Action), 118 | keyKind: keySimulated, 119 | playerID: h.id, 120 | pos: e.Pos, 121 | startPos: e.StartPos, 122 | }) 123 | } 124 | 125 | // AnyKeyJustPressed reports whether any action key was just pressed. 126 | // It checks for keyboard keys, gamepad buttons and touch taps. 127 | // 128 | // This function is useful for "press any key" contexts where you 129 | // don't care which key was used to trigger the event. 130 | // 131 | // This method does respect simulated inputs. 132 | // 133 | // This method does not support gamepad pseudo-keys like KeyGamepadLStickUp. 134 | func (h *Handler) AnyKeyJustPressed() bool { 135 | h.sys.keySlice = inpututil.AppendJustPressedKeys(h.sys.keySlice[:0]) 136 | if len(h.sys.keySlice) != 0 { 137 | return true 138 | } 139 | 140 | if len(h.sys.gamepadIDs) != 0 { 141 | h.sys.gamepadKeySlice = inpututil.AppendJustPressedGamepadButtons(ebiten.GamepadID(h.id), h.sys.gamepadKeySlice[:0]) 142 | if len(h.sys.gamepadKeySlice) != 0 { 143 | return true 144 | } 145 | } 146 | 147 | if h.sys.touchEnabled { 148 | if h.sys.touchHasTap || h.sys.touchHasLongTap { 149 | return true 150 | } 151 | } 152 | 153 | if h.sys.hasSimulatedActions { 154 | for _, e := range h.sys.simulatedEvents { 155 | if keyNeedID(e.keyKind) && e.playerID != h.id { 156 | continue 157 | } 158 | return true 159 | } 160 | } 161 | 162 | return false 163 | } 164 | 165 | // ActionKeyNames returns a list of key names associated by this action. 166 | // 167 | // It filters the results by a given input device mask. 168 | // If you want to include all input device keys, use AnyDevice value. 169 | // 170 | // This function is useful when you want to display a list of keys 171 | // the player should press in order to activate some action. 172 | // 173 | // The filtering is useful to avoid listing the unrelated options. 174 | // For example, if player uses the gamepad, it could be weird to 175 | // show keyboard options listed. For the simple cases, you can use 176 | // DefaultInputMask() method to get the mask that will try to avoid 177 | // that situation. See its comment to learn more. 178 | // 179 | // Keys with modifiers will have them listed too. 180 | // Modifiers are separated by "+". 181 | // A "k" keyboard key with ctrl modifier will have a "ctrl+k" name. 182 | // 183 | // Note: this function doesn't check whether some input device is available or not. 184 | // For example, if mask contains a TouchDevice, but touch actions are not 185 | // available on a machine, touch-related keys will still be returned. 186 | // It's up to the caller to specify a correct device mask. 187 | // Using a AnyDevice mask would return all mapped keys for the action. 188 | func (h *Handler) ActionKeyNames(action Action, mask DeviceKind) []string { 189 | keys, ok := h.keymap[action] 190 | if !ok { 191 | return nil 192 | } 193 | result := make([]string, 0, len(keys)) 194 | for _, k := range keys { 195 | if !h.keyIsEnabled(k, mask) { 196 | continue 197 | } 198 | result = append(result, k.String()) 199 | } 200 | return result 201 | } 202 | 203 | func (h *Handler) keyIsEnabled(k Key, mask DeviceKind) bool { 204 | switch k.kind { 205 | case keyKeyboardWithCtrlShift: 206 | return mask&KeyboardDevice != 0 207 | case keyKeyboardWithCtrl: 208 | return mask&KeyboardDevice != 0 209 | case keyKeyboardWithShift: 210 | return mask&KeyboardDevice != 0 211 | case keyKeyboard: 212 | return mask&KeyboardDevice != 0 213 | case keyMouseWithCtrlShift: 214 | return mask&MouseDevice != 0 215 | case keyMouseWithCtrl: 216 | return mask&MouseDevice != 0 217 | case keyMouseWithShift: 218 | return mask&MouseDevice != 0 219 | case keyMouse: 220 | return mask&MouseDevice != 0 221 | case keyWheel, keyWheelWithCtrl, keyWheelWithShift, keyWheelWithCtrlShift: 222 | return mask&MouseDevice != 0 223 | case keyGamepad, keyGamepadLeftStick, keyGamepadRightStick, keyGamepadStickMotion: 224 | return mask&GamepadDevice != 0 225 | case keyTouch, keyTouchDrag: 226 | return mask&TouchDevice != 0 227 | } 228 | return true 229 | } 230 | 231 | // JustReleasedActionInfo is like ActionIsJustReleased, but with more information. 232 | // 233 | // This method has the same limitations as ActionIsJustReleased (see its comments). 234 | // 235 | // The first return value will hold the extra event info. 236 | // The second return value is false if given action is not just released. 237 | // 238 | // See EventInfo comment to learn more. 239 | // 240 | // Note: this action event is never simulated (see #35). 241 | func (h *Handler) JustReleasedActionInfo(action Action) (EventInfo, bool) { 242 | keys, ok := h.keymap[action] 243 | if !ok { 244 | return EventInfo{}, false 245 | } 246 | for _, k := range keys { 247 | if !h.keyIsJustReleased(k) { 248 | continue 249 | } 250 | // TODO: maybe move this EventInfo initialization code to a function? 251 | // It look like it's the same code in every *ActionInfo method. 252 | // We don't need the StartPos here yet, because touch events 253 | // are not handled in release events, but that's just a minutiae. 254 | var info EventInfo 255 | info.kind = k.kind 256 | info.hasPos = keyHasPos(k.kind) 257 | info.Pos = h.getKeyPos(k) 258 | info.StartPos = h.getKeyStartPos(k) 259 | h.updateLastDevice(k.kind) 260 | return info, true 261 | } 262 | return EventInfo{}, false 263 | } 264 | 265 | // ActionIsJustReleased is like inpututil.IsKeyJustReleased, but operates 266 | // on the action level and works with any kinds of "keys". 267 | // It returns true if any of the keys bound to the action was released during this frame. 268 | // 269 | // Implementation limitation: for now it doesn't work for some of the key types. 270 | // It's easier to list the supported list: 271 | // - Keyboard events 272 | // - Mouse events 273 | // - Gamepad normal buttons events (doesn't include joystick D-pad emulation events like KeyGamepadLStickUp) 274 | // 275 | // For the keys with modifiers it doesn't require the modifier keys to be released simultaneously with a main key. 276 | // These modifier keys can be in either "pressed" or "just released" state. 277 | // This makes the "ctrl+left click just released" event easier to perform on the user's side 278 | // (try releasing ctrl on the same frame as left click, it's hard!) 279 | // 280 | // TODO: implement other "action released" events if feasible. 281 | // The touch tap events, for example, doesn't sound useful here: a tap is 282 | // only registered when the gesture was already finished. 283 | // Therefore, the tap release event would be identical to a tap activation event. 284 | // We could re-word this event by saying that "released" happens when previous 285 | // frame ActionIsPressed reported true and the current frame reported false. 286 | // But that's a more complicated task. 287 | // Let's wait until users report their use cases. 288 | // 289 | // Note: this action event is never simulated (see #35). 290 | func (h *Handler) ActionIsJustReleased(action Action) bool { 291 | keys, ok := h.keymap[action] 292 | if !ok { 293 | return false 294 | } 295 | for _, k := range keys { 296 | if h.keyIsJustReleased(k) { 297 | h.updateLastDevice(k.kind) 298 | return true 299 | } 300 | } 301 | return false 302 | } 303 | 304 | // JustPressedActionInfo is like ActionIsJustPressed, but with more information. 305 | // 306 | // The first return value will hold the extra event info. 307 | // The second return value is false if given action is not activated. 308 | // 309 | // See EventInfo comment to learn more. 310 | func (h *Handler) JustPressedActionInfo(action Action) (EventInfo, bool) { 311 | keys, ok := h.keymap[action] 312 | if !ok { 313 | return EventInfo{}, false 314 | } 315 | for _, k := range keys { 316 | if info, status := h.pressedSimulatedKeyInfo(true, k); status == bool3true { 317 | return info, true 318 | } 319 | if !h.keyIsJustPressed(k) { 320 | continue 321 | } 322 | var info EventInfo 323 | info.kind = k.kind 324 | info.hasPos = keyHasPos(k.kind) 325 | info.Pos = h.getKeyPos(k) 326 | info.StartPos = h.getKeyStartPos(k) 327 | h.updateLastDevice(k.kind) 328 | return info, true 329 | } 330 | if h.sys.hasSimulatedActions { 331 | info, status := h.pressedSimulatedKeyInfo(true, Key{ 332 | code: int(action), 333 | kind: keySimulated, 334 | }) 335 | return info, status == bool3true 336 | } 337 | return EventInfo{}, false 338 | } 339 | 340 | // PressedActionInfo is like ActionIsPressed, but with more information. 341 | // 342 | // The first return value will hold the extra event info. 343 | // The second return value is false if given action is not activated. 344 | // 345 | // See EventInfo comment to learn more. 346 | func (h *Handler) PressedActionInfo(action Action) (EventInfo, bool) { 347 | keys, ok := h.keymap[action] 348 | if !ok { 349 | return EventInfo{}, false 350 | } 351 | for _, k := range keys { 352 | if info, status := h.pressedSimulatedKeyInfo(false, k); status == bool3true { 353 | return info, true 354 | } 355 | if !h.keyIsPressed(k) { 356 | continue 357 | } 358 | var info EventInfo 359 | info.kind = k.kind 360 | info.hasPos = keyHasPos(k.kind) 361 | info.Pos = h.getKeyPos(k) 362 | info.StartPos = h.getKeyStartPos(k) 363 | info.hasDuration = keyHasDuration(k.kind) 364 | info.Duration = h.getKeyPressDuration(k) 365 | h.updateLastDevice(k.kind) 366 | return info, true 367 | } 368 | return EventInfo{}, false 369 | } 370 | 371 | // ActionIsJustPressed is like inpututil.IsKeyJustPressed, but operates 372 | // on the action level and works with any kinds of "keys". 373 | // It returns true if any of the keys bound to the action was pressed during this frame. 374 | func (h *Handler) ActionIsJustPressed(action Action) bool { 375 | keys, ok := h.keymap[action] 376 | if !ok { 377 | return false 378 | } 379 | for _, k := range keys { 380 | if len(h.sys.simulatedEvents) != 0 { 381 | // We want to avoid a situation when simulated input 382 | // things that the key is still being pressed and then 383 | // receive a real input from the bottom of inpututil that 384 | // this key was actually "just pressed". To avoid that, 385 | // we skip checking the real input if simulated input still 386 | // holds that button down. This is why we need a bool3 here. 387 | _, isPressed := h.pressedSimulatedKeyInfo(true, k) 388 | if isPressed != bool3unset { 389 | return isPressed == bool3true 390 | } 391 | } 392 | if h.keyIsJustPressed(k) { 393 | h.updateLastDevice(k.kind) 394 | return true 395 | } 396 | } 397 | if h.sys.hasSimulatedActions { 398 | _, isPressed := h.pressedSimulatedKeyInfo(true, Key{ 399 | code: int(action), 400 | kind: keySimulated, 401 | }) 402 | return isPressed == bool3true 403 | } 404 | return false 405 | } 406 | 407 | // ActionIsPressed is like ebiten.IsKeyPressed, but operates 408 | // on the action level and works with any kinds of "keys". 409 | // It returns true if any of the keys bound to the action is being pressed. 410 | func (h *Handler) ActionIsPressed(action Action) bool { 411 | keys, ok := h.keymap[action] 412 | if !ok { 413 | return false 414 | } 415 | for _, k := range keys { 416 | if len(h.sys.simulatedEvents) != 0 && h.simulatedKeyIsPressed(k) { 417 | return true 418 | } 419 | if h.keyIsPressed(k) { 420 | h.updateLastDevice(k.kind) 421 | return true 422 | } 423 | } 424 | if h.sys.hasSimulatedActions { 425 | return h.simulatedKeyIsPressed(Key{ 426 | code: int(action), 427 | kind: keySimulated, 428 | }) 429 | } 430 | return false 431 | } 432 | 433 | // LastDevice returns a set of devices that were used to trigger the last event. 434 | // May be useful for swapping button prompts when the user changes device. 435 | func (h *Handler) LastDevice() DeviceKind { 436 | return h.last 437 | } 438 | 439 | func (h *Handler) keyIsJustReleased(k Key) bool { 440 | // Several key kinds are not handled here. 441 | // TODO: extend the supported key kinds list? 442 | switch k.kind { 443 | case keyMouse: 444 | return inpututil.IsMouseButtonJustReleased(ebiten.MouseButton(k.code)) 445 | case keyMouseDrag: 446 | return h.sys.mouseJustReleasedDrag 447 | case keyGamepad: 448 | return h.gamepadKeyIsJustReleased(k) 449 | case keyMouseWithCtrl: 450 | return h.ebitenKeyIsPressedOrJustReleased(ebiten.KeyControl) && 451 | inpututil.IsMouseButtonJustReleased(ebiten.MouseButton(k.code)) 452 | case keyMouseWithShift: 453 | return h.ebitenKeyIsPressedOrJustReleased(ebiten.KeyShift) && 454 | inpututil.IsMouseButtonJustReleased(ebiten.MouseButton(k.code)) 455 | case keyMouseWithCtrlShift: 456 | return h.ebitenKeyIsPressedOrJustReleased(ebiten.KeyControl) && 457 | h.ebitenKeyIsPressedOrJustReleased(ebiten.KeyShift) && 458 | inpututil.IsMouseButtonJustReleased(ebiten.MouseButton(k.code)) 459 | case keyKeyboardWithCtrl: 460 | return h.ebitenKeyIsPressedOrJustReleased(ebiten.KeyControl) && 461 | inpututil.IsKeyJustReleased(ebiten.Key(k.code)) 462 | case keyKeyboardWithShift: 463 | return h.ebitenKeyIsPressedOrJustReleased(ebiten.KeyShift) && 464 | inpututil.IsKeyJustReleased(ebiten.Key(k.code)) 465 | case keyKeyboardWithCtrlShift: 466 | return h.ebitenKeyIsPressedOrJustReleased(ebiten.KeyControl) && 467 | h.ebitenKeyIsPressedOrJustReleased(ebiten.KeyShift) && 468 | inpututil.IsKeyJustReleased(ebiten.Key(k.code)) 469 | case keyKeyboard: 470 | return inpututil.IsKeyJustReleased(ebiten.Key(k.code)) 471 | default: 472 | return false 473 | } 474 | } 475 | 476 | func (h *Handler) ebitenKeyIsPressedOrJustReleased(k ebiten.Key) bool { 477 | return ebiten.IsKeyPressed(k) || inpututil.IsKeyJustReleased(k) 478 | } 479 | 480 | func (h *Handler) keyIsJustPressed(k Key) bool { 481 | switch k.kind { 482 | case keyTouch: 483 | if k.code == int(touchTap) { 484 | return h.sys.touchHasTap 485 | } 486 | if k.code == int(touchLongTap) { 487 | return h.sys.touchHasLongTap 488 | } 489 | return false 490 | case keyTouchDrag: 491 | return h.sys.touchJustHadDrag 492 | case keyMouseDrag: 493 | return h.sys.mouseJustHadDrag 494 | case keyGamepad: 495 | return h.gamepadKeyIsJustPressed(k) 496 | case keyGamepadLeftStick: 497 | return h.gamepadStickIsJustPressed(stickCode(k.code), ebiten.StandardGamepadAxisLeftStickHorizontal, ebiten.StandardGamepadAxisLeftStickVertical) 498 | case keyGamepadRightStick: 499 | return h.gamepadStickIsJustPressed(stickCode(k.code), ebiten.StandardGamepadAxisRightStickHorizontal, ebiten.StandardGamepadAxisRightStickVertical) 500 | case keyGamepadStickMotion: 501 | return h.gamepadStickMotionIsJustPressed(stickCode(k.code)) 502 | case keyMouse: 503 | return inpututil.IsMouseButtonJustPressed(ebiten.MouseButton(k.code)) 504 | case keyMouseWithCtrl: 505 | return ebiten.IsKeyPressed(ebiten.KeyControl) && 506 | inpututil.IsMouseButtonJustPressed(ebiten.MouseButton(k.code)) 507 | case keyMouseWithShift: 508 | return ebiten.IsKeyPressed(ebiten.KeyShift) && 509 | inpututil.IsMouseButtonJustPressed(ebiten.MouseButton(k.code)) 510 | case keyMouseWithCtrlShift: 511 | return ebiten.IsKeyPressed(ebiten.KeyControl) && 512 | ebiten.IsKeyPressed(ebiten.KeyShift) && 513 | inpututil.IsMouseButtonJustPressed(ebiten.MouseButton(k.code)) 514 | case keyKeyboardWithCtrl: 515 | return ebiten.IsKeyPressed(ebiten.KeyControl) && 516 | inpututil.IsKeyJustPressed(ebiten.Key(k.code)) 517 | case keyKeyboardWithShift: 518 | return ebiten.IsKeyPressed(ebiten.KeyShift) && 519 | inpututil.IsKeyJustPressed(ebiten.Key(k.code)) 520 | case keyKeyboardWithCtrlShift: 521 | return ebiten.IsKeyPressed(ebiten.KeyControl) && 522 | ebiten.IsKeyPressed(ebiten.KeyShift) && 523 | inpututil.IsKeyJustPressed(ebiten.Key(k.code)) 524 | case keyWheel: 525 | return h.wheelIsJustPressed(wheelCode(k.code)) 526 | case keyWheelWithCtrl: 527 | return ebiten.IsKeyPressed(ebiten.KeyControl) && 528 | h.wheelIsJustPressed(wheelCode(k.code)) 529 | case keyWheelWithShift: 530 | return ebiten.IsKeyPressed(ebiten.KeyShift) && 531 | h.wheelIsJustPressed(wheelCode(k.code)) 532 | case keyWheelWithCtrlShift: 533 | return ebiten.IsKeyPressed(ebiten.KeyControl) && 534 | ebiten.IsKeyPressed(ebiten.KeyShift) && 535 | h.wheelIsJustPressed(wheelCode(k.code)) 536 | default: 537 | return inpututil.IsKeyJustPressed(ebiten.Key(k.code)) 538 | } 539 | } 540 | 541 | func (h *Handler) getKeyStartPos(k Key) Vec { 542 | var result Vec 543 | switch k.kind { 544 | case keyTouchDrag: 545 | result = h.sys.touchStartPos 546 | case keyMouseDrag: 547 | result = h.sys.mouseStartPos 548 | } 549 | return result 550 | } 551 | 552 | func (h *Handler) getKeyPos(k Key) Vec { 553 | var result Vec 554 | switch k.kind { 555 | case keyMouse, keyMouseWithCtrl, keyMouseWithShift, keyMouseWithCtrlShift: 556 | result = h.sys.cursorPos 557 | case keyTouch: 558 | result = h.sys.touchTapPos 559 | case keyTouchDrag: 560 | result = h.sys.touchDragPos 561 | case keyMouseDrag: 562 | result = h.sys.mouseDragPos 563 | case keyWheel, keyWheelWithCtrl, keyWheelWithShift, keyWheelWithCtrlShift: 564 | result = h.sys.wheel 565 | case keyGamepadStickMotion: 566 | axis1, axis2 := h.getStickAxes(stickCode(k.code)) 567 | result = h.getStickVec(axis1, axis2) 568 | } 569 | return result 570 | } 571 | 572 | // getKeyPressDuration returns how long the key has been pressed in ticks same as inpututil.KeyPressDuration. 573 | // When looking at a key press with modifiers it will return the lowest duration of all key presses. 574 | func (h *Handler) getKeyPressDuration(k Key) int { 575 | switch k.kind { 576 | case keyKeyboardWithShift: 577 | return minOf(inpututil.KeyPressDuration(ebiten.Key(k.code)), inpututil.KeyPressDuration(ebiten.KeyShift)) 578 | case keyKeyboardWithCtrl: 579 | return minOf(inpututil.KeyPressDuration(ebiten.Key(k.code)), inpututil.KeyPressDuration(ebiten.KeyControl)) 580 | case keyKeyboardWithCtrlShift: 581 | return minOf( 582 | inpututil.KeyPressDuration(ebiten.Key(k.code)), 583 | minOf( 584 | inpututil.KeyPressDuration(ebiten.KeyShift), 585 | inpututil.KeyPressDuration(ebiten.KeyControl))) 586 | case keyKeyboard: 587 | return inpututil.KeyPressDuration(ebiten.Key(k.code)) 588 | } 589 | 590 | return 0 591 | } 592 | 593 | func (h *Handler) keyIsPressed(k Key) bool { 594 | switch k.kind { 595 | case keyTouch: 596 | if k.code == int(touchTap) { 597 | return h.sys.touchHasTap 598 | } 599 | if k.code == int(touchLongTap) { 600 | return h.sys.touchHasLongTap 601 | } 602 | return false 603 | case keyTouchDrag: 604 | return h.sys.touchHasDrag 605 | case keyMouseDrag: 606 | return h.sys.mouseHasDrag 607 | case keyGamepad: 608 | return h.gamepadKeyIsPressed(k) 609 | case keyGamepadLeftStick: 610 | return h.gamepadStickIsPressed(stickCode(k.code), ebiten.StandardGamepadAxisLeftStickHorizontal, ebiten.StandardGamepadAxisLeftStickVertical) 611 | case keyGamepadRightStick: 612 | return h.gamepadStickIsPressed(stickCode(k.code), ebiten.StandardGamepadAxisRightStickHorizontal, ebiten.StandardGamepadAxisRightStickVertical) 613 | case keyGamepadStickMotion: 614 | return h.gamepadStickMotionIsPressed(stickCode(k.code)) 615 | case keyMouse: 616 | return ebiten.IsMouseButtonPressed(ebiten.MouseButton(k.code)) 617 | case keyMouseWithCtrl: 618 | return ebiten.IsKeyPressed(ebiten.KeyControl) && 619 | ebiten.IsMouseButtonPressed(ebiten.MouseButton(k.code)) 620 | case keyMouseWithShift: 621 | return ebiten.IsKeyPressed(ebiten.KeyShift) && 622 | ebiten.IsMouseButtonPressed(ebiten.MouseButton(k.code)) 623 | case keyMouseWithCtrlShift: 624 | return ebiten.IsKeyPressed(ebiten.KeyControl) && 625 | ebiten.IsKeyPressed(ebiten.KeyShift) && 626 | ebiten.IsMouseButtonPressed(ebiten.MouseButton(k.code)) 627 | case keyKeyboardWithCtrl: 628 | return ebiten.IsKeyPressed(ebiten.KeyControl) && 629 | ebiten.IsKeyPressed(ebiten.Key(k.code)) 630 | case keyKeyboardWithShift: 631 | return ebiten.IsKeyPressed(ebiten.KeyShift) && 632 | ebiten.IsKeyPressed(ebiten.Key(k.code)) 633 | case keyKeyboardWithCtrlShift: 634 | return ebiten.IsKeyPressed(ebiten.KeyControl) && 635 | ebiten.IsKeyPressed(ebiten.KeyShift) && 636 | ebiten.IsKeyPressed(ebiten.Key(k.code)) 637 | default: 638 | return ebiten.IsKeyPressed(ebiten.Key(k.code)) 639 | } 640 | } 641 | 642 | func (h *Handler) eventSliceFind(slice []simulatedEvent, k Key) int { 643 | for i, e := range slice { 644 | if e.code == k.code && e.keyKind == k.kind { 645 | if keyNeedID(e.keyKind) && e.playerID != h.id { 646 | continue 647 | } 648 | return i 649 | } 650 | } 651 | return -1 652 | } 653 | 654 | func (h *Handler) eventSliceContains(slice []simulatedEvent, k Key) bool { 655 | return h.eventSliceFind(slice, k) != -1 656 | } 657 | 658 | func (h *Handler) pressedSimulatedKeyInfo(justPressed bool, k Key) (EventInfo, bool3) { 659 | var info EventInfo 660 | i := h.eventSliceFind(h.sys.simulatedEvents, k) 661 | if i != -1 { 662 | if justPressed && h.eventSliceContains(h.sys.prevSimulatedEvents, k) { 663 | return info, bool3false 664 | } 665 | info.Pos = h.sys.simulatedEvents[i].pos 666 | info.StartPos = h.sys.simulatedEvents[i].startPos 667 | info.kind = k.kind 668 | info.hasPos = keyHasPos(k.kind) 669 | return info, bool3true 670 | } 671 | return info, bool3unset 672 | } 673 | 674 | func (h *Handler) simulatedKeyIsPressed(k Key) bool { 675 | return h.eventSliceContains(h.sys.simulatedEvents, k) 676 | } 677 | 678 | func (h *Handler) bumperIsActive(v float64) bool { 679 | return v >= 0.9 680 | } 681 | 682 | func (h *Handler) isDPadAxisActive(code int, vec Vec) bool { 683 | switch ebiten.StandardGamepadButton(code) { 684 | case ebiten.StandardGamepadButtonLeftTop: 685 | return vec.Y == -1 686 | case ebiten.StandardGamepadButtonLeftRight: 687 | return vec.X == 1 688 | case ebiten.StandardGamepadButtonLeftBottom: 689 | return vec.Y == 1 690 | case ebiten.StandardGamepadButtonLeftLeft: 691 | return vec.X == -1 692 | } 693 | return false 694 | } 695 | 696 | func (h *Handler) wheelIsJustPressed(code wheelCode) bool { 697 | switch code { 698 | case wheelDown: 699 | return h.sys.wheel.Y > 0 700 | case wheelUp: 701 | return h.sys.wheel.Y < 0 702 | case wheelVertical: 703 | return h.sys.wheel.Y != 0 704 | default: 705 | return false 706 | } 707 | } 708 | 709 | func (h *Handler) gamepadKeyIsJustReleased(k Key) bool { 710 | if h.gamepadInfo().model == gamepadStandard { 711 | return inpututil.IsStandardGamepadButtonJustReleased(ebiten.GamepadID(h.id), ebiten.StandardGamepadButton(k.code)) 712 | } 713 | return inpututil.IsGamepadButtonJustReleased(ebiten.GamepadID(h.id), h.mappedGamepadKey(k.code)) 714 | } 715 | 716 | func (h *Handler) gamepadKeyIsJustPressed(k Key) bool { 717 | if h.gamepadInfo().model == gamepadStandard { 718 | return inpututil.IsStandardGamepadButtonJustPressed(ebiten.GamepadID(h.id), ebiten.StandardGamepadButton(k.code)) 719 | } 720 | if h.gamepadInfo().model == gamepadFirefoxXinput { 721 | if isDPadButton(k.code) { 722 | return !h.isDPadAxisActive(k.code, h.getStickPrevVec(6, 7)) && 723 | h.isDPadAxisActive(k.code, h.getStickVec(6, 7)) 724 | } 725 | if k.code == int(ebiten.StandardGamepadButtonFrontBottomLeft) { 726 | return !h.bumperIsActive(h.gamepadInfo().prevAxisValues[2]) && 727 | h.bumperIsActive(h.gamepadInfo().axisValues[2]) 728 | } 729 | if k.code == int(ebiten.StandardGamepadButtonFrontBottomRight) { 730 | return !h.bumperIsActive(h.gamepadInfo().prevAxisValues[5]) && 731 | h.bumperIsActive(h.gamepadInfo().axisValues[5]) 732 | } 733 | } 734 | return inpututil.IsGamepadButtonJustPressed(ebiten.GamepadID(h.id), h.mappedGamepadKey(k.code)) 735 | } 736 | 737 | func (h *Handler) gamepadKeyIsPressed(k Key) bool { 738 | if h.gamepadInfo().model == gamepadStandard { 739 | return ebiten.IsStandardGamepadButtonPressed(ebiten.GamepadID(h.id), ebiten.StandardGamepadButton(k.code)) 740 | } 741 | if h.gamepadInfo().model == gamepadFirefoxXinput { 742 | if isDPadButton(k.code) { 743 | return h.isDPadAxisActive(k.code, h.getStickVec(6, 7)) 744 | } 745 | if k.code == int(ebiten.StandardGamepadButtonFrontBottomLeft) { 746 | return h.bumperIsActive(h.gamepadInfo().axisValues[2]) 747 | } 748 | if k.code == int(ebiten.StandardGamepadButtonFrontBottomRight) { 749 | return h.bumperIsActive(h.gamepadInfo().axisValues[5]) 750 | } 751 | } 752 | return ebiten.IsGamepadButtonPressed(ebiten.GamepadID(h.id), h.mappedGamepadKey(k.code)) 753 | } 754 | 755 | func (h *Handler) gamepadStickIsActive(code stickCode, vec Vec) bool { 756 | if vecLen(vec) < 0.5 { 757 | return false 758 | } 759 | // Allow some axis overlap to emulate double direction pressing, 760 | // like with D-pad buttons. 761 | const overlap float64 = math.Pi / 7 762 | switch code { 763 | case stickUp: 764 | angle := angleNormalized(vecAngle(vec)) 765 | return angle > (math.Pi+math.Pi/4)-overlap && angle <= (2*math.Pi-math.Pi/4)+overlap 766 | case stickRight: 767 | angle := angleNormalized(vecAngle(vec)) 768 | return angle <= (math.Pi/4)+overlap || angle > (2*math.Pi-math.Pi/4)-overlap 769 | case stickDown: 770 | angle := angleNormalized(vecAngle(vec)) 771 | return angle > (math.Pi/4)-overlap && angle <= (math.Pi-math.Pi/4)+overlap 772 | case stickLeft: 773 | angle := angleNormalized(vecAngle(vec)) 774 | return angle > (math.Pi-math.Pi/4)-overlap && angle <= (math.Pi+math.Pi/4)+overlap 775 | } 776 | return false 777 | } 778 | 779 | func (h *Handler) gamepadStickIsJustPressed(code stickCode, axis1, axis2 ebiten.StandardGamepadAxis) bool { 780 | return !h.gamepadStickIsActive(code, h.getStickPrevVec(int(axis1), int(axis2))) && 781 | h.gamepadStickIsActive(code, h.getStickVec(int(axis1), int(axis2))) 782 | } 783 | 784 | func (h *Handler) getStickAxes(code stickCode) (int, int) { 785 | var axis1 int 786 | var axis2 int 787 | if h.gamepadInfo().model == gamepadFirefoxXinput { 788 | if code == stickLeft { 789 | axis1 = 0 790 | axis2 = 1 791 | } else { 792 | axis1 = 3 793 | axis2 = 4 794 | } 795 | } else { 796 | if code == stickLeft { 797 | axis1 = int(ebiten.StandardGamepadAxisLeftStickHorizontal) 798 | axis2 = int(ebiten.StandardGamepadAxisLeftStickVertical) 799 | } else { 800 | axis1 = int(ebiten.StandardGamepadAxisRightStickHorizontal) 801 | axis2 = int(ebiten.StandardGamepadAxisRightStickVertical) 802 | } 803 | } 804 | return axis1, axis2 805 | } 806 | 807 | func (h *Handler) gamepadStickMotionIsJustPressed(code stickCode) bool { 808 | return !h.gamepadStickMotionIsActive(h.getStickPrevVec(h.getStickAxes(code))) && 809 | h.gamepadStickMotionIsActive(h.getStickVec(h.getStickAxes(code))) 810 | } 811 | 812 | func (h *Handler) gamepadStickMotionIsPressed(code stickCode) bool { 813 | return h.gamepadStickMotionIsActive(h.getStickVec(h.getStickAxes(code))) 814 | } 815 | 816 | func (h *Handler) gamepadStickMotionIsActive(vec Vec) bool { 817 | // Some gamepads could register a slight movement all the time, 818 | // even if the stick is in its home position. 819 | return math.Abs(vec.X)+math.Abs(vec.Y) >= h.GamepadDeadzone 820 | } 821 | 822 | func (h *Handler) gamepadStickIsPressed(code stickCode, axis1, axis2 ebiten.StandardGamepadAxis) bool { 823 | vec := h.getStickVec(int(axis1), int(axis2)) 824 | return h.gamepadStickIsActive(code, vec) 825 | } 826 | 827 | func (h *Handler) getStickPrevVec(axis1, axis2 int) Vec { 828 | return Vec{ 829 | X: h.gamepadInfo().prevAxisValues[axis1], 830 | Y: h.gamepadInfo().prevAxisValues[axis2], 831 | } 832 | } 833 | 834 | func (h *Handler) getStickVec(axis1, axis2 int) Vec { 835 | return Vec{ 836 | X: h.gamepadInfo().axisValues[axis1], 837 | Y: h.gamepadInfo().axisValues[axis2], 838 | } 839 | } 840 | 841 | func (h *Handler) gamepadInfo() *gamepadInfo { 842 | return &h.sys.gamepadInfo[h.id] 843 | } 844 | 845 | func (h *Handler) mappedGamepadKey(keyCode int) ebiten.GamepadButton { 846 | b := ebiten.StandardGamepadButton(keyCode) 847 | switch h.gamepadInfo().model { 848 | case gamepadMicront: 849 | return microntToXbox(b) 850 | case gamepadFirefoxXinput: 851 | return firefoxXinputToXbox(b) 852 | default: 853 | return ebiten.GamepadButton(keyCode) 854 | } 855 | } 856 | 857 | func (h *Handler) updateLastDevice(kind keyKind) { 858 | h.last = kind.device() 859 | } 860 | -------------------------------------------------------------------------------- /input.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Action is an ID that represents an abstract action that can be activeted by the input. 8 | type Action uint32 9 | 10 | // Keymap associates a list of keys with an action. 11 | // Any of the keys from the list can activate the action. 12 | type Keymap map[Action][]Key 13 | 14 | // Clone creates a deep copy of a keymap. 15 | // The returned keymap can be modified without changing the original keymap. 16 | func (m Keymap) Clone() Keymap { 17 | cloned := make(Keymap, len(m)) 18 | for k, list := range m { 19 | clonedList := make([]Key, len(list)) 20 | copy(clonedList, list) 21 | cloned[k] = clonedList 22 | } 23 | return cloned 24 | } 25 | 26 | // MergeKeymaps merges a list of keymaps into one. 27 | // 28 | // Given maps are not modified. 29 | // The resulting keymap contains no references to input keymaps. 30 | // 31 | // Any key duplicates for a single action are ignored, therefore: 32 | // 33 | // {Action1: [KeyA, KeyB]} + {Action1: [KeyC, KeyB]} = {Action1: [KeyA, KeyB, KeyC]} 34 | // 35 | // The keys order depends on the input keymaps arguments order. 36 | // Keymaps with keys of higher priorities should go first. 37 | // So, if you have a keyboardKeymap and gamepadKeymap keymaps, 38 | // passing gamepadKeymap before keyboardKeymap would make the gamepad key 39 | // binds take priority: MergeKeymaps(gamepadKeymap, keyboardKeymap). 40 | func MergeKeymaps(maps ...Keymap) Keymap { 41 | type keyCombination struct { 42 | a Action 43 | k Key 44 | } 45 | keyCombinations := make(map[keyCombination]struct{}) 46 | merged := make(Keymap) 47 | for _, m := range maps { 48 | for a, keys := range m { 49 | for _, k := range keys { 50 | ck := keyCombination{a, k} 51 | if _, ok := keyCombinations[ck]; ok { 52 | continue 53 | } 54 | keyCombinations[ck] = struct{}{} 55 | merged[a] = append(merged[a], k) 56 | } 57 | } 58 | } 59 | return merged 60 | } 61 | 62 | // DeviceKind is used as a bit mask to select the enabled input devices. 63 | // See constants like KeyboardDevice and GamepadDevice. 64 | // Combine them like KeyboardDevice|GamepadDevice to get a bit mask that includes multiple entries. 65 | // Use AnyDevice if you want to have a mask covering all devices. 66 | type DeviceKind uint8 67 | 68 | const ( 69 | KeyboardDevice DeviceKind = 1 << iota 70 | GamepadDevice 71 | MouseDevice 72 | TouchDevice 73 | ) 74 | 75 | // IsTouch reports whether this set of devices has touch device. 76 | func (d DeviceKind) IsTouch() bool { 77 | return d&TouchDevice != 0 78 | } 79 | 80 | // IsKeyboard reports whether this set of devices has keyboard device. 81 | func (d DeviceKind) IsKeyboard() bool { 82 | return d&KeyboardDevice != 0 83 | } 84 | 85 | // IsMouse reports whether this set of devices has mouse device. 86 | func (d DeviceKind) IsMouse() bool { 87 | return d&MouseDevice != 0 88 | } 89 | 90 | // IsGamepad reports whether this set of devices has gamepad device. 91 | func (d DeviceKind) IsGamepad() bool { 92 | return d&GamepadDevice != 0 93 | } 94 | 95 | // String returns a pretty-printed representation of the input device mask. 96 | func (d DeviceKind) String() string { 97 | if d == 0 { 98 | return "" 99 | } 100 | parts := make([]string, 0, 4) 101 | if d&KeyboardDevice != 0 { 102 | parts = append(parts, "keyboard") 103 | } 104 | if d&GamepadDevice != 0 { 105 | parts = append(parts, "gamepad") 106 | } 107 | if d&MouseDevice != 0 { 108 | parts = append(parts, "mouse") 109 | } 110 | if d&TouchDevice != 0 { 111 | parts = append(parts, "touch") 112 | } 113 | if len(parts) == 0 { 114 | return "" 115 | } 116 | return strings.Join(parts, "|") 117 | } 118 | 119 | // AnyDevice includes all input devices. 120 | const AnyDevice DeviceKind = KeyboardDevice | GamepadDevice | MouseDevice | TouchDevice 121 | -------------------------------------------------------------------------------- /input_test.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestKeymapMerge(t *testing.T) { 10 | tests := []struct { 11 | keymaps []Keymap 12 | want Keymap 13 | }{ 14 | // A simple case with 4 keymaps with no duplicates. 15 | { 16 | []Keymap{ 17 | { 18 | 3: {KeyDown, KeyS}, 19 | 4: {KeySpace}, 20 | 5: {KeyShift}, 21 | }, 22 | { 23 | 4: {KeyMouseLeft}, 24 | 5: {KeyMouseRight}, 25 | }, 26 | { 27 | 6: {KeyMouseRight}, 28 | }, 29 | { 30 | 7: {KeyGamepadA, KeyGamepadB}, 31 | 3: {KeyGamepadA, KeyGamepadB}, 32 | }, 33 | { 34 | 3: {KeyGamepadL1}, 35 | 6: {KeyGamepadL2}, 36 | }, 37 | }, 38 | Keymap{ 39 | 3: {KeyDown, KeyS, KeyGamepadA, KeyGamepadB, KeyGamepadL1}, 40 | 4: {KeySpace, KeyMouseLeft}, 41 | 5: {KeyShift, KeyMouseRight}, 42 | 6: {KeyMouseRight, KeyGamepadL2}, 43 | 7: {KeyGamepadA, KeyGamepadB}, 44 | }, 45 | }, 46 | 47 | { 48 | []Keymap{ 49 | { 50 | 3: {KeyDown, KeyS}, 51 | 4: {KeySpace}, 52 | 5: {KeyShift}, 53 | }, 54 | { 55 | 4: {KeyMouseLeft, KeySpace}, // extra duplicate 56 | 5: {KeyMouseRight}, 57 | }, 58 | }, 59 | Keymap{ 60 | 3: {KeyDown, KeyS}, 61 | 4: {KeySpace, KeyMouseLeft}, 62 | 5: {KeyShift, KeyMouseRight}, 63 | }, 64 | }, 65 | 66 | // Merging with 3 keymaps, checking that the priority is preserved. 67 | { 68 | []Keymap{ 69 | { 70 | 3: {KeyGamepadA}, 71 | 4: {KeySpace, KeyMouseLeft}, 72 | }, 73 | { 74 | 3: {KeyDown, KeyS}, 75 | 4: {KeySpace}, 76 | 5: {KeyShift}, 77 | }, 78 | { 79 | 4: {KeyMouseLeft, KeySpace}, 80 | 5: {KeyMouseRight}, 81 | }, 82 | }, 83 | Keymap{ 84 | 3: {KeyGamepadA, KeyDown, KeyS}, 85 | 4: {KeySpace, KeyMouseLeft}, 86 | 5: {KeyShift, KeyMouseRight}, 87 | }, 88 | }, 89 | 90 | // Merging with an empty keymap. 91 | { 92 | []Keymap{ 93 | { 94 | 3: {KeyDown, KeyS}, 95 | 4: {KeySpace}, 96 | 5: {KeyShift}, 97 | }, 98 | {}, 99 | }, 100 | Keymap{ 101 | 3: {KeyDown, KeyS}, 102 | 4: {KeySpace}, 103 | 5: {KeyShift}, 104 | }, 105 | }, 106 | 107 | // Merging identical keymaps. 108 | { 109 | []Keymap{ 110 | { 111 | 3: {KeyDown, KeyS}, 112 | 4: {KeySpace}, 113 | 5: {KeyShift}, 114 | }, 115 | { 116 | 3: {KeyDown, KeyS}, 117 | 4: {KeySpace}, 118 | 5: {KeyShift}, 119 | }, 120 | { 121 | 3: {KeyDown, KeyS}, 122 | 4: {KeySpace}, 123 | 5: {KeyShift}, 124 | }, 125 | }, 126 | Keymap{ 127 | 3: {KeyDown, KeyS}, 128 | 4: {KeySpace}, 129 | 5: {KeyShift}, 130 | }, 131 | }, 132 | 133 | // Merging a single map results in the same keymap. 134 | { 135 | []Keymap{ 136 | { 137 | 3: {KeyDown, KeyS}, 138 | 4: {KeySpace}, 139 | 5: {KeyShift}, 140 | }, 141 | }, 142 | Keymap{ 143 | 3: {KeyDown, KeyS}, 144 | 4: {KeySpace}, 145 | 5: {KeyShift}, 146 | }, 147 | }, 148 | } 149 | 150 | for i := range tests { 151 | test := tests[i] 152 | t.Run(fmt.Sprintf("test%d", i), func(t *testing.T) { 153 | have := MergeKeymaps(test.keymaps...) 154 | if !reflect.DeepEqual(have, test.want) { 155 | t.Fatalf("invalid merge results:\nhave: %#v\nwant: %#v\ninputs: #%v", have, test.want, test.keymaps) 156 | } 157 | }) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /internal_bool3.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | type bool3 uint8 4 | 5 | const ( 6 | bool3unset bool3 = iota 7 | bool3false 8 | bool3true 9 | ) 10 | -------------------------------------------------------------------------------- /internal_firefox.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | 3 | package input 4 | 5 | func isFirefox() bool { 6 | return false 7 | } 8 | 9 | func guessFirefoxGamepadModel(id int) gamepadModel { 10 | _ = id 11 | panic("should not be called") 12 | } 13 | -------------------------------------------------------------------------------- /internal_firefox_js.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "strings" 5 | "syscall/js" 6 | ) 7 | 8 | func isFirefox() bool { 9 | ua := js.Global().Get("navigator").Get("userAgent").String() 10 | return strings.Contains(strings.ToLower(ua), "firefox") 11 | } 12 | 13 | func guessFirefoxGamepadModel(id int) gamepadModel { 14 | gamepads := js.Global().Get("navigator").Call("getGamepads") 15 | if gamepads.IsNull() || gamepads.Type() != js.TypeObject { 16 | return gamepadUnknown 17 | } 18 | g := gamepads.Index(id) 19 | if g.IsNull() { 20 | return gamepadUnknown 21 | } 22 | gamepadID := strings.ToLower(g.Get("id").String()) 23 | for _, pattern := range firefoxKnownXinput { 24 | if strings.Contains(gamepadID, pattern) { 25 | return gamepadFirefoxXinput 26 | } 27 | } 28 | return gamepadUnknown 29 | } 30 | 31 | var firefoxKnownXinput = []string{ 32 | // Generic keys. 33 | "xinput", 34 | "x-input", 35 | "x_input", 36 | "xbox", 37 | "x-box", 38 | "x_box", 39 | 40 | // Specific models that do not contain any xinput keys in their name. 41 | "logitech gamepad f310", 42 | } 43 | -------------------------------------------------------------------------------- /internal_gamepad_utils.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/hajimehoshi/ebiten/v2" 7 | ) 8 | 9 | type gamepadModel int 10 | 11 | const ( 12 | gamepadUnknown gamepadModel = iota 13 | gamepadStandard 14 | gamepadFirefoxXinput 15 | gamepadMicront 16 | ) 17 | 18 | func guessGamepadModel(s string) gamepadModel { 19 | s = strings.ToLower(s) 20 | if s == "micront" { 21 | return gamepadMicront 22 | } 23 | return gamepadUnknown 24 | } 25 | 26 | type gamepadInfo struct { 27 | model gamepadModel 28 | modelName string 29 | 30 | axisCount int 31 | axisValues [8]float64 32 | prevAxisValues [8]float64 33 | } 34 | 35 | func isDPadButton(code int) bool { 36 | switch ebiten.StandardGamepadButton(code) { 37 | case ebiten.StandardGamepadButtonLeftTop: 38 | return true 39 | case ebiten.StandardGamepadButtonLeftRight: 40 | return true 41 | case ebiten.StandardGamepadButtonLeftBottom: 42 | return true 43 | case ebiten.StandardGamepadButtonLeftLeft: 44 | return true 45 | default: 46 | return false 47 | } 48 | } 49 | 50 | func firefoxXinputToXbox(b ebiten.StandardGamepadButton) ebiten.GamepadButton { 51 | switch b { 52 | case ebiten.StandardGamepadButtonCenterLeft: 53 | return 6 54 | case ebiten.StandardGamepadButtonCenterRight: 55 | return 7 56 | case ebiten.StandardGamepadButtonRightStick: 57 | return 10 58 | case ebiten.StandardGamepadButtonLeftStick: 59 | return 9 60 | default: 61 | return ebiten.GamepadButton(b) 62 | } 63 | } 64 | 65 | func microntToXbox(b ebiten.StandardGamepadButton) ebiten.GamepadButton { 66 | switch b { 67 | case ebiten.StandardGamepadButtonLeftTop: 68 | return ebiten.GamepadButton12 69 | case ebiten.StandardGamepadButtonLeftRight: 70 | return ebiten.GamepadButton13 71 | case ebiten.StandardGamepadButtonLeftBottom: 72 | return ebiten.GamepadButton14 73 | case ebiten.StandardGamepadButtonLeftLeft: 74 | return ebiten.GamepadButton15 75 | 76 | case ebiten.StandardGamepadButtonRightTop: 77 | return ebiten.GamepadButton0 78 | case ebiten.StandardGamepadButtonRightRight: 79 | return ebiten.GamepadButton1 80 | case ebiten.StandardGamepadButtonRightBottom: 81 | return ebiten.GamepadButton2 82 | case ebiten.StandardGamepadButtonRightLeft: 83 | return ebiten.GamepadButton3 84 | 85 | default: 86 | return ebiten.GamepadButton(b) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal_key.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | type keyKind uint8 4 | 5 | const ( 6 | keyKeyboard keyKind = iota 7 | keyKeyboardWithCtrl 8 | keyKeyboardWithShift 9 | keyKeyboardWithCtrlShift 10 | keyGamepad 11 | keyGamepadLeftStick 12 | keyGamepadRightStick 13 | keyGamepadStickMotion 14 | keyMouse 15 | keyMouseWithCtrl 16 | keyMouseWithShift 17 | keyMouseWithCtrlShift 18 | keyMouseDrag 19 | keyTouch 20 | keyTouchDrag 21 | keyWheel 22 | keyWheelWithCtrl 23 | keyWheelWithShift 24 | keyWheelWithCtrlShift 25 | keySimulated 26 | ) 27 | 28 | func (k keyKind) device() DeviceKind { 29 | switch k { 30 | case keyKeyboard, keyKeyboardWithCtrl, keyKeyboardWithShift, keyKeyboardWithCtrlShift: 31 | return KeyboardDevice 32 | case keyGamepad, keyGamepadLeftStick, keyGamepadRightStick, keyGamepadStickMotion: 33 | return GamepadDevice 34 | case keyMouse, keyMouseDrag: 35 | return MouseDevice 36 | case keyWheel, keyWheelWithCtrl, keyWheelWithShift, keyWheelWithCtrlShift: 37 | return MouseDevice 38 | case keyMouseWithCtrl, keyMouseWithShift, keyMouseWithCtrlShift: 39 | return MouseDevice | KeyboardDevice 40 | case keyTouch, keyTouchDrag: 41 | return TouchDevice 42 | default: 43 | return KeyboardDevice 44 | } 45 | } 46 | 47 | type touchCode int 48 | 49 | const ( 50 | touchUnknown touchCode = iota 51 | touchTap 52 | touchLongTap 53 | touchDrag 54 | ) 55 | 56 | type wheelCode int 57 | 58 | const ( 59 | wheelUnknown wheelCode = iota 60 | wheelUp 61 | wheelDown 62 | wheelVertical 63 | ) 64 | 65 | type stickCode int 66 | 67 | const ( 68 | stickUnknown stickCode = iota 69 | stickUp 70 | stickRight 71 | stickDown 72 | stickLeft 73 | ) 74 | 75 | type keyKindFlag uint8 76 | 77 | const ( 78 | keyFlagHasPos keyKindFlag = 1 << iota 79 | keyFlagNeedID 80 | keyFlagHasDuration 81 | ) 82 | 83 | func keyHasPos(k keyKind) bool { return keyKindFlagTable[k]&keyFlagHasPos != 0 } 84 | func keyNeedID(k keyKind) bool { return keyKindFlagTable[k]&keyFlagNeedID != 0 } 85 | func keyHasDuration(k keyKind) bool { return keyKindFlagTable[k]&keyFlagHasDuration != 0 } 86 | 87 | // Using a 256-byte LUT to get a fast map-like lookup without a bound check. 88 | var keyKindFlagTable = [256]keyKindFlag{ 89 | keySimulated: keyFlagHasPos | keyFlagNeedID, 90 | 91 | keyKeyboard: keyFlagHasDuration, 92 | keyKeyboardWithCtrl: keyFlagHasDuration, 93 | keyKeyboardWithShift: keyFlagHasDuration, 94 | keyKeyboardWithCtrlShift: keyFlagHasDuration, 95 | 96 | keyGamepad: keyFlagNeedID, 97 | keyGamepadLeftStick: keyFlagNeedID, 98 | keyGamepadRightStick: keyFlagNeedID, 99 | 100 | keyGamepadStickMotion: keyFlagHasPos | keyFlagNeedID, 101 | 102 | keyMouse: keyFlagHasPos, 103 | keyMouseWithCtrl: keyFlagHasPos, 104 | keyMouseWithShift: keyFlagHasPos, 105 | keyMouseWithCtrlShift: keyFlagHasPos, 106 | keyMouseDrag: keyFlagHasPos, 107 | keyTouch: keyFlagHasPos, 108 | keyWheel: keyFlagHasPos, 109 | keyWheelWithCtrl: keyFlagHasPos, 110 | keyWheelWithShift: keyFlagHasPos, 111 | keyWheelWithCtrlShift: keyFlagHasPos, 112 | } 113 | -------------------------------------------------------------------------------- /internal_key_list.go: -------------------------------------------------------------------------------- 1 | // Code generated by "_scripts/gen_key_list"; DO NOT EDIT. 2 | 3 | package input 4 | 5 | // allKeys contains all basic keys provided by a package. 6 | // This slice is sorted by key names (see ActionKeyNames). 7 | var allKeys = []Key{ 8 | Key0, 9 | Key1, 10 | Key2, 11 | Key3, 12 | Key4, 13 | Key5, 14 | Key6, 15 | Key7, 16 | Key8, 17 | Key9, 18 | KeyA, 19 | KeyAlt, 20 | KeyAltLeft, 21 | KeyAltRight, 22 | KeyB, 23 | KeyBackquote, 24 | KeyBackslash, 25 | KeyBackspace, 26 | KeyBracketLeft, 27 | KeyBracketRight, 28 | KeyC, 29 | KeyCapsLock, 30 | KeyComma, 31 | KeyContextMenu, 32 | KeyControl, 33 | KeyControlLeft, 34 | KeyControlRight, 35 | KeyD, 36 | KeyDelete, 37 | KeyDown, 38 | KeyE, 39 | KeyEnd, 40 | KeyEnter, 41 | KeyEqual, 42 | KeyEscape, 43 | KeyF, 44 | KeyF1, 45 | KeyF10, 46 | KeyF11, 47 | KeyF12, 48 | KeyF2, 49 | KeyF3, 50 | KeyF4, 51 | KeyF5, 52 | KeyF6, 53 | KeyF7, 54 | KeyF8, 55 | KeyF9, 56 | KeyG, 57 | KeyGamepadA, 58 | KeyGamepadB, 59 | KeyGamepadBack, 60 | KeyGamepadDown, 61 | KeyGamepadHome, 62 | KeyGamepadL1, 63 | KeyGamepadL2, 64 | KeyGamepadLeft, 65 | KeyGamepadLStick, 66 | KeyGamepadLStickDown, 67 | KeyGamepadLStickLeft, 68 | KeyGamepadLStickMotion, 69 | KeyGamepadLStickRight, 70 | KeyGamepadLStickUp, 71 | KeyGamepadR1, 72 | KeyGamepadR2, 73 | KeyGamepadRight, 74 | KeyGamepadRStick, 75 | KeyGamepadRStickDown, 76 | KeyGamepadRStickLeft, 77 | KeyGamepadRStickMotion, 78 | KeyGamepadRStickRight, 79 | KeyGamepadRStickUp, 80 | KeyGamepadStart, 81 | KeyGamepadUp, 82 | KeyGamepadX, 83 | KeyGamepadY, 84 | KeyH, 85 | KeyHome, 86 | KeyI, 87 | KeyInsert, 88 | KeyJ, 89 | KeyK, 90 | KeyL, 91 | KeyLeft, 92 | KeyM, 93 | KeyMinus, 94 | KeyMouseBack, 95 | KeyMouseForward, 96 | KeyMouseLeft, 97 | KeyMouseLeftDrag, 98 | KeyMouseMiddle, 99 | KeyMouseRight, 100 | KeyN, 101 | KeyNumLock, 102 | KeyNum0, 103 | KeyNum1, 104 | KeyNum2, 105 | KeyNum3, 106 | KeyNum4, 107 | KeyNum5, 108 | KeyNum6, 109 | KeyNum7, 110 | KeyNum8, 111 | KeyNum9, 112 | KeyNumAdd, 113 | KeyNumDivide, 114 | KeyNumEnter, 115 | KeyNumMultiply, 116 | KeyNumPeriod, 117 | KeyNumSubtract, 118 | KeyO, 119 | KeyP, 120 | KeyPageDown, 121 | KeyPageUp, 122 | KeyPause, 123 | KeyPeriod, 124 | KeyPrintScreen, 125 | KeyQ, 126 | KeyQuote, 127 | KeyR, 128 | KeyRight, 129 | KeyS, 130 | KeyScrollLock, 131 | KeySemicolon, 132 | KeyShift, 133 | KeyShiftLeft, 134 | KeyShiftRight, 135 | KeySlash, 136 | KeySpace, 137 | KeyT, 138 | KeyTab, 139 | KeyTouchDrag, 140 | KeyTouchLongTap, 141 | KeyTouchTap, 142 | KeyU, 143 | KeyUp, 144 | KeyV, 145 | KeyW, 146 | KeyWheelDown, 147 | KeyWheelUp, 148 | KeyWheelVertical, 149 | KeyX, 150 | KeyY, 151 | KeyZ, 152 | } 153 | -------------------------------------------------------------------------------- /internal_min.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import "golang.org/x/exp/constraints" 4 | 5 | // minOf is a placeholder implementation until Go 1.21's builtin min implementation is available. 6 | func minOf[T constraints.Ordered](a, b T) T { 7 | if a < b { 8 | return a 9 | } 10 | return b 11 | } 12 | -------------------------------------------------------------------------------- /key.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "github.com/hajimehoshi/ebiten/v2" 5 | ) 6 | 7 | //go:generate go run --tags generate ./_scripts/gen_key_list -o internal_key_list.go key.go 8 | 9 | // Key represents an input method that can be used to activate Action. 10 | // Key could be a keyboard key, a gamepad key, a mouse button, etc. 11 | // 12 | // Use the predefined global vars like KeyMouseLeft and KeyTab to create a Keymap. 13 | type Key struct { 14 | code int 15 | kind keyKind 16 | name string 17 | } 18 | 19 | func (k Key) String() string { 20 | ctrlMod := false 21 | shiftMod := false 22 | switch k.kind { 23 | case keyKeyboardWithCtrlShift, keyMouseWithCtrlShift, keyWheelWithCtrlShift: 24 | ctrlMod = true 25 | shiftMod = true 26 | case keyKeyboardWithCtrl, keyMouseWithCtrl, keyWheelWithCtrl: 27 | ctrlMod = true 28 | case keyKeyboardWithShift, keyMouseWithShift, keyWheelWithShift: 29 | shiftMod = true 30 | } 31 | name := k.name 32 | if shiftMod { 33 | name = "shift+" + name 34 | } 35 | if ctrlMod { 36 | name = "ctrl+" + name 37 | } 38 | return name 39 | } 40 | 41 | type KeyModifier uint8 42 | 43 | const ( 44 | ModUnknown KeyModifier = iota 45 | ModControl 46 | ModShift 47 | ModControlShift 48 | ) 49 | 50 | // KeyWithModifier turns k into a combined modifier+k key. 51 | // For instance, KeyUp+ModControl will trigger an action 52 | // only if both of these keys are being pressed. 53 | func KeyWithModifier(k Key, mod KeyModifier) Key { 54 | switch k.kind { 55 | case keyKeyboard: 56 | switch mod { 57 | case ModControl: 58 | k.kind = keyKeyboardWithCtrl 59 | case ModShift: 60 | k.kind = keyKeyboardWithShift 61 | case ModControlShift: 62 | k.kind = keyKeyboardWithCtrlShift 63 | default: 64 | panic("unexpected keyboard key modifier") 65 | } 66 | case keyMouse: 67 | switch mod { 68 | case ModControl: 69 | k.kind = keyMouseWithCtrl 70 | case ModShift: 71 | k.kind = keyMouseWithShift 72 | case ModControlShift: 73 | k.kind = keyMouseWithCtrlShift 74 | default: 75 | panic("unexpected mouse key modifier") 76 | } 77 | case keyWheel: 78 | switch mod { 79 | case ModControl: 80 | k.kind = keyWheelWithCtrl 81 | case ModShift: 82 | k.kind = keyWheelWithShift 83 | case ModControlShift: 84 | k.kind = keyWheelWithCtrlShift 85 | default: 86 | panic("unexpected wheel key modifier") 87 | } 88 | default: 89 | panic("only keyboard and mouse keys support modifiers") 90 | } 91 | return k 92 | } 93 | 94 | // Wheel keys. 95 | // 96 | // Wheel keys do not have constantly pressed state, 97 | // scrolling the wheel triggers JustPressed events every wheel movement. 98 | var ( 99 | // KeyWheelUp handles only scroll-up movement. 100 | KeyWheelUp = Key{code: int(wheelUp), kind: keyWheel, name: "wheel_up"} 101 | 102 | // KeyWheelDown handles only scroll-down movement. 103 | KeyWheelDown = Key{code: int(wheelDown), kind: keyWheel, name: "wheel_down"} 104 | 105 | // KeyWheelVertical handles both up and down movements. 106 | KeyWheelVertical = Key{code: int(wheelVertical), kind: keyWheel, name: "wheel_vertical"} 107 | ) 108 | 109 | // Mouse keys. 110 | var ( 111 | KeyMouseLeft = Key{code: int(ebiten.MouseButtonLeft), kind: keyMouse, name: "mouse_left_button"} 112 | KeyMouseRight = Key{code: int(ebiten.MouseButtonRight), kind: keyMouse, name: "mouse_right_button"} 113 | KeyMouseMiddle = Key{code: int(ebiten.MouseButtonMiddle), kind: keyMouse, name: "mouse_middle_button"} 114 | KeyMouseBack = Key{code: int(ebiten.MouseButton3), kind: keyMouse, name: "mouse_back_button"} 115 | KeyMouseForward = Key{code: int(ebiten.MouseButton4), kind: keyMouse, name: "mouse_forward_button"} 116 | 117 | // A special event that is triggered if the left mouse button is being pressed 118 | // and the cursor is moved. This is useful for UI interfaces to detect drag-and-drop triggers. 119 | KeyMouseLeftDrag = Key{code: int(ebiten.MouseButtonLeft), kind: keyMouseDrag, name: "mouse_left_drag"} 120 | ) 121 | 122 | // Touch keys. 123 | // Experimental: touch keys API is not stable yet! 124 | var ( 125 | KeyTouchTap = Key{code: int(touchTap), kind: keyTouch, name: "touch_tap"} 126 | 127 | // Like a tap, but user was holding that gesture for at least 0.5s. 128 | KeyTouchLongTap = Key{code: int(touchLongTap), kind: keyTouch, name: "touch_long_tap"} 129 | 130 | KeyTouchDrag = Key{kind: keyTouchDrag, name: "touch_drag"} 131 | ) 132 | 133 | // Keyboard keys. 134 | var ( 135 | KeyLeft = Key{code: int(ebiten.KeyLeft), name: "left"} 136 | KeyRight = Key{code: int(ebiten.KeyRight), name: "right"} 137 | KeyUp = Key{code: int(ebiten.KeyUp), name: "up"} 138 | KeyDown = Key{code: int(ebiten.KeyDown), name: "down"} 139 | 140 | Key0 = Key{code: int(ebiten.Key0), name: "0"} 141 | Key1 = Key{code: int(ebiten.Key1), name: "1"} 142 | Key2 = Key{code: int(ebiten.Key2), name: "2"} 143 | Key3 = Key{code: int(ebiten.Key3), name: "3"} 144 | Key4 = Key{code: int(ebiten.Key4), name: "4"} 145 | Key5 = Key{code: int(ebiten.Key5), name: "5"} 146 | Key6 = Key{code: int(ebiten.Key6), name: "6"} 147 | Key7 = Key{code: int(ebiten.Key7), name: "7"} 148 | Key8 = Key{code: int(ebiten.Key8), name: "8"} 149 | Key9 = Key{code: int(ebiten.Key9), name: "9"} 150 | KeyMinus = Key{code: int(ebiten.KeyMinus), name: "minus"} 151 | KeyEqual = Key{code: int(ebiten.KeyEqual), name: "equal"} 152 | 153 | KeyQuote = Key{code: int(ebiten.KeyQuote), name: "quote"} 154 | KeyBackquote = Key{code: int(ebiten.KeyBackquote), name: "backquote"} 155 | 156 | KeyQ = Key{code: int(ebiten.KeyQ), name: "q"} 157 | KeyW = Key{code: int(ebiten.KeyW), name: "w"} 158 | KeyE = Key{code: int(ebiten.KeyE), name: "e"} 159 | KeyR = Key{code: int(ebiten.KeyR), name: "r"} 160 | KeyT = Key{code: int(ebiten.KeyT), name: "t"} 161 | KeyY = Key{code: int(ebiten.KeyY), name: "y"} 162 | KeyU = Key{code: int(ebiten.KeyU), name: "u"} 163 | KeyI = Key{code: int(ebiten.KeyI), name: "i"} 164 | KeyO = Key{code: int(ebiten.KeyO), name: "o"} 165 | KeyP = Key{code: int(ebiten.KeyP), name: "p"} 166 | KeyA = Key{code: int(ebiten.KeyA), name: "a"} 167 | KeyS = Key{code: int(ebiten.KeyS), name: "s"} 168 | KeyD = Key{code: int(ebiten.KeyD), name: "d"} 169 | KeyF = Key{code: int(ebiten.KeyF), name: "f"} 170 | KeyG = Key{code: int(ebiten.KeyG), name: "g"} 171 | KeyH = Key{code: int(ebiten.KeyH), name: "h"} 172 | KeyJ = Key{code: int(ebiten.KeyJ), name: "j"} 173 | KeyK = Key{code: int(ebiten.KeyK), name: "k"} 174 | KeyL = Key{code: int(ebiten.KeyL), name: "l"} 175 | KeyZ = Key{code: int(ebiten.KeyZ), name: "z"} 176 | KeyX = Key{code: int(ebiten.KeyX), name: "x"} 177 | KeyC = Key{code: int(ebiten.KeyC), name: "c"} 178 | KeyV = Key{code: int(ebiten.KeyV), name: "v"} 179 | KeyB = Key{code: int(ebiten.KeyB), name: "b"} 180 | KeyN = Key{code: int(ebiten.KeyN), name: "n"} 181 | KeyM = Key{code: int(ebiten.KeyM), name: "m"} 182 | 183 | KeyBackslash = Key{code: int(ebiten.KeyBackslash), name: "backslash"} 184 | KeyBackspace = Key{code: int(ebiten.KeyBackspace), name: "backspace"} 185 | KeyBracketLeft = Key{code: int(ebiten.KeyBracketLeft), name: "bracket_left"} 186 | KeyBracketRight = Key{code: int(ebiten.KeyBracketRight), name: "bracket_right"} 187 | KeyCapsLock = Key{code: int(ebiten.KeyCapsLock), name: "caps_lock"} 188 | KeyComma = Key{code: int(ebiten.KeyComma), name: "comma"} 189 | KeyContextMenu = Key{code: int(ebiten.KeyContextMenu), name: "context_menu"} 190 | KeyAlt = Key{code: int(ebiten.KeyAlt), name: "alt"} 191 | KeyAltLeft = Key{code: int(ebiten.KeyAltLeft), name: "alt_left"} 192 | KeyAltRight = Key{code: int(ebiten.KeyAltRight), name: "alt_right"} 193 | KeyControl = Key{code: int(ebiten.KeyControl), name: "control"} 194 | KeyControlLeft = Key{code: int(ebiten.KeyControlLeft), name: "control_left"} 195 | KeyControlRight = Key{code: int(ebiten.KeyControlRight), name: "control_right"} 196 | KeyDelete = Key{code: int(ebiten.KeyDelete), name: "delete"} 197 | KeyEnd = Key{code: int(ebiten.KeyEnd), name: "end"} 198 | KeyEnter = Key{code: int(ebiten.KeyEnter), name: "enter"} 199 | KeyEscape = Key{code: int(ebiten.KeyEscape), name: "escape"} 200 | KeyHome = Key{code: int(ebiten.KeyHome), name: "home"} 201 | KeyInsert = Key{code: int(ebiten.KeyInsert), name: "insert"} 202 | KeyNumLock = Key{code: int(ebiten.KeyNumLock), name: "num_lock"} 203 | KeyPageDown = Key{code: int(ebiten.KeyPageDown), name: "page_down"} 204 | KeyPageUp = Key{code: int(ebiten.KeyPageUp), name: "page_up"} 205 | KeyPause = Key{code: int(ebiten.KeyPause), name: "pause"} 206 | KeyPeriod = Key{code: int(ebiten.KeyPeriod), name: "period"} 207 | KeyPrintScreen = Key{code: int(ebiten.KeyPrintScreen), name: "print_screen"} 208 | KeyScrollLock = Key{code: int(ebiten.KeyScrollLock), name: "scroll_lock"} 209 | KeySemicolon = Key{code: int(ebiten.KeySemicolon), name: "semicolon"} 210 | KeyShift = Key{code: int(ebiten.KeyShift), name: "shift"} 211 | KeyShiftLeft = Key{code: int(ebiten.KeyShiftLeft), name: "shift_left"} 212 | KeyShiftRight = Key{code: int(ebiten.KeyShiftRight), name: "shift_right"} 213 | KeySlash = Key{code: int(ebiten.KeySlash), name: "slash"} 214 | KeySpace = Key{code: int(ebiten.KeySpace), name: "space"} 215 | KeyTab = Key{code: int(ebiten.KeyTab), name: "tab"} 216 | 217 | KeyF1 = Key{code: int(ebiten.KeyF1), name: "f1"} 218 | KeyF2 = Key{code: int(ebiten.KeyF2), name: "f2"} 219 | KeyF3 = Key{code: int(ebiten.KeyF3), name: "f3"} 220 | KeyF4 = Key{code: int(ebiten.KeyF4), name: "f4"} 221 | KeyF5 = Key{code: int(ebiten.KeyF5), name: "f5"} 222 | KeyF6 = Key{code: int(ebiten.KeyF6), name: "f6"} 223 | KeyF7 = Key{code: int(ebiten.KeyF7), name: "f7"} 224 | KeyF8 = Key{code: int(ebiten.KeyF8), name: "f8"} 225 | KeyF9 = Key{code: int(ebiten.KeyF9), name: "f9"} 226 | KeyF10 = Key{code: int(ebiten.KeyF10), name: "f10"} 227 | KeyF11 = Key{code: int(ebiten.KeyF11), name: "f11"} 228 | KeyF12 = Key{code: int(ebiten.KeyF12), name: "f12"} 229 | 230 | KeyNum0 = Key{code: int(ebiten.KeyNumpad0), name: "numpad_0"} 231 | KeyNum1 = Key{code: int(ebiten.KeyNumpad1), name: "numpad_1"} 232 | KeyNum2 = Key{code: int(ebiten.KeyNumpad2), name: "numpad_2"} 233 | KeyNum3 = Key{code: int(ebiten.KeyNumpad3), name: "numpad_3"} 234 | KeyNum4 = Key{code: int(ebiten.KeyNumpad4), name: "numpad_4"} 235 | KeyNum5 = Key{code: int(ebiten.KeyNumpad5), name: "numpad_5"} 236 | KeyNum6 = Key{code: int(ebiten.KeyNumpad6), name: "numpad_6"} 237 | KeyNum7 = Key{code: int(ebiten.KeyNumpad7), name: "numpad_7"} 238 | KeyNum8 = Key{code: int(ebiten.KeyNumpad8), name: "numpad_8"} 239 | KeyNum9 = Key{code: int(ebiten.KeyNumpad9), name: "numpad_9"} 240 | 241 | KeyNumAdd = Key{code: int(ebiten.KeyNumpadAdd), name: "numpad_add"} 242 | KeyNumDivide = Key{code: int(ebiten.KeyNumpadDivide), name: "numpad_divide"} 243 | KeyNumEnter = Key{code: int(ebiten.KeyNumpadEnter), name: "numpad_enter"} 244 | KeyNumMultiply = Key{code: int(ebiten.KeyNumpadMultiply), name: "numpad_multiply"} 245 | KeyNumPeriod = Key{code: int(ebiten.KeyNumpadDecimal), name: "numpad_period"} 246 | KeyNumSubtract = Key{code: int(ebiten.KeyNumpadSubtract), name: "numpad_subtract"} 247 | ) 248 | 249 | // Gamepad keys (we're trying to follow the Xbox naming conventions here). 250 | var ( 251 | KeyGamepadStart = Key{code: int(ebiten.StandardGamepadButtonCenterRight), kind: keyGamepad, name: "gamepad_start"} 252 | KeyGamepadBack = Key{code: int(ebiten.StandardGamepadButtonCenterLeft), kind: keyGamepad, name: "gamepad_back"} 253 | KeyGamepadHome = Key{code: int(ebiten.StandardGamepadButtonCenterCenter), kind: keyGamepad, name: "gamepad_home"} 254 | 255 | KeyGamepadUp = Key{code: int(ebiten.StandardGamepadButtonLeftTop), kind: keyGamepad, name: "gamepad_up"} 256 | KeyGamepadRight = Key{code: int(ebiten.StandardGamepadButtonLeftRight), kind: keyGamepad, name: "gamepad_right"} 257 | KeyGamepadDown = Key{code: int(ebiten.StandardGamepadButtonLeftBottom), kind: keyGamepad, name: "gamepad_down"} 258 | KeyGamepadLeft = Key{code: int(ebiten.StandardGamepadButtonLeftLeft), kind: keyGamepad, name: "gamepad_left"} 259 | 260 | // Stick keys that simulate the D-pad like events. 261 | KeyGamepadLStickUp = Key{code: int(stickUp), kind: keyGamepadLeftStick, name: "gamepad_lstick_up"} 262 | KeyGamepadLStickRight = Key{code: int(stickRight), kind: keyGamepadLeftStick, name: "gamepad_lstick_right"} 263 | KeyGamepadLStickDown = Key{code: int(stickDown), kind: keyGamepadLeftStick, name: "gamepad_lstick_down"} 264 | KeyGamepadLStickLeft = Key{code: int(stickLeft), kind: keyGamepadLeftStick, name: "gamepad_lstick_left"} 265 | KeyGamepadRStickUp = Key{code: int(stickUp), kind: keyGamepadRightStick, name: "gamepad_rstick_up"} 266 | KeyGamepadRStickRight = Key{code: int(stickRight), kind: keyGamepadRightStick, name: "gamepad_rstick_right"} 267 | KeyGamepadRStickDown = Key{code: int(stickDown), kind: keyGamepadRightStick, name: "gamepad_rstick_down"} 268 | KeyGamepadRStickLeft = Key{code: int(stickLeft), kind: keyGamepadRightStick, name: "gamepad_rstick_left"} 269 | 270 | // Stick button press. 271 | KeyGamepadLStick = Key{code: int(ebiten.StandardGamepadButtonLeftStick), kind: keyGamepad, name: "gamepad_lstick"} 272 | KeyGamepadRStick = Key{code: int(ebiten.StandardGamepadButtonRightStick), kind: keyGamepad, name: "gamepad_rstick"} 273 | 274 | // Stick keys that can be used for the smooth movement. 275 | KeyGamepadLStickMotion = Key{code: int(stickLeft), kind: keyGamepadStickMotion, name: "gamepad_lstick_motion"} 276 | KeyGamepadRStickMotion = Key{code: int(stickRight), kind: keyGamepadStickMotion, name: "gamepad_rstick_motion"} 277 | 278 | KeyGamepadA = Key{code: int(ebiten.StandardGamepadButtonRightBottom), kind: keyGamepad, name: "gamepad_a"} 279 | KeyGamepadB = Key{code: int(ebiten.StandardGamepadButtonRightRight), kind: keyGamepad, name: "gamepad_b"} 280 | KeyGamepadX = Key{code: int(ebiten.StandardGamepadButtonRightLeft), kind: keyGamepad, name: "gamepad_x"} 281 | KeyGamepadY = Key{code: int(ebiten.StandardGamepadButtonRightTop), kind: keyGamepad, name: "gamepad_y"} 282 | 283 | KeyGamepadL1 = Key{code: int(ebiten.StandardGamepadButtonFrontTopLeft), kind: keyGamepad, name: "gamepad_l1"} 284 | KeyGamepadL2 = Key{code: int(ebiten.StandardGamepadButtonFrontBottomLeft), kind: keyGamepad, name: "gamepad_l2"} 285 | KeyGamepadR1 = Key{code: int(ebiten.StandardGamepadButtonFrontTopRight), kind: keyGamepad, name: "gamepad_r1"} 286 | KeyGamepadR2 = Key{code: int(ebiten.StandardGamepadButtonFrontBottomRight), kind: keyGamepad, name: "gamepad_r2"} 287 | ) 288 | -------------------------------------------------------------------------------- /math_gmath.go: -------------------------------------------------------------------------------- 1 | //go:build gmath 2 | 3 | package input 4 | 5 | import ( 6 | "github.com/quasilyte/gmath" 7 | ) 8 | 9 | type Vec = gmath.Vec 10 | 11 | func vecDistance(v, v2 Vec) float64 { 12 | return v.DistanceTo(v2) 13 | } 14 | 15 | func vecDot(v, v2 Vec) float64 { 16 | return v.Dot(v2) 17 | } 18 | 19 | func vecLenSquared(v Vec) float64 { 20 | return v.LenSquared() 21 | } 22 | 23 | func vecLen(v Vec) float64 { 24 | return v.Len() 25 | } 26 | 27 | func vecAngle(v Vec) float64 { 28 | return float64(v.Angle()) 29 | } 30 | 31 | func angleNormalized(radians float64) float64 { 32 | return float64(gmath.Rad(radians).Normalized()) 33 | } 34 | -------------------------------------------------------------------------------- /math_nodeps.go: -------------------------------------------------------------------------------- 1 | //go:build !gmath 2 | 3 | package input 4 | 5 | import ( 6 | "math" 7 | ) 8 | 9 | // We're not using any math/vector library to make it possible for the users 10 | // to use any kind of math library they like without having to have more 11 | // than one math library inside their project. 12 | // 13 | // This means that we'll have to implement some math here, 14 | // but it's worth it. 15 | 16 | // Vec is a simple wrapper around a pair of float64 coordinates. 17 | // 18 | // Since most games use float values for most values, input library 19 | // converts int pair to the float pair once per Update() call so 20 | // all usages inside the frame can use already converted values. 21 | // 22 | // We're not using some vector2d library to avoid extra dependencies. 23 | // It should be easy to convert this Point object into any other structure. 24 | type Vec struct { 25 | X float64 26 | Y float64 27 | } 28 | 29 | func vecDistance(v, v2 Vec) float64 { 30 | return math.Sqrt((v.X-v2.X)*(v.X-v2.X) + (v.Y-v2.Y)*(v.Y-v2.Y)) 31 | } 32 | 33 | func vecDot(v, v2 Vec) float64 { 34 | return (v.X * v2.X) + (v.Y * v2.Y) 35 | } 36 | 37 | func vecLenSquared(v Vec) float64 { 38 | return vecDot(v, v) 39 | } 40 | 41 | func vecLen(v Vec) float64 { 42 | return math.Sqrt(vecLenSquared(v)) 43 | } 44 | 45 | func vecAngle(v Vec) float64 { 46 | return math.Atan2(v.Y, v.X) 47 | } 48 | 49 | func angleNormalized(radians float64) float64 { 50 | radians -= math.Floor(radians/(2*math.Pi)) * 2 * math.Pi 51 | return radians 52 | } 53 | -------------------------------------------------------------------------------- /parse_key.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "errors" 5 | "sort" 6 | "strings" 7 | ) 8 | 9 | // ParseKeys tries to construct an appropriate Key object given its name. 10 | // 11 | // It can also be used as a string->key constructor: 12 | // 13 | // ParseKey("left") // returns KeyLeft 14 | // ParseKey("gamepad_left") // returns KeyGamepadLeft 15 | // 16 | // The format is one of the following: 17 | // 18 | // - keyname 19 | // - mod+keyname 20 | // - mod+mod+keyname 21 | // 22 | // Some valid input examples: 23 | // 24 | // - "gamepad_left" 25 | // - "left" 26 | // - "ctrl+left" 27 | // - "ctrl+shift+left" 28 | // - "shift+ctrl+left" 29 | // 30 | // See Handler.ActionKeyNames() for more information about the key names. 31 | func ParseKey(s string) (Key, error) { 32 | plusPos := strings.LastIndex(s, "+") 33 | if plusPos == -1 { 34 | k := keyByName(s) 35 | if (k == Key{}) { 36 | return k, errors.New("unknown key: " + s) 37 | } 38 | return k, nil 39 | } 40 | modName := s[:plusPos] 41 | keyName := s[plusPos+1:] 42 | mod := keyModifierByName(modName) 43 | if mod == ModUnknown { 44 | return Key{}, errors.New("unknown key modifier: " + modName) 45 | } 46 | k := keyByName(keyName) 47 | if (k == Key{}) { 48 | return k, errors.New("unknown key: " + keyName) 49 | } 50 | return KeyWithModifier(k, mod), nil 51 | } 52 | 53 | func keyModifierByName(name string) KeyModifier { 54 | switch name { 55 | case "ctrl": 56 | return ModControl 57 | case "shift": 58 | return ModShift 59 | case "ctrl+shift", "shift+ctrl": 60 | return ModControlShift 61 | default: 62 | return ModUnknown 63 | } 64 | } 65 | 66 | func keyByName(name string) Key { 67 | // Keys are sorted by a name, so we can use a binary search here. 68 | i := sort.Search(len(allKeys), func(i int) bool { 69 | return allKeys[i].name >= name 70 | }) 71 | if i < len(allKeys) && allKeys[i].name == name { 72 | return allKeys[i] 73 | } 74 | return Key{} 75 | } 76 | -------------------------------------------------------------------------------- /scan_key.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "github.com/hajimehoshi/ebiten/v2" 5 | "github.com/hajimehoshi/ebiten/v2/inpututil" 6 | ) 7 | 8 | // KeyScanStatus represents the KeyScanner.Scan operation result. 9 | type KeyScanStatus int 10 | 11 | const ( 12 | KeyScanUnchanged KeyScanStatus = iota 13 | KeyScanChanged 14 | KeyScanCompleted 15 | ) 16 | 17 | // KeyScanner checks the currently pressed keys and buttons and tries to map them 18 | // to a local Key type that can be used in a Keymap. 19 | // 20 | // Use NewKeyScanner to create a usable object of this type. 21 | // 22 | // Experimental: this is a part of a key remapping API, which is not stable yet. 23 | type KeyScanner struct { 24 | lastNumKeys int 25 | canScan bool 26 | key Key 27 | h *Handler 28 | } 29 | 30 | // NewKeyScanner creates a key scanner for the specifier input Handler. 31 | // 32 | // You don't have to create a new scanner for every remap; they can be reused. 33 | // 34 | // It's important to have the correct Handler though: their ID is used to 35 | // check the appropriate device keys. 36 | // 37 | // Experimental: this is a part of a key remapping API, which is not stable yet. 38 | func NewKeyScanner(h *Handler) *KeyScanner { 39 | return &KeyScanner{h: h} 40 | } 41 | 42 | // Scan reads the buttons state and tries to map them to a Key. 43 | // 44 | // It's intended to work with keyboard keys as well as gamepad buttons, 45 | // but right now it only works for the keyboard. 46 | // 47 | // This function should be called on every frame where you're reading 48 | // the new keybind combination. 49 | // See the remap example for more info. 50 | // 51 | // The function can return these result statuses: 52 | // * Unchanged - nothing updated since the last Scan() operation 53 | // * Changed - some keys changed, you may want to update the prompt to the user 54 | // * Completed - the user finished specifying the keys combination, you can use the Key as a new binding 55 | func (s *KeyScanner) Scan() (Key, KeyScanStatus) { 56 | // TODO: respect the enabled input devices. 57 | // TODO: scan the gamepad buttons as well. 58 | 59 | // Note that this function may not be needed by some users, 60 | // so we're better of making it as independent as possible, so it 61 | // doesn't make the package more expensive if you don't use it. 62 | // 63 | // This function doesn't have to be very fast, but it should be relatively 64 | // inexpensive for the "no keys were pressed" case. 65 | // When some keys combo is being pressed, it's OK to spend some resources. 66 | 67 | // This slice is stack-allocated; for the most cases, 4 keys are enough. 68 | keys := make([]ebiten.Key, 0, 4) 69 | keys = inpututil.AppendPressedKeys(keys) 70 | 71 | if !s.canScan { 72 | if len(keys) != 0 { 73 | return Key{}, KeyScanUnchanged 74 | } 75 | s.canScan = true 76 | } 77 | 78 | if len(keys) == s.lastNumKeys { 79 | // It's either empty or we're still collecting the keys. 80 | return Key{}, KeyScanUnchanged 81 | } 82 | 83 | if len(keys) < s.lastNumKeys { 84 | // One or more keys are released. 85 | // Consider it to be a confirmation event. 86 | result := s.key 87 | s.lastNumKeys = 0 88 | s.key = Key{} 89 | s.canScan = false 90 | return result, KeyScanCompleted 91 | } 92 | 93 | s.lastNumKeys = len(keys) 94 | 95 | k, ok := scanKey(keys) 96 | status := KeyScanUnchanged 97 | if ok { 98 | s.key = k 99 | status = KeyScanChanged 100 | } 101 | return k, status 102 | } 103 | 104 | func scanKey(keys []ebiten.Key) (Key, bool) { 105 | if len(keys) == 0 { 106 | return Key{}, false 107 | } 108 | 109 | containsKeyCode := func(keys []ebiten.Key, code int) bool { 110 | for _, k := range keys { 111 | if int(k) == code { 112 | return true 113 | } 114 | } 115 | return false 116 | } 117 | 118 | // Parse the keys combination into something that this library can handle. 119 | 120 | // Round 1: walk the actual keys that are being pressed and collect the modifiers. 121 | // Remove the modifiers from the slice (inplace). 122 | var ctrlKey Key 123 | var shiftKey Key 124 | keysWithoutMods := keys[:0] 125 | for _, k := range keys { 126 | switch k { 127 | case ebiten.KeyControl, ebiten.KeyShift: 128 | // Just omit them from the slice. 129 | case ebiten.KeyControlLeft: 130 | ctrlKey = KeyControlLeft 131 | case ebiten.KeyControlRight: 132 | ctrlKey = KeyControlRight 133 | case ebiten.KeyShiftLeft: 134 | shiftKey = KeyShiftLeft 135 | case ebiten.KeyShiftRight: 136 | shiftKey = KeyShiftRight 137 | default: 138 | keysWithoutMods = append(keysWithoutMods, k) 139 | } 140 | } 141 | hasCtrl := ctrlKey.name != "" 142 | hasShift := shiftKey.name != "" 143 | 144 | var mappedKey Key 145 | 146 | // Round 2: map the Ebitengine keys to the local types. 147 | // In theory, we could generate a big LUT to make this mapping very fast. 148 | // But this would mean more data reserved for this package. 149 | // Since this part of the code is not that performance-sensitive, 150 | // we'll handle it in a less efficient, but less memory-hungry way. 151 | Loop: 152 | for _, lk := range allKeys { 153 | switch lk.kind { 154 | case keyKeyboard: 155 | if containsKeyCode(keysWithoutMods, lk.code) { 156 | mappedKey = lk 157 | break Loop 158 | } 159 | } 160 | } 161 | 162 | if mappedKey.name == "" { 163 | switch { 164 | case hasCtrl: 165 | return ctrlKey, true 166 | case hasShift: 167 | return shiftKey, true 168 | } 169 | } 170 | 171 | var keymod KeyModifier 172 | switch { 173 | case hasCtrl && hasShift: 174 | keymod = ModControlShift 175 | case hasCtrl: 176 | keymod = ModControl 177 | case hasShift: 178 | keymod = ModShift 179 | } 180 | if keymod != ModUnknown { 181 | switch mappedKey.kind { 182 | case keyKeyboard, keyMouse: 183 | mappedKey = KeyWithModifier(mappedKey, keymod) 184 | } 185 | } 186 | 187 | return mappedKey, mappedKey.name != "" 188 | } 189 | -------------------------------------------------------------------------------- /scan_key_test.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hajimehoshi/ebiten/v2" 7 | ) 8 | 9 | func TestScanKey(t *testing.T) { 10 | tests := []struct { 11 | keys []ebiten.Key 12 | want Key 13 | }{ 14 | // Sanity tests. 15 | {[]ebiten.Key{}, Key{}}, 16 | 17 | // The simple cases with a single key. 18 | {[]ebiten.Key{ebiten.KeyB}, KeyB}, 19 | {[]ebiten.Key{ebiten.KeyEnter}, KeyEnter}, 20 | {[]ebiten.Key{ebiten.KeyControlLeft, ebiten.KeyControl}, KeyControlLeft}, 21 | {[]ebiten.Key{ebiten.KeyControlRight, ebiten.KeyControl}, KeyControlRight}, 22 | {[]ebiten.Key{ebiten.KeyControl, ebiten.KeyControlRight}, KeyControlRight}, 23 | 24 | // Multiple key candidates without a way to merge them into a single Key. 25 | {[]ebiten.Key{ebiten.KeyB, ebiten.KeyA}, KeyA}, 26 | {[]ebiten.Key{ebiten.KeyA, ebiten.KeyB}, KeyA}, 27 | 28 | // Control modifiers. 29 | {[]ebiten.Key{ebiten.KeyC, ebiten.KeyControlLeft, ebiten.KeyControl}, KeyWithModifier(KeyC, ModControl)}, 30 | {[]ebiten.Key{ebiten.KeyC, ebiten.KeyControl, ebiten.KeyControlLeft}, KeyWithModifier(KeyC, ModControl)}, 31 | {[]ebiten.Key{ebiten.KeyControl, ebiten.KeyControlLeft, ebiten.KeyC}, KeyWithModifier(KeyC, ModControl)}, 32 | {[]ebiten.Key{ebiten.KeyE, ebiten.KeyControlLeft, ebiten.KeyControl}, KeyWithModifier(KeyE, ModControl)}, 33 | {[]ebiten.Key{ebiten.KeyControlLeft, ebiten.KeyControl, ebiten.KeyE}, KeyWithModifier(KeyE, ModControl)}, 34 | 35 | // Shift modifiers. 36 | {[]ebiten.Key{ebiten.KeyF, ebiten.KeyShiftLeft, ebiten.KeyShift}, KeyWithModifier(KeyF, ModShift)}, 37 | 38 | // Control+Shift modifiers. 39 | {[]ebiten.Key{ebiten.KeyC, ebiten.KeyControlLeft, ebiten.KeyControl, ebiten.KeyShiftLeft, ebiten.KeyShift}, KeyWithModifier(KeyC, ModControlShift)}, 40 | {[]ebiten.Key{ebiten.KeyA, ebiten.KeyControlLeft, ebiten.KeyControl, ebiten.KeyShiftLeft, ebiten.KeyShift}, KeyWithModifier(KeyA, ModControlShift)}, 41 | {[]ebiten.Key{ebiten.KeyA, ebiten.KeyControlLeft, ebiten.KeyShiftLeft}, KeyWithModifier(KeyA, ModControlShift)}, 42 | {[]ebiten.Key{ebiten.KeyA, ebiten.KeyControlRight, ebiten.KeyShiftRight}, KeyWithModifier(KeyA, ModControlShift)}, 43 | {[]ebiten.Key{ebiten.KeyControlLeft, ebiten.KeyA, ebiten.KeyShiftRight}, KeyWithModifier(KeyA, ModControlShift)}, 44 | } 45 | 46 | for i, test := range tests { 47 | have, _ := scanKey(test.keys) 48 | if have != test.want { 49 | t.Fatalf("test[%d] failed:\nhave: %s (%#v)\nwant: %s (%#v)", 50 | i, have, have, test.want, test.want) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /system.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "github.com/hajimehoshi/ebiten/v2" 5 | "github.com/hajimehoshi/ebiten/v2/inpututil" 6 | ) 7 | 8 | // System is the main component of the input library. 9 | // 10 | // You usually need only one input system object. 11 | // 12 | // Store System object (by value) inside your game context/state object like this: 13 | // 14 | // struct GameState { 15 | // InputSystem input.System 16 | // } 17 | // 18 | // When ebitengine game is executed, call gameState.InputSystem.Init() once. 19 | // 20 | // On every ebitengine Update() call, use gameState.InputSystem.Update(). 21 | // 22 | // The system is usually not used directly after the input handlers are created. 23 | // Use input handlers to handle the user input. 24 | type System struct { 25 | gamepadIDs []ebiten.GamepadID 26 | gamepadInfo []gamepadInfo 27 | 28 | // This is a scratch slice for ebiten.AppendPressedKeys operation. 29 | keySlice []ebiten.Key 30 | gamepadKeySlice []ebiten.GamepadButton 31 | 32 | pendingEvents []simulatedEvent 33 | prevSimulatedEvents []simulatedEvent 34 | simulatedEvents []simulatedEvent 35 | hasSimulatedActions bool 36 | 37 | touchEnabled bool 38 | touchHasTap bool 39 | touchHasLongTap bool 40 | touchJustHadDrag bool 41 | touchHasDrag bool 42 | touchDragging bool 43 | touchIDs []ebiten.TouchID // This is a scratch slice, we don't support multi-touches yet 44 | touchActiveID ebiten.TouchID 45 | touchTapPos Vec 46 | touchDragPos Vec 47 | touchStartPos Vec 48 | touchTime float64 49 | 50 | mouseEnabled bool 51 | mouseHasDrag bool // For "drag" event 52 | mouseDragging bool // For "drag" event 53 | mouseJustHadDrag bool // For "drag" event 54 | mouseJustReleasedDrag bool // For "drag" event 55 | mousePressed bool // For "drag" event 56 | mouseStartPos Vec // For "drag" event 57 | mouseDragPos Vec // For "drag" event 58 | cursorPos Vec 59 | wheel Vec 60 | } 61 | 62 | // SystemConfig configures the input system. 63 | // This configuration can't be changed once created. 64 | type SystemConfig struct { 65 | // DevicesEnabled selects the input devices that should be handled. 66 | // For the most cases, AnyDevice value is a good option. 67 | DevicesEnabled DeviceKind 68 | } 69 | 70 | func (sys *System) Init(config SystemConfig) { 71 | sys.keySlice = make([]ebiten.Key, 0, 4) 72 | sys.gamepadKeySlice = make([]ebiten.GamepadButton, 0, 2) 73 | 74 | sys.touchEnabled = config.DevicesEnabled&TouchDevice != 0 75 | sys.mouseEnabled = config.DevicesEnabled&MouseDevice != 0 76 | 77 | sys.gamepadIDs = make([]ebiten.GamepadID, 0, 8) 78 | sys.gamepadInfo = make([]gamepadInfo, 8) 79 | 80 | if sys.touchEnabled { 81 | sys.touchIDs = make([]ebiten.TouchID, 0, 8) 82 | sys.touchActiveID = -1 83 | } 84 | } 85 | 86 | // UpdateWithDelta is like Update(), but it allows you to specify the time delta. 87 | func (sys *System) UpdateWithDelta(delta float64) { 88 | // Rotate the events slices. 89 | // Pending events become simulated in this frame. 90 | // Re-use the other slice capacity to push new events. 91 | // prev simulated <- simulated 92 | // pending <- prev simulated 93 | // simulated <- pending 94 | sys.prevSimulatedEvents, sys.pendingEvents, sys.simulatedEvents = 95 | sys.simulatedEvents, sys.prevSimulatedEvents, sys.pendingEvents 96 | sys.pendingEvents = sys.pendingEvents[:0] 97 | sys.hasSimulatedActions = false 98 | for i := range sys.simulatedEvents { 99 | if sys.simulatedEvents[i].keyKind == keySimulated { 100 | sys.hasSimulatedActions = true 101 | break 102 | } 103 | } 104 | 105 | sys.gamepadIDs = ebiten.AppendGamepadIDs(sys.gamepadIDs[:0]) 106 | if len(sys.gamepadIDs) != 0 { 107 | for i, id := range sys.gamepadIDs { 108 | info := &sys.gamepadInfo[i] 109 | info.axisCount = ebiten.GamepadAxisCount(id) 110 | modelName := ebiten.GamepadName(id) 111 | if info.modelName != modelName { 112 | info.modelName = modelName 113 | switch { 114 | case ebiten.IsStandardGamepadLayoutAvailable(id): 115 | info.model = gamepadStandard 116 | case isFirefox(): 117 | info.model = guessFirefoxGamepadModel(int(id)) 118 | default: 119 | info.model = guessGamepadModel(modelName) 120 | } 121 | } 122 | sys.updateGamepadInfo(id, info) 123 | } 124 | } 125 | 126 | if sys.touchEnabled { 127 | sys.touchHasTap = false 128 | sys.touchHasLongTap = false 129 | sys.touchHasDrag = false 130 | sys.touchJustHadDrag = false 131 | // Track the touch gesture release. 132 | // If it was a tap, set a flag. 133 | if sys.touchActiveID != -1 && inpututil.IsTouchJustReleased(sys.touchActiveID) { 134 | if !sys.touchDragging { 135 | if sys.touchTime >= 0.5 { 136 | sys.touchHasLongTap = true 137 | } else { 138 | sys.touchHasTap = true 139 | } 140 | sys.touchTapPos = sys.touchStartPos 141 | } 142 | sys.touchActiveID = -1 143 | sys.touchDragging = false 144 | } 145 | // Check if this gesture entered a drag mode. 146 | // Drag mode gestures will not trigger a tap when released. 147 | // Drag events emit a pos delta relative to a start pos every frame. 148 | if sys.touchActiveID != -1 { 149 | x, y := ebiten.TouchPosition(sys.touchActiveID) 150 | currentPos := Vec{X: float64(x), Y: float64(y)} 151 | if sys.touchDragging { 152 | sys.touchHasDrag = true 153 | sys.touchDragPos = currentPos 154 | } else { 155 | sys.touchTime += delta 156 | if vecDistance(sys.touchStartPos, currentPos) > 5 { 157 | sys.touchDragging = true 158 | sys.touchJustHadDrag = true 159 | sys.touchHasDrag = true 160 | sys.touchDragPos = currentPos 161 | } 162 | } 163 | } 164 | // Check if a new touch gesture is started. 165 | if sys.touchActiveID == -1 { 166 | sys.touchIDs = inpututil.AppendJustPressedTouchIDs(sys.touchIDs[:0]) 167 | for _, id := range sys.touchIDs { 168 | x, y := ebiten.TouchPosition(id) 169 | sys.touchStartPos = Vec{X: float64(x), Y: float64(y)} 170 | sys.touchActiveID = id 171 | sys.touchTime = 0 172 | break 173 | } 174 | } 175 | } 176 | 177 | if sys.mouseEnabled { 178 | x, y := ebiten.CursorPosition() 179 | sys.cursorPos = Vec{X: float64(x), Y: float64(y)} 180 | 181 | // We copy a lot from the touch-style drag gesture. 182 | // This is not mandatory as getting a cursor pos is much easier on PC. 183 | // But I do value the consistency and easier cross-platform coding, 184 | // so let's try to make them behave as close to each other as feasible. 185 | sys.mouseHasDrag = false 186 | sys.mouseJustHadDrag = false 187 | sys.mouseJustReleasedDrag = false 188 | if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) { 189 | if sys.mouseDragging { 190 | sys.mouseJustReleasedDrag = true 191 | } 192 | sys.mouseDragging = false 193 | sys.mousePressed = false 194 | } 195 | if sys.mousePressed { 196 | if sys.mouseDragging { 197 | sys.mouseHasDrag = true 198 | sys.mouseDragPos = sys.cursorPos 199 | } else { 200 | // Mouse pointer is more precise than a finger gesture, 201 | // therefore we can have a lower threshold here. 202 | if vecDistance(sys.mouseStartPos, sys.cursorPos) > 1 { 203 | sys.mouseDragging = true 204 | sys.mouseJustHadDrag = true 205 | sys.mouseHasDrag = true 206 | sys.mouseDragPos = sys.cursorPos 207 | } 208 | } 209 | } 210 | if !sys.mousePressed && inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { 211 | sys.mouseStartPos = sys.cursorPos 212 | sys.mousePressed = true 213 | } 214 | } 215 | 216 | if sys.mouseEnabled || sys.touchEnabled { 217 | x, y := ebiten.Wheel() 218 | sys.wheel = Vec{X: x, Y: y} 219 | } 220 | } 221 | 222 | // Update reads the input state and updates the information 223 | // available to all input handlers. 224 | // Generally, you call this method from your ebiten.Game.Update() method. 225 | // 226 | // Since ebitengine uses a fixed timestep architecture, 227 | // a time delta of 1.0/60.0 is implied. 228 | // If you need a control over that, use UpdateWithDelta() instead. 229 | // 230 | // The time delta mostly needed for things like press gesture 231 | // detection: we need to calculate when a tap becomes a [long] press. 232 | func (sys *System) Update() { 233 | sys.UpdateWithDelta(1.0 / 60.0) 234 | } 235 | 236 | func (sys *System) updateGamepadInfo(id ebiten.GamepadID, info *gamepadInfo) { 237 | switch info.model { 238 | case gamepadStandard: 239 | copy(info.prevAxisValues[:], info.axisValues[:]) 240 | for axis := ebiten.StandardGamepadAxisLeftStickHorizontal; axis <= ebiten.StandardGamepadAxisMax; axis++ { 241 | v := ebiten.StandardGamepadAxisValue(id, axis) 242 | info.axisValues[int(axis)] = v 243 | } 244 | case gamepadFirefoxXinput: 245 | copy(info.prevAxisValues[:], info.axisValues[:]) 246 | for axis := 0; axis < info.axisCount; axis++ { 247 | v := ebiten.GamepadAxisValue(id, axis) 248 | info.axisValues[axis] = v 249 | } 250 | } 251 | } 252 | 253 | // NewHandler creates a handler associated with player/device ID. 254 | // IDs should start with 0 with a step of 1. 255 | // So, NewHandler(0, ...) then NewHandler(1, ...). 256 | // 257 | // If you want to configure the handler further, use Handler fields/methods 258 | // to do that. For example, see Handler.GamepadDeadzone. 259 | func (sys *System) NewHandler(playerID uint8, keymap Keymap) *Handler { 260 | return &Handler{ 261 | id: playerID, 262 | keymap: keymap, 263 | sys: sys, 264 | 265 | // My gamepads may have false positive activations with a 266 | // value lower than 0.03; we're using 0.055 here just to be safe. 267 | // Various sources indicate that a value of ~0.05 is optimal for a default. 268 | GamepadDeadzone: 0.055, 269 | } 270 | } 271 | --------------------------------------------------------------------------------