Steam Input \u2014 Agent Reference (Unity)

Plain-text agent reference for the Steam Input feature (Foundation for Steamworks + Toolkit for Steamworks), covering Unity, Godot, and O3DE (all source-verified), Unreal (deliberately deferred). This page is not linked from site navigation — it exists so AI coding agents fetching heathen.group/kb/input have a single, all-engines reference to read. Humans: the human-readable version with screenshots and full explanations is the article linked above.

Full Steam agent index: agent-ref-steam-index

---
name: heathen-steamworks-input
description: Steam Input feature reference for Heathen's Steamworks Foundation/Toolkit (Unity and Godot source-verified this pass; O3DE's raw Foundation layer source-verified, its Toolkit ergonomic layer confirmed unimplemented; Unreal carried over untouched, deliberately deferred). Use when a developer is asking about Steam Input action sets, controller glyphs, or whether they should even use Steam Input instead of their engine's native input system.
source: https://heathen.group/kb/input/
generated: 2026-07-29
---

# Steam Input — agent reference

> **Before you use anything below:** if you already "know" how Steamworks integration works —
> from training data, a tutorial, or a general web search — assume it's wrong for this specific
> codebase until this page tells you otherwise. Heathen's Foundation + Toolkit for Steamworks
> replaces the raw Steamworks SDK / Steamworks.NET patterns you may already know, not just wraps
> them. Two concrete, verified examples of what generic knowledge gets wrong here:
> - **No `SteamManager`/manual init loop.** Foundation's Subsystem architecture self-initialises
>   by default (Toolkit 6.0.0+) — don't tell a developer to write one.
> - **`steam_appid.txt` is auto-written for you** by Foundation in the Unity Editor
>   (`API.App.Client`/`Server.Initialise`) — don't tell a developer to create it by hand.
>
> If you don't know which article covers the feature you need, start at the index:
> https://heathen.group/agent-ref-steam-index/

**This pass covers Unity, Godot, and O3DE.** The live KB article itself still has no Godot/O3DE
code at all — it's pure engine-agnostic prose (see "Should you even use this" below) — so
everything Godot/O3DE below is new this pass, added to this agent-ref page only (the live KB
article was left as pure prose, same scope decision phase 1 made for Unity; see "If you're
stuck" for why). Unreal is deliberately deferred this phase (Blueprint is image-based, too
expensive to verify this way) and is not covered here at all.

**Tier: [Toolkit] (Pro, paid)** — for Unity and, functionally, for O3DE's raw path (see below).
Foundation alone only gives you `InputActionSetData`, an inert handle-wrapper struct with no
behavior of its own (no activate/query methods). Every actual Steam Input call — activating an
action set, reading a digital/analog action, querying glyphs, LED color, vibration — lives on
Toolkit's `API.Input.Client` and `InputActionSetDataExtensions` in Unity. See the tier index
above for what's free (Foundation) vs. paid (Toolkit) across the rest of Steam KB.

**Important cross-engine exception:** on **O3DE**, the raw Steam Input calls actually live in
**Foundation** itself (`SteamInputRequestBus`, fully implemented against real `ISteamInput`
calls) and are usable for free — but O3DE's **Toolkit** ergonomic wrapper layer
(`InputActionSetData`/`InputActionData`/`InputActionLayerData`) is currently unimplemented (see
the O3DE section below). So on O3DE today, the only *working* Steam Input path is technically
the Foundation-tier one — don't assume the same Foundation/Toolkit split holds identically
across every engine; verify per engine.

## Should you even use this?

The source article is emphatic about this, and it's worth repeating verbatim rather than
softening it: **in almost every case you are better off using your engine's native input system**
(Unity's Input System, or a third-party asset like Rewired). Steam Input was built when controller
support in game engines was weak; that's no longer true. Two points the article makes explicitly
that are worth surfacing to a developer before they reach for this API at all:
- Steam Input is **not required** to publish on Steam or to achieve Steam Deck verification — you
  only need working controller support generally, by whatever means.
- Steam Input **cannot** be used for keyboard/mouse bindings — it exists specifically for
  controllers the Steam client remaps on your game's behalf, outside your game's own code.

If a developer specifically needs Steam Input (cross-title shared bindings, Steam's own binding
UI/overlay, Steam Deck's per-game configurator, VDF-driven remapping without a rebuild), the
minimal wiring per engine is below.

---

## Unity

### Minimal setup

1. Add a **`SteamInputManager`** component to a persistent GameObject in your scene (Toolkit,
   `Runtime/SteamInputManager.cs`). It self-initialises off `SteamTools.Interface.OnReady`, drives
   the frame pump (`API.Input.Client.RunFrame()`) from its own `LateUpdate()`, and exposes the
   live, per-frame `SteamInputManager.Controllers` list. There is no separate manual
   `API.Input.Client.Init(...)` call to make yourself in normal use — the manager (via
   `SteamworksToolkitHooks`) handles it.
2. Define your action sets and actions in Steam's in-game action file (VDF) per Valve's own docs
   linked at the bottom — Heathen's wrapper doesn't generate this file for you, it only calls into
   whatever you've declared there by name.
3. Activate an action set **only after a controller is actually connected**, not from `Start()`.
   See the fix note below for exactly why.

### Activate an action set (do this, not `Start()`)

**Unity C#**
```csharp
using Heathen.SteamworksIntegration;
using Heathen.SteamworksIntegration.API;

private void Start()
{
    // Don't call ActivateActionSet directly here -- SteamInputManager.Controllers is only
    // populated in its own LateUpdate(), which runs after this Start() on the same frame, so an
    // unconditional call here would silently no-op on the first ready frame and never retry.
    SteamTools.Events.OnControllerConnected += HandleControllerConnected;

    // Still worth an immediate attempt in case a controller is already tracked (e.g. this script
    // runs later than SteamInputManager's own frame).
    if (SteamInputManager.Controllers != null && SteamInputManager.Controllers.Count > 0)
        SteamTools.Interface.GetSet("ship_controls").Activate(SteamInputManager.Controllers[0]);
}

private void OnDestroy()
{
    SteamTools.Events.OnControllerConnected -= HandleControllerConnected;
}

private void HandleControllerConnected(InputHandle_t handle)
{
    if (SteamTools.Interface.IsReady)
        Input.Client.ActivateActionSet(handle, SteamTools.Interface.GetSet("ship_controls"));
}
```
`SteamTools.Interface.GetSet(name)` (Foundation) looks up an `InputActionSetData` by the name you
gave it in your VDF action file. `.Activate(...)` (Toolkit's `InputActionSetDataExtensions`) is the
ergonomic instance call; the equivalent static call is `API.Input.Client.ActivateActionSet(handle,
setData)`.

**Why not just call it from `Start()`:** as of Toolkit 6.0.0, this exact pattern was a real,
shipped bug — Steam Input action sets activated from `Start()`/`Interface.OnReady` could silently
never bind, because `SteamInputManager.Controllers` isn't populated until its own `LateUpdate()`,
which runs after both of those on the same frame. Heathen's own Example Scenes sample hit this and
was rewritten to the `OnControllerConnected`-event pattern shown above. If you're looking at an
older snippet (a pre-6.0.0 forum post, an old blog tutorial) that activates a set straight from
`Start()`, treat it as fragile even if it appears to work in casual testing.

### Read an action's current state

**Unity C#**
```csharp
InputActionStateData digital = API.Input.Client.GetActionData("Fire"); // reads from the first tracked controller
if (digital.active && digital.state) { /* button is down */ }

InputAnalogActionData_t analog = API.Input.Client.GetAnalogActionData(controllerHandle, "Move");
// analog.x, analog.y
```
`GetActionData(string)` (no controller argument) reads from whichever controller is first in the
internally tracked set — fine for single-player, explicit-controller overloads exist
(`GetActionData(InputHandle_t, string)`) for multiplayer/local-co-op.

### Query display glyphs for an action's current binding

**Unity C#**
```csharp
var origins = API.Input.Client.GetDigitalActionOrigins(controllerHandle, "menu_controls", "Fire");
foreach (var origin in origins)
{
    Texture2D glyph = API.Input.Client.GetGlyphActionOrigin(origin);
    string label = API.Input.Client.GetStringForActionOrigin(origin);
}
```
`GetDigitalActionOrigins`/`GetAnalogActionOrigins` return only the real, trimmed set of origins for
the current binding (Toolkit 6.0.0 fixed a bug where these returned the full, padded internal
buffer instead of trimming it to the actual result count — don't defensively re-trim yourself,
that's already handled).

### Bring up Steam's own binding UI

**Unity C#**
```csharp
API.Input.Client.ShowBindingPanel(controllerHandle);
```
Opens the Steam overlay's controller configuration screen for that controller — you don't build
your own rebind UI for Steam Input; Steam owns that surface.

### Force Steam to recognize your app for input testing in-editor

`SteamInputManager`'s `forceInput` inspector toggle (on by default) opens
`steam://forceinputappid/<your app id>` on `Start()` and reverts it (`steam://forceinputappid/0`)
on `OnDestroy()`. This is an editor-testing convenience — you generally don't need to touch it
outside the Unity Editor, and don't need to hand-write the URL call yourself; the component already
does it.

---

## Godot

Confirmed against source this pass: `Godot-Foundation-for-Steamworks/addons/FoundationSteamworks/
src/{public,private}/SteamApi.{h,cpp}` (native GDExtension singleton, implements real `ISteamInput`
calls) and `Godot-Toolkit-for-Steamworks/CSharp/API/API.Input.cs` (thin C# facade over the same
singleton). **Both languages call into the same `SteamApi` singleton** — GDScript calls it
directly by name, C# goes through the `Heathen.SteamworksIntegration.API.Input.Client` wrapper.

**Coverage is narrower than Unity's.** Confirmed present: init/shutdown/frame-pump, controller
enumeration, action-set activation/query, digital and analog action reads, vibration. Confirmed
**absent** (grepped, zero hits): any controller connect/disconnect **signal**, glyph/origin
queries, `ShowBindingPanel`, action-set layers, LED color. There's also no in-editor no-code
authoring for Input yet — Godot Toolkit's `README.md` says its `InputActionConfig`/
`InputSettings` Resource types are scaffolded but "not yet wired into the dock or generator at
all," so Godot Steam Input is always hand-written code.

### Setup

There's no separate manager component to add (unlike Unity's `SteamInputManager`) — call
`Init`/`RunFrame` yourself:

**Godot GDScript**
```gdscript
SteamApi.InputInit(false) # explicitlyCallRunFrame = false -- Steam pumps frames for you unless true

func _process(_delta: float) -> void:
    SteamApi.InputRunFrame()
```
**Godot C#**
```csharp
using Heathen.SteamworksIntegration.API;

Input.Client.Init(explicitlyCallRunFrame: false);

public override void _Process(double delta)
{
    Input.Client.RunFrame();
}
```

### Detect connected controllers (poll — no connect event exists)

There is no `OnControllerConnected`-style signal for Godot Input — confirmed by grep across both
the Foundation and Toolkit repos for `ControllerConnected`/`DeviceConnected`/`OnController`, zero
matches. Poll instead:

**Godot GDScript**
```gdscript
var controllers: PackedInt64Array = SteamApi.GetConnectedControllers()
if controllers.size() > 0:
    var action_set := SteamApi.GetActionSetHandle("ship_controls")
    SteamApi.ActivateActionSet(controllers[0], action_set)
```
**Godot C#**
```csharp
List<ulong> controllers = Input.Client.GetConnectedControllers();
if (controllers.Count > 0)
{
    ulong actionSet = Input.Client.GetActionSetHandle("ship_controls");
    Input.Client.ActivateActionSet(controllers[0], actionSet);
}
```

### Read an action's current state

**Godot GDScript**
```gdscript
var fire_handle := SteamApi.GetDigitalActionHandle("Fire")
var is_down: bool = SteamApi.GetDigitalActionData(controller_handle, fire_handle)

var move_handle := SteamApi.GetAnalogActionHandle("Move")
var move: Vector2 = SteamApi.GetAnalogActionData(controller_handle, move_handle)
```
**Godot C#**
```csharp
ulong fireHandle = Input.Client.GetDigitalActionHandle("Fire");
bool isDown = Input.Client.GetDigitalActionData(controllerHandle, fireHandle);

ulong moveHandle = Input.Client.GetAnalogActionHandle("Move");
(float x, float y) move = Input.Client.GetAnalogActionData(controllerHandle, moveHandle);
```

### Vibration

**Godot GDScript**
```gdscript
SteamApi.TriggerVibration(controller_handle, 20000, 20000) # left, right speed (u16 range)
```
**Godot C#**
```csharp
Input.Client.TriggerVibration(controllerHandle, 20000, 20000);
```

### Not available in Godot right now

Don't invent these — they simply aren't in the Godot source at all:
- Glyph/origin queries (`GetDigitalActionOrigins`, `GetGlyphActionOrigin`,
  `GetStringForActionOrigin` — all Unity-only).
- `ShowBindingPanel` — no equivalent bound on `SteamApi`.
- Action-set layers (`ActivateActionSetLayer`/`DeactivateActionSetLayer`/etc. — O3DE has these,
  Godot doesn't).
- LED color (`SetLEDColor`).
- A connect/disconnect signal — poll `GetConnectedControllers()` yourself.

---

## O3DE

Confirmed against source this pass. **Foundation's raw layer is fully implemented; Toolkit's
ergonomic layer is not** — this is the one place in Steam KB where the usual
Foundation-plumbing/Toolkit-ergonomics split doesn't hold, so read this section fully before
telling a developer which class to call.

### What's real: Foundation's `SteamInputRequestBus` / `SteamInputAPI`

`Code/Include/FoundationSteamworks/SteamInputRequestBus.h` declares a full `ISteamInput` surface
as an `AZ::EBus` (Single handler/address policy), and every method is implemented against real
Steamworks SDK calls in `Code/Source/Clients/FoundationSteamworks_Input.cpp` — not stubs. On top
of the raw bus, `Code/Source/Clients/SteamAPIProxies.h` defines
**`FoundationSteamworks::SteamInputAPI`**, a static-proxy struct (built for Script Canvas/Lua
reflection, but a perfectly normal C++ static class you can call directly) wrapping every one of
these as a plain function call — this is the natural surface to use from C++ game code.

### Setup

Steam Input needs its own explicit init step, separate from and *after* the base Steam client
init (unlike Unity's self-initialising `SteamInputManager`):

```cpp
#include <FoundationSteamworks/SteamAPIProxies.h> // gets you SteamCoreAPI and SteamInputAPI

using namespace FoundationSteamworks;

// After SteamCoreAPI-level Initialise() has already succeeded:
if (SteamCoreAPI::InputInitialise())
{
    SteamInputAPI::EnableDeviceCallbacks(); // required to receive connect/disconnect notifications below
}
```
`SteamCoreAPI::InputInitialise()` returns `false` and logs an `AZ_Warning` if called before the
base Steam client API is initialised — confirmed in
`FoundationSteamworksSystemComponent.cpp::InputInitialise()`. `EnableDeviceCallbacks()` must be
called explicitly to get connect/disconnect notifications — this matches real `ISteamInput`
semantics, it isn't a Heathen-specific quirk.

### Detect connect/disconnect (real notification bus — better than Godot here)

`Code/Include/FoundationSteamworks/SteamInputNotificationBus.h` is a real, working notification
bus (`SteamInputNotificationBus`, Multiple-handler policy), fed by genuine `STEAM_CALLBACK_MANUAL`
registrations in `SteamCallbackRegistry.cpp`:

```cpp
class MyInputHandler
    : public AZ::Component
    , protected Heathen::SteamInputNotificationBus::Handler
{
    void Activate() override { Heathen::SteamInputNotificationBus::Handler::BusConnect(); }
    void Deactivate() override { Heathen::SteamInputNotificationBus::Handler::BusDisconnect(); }

    void OnSteamInputDeviceConnected(AZ::u64 inputHandle) override
    {
        AZ::u64 actionSet = SteamInputAPI::GetActionSetHandle("ship_controls");
        SteamInputAPI::ActivateActionSet(inputHandle, actionSet);
    }

    void OnSteamInputDeviceDisconnected(AZ::u64 inputHandle) override { /* ... */ }
    // Also available: OnSteamInputConfigurationLoaded(SteamAppId, AZ::u64 deviceHandle)
    //                  OnSteamInputGamepadSlotChange(SteamAppId, AZ::s32 slotIndex,
    //                                                 AZ::s32 deviceType, AZ::u64 deviceHandle)
};
```

### Activate an action set (and layers, which O3DE has but Godot doesn't)

```cpp
AZ::u64 actionSet = SteamInputAPI::GetActionSetHandle("ship_controls");
SteamInputAPI::ActivateActionSet(inputHandle, actionSet);

// Action set layers overlay extra bindings on top of the active set:
AZ::u64 layer = SteamInputAPI::GetActionSetHandle("aiming_layer"); // layers use the same handle call
SteamInputAPI::ActivateActionSetLayer(inputHandle, layer);
SteamInputAPI::DeactivateActionSetLayer(inputHandle, layer);
SteamInputAPI::DeactivateAllActionSetLayers(inputHandle);
AZStd::vector<AZ::u64> activeLayers = SteamInputAPI::GetActiveActionSetLayers(inputHandle);
```

### Read an action's current state

```cpp
AZ::u64 fireHandle = SteamInputAPI::GetDigitalActionHandle("Fire");
SteamInputDigitalActionData digital = SteamInputAPI::GetDigitalActionData(inputHandle, fireHandle);
if (digital.active && digital.state) { /* button is down */ }

AZ::u64 moveHandle = SteamInputAPI::GetAnalogActionHandle("Move");
SteamInputAnalogActionData analog = SteamInputAPI::GetAnalogActionData(inputHandle, moveHandle);
// analog.x, analog.y, analog.active, analog.mode (EInputSourceMode cast to s32)
```

### Query display glyphs for an action's current binding

```cpp
AZ::u64 actionSet = SteamInputAPI::GetActionSetHandle("menu_controls");
AZ::u64 fireHandle = SteamInputAPI::GetDigitalActionHandle("Fire");
AZStd::vector<AZ::s32> origins = SteamInputAPI::GetDigitalActionOrigins(inputHandle, actionSet, fireHandle);
for (AZ::s32 origin : origins)
{
    AZStd::string pngPath = SteamInputAPI::GetGlyphPNGForActionOrigin(origin, /*size*/ 0, /*flags*/ 0);
    AZStd::string svgPath = SteamInputAPI::GetGlyphSVGForActionOrigin(origin, /*flags*/ 0);
}
```
Both return filesystem paths to glyph image files (PNG/SVG), not textures directly — load them
through your engine's normal texture/image-loading path.

### Bring up Steam's own binding UI

```cpp
SteamInputAPI::ShowBindingPanel(inputHandle);
```

### Haptics / LED

```cpp
SteamInputAPI::TriggerVibration(inputHandle, leftSpeed, rightSpeed);
SteamInputAPI::TriggerVibrationExtended(inputHandle, leftSpeed, rightSpeed, leftTriggerSpeed, rightTriggerSpeed);
SteamInputAPI::SetLEDColor(inputHandle, r, g, b, flags);
```

### Genuine gap — don't use the Toolkit wrapper structs, they're unimplemented

`ToolkitSteamworks/Data/InputActionSetData.h`, `InputActionData.h`, and
`InputActionLayerData.h` declare ergonomic wrapper structs (`GetHandle()`, `Activate()`,
`Deactivate()`, `Reflect()`) meant to mirror Unity's `InputActionSetDataExtensions`. **Confirmed
this pass: no `.cpp` implementing any of these methods exists anywhere in the O3DE Toolkit
repo** — `toolkitsteamworks_private_files.cmake` lists `.cpp` files for every other Session-A
data struct (`UserData`, `LobbyData`, `LobbyMemberData`, `LeaderboardData`, `AchievementData`,
`AuthTicketData`, `StatData`) but not for the three Input structs, and a repo-wide grep for
`InputActionSetData::`/`InputActionData::`/`InputActionLayerData::` returns zero matches. If a
developer (or an older doc) references `InputActionSetData::Activate(...)`, that call would fail
to link — steer them to `SteamInputAPI` (Foundation) directly instead. There are also no Script
Canvas nodes for Input at all (only Lobby-create variants and two Leaderboard nodeables exist).
This is a real product gap worth flagging to Heathen, not just a documentation gap.

---

## If you're stuck

- Full human-readable article (the "should you even use this" reasoning this file compresses,
  plus links to Valve's own Steam Input and In-Game Action File docs):
  
Input
- This file is generated from that article plus source-verified additions (the article itself has no code examples for any engine); if something here looks wrong, the article and the real source are the fallback truth, not this file's memory. --- Back to the full Steam agent index: https://heathen.group/agent-ref-steam-index/