Skip to content

Input & Controls

All input is polled through the static Input class, typically from OnUpdate. The scripts that reach for it most often are data-collection controllers (start and stop recording, mark episode outcomes, switch tasks) and robot driver scripts (WASD-style locomotion control).

In short

IsKeyDown is reliable in every callback and suits continuous actions such as driving a robot. IsKeyPressed and IsKeyReleased are true for one rendered frame, while OnUpdate runs on fixed simulation steps, so for one-shot actions in OnUpdate, detect the press yourself by comparing IsKeyDown with the previous step. A few editor shortcuts stay active in Play mode, so avoid those keys for script controls.

Keyboard

Keys are identified by the KeyCode enum. Four query styles cover the common needs. In this table, a frame is a rendered frame:

Method Returns true…
Input.IsKeyPressed(key) …on the first frame the key goes down.
Input.IsKeyHeld(key) …every frame after that initial press.
Input.IsKeyDown(key) …every frame the key is down (pressed or held).
Input.IsKeyReleased(key) …on the frame the key is released.

Input.IsKeyToggledOn(key) reports whether Caps Lock or Num Lock is on.

Frames and simulation steps

OnUpdate and the other per-step callbacks do not run once per rendered frame. They run on the fixed steps of their time runner, and one rendered frame can run zero, one or several steps. The pressed and released states last exactly one rendered frame, so in a per-step callback:

  • IsKeyPressed can miss a press, when no step runs during that frame.
  • IsKeyPressed can report the same press more than once, when several steps run during that frame.

IsKeyDown and IsKeyHeld stay true for as long as the key is held, so they work in any callback. The same applies to mouse and controller buttons.

Use edge detection for one-shot actions in OnUpdate

Do not rely on IsKeyPressed or IsKeyReleased in OnUpdate for actions that must happen exactly once per press, such as starting a recording or ending an episode. Compare IsKeyDown with its value from the previous step instead.

Detecting a press in a per-step callback

A small helper turns IsKeyDown into a "pressed this step" signal:

using Hazel;

// Reports a key press once, on the first step the key is down.
public sealed class KeyEdge
{
    private readonly KeyCode m_Key;
    private bool m_WasDown;

    public KeyEdge(KeyCode key) => m_Key = key;

    // Call once per step, every step.
    public bool Pressed()
    {
        bool down = Input.IsKeyDown(m_Key);
        bool pressed = down && !m_WasDown;
        m_WasDown = down;
        return pressed;
    }
}

A recording controller then uses one KeyEdge per key:

using Hazel;
using Hazel.Data;

public class RecordingControls : Entity
{
    private readonly KeyEdge m_Start   = new(KeyCode.Enter);
    private readonly KeyEdge m_Fail    = new(KeyCode.X);
    private readonly KeyEdge m_Success = new(KeyCode.Y);
    private readonly KeyEdge m_Stop    = new(KeyCode.Backspace);

    protected override void OnUpdate(float ts)
    {
        // One-shot recording controls: each press triggers exactly once.
        if (m_Start.Pressed())
            Observer.StartRecording();
        if (m_Fail.Pressed())
            Observer.EndCurrentEpisode(success: false);
        if (m_Success.Pressed())
            Observer.EndCurrentEpisode(success: true);
        if (m_Stop.Pressed())
            Observer.StopRecording();
    }
}

Call Pressed() for every key on every step, before any early return. A key that is skipped for a few steps can report a stale press when it is next checked.

Alternative: a free-running callback

OnFreePreUpdate runs once per rendered frame, so IsKeyPressed reports each press exactly once there. It only runs when Enable Free Step Update Scripts is turned on in Settings → Scene → Scripts. Because it is not tied to the simulation steps, set a flag there, then handle and clear the flag in OnUpdate. See Entity Lifecycle.

Continuous actions

For actions that should happen on every step while a key is held, such as driving a robot or holding a gripper closed, use IsKeyDown:

protected override void OnUpdate(float ts)
{
    // Continuous robot drive: every step while the key is held.
    if (Input.IsKeyDown(KeyCode.W))
        DriveForward(ts);
    if (Input.IsKeyDown(KeyCode.A))
        TurnLeft(ts);
}

IsKeyHeld excludes the first frame of the press. For continuous actions in a per-step callback, IsKeyDown is the safer choice.

Common bindings

There are no engine-defined keybindings for script-level work. These are conventions worth following so users see consistent controls across scenes:

Key Common use
Enter Start a recording session
X End the current episode as failed
Y End the current episode as successful
Backspace Stop recording
R Reset the scene to its starting state
Space Pause / step / toggle a debug flag
1 – 9 Switch task index (with Observer.SetCurrentTaskIndex)
W A S D Drive a robot

Editor keys in Play mode

Some editor shortcuts stay active while the scene is playing. Scripts still receive these keys, but the editor acts on them too, so avoid them for script controls:

Key What the editor does in Play mode
F5 Stops Play while the scene runs, unless you are typing in a text field or the Content Browser or File Tree has focus. It doesn't stop a paused scene: click Stop.
Alt+P Stops Play while the scene runs and the Scene viewport or Scene Hierarchy has focus. It doesn't stop a paused scene.
Alt+C Toggles the editor camera.
Esc Returns a hidden or locked cursor to normal.
Tab Toggles Fullscreen while the Scene viewport has focus.
Ctrl shortcuts Most of the editor's Ctrl shortcuts also work in Play mode, for example Ctrl+S (save the scene), Ctrl+O (open a project) and Ctrl+B (build the scripts). Only Ctrl+D, Ctrl+C and Ctrl+V are off. Settings lists them all.
V, R, C, U, S, D, Esc In Sim High Perf. mode, with the pointer over the Scene viewport, these keys press the buttons of the HIGH PERFORMANCE MODE panel: V Disable Viewport Rendering (or Resume Viewport Rendering), R Render Single Frame, C Clear Screen, U UI Responsiveness, S Switch to Realtime, D Open Data Observer and Esc Stop Simulation.

The Alt shortcuts use the left Alt key.

See Recording with the Observer for the full episode-lifecycle API and Getting Started for the WASD walker example.

Mouse

Input exposes the same four query styles for mouse buttons: IsMouseButtonPressed, IsMouseButtonHeld, IsMouseButtonDown and IsMouseButtonReleased. Mouse buttons are identified by the MouseButton enum (Left, Right, Middle, or Button0 to Button5). The frame-versus-step advice above applies to them too.

GetMousePosition() returns the cursor position relative to the editor window, not to the Scene viewport.

if (Input.IsMouseButtonDown(MouseButton.Left))
    Log.Info($"Cursor at {Input.GetMousePosition()}");

Input.SetCursorMode hides or locks the cursor with CursorMode Hidden or Locked, and Normal restores it. In Play mode, Esc also returns the cursor to normal.

The scripting API has no mouse-wheel query.

Controllers

Connected gamepads and joysticks are queried by ID:

  • Input.GetConnectedControllerIDs() returns the IDs currently plugged in. IsControllerPresent(id) and GetControllerName(id) check one controller.
  • IsControllerButtonPressed, IsControllerButtonHeld, IsControllerButtonDown and IsControllerButtonReleased read a button. They take a GamepadButton value or a plain button index.
  • GetControllerAxis(id, axis) returns a stick or trigger position as a float. GetControllerHat(id, hat) returns the state of a hat (D-pad) as a byte. Axes and hats are plain integer indices.
  • SetControllerDeadzone(id, axis, deadzone) makes an axis read 0 while its value is within the dead zone. The default dead zone is 0.

Axis and button numbering depends on the controller. To find the axis you need, log GetControllerAxis for each index while you move the stick.

A gamepad driver looks the same as the keyboard one, with the axis value in place of IsKeyDown:

private const int k_DriveAxis = 1;   // check the numbering for your controller

protected override void OnUpdate(float ts)
{
    int[] pads = Input.GetConnectedControllerIDs();
    if (pads.Length == 0)
        return;

    float drive = Input.GetControllerAxis(pads[0], k_DriveAxis);
    DriveForward(drive * ts);
}

Where to go next