diff --git a/src/input/gamepad_config.cpp b/src/input/gamepad_config.cpp index 4733cff4b30..88e47e06797 100644 --- a/src/input/gamepad_config.cpp +++ b/src/input/gamepad_config.cpp @@ -30,6 +30,7 @@ #ifndef SERVER_ONLY #include "input/sdl_controller.hpp" +#include #include enum AxisWithDirection : unsigned @@ -65,6 +66,8 @@ GamepadConfig::GamepadConfig( const std::string &name, m_desensitize = false; m_use_force_feedback = true; m_auto_center = 20; + m_force_feedback_strength = 70; + m_force_feedback_invert = false; setDefaultBinds(); } // GamepadConfig @@ -79,6 +82,8 @@ GamepadConfig::GamepadConfig() : DeviceConfig() m_desensitize = false; m_use_force_feedback = true; m_auto_center = 20; + m_force_feedback_strength = 70; + m_force_feedback_invert = false; setDefaultBinds(); } // GamepadConfig @@ -93,6 +98,10 @@ bool GamepadConfig::load(const XMLNode *config) config->get("desensitize", &m_desensitize ); config->get("force-feedback", &m_use_force_feedback); config->get("auto-center", &m_auto_center); + config->get("force-feedback-strength", &m_force_feedback_strength); + config->get("force-feedback-invert", &m_force_feedback_invert); + m_force_feedback_strength = + std::max(0, std::min(100, m_force_feedback_strength)); bool ok = DeviceConfig::load(config); if(getName()=="") @@ -115,7 +124,9 @@ void GamepadConfig::save (std::ofstream& stream) << "\" deadzone=\"" << m_deadzone << "\" desensitize=\"" << m_desensitize << "\" auto-center=\"" << m_auto_center - << "\" force-feedback=\"" << m_use_force_feedback << "\" "; + << "\" force-feedback=\"" << m_use_force_feedback + << "\" force-feedback-strength=\"" << m_force_feedback_strength + << "\" force-feedback-invert=\"" << m_force_feedback_invert << "\" "; DeviceConfig::save(stream); stream << "\n\n"; } // save diff --git a/src/input/gamepad_config.hpp b/src/input/gamepad_config.hpp index d0b992fefab..e9e1121980b 100644 --- a/src/input/gamepad_config.hpp +++ b/src/input/gamepad_config.hpp @@ -62,6 +62,13 @@ class GamepadConfig : public DeviceConfig bool m_use_force_feedback; int m_auto_center; + /** Overall steering force in percent, 0-100. */ + int m_force_feedback_strength; + + /** Flips the steering force direction for wheels whose driver reports + * the axis the other way round. */ + bool m_force_feedback_invert; + std::map, int> m_sdl_mapping; std::set m_digital_axes; @@ -128,6 +135,10 @@ class GamepadConfig : public DeviceConfig // ------------------------------------------------------------------------ int getAutoCenterStrength() const { return m_auto_center; } // ------------------------------------------------------------------------ + int getForceFeedbackStrength() const { return m_force_feedback_strength; } + // ------------------------------------------------------------------------ + bool isForceFeedbackInverted() const { return m_force_feedback_invert; } + // ------------------------------------------------------------------------ void setAutoCenter(bool val) { m_auto_center = val; } }; // class GamepadConfig diff --git a/src/input/input_manager.cpp b/src/input/input_manager.cpp index 2ba106af78f..44400d5e630 100644 --- a/src/input/input_manager.cpp +++ b/src/input/input_manager.cpp @@ -271,7 +271,10 @@ void InputManager::update(float dt) #ifndef SERVER_ONLY for (auto& controller : m_sdl_controller) + { controller.second->checkPowerLevel(); + controller.second->updateForceFeedback(); + } #endif } diff --git a/src/input/sdl_controller.cpp b/src/input/sdl_controller.cpp index 19534d886c5..8b467b9d006 100644 --- a/src/input/sdl_controller.cpp +++ b/src/input/sdl_controller.cpp @@ -21,6 +21,7 @@ #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" @@ -28,10 +29,30 @@ #include "utils/string_utils.hpp" #include "utils/translation.hpp" +#include #include +#include #include #include +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(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); + 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) diff --git a/src/input/sdl_controller.hpp b/src/input/sdl_controller.hpp index dc3fb0c2566..0294d5ece95 100644 --- a/src/input/sdl_controller.hpp +++ b/src/input/sdl_controller.hpp @@ -44,6 +44,40 @@ class SDLController SDL_Haptic* m_haptic; int m_auto_center; + /** True if the device can play SDL's simple rumble effect. Wheels such + * as the Logitech G29 on the mainline Linux driver only support + * constant force, so rumble has to be emulated with it. */ + bool m_native_rumble; + + /** Id of the constant force effect which carries all steering forces, + * -1 if the device has no constant force support. */ + int m_steering_effect; + + bool m_steering_effect_running; + + /** Steering force parameters last requested by the game, see + * setSteeringForce(). */ + float m_steering_spring; + float m_steering_damper; + float m_steering_offset; + + uint64_t m_steering_force_time; + + /** Wheel position at the previous device update, in [-1, 1] with + * positive to the right, to damp fast wheel movement. */ + float m_prev_wheel; + + /** Performance counter value of the previous device update, 0 if the + * effect was not playing then. */ + uint64_t m_prev_ffb_update; + + /** Level last uploaded to the device, to skip redundant uploads. */ + int16_t m_uploaded_level; + + float m_rumble_strength; + + uint64_t m_rumble_end_time; + int m_buttons; int m_axes; @@ -62,6 +96,8 @@ class SDLController #endif void updateAutoCenter(int state); + void stopSteeringEffect(); + bool getSteeringAxis(int* axis, float* sign) const; public: // ------------------------------------------------------------------------ SDLController(int device_id); @@ -162,6 +198,12 @@ class SDLController void checkPowerLevel(); // ------------------------------------------------------------------------ void doRumble(float strength_low, float strength_high, uint32_t duration_ms); + // ------------------------------------------------------------------------ + bool hasSteeringForce() const { return m_steering_effect >= 0; } + // ------------------------------------------------------------------------ + void setSteeringForce(float spring, float damper, float offset); + // ------------------------------------------------------------------------ + void updateForceFeedback(); GamePadDevice* getGamePadDevice() const { return m_gamepad; } }; diff --git a/src/karts/controller/local_player_controller.cpp b/src/karts/controller/local_player_controller.cpp index cc0bda814c3..43ce4de8d67 100644 --- a/src/karts/controller/local_player_controller.cpp +++ b/src/karts/controller/local_player_controller.cpp @@ -26,6 +26,7 @@ #include "graphics/camera/camera.hpp" #include "graphics/camera/camera_normal.hpp" #include "graphics/irr_driver.hpp" +#include "graphics/material.hpp" #include "graphics/particle_emitter.hpp" #include "graphics/particle_kind.hpp" #include "input/input_manager.hpp" @@ -301,6 +302,7 @@ void LocalPlayerController::update(int ticks) m_sound_schedule = false; m_kart->playSound(m_bzzt_sound); } + updateForceFeedback(); } // update //----------------------------------------------------------------------------- @@ -397,7 +399,7 @@ void LocalPlayerController::handleZipper(bool play_sound) if (!GUIEngine::isNoGraphics()) irr_driver->giveBoost(m_camera_index); #endif - + rumble(0.3f, 0.3f, 150); } // handleZipper //----------------------------------------------------------------------------- @@ -489,25 +491,100 @@ void LocalPlayerController::doCrashHaptics() { #endif } -void LocalPlayerController::rumble(float strength_low, float strength_high, uint16_t duration) { +// ---------------------------------------------------------------------------- +/** Returns the gamepad driving this kart, or NULL if there is none or the + * player turned force feedback off for it. + */ +SDLController* LocalPlayerController::getForceFeedbackController() const +{ #ifndef SERVER_ONLY - if (RewindManager::get()->isRewinding()) - return; int count = input_manager->getGamepadCount(); - while(count--) + while (count--) { SDLController* controller = input_manager->getSDLController(count); - if (controller && controller->getGamePadDevice()->getPlayer() == m_player) - { - if (!controller->getGamePadDevice()->useForceFeedback()) - return; - controller->doRumble(strength_low, strength_high, duration); - break; - } + if (!controller || controller->getGamePadDevice()->getPlayer() != m_player) + continue; + if (!controller->getGamePadDevice()->useForceFeedback()) + return NULL; + return controller; } +#endif + return NULL; +} // getForceFeedbackController + +// ---------------------------------------------------------------------------- +void LocalPlayerController::rumble(float strength_low, float strength_high, uint16_t duration) { +#ifndef SERVER_ONLY + if (RewindManager::get()->isRewinding()) + return; + SDLController* controller = getForceFeedbackController(); + if (controller) + controller->doRumble(strength_low, strength_high, duration); #endif } +// ---------------------------------------------------------------------------- +/** Computes the steering force parameters for wheels with constant force + * support. All forces are relative to full lock, positive pushes the wheel + * to the right. The wheel position itself is read by SDLController, so the + * spring works on the same, undelayed value it outputs. + */ +void LocalPlayerController::updateForceFeedback() +{ +#ifndef SERVER_ONLY + if (RewindManager::get()->isRewinding()) + return; + SDLController* controller = getForceFeedbackController(); + if (!controller || !controller->hasSteeringForce()) + return; + + const float max_speed = m_kart->getKartProperties()->getEngineMaxSpeed(); + const float speed = max_speed > 0.0f ? + std::min(1.0f, fabsf(m_kart->getSpeed()) / max_speed) : 0.0f; + + // Tyres only resist the wheel while they touch the ground. + float grip = 1.0f; + if (m_kart->getKartAnimation() || !m_kart->isOnGround()) + grip = 0.0f; + + const Skidding* skidding = m_kart->getSkidding(); + const Skidding::SkidState skid = skidding->getSkidState(); + float drift_pull = 0.0f; + if (skid == Skidding::SKID_ACCUMULATE_LEFT) + drift_pull = 1.0f; + else if (skid == Skidding::SKID_ACCUMULATE_RIGHT) + drift_pull = -1.0f; + // A drifting kart has less front grip, which makes the wheel lighter. + if (drift_pull != 0.0f) + grip *= 0.6f; + + // Even a stationary kart has some weight on the wheel, it gets heavier + // with speed like a real car. + const float spring = 0.15f + 0.55f * speed * grip; + const float damper = 0.02f + 0.06f * speed * grip; + + // The rear stepping out pulls the wheel into countersteer. + float offset = drift_pull * 0.2f * speed * grip; + + // Offroad materials (grass, sand, ...) slow the kart down, use that as a + // measure of how bumpy they are. + const Material* material = m_kart->getMaterial(); + if (grip > 0.0f && material && material->getMaxSpeedFraction() < 1.0f) + { + const float roughness = 1.0f - material->getMaxSpeedFraction(); + const float t = + stk_config->ticks2Time(World::getWorld()->getTicksSinceStart()); + // Two unrelated frequencies feel like an uneven surface rather than + // a motor buzz. + const float bumps = sinf(t * 2.0f * M_PI * 9.0f) + + 0.5f * sinf(t * 2.0f * M_PI * 23.0f); + offset += bumps * roughness * speed * 0.25f; + } + + controller->setSteeringForce(spring, damper, offset); +#endif +} // updateForceFeedback + void LocalPlayerController::crashed(const AbstractKart* k) { doCrashHaptics(); diff --git a/src/karts/controller/local_player_controller.hpp b/src/karts/controller/local_player_controller.hpp index ea346048a46..ffa152cb2de 100644 --- a/src/karts/controller/local_player_controller.hpp +++ b/src/karts/controller/local_player_controller.hpp @@ -28,6 +28,7 @@ class AbstractKart; class SFXBase; class SFXBuffer; class btTransform; +class SDLController; #ifndef SERVER_ONLY class ParticleEmitter; @@ -74,6 +75,8 @@ class LocalPlayerController : public PlayerController void nitroNotFullSound(); void doCrashHaptics(); + void updateForceFeedback(); + SDLController* getForceFeedbackController() const; void setParticleEmitterPosition(const btTransform& t); public: