Quick answer: Worn sticks never rest at exactly zero. Read movement through Input Map actions with a deadzone (0.2 is a good start) and combine them with Input.get_vector(), which applies a circular deadzone. Never drive movement from raw Input.get_joy_axis(), and give players a deadzone setting.
On your controller the character stands still. On a player's two-year-old gamepad it slowly walks left. Stick drift is a hardware reality, and the game has to absorb it.
Use actions, not raw axes
Input.get_joy_axis(0, JOY_AXIS_LEFT_X) returns the raw value, drift included, with no deadzone. Map the stick to actions in Project Settings > Input Map (move_left, move_right, move_up, move_down) and read the actions instead.
Set the deadzone per action
Each action has a Deadzone value in the Input Map (click the action to edit it). Values below it read as 0. Somewhere between 0.15 and 0.25 suits most controllers; very worn sticks need more.
Combine with get_vector
Reading two axes separately gives a square deadzone and lets one axis drift while the other is held:
# drift-prone
var dir := Vector2(
Input.get_axis("move_left", "move_right"),
Input.get_axis("move_up", "move_down"))
Input.get_vector() applies a circular deadzone to the combined stick and keeps diagonals the right length:
var dir := Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity = dir * speed
Its optional fifth argument overrides the deadzone; by default it uses the average of the four actions' deadzones.
Rescale after the deadzone
With a 0.2 deadzone, the smallest non-zero value is 0.2, which makes slow walking jumpy. get_vector() already rescales the remaining range back to 0..1. If you process raw axes yourself (for a camera, say), do the same:
func apply_deadzone(v: Vector2, dz: float) -> Vector2:
var len := v.length()
if len < dz:
return Vector2.ZERO
return v.normalized() * ((len - dz) / (1.0 - dz))
Camera drift
Right-stick camera control is where drift is most visible because it accumulates rotation every frame. Give camera actions their own, slightly larger deadzone.
The wrong device
Flight sticks, racing wheels and some audio interfaces register as joypads and report constant axis values. If the "drift" persists with the controller unplugged, check Input.get_connected_joypads() and the device id in your input events. Input Map actions listen to all devices by default; you can limit an action's event to a specific device.
Let players fix it
Expose the deadzone in your options menu and apply it at runtime:
for action in ["move_left", "move_right", "move_up", "move_down"]:
InputMap.action_set_deadzone(action, settings.deadzone)