Quick answer: Use Input.get_vector(left, right, up, down, deadzone) with deadzone = 0.2. The function applies a radial deadzone correctly. Set the same deadzone in the InputMap entries for consistent behavior with is_action_pressed.
Player character drifts forward slowly when no one is touching the controller. Worn analog sticks don’t return to zero. Deadzone is the standard solution.
The Symptom
Character moves slowly without input. Or twin-stick aim drifts to one side. Plugging a different controller may or may not fix it — depends on the new controller’s wear.
What Causes This
Analog sticks aren’t perfect. Deadzone is the threshold below which input values are clamped to zero. Godot has two deadzone systems:
- Per-action deadzone (InputMap settings).
- Function-call deadzone (parameter to
Input.get_vector, etc.).
The Fix
Step 1: Use Input.get_vector for movement.
func _physics_process(delta: float) -> void:
var dir := Input.get_vector(
"move_left", "move_right",
"move_up", "move_down",
0.2 # deadzone
)
velocity = dir * SPEED
move_and_slide()
get_vector reads four actions, builds a Vector2, applies a radial deadzone (magnitude check), and rescales the remaining range so the boundary isn’t a hard step.
Step 2: Set per-action deadzone in InputMap. Project Settings → Input Map → expand each action → Deadzone field. Set to 0.2.
Per-action deadzone affects is_action_pressed (which returns false below threshold) and get_action_strength (which reads 0).
Why Radial
Axial deadzone (rectangle dead area) produces a square hole. Pushing the stick toward the corner with low magnitude misses the corner of the dead-square — pushing left and up by 0.15 each is below the per-axis threshold but a magnitude of 0.21 in the diagonal direction. The player gets motion at cardinals but not at diagonals.
Radial uses Vector2 magnitude. The dead area is a circle. Magnitude below 0.2 = zero motion regardless of direction. Diagonals work the same as cardinals.
Per-Controller Calibration
For better-than-default UX, calibrate per controller:
var _deadzones := {}
func calibrate(device: int) -> void:
var max_at_rest := 0.0
var samples := 60 # 1 second at 60 Hz
for i in samples:
var v := Vector2(
Input.get_joy_axis(device, JOY_AXIS_LEFT_X),
Input.get_joy_axis(device, JOY_AXIS_LEFT_Y)
)
max_at_rest = max(max_at_rest, v.length())
await get_tree().process_frame
_deadzones[device] = max_at_rest + 0.05 # buffer
Save and reuse for the controller’s GUID across sessions.
Verifying
Place the controller on the desk untouched. Print get_vector value each frame. Should be (0,0). If it shows tiny non-zero, raise the deadzone.
“Radial deadzone via get_vector. Per-action deadzone in InputMap. Drift goes to zero.”
Related Issues
For input action not firing on overlap, see Area2D signals. For input buffer pattern, see input buffer.
Radial deadzone. The character holds still.