r/GyroGaming • u/AL2009man • 19h ago
Guide Implementing Gyro Input to your game via Steam Input - Basic Tutorial/Guide
If you already implemented Gyro Controls to your video game, but have received user feedback, you might receive threads from Steam forums regarding Gyro Aiming support on either Steam Controller, Steam Deck or Nintendo Controllers and how they want to use their new Steam Controller [2nd generation] on a game that has native gyro aiming....but the game can't support it.
As the vast majority of commercially-shipped games on PC tend to use the Controller API provided by the manufacturer, they shrew into PlayStation controllers. Currently, the best way to evade this problem is by looking at the supported Input API or Plugins that enable wider Controller Support with advanced features...but not everyone can do that.
However: if you're shipping the game on Steam, you're likely to use Steamworks API to integrate online functionality, user identifications, achievements, etc. One of them is ISteamInput interface, which is part of Steam Input API suite. If you're not familiar with Steam Input API: it's a Game Action-centric Input API that primarily focuses on Game Actions over Button Inputs, but we can use Steam Input's Gamepad Emulation system (or Steam Virtual Gamepad as SDL Gamepad refers it) as an extension to your pre-existing Gamepad implementation. Let's say, glyph detection, lightbar and of course: Motion Sensors..
for the purpose of this guide: we're going to be bridging SteamInput's GetMotionData into your game's existing motion sensor implementation or using it as your main form of handling Motion Sensor support.
Quick Note: if your game or game engine is using SDL Gamepad API, starting in SDL 3.2.0: make sure you use
SDL_GetGamepadSteamHandle!
Before we begin, here's a HUGE DISCLAIMER so we can get this out of the way. the example code were code-vibed based on HastyControls' SteamInput codebase (thankfully, I got the original author to review the example code!). I would recommend reading thru HastyControls' ControllerManager.cs and SteamInputManager.cs instead; this will give you a much better idea on the ACTUAL SteamInput Gyro implementation than what AI Code slop will provide, and i will advise against using the example codes provided.
Going forward: We'll assume you already implemented Motion Sensors to your game and want to extend Controller Support onto Steam Input for wider controller support. Which is why we're going to create a separate Input Manager that specifically handles ISteamInput, but first things first: we'll need to read the ISteamInput documents first.
after that, we'll need to initialize SteamInput and then call the functions, scale the Gyro/Accel values and the IMU Axis orientation accordingly, as it'll be different from your native gyro path. we'll need to ensure that the scaling factor remains identical between Native and SteamInput path's.
// SteamInputManager.h
#pragma once
#include <cstdint>
struct SteamMotionData {
float gyroX, gyroY, gyroZ; // rad/s
float accelX, accelY, accelZ; // m/s²
};
class SteamInputManager {
public:
bool Init();
void Shutdown();
void RunFrame();
bool IsAvailable() const { return m_initialized; }
bool TryGetMotionData(uint64_t steamHandle, SteamMotionData& out) const;
private:
bool m_initialized = false;
};
--
// SteamInputManager.cpp
#include "SteamInputManager.h"
#include <steam/steam_api.h>
#include <cmath>
// rotVel* : interpolated float in [-32768, 32767], clamped to ±2000 deg/s → convert to rad/s
// posAccel*: interpolated float in [-32768, 32767], clamped to ±2G (9.80665 m/s²) → convert to m/s²
static constexpr float k_gyro_scale = 2000.0f * (M_PI / 180.0f) / 32768.0f;
static constexpr float k_accel_scale = 2.0f * 9.80665f / 32768.0f;
bool SteamInputManager::Init() {
if (!SteamAPI_IsSteamRunning()) return false;
m_initialized = SteamInput()->Init();
return m_initialized;
}
void SteamInputManager::Shutdown() {
if (m_initialized) SteamInput()->Shutdown();
m_initialized = false;
}
void SteamInputManager::RunFrame() {
if (m_initialized) SteamInput()->RunFrame();
}
bool SteamInputManager::TryGetMotionData(uint64_t steamHandle, SteamMotionData& out) const {
if (!m_initialized || steamHandle == 0) return false;
InputMotionData_t d = SteamInput()->GetMotionData(
static_cast<InputHandle_t>(steamHandle));
// Axis swizzle: Steam Input's coordinate system doesn't match SDL's
out.gyroX = d.rotVelX * k_gyro_scale;
out.gyroY = d.rotVelZ * k_gyro_scale;
out.gyroZ = d.rotVelY * k_gyro_scale;
out.accelX = d.posAccelX * k_accel_scale;
out.accelY = d.posAccelZ * k_accel_scale;
out.accelZ = -d.posAccelY * k_accel_scale;
return true;
}
Note: if the game isn't using Steamworks Callsbacks already, make sure you also call
RunFrameevery frame to actually get any input data.
Now that you wrote (and I assume you actually wrote and read the live codebase and not this example code!) SteamInput manager, you can now add it to your existing gyro pipeline:
void ControllerManager::Update(float deltaTime) {
m_steamInput.RunFrame();
for (auto& [gamepad, gyroState] : m_gyroStates) {
uint64_t steamHandle = SDL_GetGamepadSteamHandle(gamepad);
SteamMotionData motion;
if (m_steamInput.TryGetMotionData(steamHandle, motion)) {
// Steam Input path
gyroState.AddGyroSample(motion.gyroX, motion.gyroY, motion.gyroZ);
gyroState.AddAccelSample(motion.accelX, motion.accelY, motion.accelZ);
} else {
// SDL sensor path
// existing native gyro path be handled via gyroState
}
gyroState.Update(deltaTime);
}
}
Once you've got everything down, grab your game controller, forcefully enable Steam Input and launch the game and test the game's IMU systems and determine if Steam Input is able to detect Gyro. Please ensure that you set the Gyro Behavior to None or use the Gamepad Template to prevent any conflicts, as SteamInput's raw gyro/accel is now being used directly.
If your Steam Input-supported controller is able to correctly detect the gyro directly, then you have successfully integrated SteamInput interface onto your existing Controller support. Not to mention, you can extend your SteamInput Manager.
Important side-notes during testing:
before you can celebrate: there's two things you'd now need to account for:
- Calibration system: similar to Sony's PlayStation Input API; the entire gyro calibration system is being handled directly thru Steam Input on a system-wide level. If your game or game engines has a built-in imu calibration system (example: SDL Gamepad API does not have system-level IMU calibration system, requiring users to either create or use a gyro-centric header), i highly urge you to exclude SteamInput from your gyro calibration process entirely.
- Axis Orientation: This is already noted in ISteamInput documents, so i'll say it too: Implementing SteamInput Gyro to your game expands and futureproofs your game's controller support, this will include PC Handheld devices such as Steam Deck, Lenovo Legion GO, ROG Rog Ally, etc. To account for handhelds where the controller input is attached to the screen: you can either leverage
ESteamInputTypeand swap the gyro axis specifically for the handheld path, or implement Gyro Space system. - Gyro Ratcheting: since we're leveraging SteamInput Gyro directly, we can't directly use Touch Activation for this one. Please keep that in mind while adding SteamInput Gyro support. (Instead, we'll ask you to implement the Game Action-centric Steam Input API, and add a Mouse-like Camera action, as SteamInput has a dedicated Gyro Activation system that you can leverage.
That'll be all for this tutorial!