SvelteKit
GitHub

SteamInput

Project: SteamDeck

Introduction

As a concept, SteamInput is incredibly easy. Instead of requesting an API if the ‘A’ button is pressed, or the left-joystick has been moved, ask for a specified digital or analog action and let Steam bind the control to the action. This allows users to have a consistent interface for binding controls, removing the need to implement one in your game.

Actions can be grouped into action-sets, so that different actions are available at different points. Such as first-person, haggling with NPCs and in-game menus.

The API

The SteamInput API is very easy to use. The SteamWorks example code, SpaceWar, demonstrates it very well.

Initialise Steam:
SteamErrMsg errMsg = { 0 };
SteamAPI_InitEx(&errMsg);

Set the location of the manifest file (if using one):
SteamInput()->SetInputActionManifestFilePath(pzManifestPath);

Register inputs (storing the returned handle somewhere):
InputDigitalActionHandle_t dHandle = SteamInput()->GetDigitalActionHandle("mainfire");
InputAnalogActionHandle_t aHandle = SteamInput()->GetAnalogActionHandle("move");

Register action sets (storing the returned handle somewhere):
InputActionSetHandle_t sHandle = SteamInput()->GetActionSetHandle("InGameControls");

Every frame, get the active controller. This is in case it disconnects or changes:
InputHandle_t pHandles[STEAM_CONTROLLER_MAX_COUNT];
int nNumActive = SteamInput()->GetConnectedControllers(pHandles);
if (nNumActive && (m_hActiveController != pHandles[0])) {
   m_hActiveController = pHandles[0];
}

Every frame, set the active Action Set. Although this does not have to be done every frame, it is low-latency and ensures it is set correctly with no other bugs getting in the way.
SteamInput()->ActivateActionSet(hActiveController, sHandle);

Every frame, process input to determine which actions are active. For each digital handle of interest do something similar to the following:
ControllerDigitalActionData_t digitalData = SteamInput()->GetDigitalActionData(m_hActiveController,dHandle);
if (digitalData.bActive) {
    return digitalData.bState;
}

For each analog handle of interest do something similar to the following:
ControllerAnalogActionData_t analogData = SteamInput()->GetAnalogActionData(m_hActiveController, aHandle);
if (analogData.bActive) {
   *pfX = analogData.x;
   *pfY = analogData.y;
}
else {
   *pfX = 0.0f;
   *pfY = 0.0f;
}

Simple mappings, simple API, simples!

Configuration

The issue comes really from creating an initial configuration. There needs to be:

  • An In Game Actions file, stored in a directory (that you may need to create) on your windows machine.
  • A controller configuration file, based on the In Games Action file, that stores the actions and how the controller is bound to the actions
  • An action manifest file, to bundle configartions with the game itself

These are all VDF file - the important thing to note is that the VDF format, for IGA, manifests and controller files, are not JSON files – though they do look similar. They lack commas for a start. I believe it stands for Valve Data File, for storing key/value pairs.

In Games Action File

The IGA file, game_actions_480.vdf, needs to be stored at C:\Program Files (x86)\Steam\Controller_Config\ (you will probably need to create that directory).

This IGA is fairly simple to create, and an example can be found in the repo' for this project. When deploying to the SteamDeck, it is important that this file makes it into the installation folder. This project has a post-build event to copy all VDF files to the build folder, which includes this IGA file.

”In Game Actions”
{
 ”actions”
 {
 }
 ”localization”
 {
 }
}

None of this is alterable when configuring. SteamInput expects this format with details filled in for actions and localization. The next nested step is the action sets. Here we define an action set called (by the code) "InGameControls" which is the name given when we call:

InputActionSetHandle_t sHandle = SteamInput()->GetActionSetHandle("InGameControls");>

and configured like this:

”In Game Actions”
{
 ”actions”
 {
  ”InGameControls”
  {
   "title"   "#Set_Ingame"
  }
 }
 ”localization”
 {
  "english"
  {
   "Set_Ingame"   "In-Game Controls"
  }
 }
}

Note, the InGameControls is never presented to the user. It is purely used in the configuration and in the game code. The "title" section lets SteamInput know what localization token to use when displaying this action-set to the user. Here we name the action set: "In-Game Controls” – with the "#Set-Ingame" matching the English localization (without the hash/pound sign) in the English localization section.

If we added a French section to the localization, and the users Steam was set to ‘French’, instead of 'In-Game Controls' it would display "Commandes dans le jeu".

Next we will add the digital buttons for fire, jump and pause. These are all displayed in the Steam control binding panel according to what their localization token represents in the current locale:

"In Game Actions"
{
 "actions"
 {
  "InGameControls"
  {
   "title"       "#Set_Ingame"
   "Button"
   {
    "fire"       "#Action_Fire"
    "Jump"       "#Action_Jump"
    "pause_menu" "#Action_Menu"
   }
  }
 }
 "localization"
 {
  "english"
  {
   "Set_Ingame"   "In-Game Controls"
   "Action_Fire"  "Fire Weapon"
   "Action_Jump"  "Jump"
   "Action_Menu"  "Pause Menu"
  }
 }
}

Joysticks are configured in a similar way. Adding a section to the Action Set, and adding localizations to the localization section. Here we add a move and a camera joystick:

"In Game Actions"
{
 "actions"
 {
  "InGameControls"
  {
   "title"       "#Set_Ingame"
   "StickPadGyro"
   {
    "move"
    {
     "title"      "#Action_Move"
     "input_mode" "joystick_move"
    }
    "camera"
    {
     "title"      "#Action_Camera"
     "input_mode" "absolute_mouse"
    }
   }
   "Button"
   {
    "fire"       "#Action_Fire"
    "Jump"       "#Action_Jump"
    "pause_menu" "#Action_Menu"
   }
  }
 }
 "localization"
 {
  "english"
  {
   "Set_Ingame"    "In-Game Controls"
   "Action_Fire"   "Fire Weapon"
   "Action_Jump"   "Jump"
   "Action_Menu"   "Pause Menu"
  }
 }
}

Here, move and camera are configurable by the developer, and are what is passed in to the call to get the analog action handle we saw before:
InputAnalogActionHandle_t aHandle = SteamInput()->GetAnalogActionHandle("move");
and like before, #ActionFire, #Action_Jump and #Action_Menu match the localizations for "Fire Weapon", "Jump" and "Pause Menu"

The input_mode can have the following values

  • joystick_move     provides a constant deflection from a central point.
  • absolute_mouse   provides a general analog action and should always be used for camera input. SteamInput can provide data in this form from Joysticks and trackpads, but cannot provide joystick_move-style data from anything but a joystick.

You can also add analog triggers, which won't be covered here. In addition to this, the IGA file can provide action set layers, which add functionality on top of an action set instead of completely replacing it.

Ensure you have put the IGA file into the correct place (inside your Steam installation directory, as detailed above), and create a Steam shortcut to run as steam -dev, and run it. Enter "big picture mode", go to Settings => System => and enable developer mode. Scroll down to the bottom of the Settings options to the Developer options (you may need to restart Steam first) and enable "Steam Input Layout Dev Mode".

When you run the demo you should be able to access the Steam overlay by pressing shift-tab. Click the manage controller icon (ensuring that no other games are currently managing their controller), click the cog next to ‘Edit Layout’ and select layout details. If there are any errors in the IGA file, they will be displayed here.

Controller Configuration File

Once there are no errors displayed for the IGA you can create a default configuration. Run the demo again, shift-tab into the Steam overlay, and select the manage controller icon – it should have the in-game (in-demo) actions available and a blank configuration. Use the UI to create your configuration, assigning actions to buttons and joysticks.

When you are happy with the configuration, save it privately. Do this by clicking the cog next to ‘Edit Layout’ and select ‘Export Layout’. Ensure ‘New Personal Save’ is selected, and confirm to save it.

The configuration will be saved to the directory inside Steam that was referenced before. If in doubt, the URL of the ‘Layout Details’ screen will tell you where it is. This file should be added to your source folder, and copied to the SteamDeck installation folder when running from there.

Action Manifest File

This file is to bundle default controller configurations when publishing to Steam. My advice is to stay away from it until you have your own app_id and are approaching publication. Using one, I got very strange controller behaviour where, although it was connected and the IGA read, the controller would not work until I had pressed the Steam button. Or sometimes not at all.

The Diamond Demo Input Code

The code we looked about above to call SteamInput obviously needs structure around it. Inside the demo engine code there is an EngineInput class that derives from the interface IEngineInput, which is what the game code uses to register action sets, controller actions, and to see if an action has been triggered.

On initialisation, the EngineInput instance takes a reference to the game codes GameInput instance via a pointer to a IGameInput interface. This reference is then used to request the game to register inputs and action sets, inform it when controllers are disconnected or connected, and request it to process input (every frame).

When registering inputs, the game code calls methods on the IEngineInput interface such as RegisterDigitalAction(), RegisterAnalogAction() and RegisterActionSet(). Note that when registering a digital action, there is a parameter to indicate if the button press event is on press or release.

When processing input, via a call to ProcessInput, the code calls methods on the IEngineInput interface to determine if any digital or analog actions have transpired, passing in values that it used when registering the actions. For instance, if the following was used to register the pause menu action:
pEngineInput->RegisterDigitalAction(eDigitalInput_PauseMenu, "pause_menu", true);
then this will determine if the button has been pressed and released (because true was passed as the last parameter):
if (pEngineInput->IsControllerActionActive(eDigitalInput_PauseMenu)) {…}

The code for this can be found relaxing at its repo'.

Previous articles on this project are Getting started with SteamDeck development and About the Diamond Demo Code.

© P Bentley 2023-2025. Created with svelte