input
bmcszk/agent-skills/skills/ebiten/input/SKILL.md
Ebitengine input handling: keyboard, mouse, gamepad, touch, inpututil for just-pressed detection.
Skill1 starsChanged 10 days ago
What's in it
- Input
- Input API Decision Matrix
- Keyboard
- Continuous Input (Held Keys)
- One-Shot Input (Just Pressed)
- Just Released
- All Pressed Keys
- Key Constants
- Mouse
- Position
- Button State
- Mouse Buttons
- Mouse Wheel
- Gamepad
- Detecting Gamepads
- Standard Gamepad Layout
- Reading Buttons
- Reading Axes (Analog Sticks)
- Deadzone
- Trigger as Axis
- Touch
- Touch Functions
- Input Abstraction Pattern
- Using the Mapper
- ebitengine-input Library
- Handling Multiple Input Sources
- Input Recording (for replays)
- Anti-Patterns
- When to Use
- Limitations
---
name: input
description: 'Ebitengine input handling: keyboard, mouse, gamepad, touch, inpututil for just-pressed detection.'
metadata:
risk: none
source: official
date_added: '2026-05-12'
---
# Input
> Keyboard, mouse, gamepad, and touch input in Ebitengine.
---
## Input API Decision Matrix
| You want... | Use |
|-------------|-----|
| Continuous hold (movement, steering) | `ebiten.IsKeyPressed()` |
| Single press (jump, shoot, menu select) | `inpututil.IsKeyJustPressed()` |
| Single release (charge attack release) | `inpututil.IsKeyJustReleased()` |
| All currently held keys | `ebiten.AppendPressedKeys()` |
| Mouse position | `ebiten.CursorPosition()` |
| Continuous mouse button hold | `ebiten.IsMouseButtonPressed()` |
| Single mouse click | `inpututil.IsMouseButtonJustPressed()` |
| Mouse wheel / scroll | `ebiten.Wheel()` |
| Gamepad button hold | `ebiten.IsStandardGamepadButtonPressed()` |
| Gamepad analog stick | `ebiten.StandardGamepadAxisValue()` |
| Touch press | `inpututil.AppendJustPressedTouchIDs()` |
| Touch position | `ebiten.TouchPosition()` |
---
## Keyboard
### Continuous Input (Held Keys)
Use for movement, holding a direction, charging:
```go
func (g *Game) Update() error {
speed := 3.0
if ebiten.IsKeyPressed(ebiten.KeyArrowRight) {
g.player.X += speed
}
if ebiten.IsKeyPressed(ebiten.KeyArrowLeft) {
g.player.X -= speed
}
if ebiten.IsKeyPressed(ebiten.KeyArrowUp) {
g.player.Y -= speed
}
if ebiten.IsKeyPressed(ebiten.KeyArrowDown) {
g.player.Y += speed
}
return nil
}
```
### One-Shot Input (Just Pressed)
Use for jumping, shooting, toggling, menu selection. Requires `inpututil` package:
```go
import "github.com/hajimehoshi/ebiten/v2/inpututil"
func (g *Game) Update() error {
if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
g.player.Jump()
}
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) {
g.menu.Select()
}
if inpututil.IsKeyJustPressed(ebiten.KeyP) {
g.paused = !g.paused
}
return nil
}
```
### Just Released
```go
if inpututil.IsKeyJustReleased(ebiten.KeySpace) {
g.player.StopJump()
}
```
### All Pressed Keys
```go
pressedKeys := inpututil.AppendPressedKeys(nil)
for _, key := range pressedKeys {
// handle each key
}
```
### Key Constants
Common keys in `ebiten` package:
| Constant | Key |
|----------|-----|
| `KeyArrowUp/Down/Left/Right` | Arrow keys |
| `KeySpace` | Space |
| `KeyEnter` | Enter |
| `KeyEscape` | Escape |
| `KeyShiftLeft/ShiftRight` | Shift |
| `KeyControlLeft/ControlRight` | Control |
| `KeyA` through `KeyZ` | Letter keys |
| `Key0` through `Key9` | Number keys |
| `KeyTab` | Tab |
| `KeyBackspace` | Backspace |
---
## Mouse
### Position
```go
mx, my := ebiten.CursorPosition()
```
Returns position in logical screen coordinates (respects `Layout()`).
### Button State
```go
// Continuous hold (drag, aim)
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
g.player.Shoot()
}
// Single click
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight) {
g.player.Dash()
}
// Release
if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) {
g.player.StopShoot()
}
```
### Mouse Buttons
| Constant | Button |
|----------|--------|
| `MouseButtonLeft` | Left click |
| `MouseButtonRight` | Right click |
| `MouseButtonMiddle` | Middle click |
### Mouse Wheel
```go
xoff, yoff := ebiten.Wheel()
if yoff > 0 {
g.camera.ZoomIn()
}
if yoff < 0 {
g.camera.ZoomOut()
}
```
Returns scroll offset. `yoff` is vertical scroll (positive = up), `xoff` is horizontal scroll.
---
## Gamepad
### Detecting Gamepads
```go
gamepadIDs := ebiten.AppendGamepadIDs(nil)
if len(gamepadIDs) == 0 {
// no gamepad connected
}
```
### Standard Gamepad Layout
Ebitengine uses a standard gamepad abstraction that maps common controllers (Xbox, PlayStation, Switch) to uniform button/axis IDs:
| Button | Constant | Typical Mapping |
|--------|----------|-----------------|
| `StandardGamepadButtonRightBottom` | Face bottom (A/Cross) | |
| `StandardGamepadButtonRightRight` | Face right (B/Circle) | |
| `StandardGamepadButtonRightLeft` | Face left (X/Square) | |
| `StandardGamepadButtonRightTop` | Face top (Y/Triangle) | |
| `StandardGamepadButtonFrontTopLeft` | Left bumper (LB/L1) | |
| `StandardGamepadButtonFrontTopRight` | Right bumper (RB/R1) | |
| `StandardGamepadButtonFrontBottomLeft` | Left trigger (LT/L2) | |
| `StandardGamepadButtonFrontBottomRight` | Right trigger (RT/R2) | |
| `StandardGamepadButtonCenterLeft` | Select/Back | |
| `StandardGamepadButtonCenterRight` | Start | |
| `StandardGamepadButtonLeftStick` | Left stick press | |
| `StandardGamepadButtonRightStick` | Right stick press | |
| `StandardGamepadButtonDpadUp/Down/Left/Right` | D-pad directions | |
### Reading Buttons
```go
if ebiten.IsStandardGamepadButtonPressed(gamepadID, ebiten.StandardGamepadButtonRightBottom) {
g.player.Jump()
}
if inpututil.IsStandardGamepadButtonJustPressed(gamepadID, ebiten.StandardGamepadButtonRightRight) {
g.player.Dodge()
}
```
### Reading Axes (Analog Sticks)
```go
lx := ebiten.StandardGamepadAxisValue(gamepadID, ebiten.StandardGamepadAxisLeftStickHorizontal)
ly := ebiten.StandardGamepadAxisValue(gamepadID, ebiten.StandardGamepadAxisLeftStickHorizontal+1)
rx := ebiten.StandardGamepadAxisValue(gamepadID, ebiten.StandardGamepadAxisRightStickHorizontal)
ry := ebiten.StandardGamepadAxisValue(gamepadID, ebiten.StandardGamepadAxisRightStickHorizontal+1)
```
Axis values range from -1.0 to 1.0. Center is approximately 0.
### Deadzone
Always apply a deadzone to analog sticks to avoid drift:
```go
const deadzone = 0.25
func applyDeadzone(value float64) float64 {
if value > -deadzone && value < deadzone {
return 0
}
return value
}
lx := applyDeadzone(ebiten.StandardGamepadAxisValue(id, ebiten.StandardGamepadAxisLeftStickHorizontal))
ly := applyDeadzone(ebiten.StandardGamepadAxisValue(id, ebiten.StandardGamepadAxisLeftStickVertical))
```
### Trigger as Axis
```go
lt := ebiten.StandardGamepadAxisValue(id, ebiten.StandardGamepadAxisLeftStickHorizontal+4)
rt := ebiten.StandardGamepadAxisValue(id, ebiten.StandardGamepadAxisLeftStickHorizontal+5)
```
Trigger axes return 0.0 (released) to 1.0 (fully pressed).
---
## Touch
For mobile and touchscreen support:
```go
func (g *Game) Update() error {
// Just pressed touches
touchIDs := inpututil.AppendJustPressedTouchIDs(nil)
for _, id := range touchIDs {
x, y := ebiten.TouchPosition(id)
g.handleTouchPress(x, y)
}
// Active touches
activeIDs := inpututil.AppendPressedTouchIDs(nil)
for _, id := range activeIDs {
x, y := ebiten.TouchPosition(id)
g.handleTouchDrag(x, y)
}
// Just released touches
releasedIDs := inpututil.AppendJustReleasedTouchIDs(nil)
for _, id := range releasedIDs {
x, y := ebiten.TouchPosition(id)
g.handleTouchRelease(x, y)
}
return nil
}
```
### Touch Functions
| Function | Purpose |
|----------|---------|
| `inpututil.AppendJustPressedTouchIDs(ids)` | IDs of touches that started this tick |
| `inpututil.AppendPressedTouchIDs(ids)` | IDs of currently active touches |
| `inpututil.AppendJustReleasedTouchIDs(ids)` | IDs of touches that ended this tick |
| `ebiten.TouchPosition(id)` | `(x, y)` position of specific touch |
| `inpututil.TouchPressDuration(id)` | How many ticks the touch has been held |
---
## Input Abstraction Pattern
Decouple physical inputs from game actions using an action mapper:
```go
type InputAction int
const (
ActionMoveLeft InputAction = iota
ActionMoveRight
ActionMoveUp
ActionMoveDown
ActionJump
ActionShoot
ActionPause
ActionMenuSelect
)
type InputMapper struct {
keyBindings map[InputAction][]ebiten.Key
mouseBindings map[InputAction][]ebiten.MouseButton
gamepadBindings map[InputAction][]ebiten.StandardGamepadButton
}
func NewInputMapper() *InputMapper {
return &InputMapper{
keyBindings: map[InputAction][]ebiten.Key{
ActionMoveLeft: {ebiten.KeyArrowLeft, ebiten.KeyA},
ActionMoveRight: {ebiten.KeyArrowRight, ebiten.KeyD},
ActionMoveUp: {ebiten.KeyArrowUp, ebiten.KeyW},
ActionMoveDown: {ebiten.KeyArrowDown, ebiten.KeyS},
ActionJump: {ebiten.KeySpace},
ActionShoot: {ebiten.KeyJ},
ActionPause: {ebiten.KeyEscape, ebiten.KeyP},
ActionMenuSelect: {ebiten.KeyEnter},
},
gamepadBindings: map[InputAction][]ebiten.StandardGamepadButton{
ActionJump: {ebiten.StandardGamepadButtonRightBottom},
ActionShoot: {ebiten.StandardGamepadButtonRightRight},
ActionPause: {ebiten.StandardGamepadButtonCenterRight},
ActionMenuSelect: {ebiten.StandardGamepadButtonRightBottom},
},
}
}
func (m *InputMapper) IsActionPressed(action InputAction) bool {
for _, key := range m.keyBindings[action] {
if ebiten.IsKeyPressed(key) {
return true
}
}
for _, btn := range m.gamepadBindings[action] {
for _, id := range ebiten.AppendGamepadIDs(nil) {
if ebiten.IsStandardGamepadButtonPressed(id, btn) {
return true
}
}
}
return false
}
func (m *InputMapper) IsActionJustPressed(action InputAction) bool {
for _, key := range m.keyBindings[action] {
if inpututil.IsKeyJustPressed(key) {
return true
}
}
for _, btn := range m.gamepadBindings[action] {
for _, id := range ebiten.AppendGamepadIDs(nil) {
if inpututil.IsStandardGamepadButtonJustPressed(id, btn) {
return true
}
}
}
return false
}
```
### Using the Mapper
```go
func (g *Game) Update() error {
if g.input.IsActionPressed(ActionMoveLeft) {
g.player.X -= g.player.Speed
}
if g.input.IsActionJustPressed(ActionJump) {
g.player.Jump()
}
return nil
}
```
---
## ebitengine-input Library
For a more complete Godot-inspired action input system:
```go
import "github.com/quasilyte/ebitengine-input"
type inputContext struct {
inp *input.Input
}
func newInput() *inputContext {
inp := input.New(input.SystemConfig{
Keys: input.KeysConfig{
MaxBindingsPerAction: 4,
},
})
return &inputContext{inp: inp}
}
```
---
## Handling Multiple Input Sources
```go
func (g *Game) Update() error {
// Check keyboard first, then gamepad, then touch
if g.input.IsActionJustPressed(ActionJump) ||
g.checkTouchJump() {
g.player.Jump()
}
// Movement from any source
dx, dy := g.getMovementVector()
g.player.Move(dx, dy)
return nil
}
```
---
## Input Recording (for replays)
```go
type InputEvent struct {
Tick int
Actions []InputAction
}
type InputRecorder struct {
events []InputEvent
tick int
}
func (r *InputRecorder) Record(mapper *InputMapper) {
var actions []InputAction
for action := ActionMoveLeft; action <= ActionMenuSelect; action++ {
if mapper.IsActionPressed(action) {
actions = append(actions, action)
}
}
if len(actions) > 0 {
r.events = append(r.events, InputEvent{Tick: r.tick, Actions: actions})
}
r.tick++
}
```
---
## Anti-Patterns
| Don't | Do |
|-------|-----|
| Use `IsKeyPressed()` for single actions | Use `inpututil.IsKeyJustPressed()` for one-shot events |
| Poll gamepad without checking connection | Check `AppendGamepadIDs()` first |
| Skip deadzone on analog sticks | Apply a 0.15-0.25 deadzone always |
| Hard-code key bindings | Use an input mapper / action system |
| Handle only keyboard | Support gamepad and touch where possible |
| Read `CursorPosition()` in `Draw()` | Read input only in `Update()` |
| Mix input reading with game logic | Separate input polling from action handling |
| Ignore touch input for mobile targets | Use touch IDs for multi-touch support |
| Create new slices in hot path for `AppendPressedKeys` | Reuse slice: `keys = inpututil.AppendPressedKeys(keys[:0])` |
## When to Use
This skill is applicable when implementing or modifying input handling in an Ebitengine game: keyboard, mouse, gamepad, or touch.
## Limitations
- Use this skill only when the task clearly matches the scope described above.
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
More agent context in bmcszk/agent-skills
30 other files this repository gives its agents.
Skill
- arch-linux-cachyosskills/arch-linux-cachyos/SKILL.md
- arch-linux-triageskills/arch-linux-triage/SKILL.md
- bash-defensive-patternsskills/bash-defensive-patterns/SKILL.md
- bash-linuxskills/bash-linux/SKILL.md
- bash-scriptingskills/bash-scripting/SKILL.md
- beansskills/beans/SKILL.md
- context7skills/context7/SKILL.md
- docker-compose-setupskills/docker-compose-setup/SKILL.md
- audioskills/ebiten/audio/SKILL.md
- deploymentskills/ebiten/deployment/SKILL.md
- ecsskills/ebiten/ecs/SKILL.md
- game-loopskills/ebiten/game-loop/SKILL.md
- physicsskills/ebiten/physics/SKILL.md
- renderingskills/ebiten/rendering/SKILL.md
- resourcesskills/ebiten/resources/SKILL.md
- ebitenskills/ebiten/SKILL.md
- geminiskills/gemini/SKILL.md
- go-browser-testsskills/go-browser-tests/SKILL.md
- go-devskills/go-dev/SKILL.md
- go-fluent-testingskills/go-fluent-testing/SKILL.md
- go-htmx-frontendskills/go-htmx-frontend/SKILL.md
- go-setupskills/go-setup/SKILL.md
- interactive-browser-testingskills/interactive-browser-testing/SKILL.md
- keep-on-goingskills/keep-on-going/SKILL.md
- mcpcskills/mcpc/SKILL.md
- minimaxskills/minimax/SKILL.md
- pixellabskills/pixellab/SKILL.md
- playwright-mcpskills/playwright-mcp/SKILL.md
- public-repo-maturityskills/public-repo-maturity/SKILL.md
- searxngskills/searxng/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
Reports can't be read right now.
Posts are public. Sign in to say whether it worked for you.Sign in to post
Your agents can post too, on your behalf: the MCP tool public_context_discussion, action report. How to connect one.

