Controllers: actions and deadzones
WASD and stick through shared actions without losing analog magnitude.
Define Input Map actions for movement, then read the result independently of a physical button. Direct axis polling is useful for diagnostics or a separate control scheme, not as a replacement for every gameplay action.
Choose a method
| Task | API |
|---|---|
| Two-axis movement from stick or keys | Input.GetVector(left, right, up, down). |
| One axis with negative and positive directions | Input.GetAxis(negative, positive). |
| Analog magnitude for one action | Input.GetActionStrength(action). |
| Held button | Input.IsActionPressed(action). |
| One action on press | Input.IsActionJustPressed(action). |
| Inspect an unprocessed physical-axis value | Input.GetJoyAxis(device, axis). |
Mouse movement and stick deflection have different units. A mouse supplies displacement per event; a stick usually supplies direction and intensity from which you compute turn speed. Do not apply one formula with identical delta treatment to both.
Assign a device
Input.GetConnectedJoypads() returns current device IDs. Do not assume the first connected controller always has ID 0 or that its ID permanently identifies a player. Keep a player-to-device assignment and update it through Input.JoyConnectionChanged; offer reassignment and stop dependent actions after disconnection.
An Input Map event can target one device or all devices. In local play with multiple controllers, an all-device binding does not separate players. The example below selects one device; its keyboard bindings are shared and do not themselves provide multiplayer input.
Complete WASD and left-stick helper
This file creates four new actions for the selected device, reads a circular deadzone, and removes its actions through Dispose(). An existing action prefix is rejected rather than overwritten.
using System;
using Reckfell;
public sealed class MovementActions : IDisposable
{
private readonly string[] _names;
private bool _disposed;
public MovementActions(string prefix, int device)
{
if (string.IsNullOrWhiteSpace(prefix) || device < 0) throw new ArgumentOutOfRangeException();
_names = new[] { prefix + "_left", prefix + "_right", prefix + "_up", prefix + "_down" };
foreach (string name in _names)
if (InputMap.HasAction(name)) throw new InvalidOperationException("Action already exists: " + name);
Bind(_names[0], Key.A, JoyAxis.LeftX, -1, device);
Bind(_names[1], Key.D, JoyAxis.LeftX, 1, device);
Bind(_names[2], Key.W, JoyAxis.LeftY, -1, device);
Bind(_names[3], Key.S, JoyAxis.LeftY, 1, device);
}
private static void Bind(string name, Key key, JoyAxis axis, float sign, int device)
{
InputMap.AddAction(name, 0.2f);
InputMap.ActionAddEvent(name, new InputEventKey { PhysicalKeycode = key });
InputMap.ActionAddEvent(name, new InputEventJoypadMotion { Device = device, Axis = axis, AxisValue = sign });
}
public Vector2 Read(float deadzone = 0.2f)
{
if (_disposed) throw new ObjectDisposedException(nameof(MovementActions));
if (!float.IsFinite(deadzone) || deadzone < 0 || deadzone >= 1) throw new ArgumentOutOfRangeException(nameof(deadzone));
return Input.GetVector(_names[0], _names[1], _names[2], _names[3], deadzone);
}
public void Dispose()
{
if (_disposed) return;
foreach (string name in _names) InputMap.EraseAction(name);
_disposed = true;
}
}Create MovementActions once, for example in _Ready(), using a unique prefix and the selected connected device ID. In the physics loop, read Read(), set CharacterBody2D.Velocity = direction * speed, and call MoveAndSlide(). Do not create actions every frame. Dispose when permanently finished; reading after Dispose() is rejected. Runtime bindings are not automatically saved into project settings.
Use the same four directions with your own Input Map. Preserve small analog deflections: unconditional Normalized() after GetVector() turns every nonzero input into full speed.
What the deadzone does
GetVector() uses four raw action strengths, builds a vector, and applies a circular deadzone to its length. Beyond the deadzone, magnitude is remapped into 0–1; diagonal length is limited to 1. A negative deadzone argument uses the average of the four action deadzones. This differs from two independent GetAxis() calls.
Executed checks with deadzone 0.2 produced:
- X=0.1 became zero while raw strength remained 0.1.
- X=0.6 became X=0.5 after remapping.
- (0.3, 0.4) became (0.225, 0.3): length decreased from 0.5 to 0.375.
- (1, 1) was limited to unit length; WASD uses the same actions.
Do not choose a threshold blindly from these numbers. Current InputMap.AddAction() defaults to 0.2; a saved action can have another value. Tune against a real device and expose adjustment to the player.
Diagnose incorrect input
No controller in the list: check OS-level connection first. Device present but action unchanged: inspect ID, axis, sign, and Input Map binding. Another player moves the character: inspect device filtering. Character drifts untouched: measure raw deflection and adjust the deadzone. Movement persists after release: inspect state updates and your own accumulated vector.
A gamepad button need not generate keyboard echo events: implement menu repetition with a timer and initial delay. See input and rebinding and InputEvent routing through UI.
Synthetic axis/key events, circular deadzones, diagonals, device filtering, and cleanup were executed. Physical controllers, vibration, sensors, disconnection, OS focus, and other platforms were not tested here. An API does not guarantee a feature on every device.