reckfellENGINE

Controllers: actions and deadzones

WASD and stick through shared actions without losing analog magnitude.

On this page

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

TaskAPI
Two-axis movement from stick or keysInput.GetVector(left, right, up, down).
One axis with negative and positive directionsInput.GetAxis(negative, positive).
Analog magnitude for one actionInput.GetActionStrength(action).
Held buttonInput.IsActionPressed(action).
One action on pressInput.IsActionJustPressed(action).
Inspect an unprocessed physical-axis valueInput.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.

C#
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.

Reckfell Engine documentationBack to top ↑
QUICK LINKSGetting startedEntity hierarchyC# & .NET 10Scenes and prefabs
Reckfell guides and full API K
reckfellENGINE
RECKFELL ACCOUNT

Sign in

Sign in to your Reckfell account.

One account for the entire Reckfell platform.