Repository navigation
Add steering force feedback for wheels #5897
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,17 +21,38 @@ | |
| #include "guiengine/message_queue.hpp" | ||
| #include "input/device_config.hpp" | ||
| #include "input/device_manager.hpp" | ||
| #include "input/gamepad_config.hpp" | ||
| #include "input/gamepad_device.hpp" | ||
| #include "input/input_manager.hpp" | ||
| #include "utils/log.hpp" | ||
| #include "utils/time.hpp" | ||
| #include "utils/string_utils.hpp" | ||
| #include "utils/translation.hpp" | ||
|
|
||
| #include <SDL_timer.h> | ||
| #include <SDL_version.h> | ||
| #include <algorithm> | ||
| #include <stdexcept> | ||
| #include <string> | ||
|
|
||
| namespace | ||
| { | ||
| SDL_HapticEffect steeringEffect(Sint16 level) | ||
| { | ||
| SDL_HapticEffect effect = {}; | ||
| effect.type = SDL_HAPTIC_CONSTANT; | ||
| effect.constant.direction.type = SDL_HAPTIC_CARTESIAN; | ||
| // SDL directions tell where the force comes from, so a force from the | ||
| // left makes a positive level push the wheel to the right. | ||
| effect.constant.direction.dir[0] = -1; | ||
| // The effect is stopped explicitly once the game stops feeding it, see | ||
| // SDLController::updateForceFeedback(). | ||
| effect.constant.length = SDL_HAPTIC_INFINITY; | ||
| effect.constant.level = level; | ||
| return effect; | ||
| } // steeringEffect | ||
| } | ||
|
|
||
| // ---------------------------------------------------------------------------- | ||
| SDLController::SDLController(int device_id) | ||
| : m_gamepad(NULL) | ||
|
|
@@ -44,6 +65,18 @@ SDLController::SDLController(int device_id) | |
| m_game_controller = NULL; | ||
| m_joystick = NULL; | ||
| m_haptic = NULL; | ||
| m_native_rumble = false; | ||
| m_steering_effect = -1; | ||
| m_steering_effect_running = false; | ||
| m_steering_spring = 0.0f; | ||
| m_steering_damper = 0.0f; | ||
| m_steering_offset = 0.0f; | ||
| m_steering_force_time = 0; | ||
| m_prev_wheel = 0.0f; | ||
| m_prev_ffb_update = 0; | ||
| m_uploaded_level = 0; | ||
| m_rumble_strength = 0.0f; | ||
| m_rumble_end_time = 0; | ||
| m_id = -1; | ||
|
|
||
| if (SDL_IsGameController(device_id)) | ||
|
|
@@ -201,23 +234,44 @@ SDLController::SDLController(int device_id) | |
|
|
||
| finish: | ||
| m_haptic = SDL_HapticOpenFromJoystick(m_joystick); | ||
| if (m_haptic) | ||
| if (!m_haptic) | ||
| return; | ||
|
|
||
| m_native_rumble = SDL_HapticRumbleSupported(m_haptic) == SDL_TRUE && | ||
| SDL_HapticRumbleInit(m_haptic) == 0; | ||
| updateAutoCenter(getGamePadDevice()->getAutoCenterStrength()); | ||
|
|
||
| if ((SDL_HapticQuery(m_haptic) & SDL_HAPTIC_CONSTANT) == 0) | ||
| return; | ||
|
|
||
| SDL_HapticEffect effect = steeringEffect(0); | ||
| m_steering_effect = SDL_HapticNewEffect(m_haptic, &effect); | ||
| if (m_steering_effect < 0) | ||
| { | ||
| SDL_HapticRumbleInit(m_haptic); | ||
| updateAutoCenter(getGamePadDevice()->getAutoCenterStrength()); | ||
| Log::warn("SDLController", "%s: cannot create constant force: %s", | ||
| name.c_str(), SDL_GetError()); | ||
| return; | ||
| } | ||
| Log::info("SDLController", "%s supports steering force feedback.", | ||
| name.c_str()); | ||
| } // SDLController | ||
|
|
||
| // ---------------------------------------------------------------------------- | ||
| SDLController::~SDLController() | ||
| { | ||
| Log::info("SDLController", "%s unplugged.", SDL_JoystickName(m_joystick)); | ||
| // Closing the joystick can free the haptic device on some backends. | ||
| if (m_haptic) | ||
| { | ||
| stopSteeringEffect(); | ||
| if (m_steering_effect >= 0) | ||
| SDL_HapticDestroyEffect(m_haptic, m_steering_effect); | ||
| SDL_HapticClose(m_haptic); | ||
| } | ||
| if (m_game_controller) | ||
| SDL_GameControllerClose(m_game_controller); | ||
| else | ||
| SDL_JoystickClose(m_joystick); | ||
| if (m_haptic) | ||
| SDL_HapticClose(m_haptic); | ||
| m_gamepad->getConfiguration()->unPlugged(); | ||
| m_gamepad->setIrrIndex(-1); | ||
| m_gamepad->setConnected(false); | ||
|
|
@@ -261,26 +315,168 @@ void SDLController::checkPowerLevel() | |
|
|
||
| void SDLController::doRumble(float strength_low, float strength_high, uint32_t duration_ms) | ||
| { | ||
| if (m_haptic) | ||
| float strength = std::min(1.0f, (strength_low + strength_high) / 2); | ||
| if (m_native_rumble) | ||
| { | ||
| SDL_HapticRumblePlay(m_haptic, (strength_low + strength_high) / 2, duration_ms); | ||
| SDL_HapticRumblePlay(m_haptic, strength, duration_ms); | ||
| return; | ||
| } | ||
| else | ||
| if (m_steering_effect >= 0) | ||
| { | ||
| m_rumble_strength = strength; | ||
| m_rumble_end_time = StkTime::getMonoTimeMs() + duration_ms; | ||
| return; | ||
| } | ||
| #if SDL_VERSION_ATLEAST(2,0,9) | ||
| uint16_t scaled_low = strength_low * pow(2, 16); | ||
| uint16_t scaled_high = strength_high * pow(2, 16); | ||
| SDL_GameControllerRumble(m_game_controller, scaled_low, scaled_high, duration_ms); | ||
| if (!m_game_controller) | ||
| return; | ||
| uint16_t scaled_low = std::min(1.0f, strength_low) * 65535; | ||
| uint16_t scaled_high = std::min(1.0f, strength_high) * 65535; | ||
| SDL_GameControllerRumble(m_game_controller, scaled_low, scaled_high, duration_ms); | ||
| #endif | ||
| } | ||
| } | ||
|
|
||
| // ---------------------------------------------------------------------------- | ||
| void SDLController::updateAutoCenter(int state) | ||
| { | ||
| m_auto_center = state; | ||
| SDL_HapticSetAutocenter(m_haptic, m_auto_center); | ||
| } | ||
|
|
||
| // ---------------------------------------------------------------------------- | ||
| /** Sets the steering force as F = offset - spring * wheel - damper * speed, | ||
| * with the wheel position in [-1, 1] and its speed in full locks per second, | ||
| * where positive pushes the wheel to the right. It has to be called | ||
| * continuously, the force fades out shortly after the game stops calling it | ||
| * (pause, race end, menus). | ||
| */ | ||
| void SDLController::setSteeringForce(float spring, float damper, float offset) | ||
| { | ||
| m_steering_spring = spring; | ||
| m_steering_damper = damper; | ||
| m_steering_offset = offset; | ||
| m_steering_force_time = StkTime::getMonoTimeMs(); | ||
| } // setSteeringForce | ||
|
|
||
| // ---------------------------------------------------------------------------- | ||
| /** Finds the axis bound to steering. The sign converts the raw axis value to | ||
| * game steering, where positive is right. | ||
| */ | ||
| bool SDLController::getSteeringAxis(int* axis, float* sign) const | ||
| { | ||
| const DeviceConfig* cfg = m_gamepad->getConfiguration(); | ||
| const Binding& right = cfg->getBinding(PA_STEER_RIGHT); | ||
| if (right.getType() == Input::IT_STICKMOTION && right.getId() < m_axes) | ||
| { | ||
| *axis = right.getId(); | ||
| *sign = right.getDirection() == Input::AD_POSITIVE ? 1.0f : -1.0f; | ||
| return true; | ||
| } | ||
| const Binding& left = cfg->getBinding(PA_STEER_LEFT); | ||
| if (left.getType() == Input::IT_STICKMOTION && left.getId() < m_axes) | ||
| { | ||
| *axis = left.getId(); | ||
| *sign = left.getDirection() == Input::AD_POSITIVE ? -1.0f : 1.0f; | ||
| return true; | ||
| } | ||
| return false; | ||
| } // getSteeringAxis | ||
|
|
||
| // ---------------------------------------------------------------------------- | ||
| /** Sends the current steering force and emulated rumble to the device. The | ||
| * wheel position is read here instead of from the game input, so it is free | ||
| * of the gamepad deadzone and the wheel speed uses the real frame time | ||
| * instead of the physics step. | ||
| */ | ||
| void SDLController::updateForceFeedback() | ||
| { | ||
| if (m_steering_effect < 0) | ||
| return; | ||
|
|
||
| const uint64_t now = StkTime::getMonoTimeMs(); | ||
| const bool steering = now < m_steering_force_time + 100; | ||
| const bool rumbling = now < m_rumble_end_time; | ||
| if (!steering && !rumbling) | ||
| { | ||
| stopSteeringEffect(); | ||
| return; | ||
| } | ||
|
|
||
| // On Linux every upload becomes a USB report (ff-memless replays the | ||
| // effect), uncapped frame rates would overflow the usbhid output queue. | ||
| const uint64_t counter = SDL_GetPerformanceCounter(); | ||
| const uint64_t frequency = SDL_GetPerformanceFrequency(); | ||
| if (m_prev_ffb_update != 0 && | ||
| (counter - m_prev_ffb_update) * 400 < frequency) | ||
| return; | ||
| const float dt = m_prev_ffb_update == 0 ? 0.0f : | ||
| float(counter - m_prev_ffb_update) / frequency; | ||
| m_prev_ffb_update = counter; | ||
|
|
||
| int axis = 0; | ||
| float axis_sign = 1.0f; | ||
| const bool has_axis = getSteeringAxis(&axis, &axis_sign); | ||
| float wheel = 0.0f; | ||
| float wheel_speed = 0.0f; | ||
| if (has_axis) | ||
| { | ||
| wheel = axis_sign * SDL_JoystickGetAxis(m_joystick, axis) / 32767.0f; | ||
| if (dt > 0.0f) | ||
| wheel_speed = (wheel - m_prev_wheel) / dt; | ||
| wheel_speed = std::max(-5.0f, std::min(5.0f, wheel_speed)); | ||
| m_prev_wheel = wheel; | ||
| } | ||
|
|
||
| float force = 0.0f; | ||
| if (steering) | ||
| { | ||
| force = m_steering_offset - m_steering_spring * wheel - | ||
| m_steering_damper * wheel_speed; | ||
| } | ||
| if (rumbling) | ||
| { | ||
| // Alternate the push direction quickly, the wheel motor can't do a | ||
| // real vibration. 50 ms per half wave stays readable at 60 fps. | ||
| force += ((now / 50) % 2 == 0 ? 1.0f : -1.0f) * m_rumble_strength * 0.6f; | ||
| } | ||
|
|
||
| // The effect pushes towards the positive end of the axis, which is left | ||
| // in the game if the player bound the axis the other way round. | ||
| force *= axis_sign; | ||
| GamepadConfig* cfg = | ||
| static_cast<GamepadConfig*>(m_gamepad->getConfiguration()); | ||
| force *= cfg->getForceFeedbackStrength() / 100.0f; | ||
| if (cfg->isForceFeedbackInverted()) | ||
| force = -force; | ||
| force = std::max(-1.0f, std::min(1.0f, force)); | ||
|
|
||
| const Sint16 level = (Sint16)(force * 32767); | ||
| if (m_steering_effect_running && level == m_uploaded_level) | ||
| return; | ||
| SDL_HapticEffect effect = steeringEffect(level); | ||
| SDL_HapticUpdateEffect(m_haptic, m_steering_effect, &effect); | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On Linux, |
||
| m_uploaded_level = level; | ||
|
|
||
| if (m_steering_effect_running) | ||
| return; | ||
| // The game provides its own centering spring while driving, the device | ||
| // autocenter on top of it would make the wheel feel stiff and dead. | ||
| updateAutoCenter(0); | ||
| SDL_HapticRunEffect(m_haptic, m_steering_effect, 1); | ||
| m_steering_effect_running = true; | ||
| } // updateForceFeedback | ||
|
|
||
| // ---------------------------------------------------------------------------- | ||
| void SDLController::stopSteeringEffect() | ||
| { | ||
| if (!m_steering_effect_running) | ||
| return; | ||
| SDL_HapticStopEffect(m_haptic, m_steering_effect); | ||
| m_steering_effect_running = false; | ||
| m_prev_ffb_update = 0; | ||
| updateAutoCenter(m_gamepad->getAutoCenterStrength()); | ||
| } // stopSteeringEffect | ||
|
|
||
| // ---------------------------------------------------------------------------- | ||
| #ifdef ANDROID | ||
| void SDLController::handleDirectScanCode(const SDL_Event& event) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This now stops and destroys the effect and sets autocenter after the joystick has already been closed above.
SDL_HapticOpenFromJoystick()docs say to close the haptic device before the joystick, because on some backends closing the joystick frees the haptic device too. Move this block above the joystick close.