This week I have mostly been looking at controller rumble! A small note on my todo list - "try implemented rumble again" - ended up filling the entire week.
Controller RumbleI've had controller rumble support in the game for ages - but it was only for Xbox controllers, using the XInput API. Then at some point I stopped using the XInput API altogether (SDL's input system provides the same functionality via its Controller feature). I had partially implemented rumble using SDL but never actually got it working.
After fixing my past mistake I then fleshed out my rumble code in various ways. Below I'll discuss some of it.
Actually enabling RumbleFirstly the important thing bit of code which actually made the rumble start working:
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER)
Became:
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC)
I needed to tell SDL to initialise the haptic system. I had totally missed this previously
Unfortunately when trying to use the haptic system without initialising it first, the errors returned from the SDL API are along the lines of "this controller doesn't support haptics" rather than "you didn't initialise the system yet". So it's worth being extra careful when following the documentation about how to use the system, so you don't miss any steps.
SDL Haptics OverviewOnce you have an SDL joystick or controller, you can open the haptic system on it. After opening the system, you then have a choice of either initialising the simple rumble system, or creating a new haptic effect.
The simple rumble system is a super-basic wrapper which you give a 0-1 value, and it'll make the device vibrate in some way. Presumably using this is a good way of ensuring basic support of any device with some kind of rumble support. I only have Xbox controllers to test with. The simple rumble system makes both of the controller's motors vibrate at the same strength.
Using a haptic effect rather than the simple rumble gives you more control over specific features of the device. There are various types of haptic effect available, which you can query whether the device supports before using. I'm guessing most of them are related to things like force feedback steering wheels. The one I care about is SDL_HAPTIC_LEFTRIGHT - this allows you to control the two motors in the Xbox controller separately.
In the Xbox controller one motor is a high frequency rumble (so more like a fine buzz) and the other motor is a low frequency rumble (so more like a heavy shake). Being able to control these separately is really nice, as it offers more nuance into the kind of feedback you give through the controller.
SDL Haptics CodeI have a rumble manager. The game tells the manager when to start a rumble effect, and what the effect is like, etc. I won't go into too much detail right now. But each frame this rumble manager combines all the active rumble effects into two values - the strength of rumble for the light motor, and the strength of rumble for the heavy motor. This data then needs to be applied to the player's controller.
The data looks like this.
struct sRumbleState
{
sRumbleState()
: LightRumble(0.0f)
, HeavyRumble(0.0f)
{
}
float ToSingleFloat() const
{
return Toolbox::highest(LightRumble * 0.33f, HeavyRumble);
}
float LightRumble;
float HeavyRumble;
};
The ToSingleFloat() function provides a way to get a single float value rather than separate LightRumble and HeavyRumble values. This single float value is used if the controller only supports the simple rumble system rather than the SDL_HAPTIC_LEFTRIGHT effect.
But before we can apply these values to the device we need to open the SDL haptic system on it.
Here's a simplified version of my function to open the haptics on the controller.
void cDeviceRumbleHelper::Open(SDL_Joystick *pJoystick)
{
m_pHaptic = SDL_HapticOpenFromJoystick(m_pJoystick);
if (m_pHaptic)
{
if ((SDL_HapticQuery(m_pHaptic) & SDL_HAPTIC_LEFTRIGHT) != 0)
{
// Controller supports SDL_HAPTIC_LEFTRIGHT rumble. Try to initialise it
SDL_memset(&m_HapticEffectParams, 0, sizeof(SDL_HapticEffect));
m_HapticEffectParams.type = SDL_HAPTIC_LEFTRIGHT;
m_HapticEffectParams.leftright.length = SDL_HAPTIC_INFINITY;
m_HapticEffectParams.leftright.large_magnitude = (Uint16)(0.0f * LEFTRIGHT_MAX_VALUE);
m_HapticEffectParams.leftright.small_magnitude = (Uint16)(0.0f * LEFTRIGHT_MAX_VALUE);
m_HapticEffectID = SDL_HapticNewEffect(m_pHaptic, &m_HapticEffectParams);
}
if (m_HapticEffectID < 0)
{
// Initialise simple rumble as a fallback
SDL_HapticRumbleInit(m_pHaptic);
}
}
}
It's easy to query whether the device supports SDL_HAPTIC_LEFTRIGHT, and then create the haptic effect. If SDL_HAPTIC_LEFTRIGHT is not supported, it falls back to initialising the simple rumble system.
And then each frame, I call this function to send the rumble to the device.
void cDeviceRumbleHelper::SetRumble(const sRumbleState &rumbleState)
{
if (m_pHaptic)
{
if (m_HapticEffectID >= 0)
{
// Use the leftright rumble effect to control both motors
if (rumbleState.HeavyRumble > 0.0f || rumbleState.LightRumble > 0.0f)
{
// Update the effect with new values
m_HapticEffectParams.leftright.large_magnitude = (Uint16)(rumbleState.HeavyRumble * LEFTRIGHT_MAX_VALUE);
m_HapticEffectParams.leftright.small_magnitude = (Uint16)(rumbleState.LightRumble * LEFTRIGHT_MAX_VALUE);
SDL_HapticUpdateEffect(m_pHaptic, m_HapticEffectID, &m_HapticEffectParams);
// Play the effect if it's not running
if (!m_HapticEffectRunning)
{
if (SDL_HapticRunEffect(m_pHaptic, m_HapticEffectID, 1) == 0)
{
m_HapticEffectRunning = true;
}
}
}
else
{
// Stop the effect if it's running
if (m_HapticEffectRunning)
{
SDL_HapticStopEffect(m_pHaptic, m_HapticEffectID);
m_HapticEffectRunning = false;
}
}
}
else
{
// Use simple rumble
float strength = rumbleState.ToSingleFloat();
if(strength > 0.0f)
{
SDL_HapticRumblePlay(m_pHaptic, strength, SDL_HAPTIC_INFINITY);
}
else
{
SDL_HapticRumbleStop(m_pHaptic);
}
}
}
}
This function has to be called every frame. I use SDL_HAPTIC_INFINITY to tell the effect/simple rumble to play forever, so the controller will only stop rumbling when SDL_HapticStopEffect / SDL_HapticRumbleStop is called.
Full code for these functions can be found here for those interested:
RumbleState.hDeviceRumbleHelper.hDeviceRumbleHelper.cppI feel it was definitely worth figuring this stuff out, I love rumble when it's done properly! Especially because Xbox controllers are so common on PC, it was a must-implement feature for me to make full use of the high/low frequency rumble motors.
Xbox One ControllerSpeaking of which, this week while working on the rumble system I was able to briefly try out a friend's Xbox One controller. I had never been interested in buying one of these before, I already have three Xbox 360 controllers and at least two other USB PC controllers.
What I was blown away by was how much better the rumble is on the Xbox One controller. It has the same basic setup as the 360 controller - a high and low frequency motor - but they are way more responsive, especially when doing gentle rumble. One of my rumble effects runs the high frequency motor at only 5% strength for 0.075 seconds, and this consistently produces a nice gentle "tap" sort of feeling. If I run the same effect on the 360 controller, basically nothing happens.
So, I bought an Xbox One controller.
I'll be trying to make full use of this fine detail rumble to enhance the feedback of what's going on in the game.
A sidenote, the Xbox One controller also has a thing called Impulse Triggers where the triggers themselves vibrate. But from what I've read online, this can only be used on the Xbox One itself, or through Windows Store apps, so I'm just ignoring this feature.
Rumble test modeFinally, since this post has been gif-free, here's the rumble test mode that I put together to make my testing easier.
It allows me to cycle through all the rumble effects in the game and trigger them. The bars show the current vibration strength of the light (left) and heavy (right) motors.
I hope this has been interesting / useful to you! Thanks for reading