From c3dc3c106ccb32911a4a6448f7b21c8beaae0c35 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Sat, 29 Dec 2018 16:06:03 -0600 Subject: [PATCH] ControllerEmu: Reorganize stick reshaping code and use it for emu wiimote tilt as well. Also make the tilt mapping indicator pretty. --- .../Config/Mapping/MappingIndicator.cpp | 11 +-- .../Config/Mapping/MappingIndicator.h | 4 +- .../ControlGroup/AnalogStick.cpp | 79 ++-------------- .../ControllerEmu/ControlGroup/AnalogStick.h | 24 +---- .../ControllerEmu/ControlGroup/Tilt.cpp | 92 ++++++++----------- .../ControllerEmu/ControlGroup/Tilt.h | 18 ++-- .../InputCommon/ControllerEmu/StickGate.cpp | 88 ++++++++++++++++++ .../InputCommon/ControllerEmu/StickGate.h | 37 ++++++++ 8 files changed, 189 insertions(+), 164 deletions(-) diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp index 18ed99fb58..c2ae2e6135 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp @@ -14,7 +14,6 @@ #include "InputCommon/ControlReference/ControlReference.h" #include "InputCommon/ControllerEmu/Control/Control.h" -#include "InputCommon/ControllerEmu/ControlGroup/AnalogStick.h" #include "InputCommon/ControllerEmu/ControlGroup/MixedTriggers.h" #include "InputCommon/ControllerEmu/Setting/NumericSetting.h" #include "InputCommon/ControllerInterface/Device.h" @@ -164,15 +163,13 @@ QPolygonF GetPolygonFromRadiusGetter(F&& radius_getter, double scale) return shape; } -void MappingIndicator::DrawStick() +void MappingIndicator::DrawReshapableInput(ControllerEmu::ReshapableInput& stick) { // Make the c-stick yellow: const bool is_c_stick = m_group->name == "C-Stick"; const QColor gate_brush_color = is_c_stick ? Qt::yellow : Qt::lightGray; const QColor gate_pen_color = gate_brush_color.darker(125); - auto& stick = *static_cast(m_group); - // TODO: This SetControllerStateNeeded interface leaks input into the game // We should probably hold the mutex for UI updates. Settings::Instance().SetControllerStateNeeded(true); @@ -334,11 +331,9 @@ void MappingIndicator::paintEvent(QPaintEvent*) case ControllerEmu::GroupType::Cursor: DrawCursor(false); break; - case ControllerEmu::GroupType::Tilt: - DrawCursor(true); - break; case ControllerEmu::GroupType::Stick: - DrawStick(); + case ControllerEmu::GroupType::Tilt: + DrawReshapableInput(*static_cast(m_group)); break; case ControllerEmu::GroupType::MixedTriggers: DrawMixedTriggers(); diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h index f4533e0403..9fcb4296df 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h +++ b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h @@ -6,6 +6,8 @@ #include +#include "InputCommon/ControllerEmu/StickGate.h" + namespace ControllerEmu { class Control; @@ -27,7 +29,7 @@ private: void BindCursorControls(bool tilt); void DrawCursor(bool tilt); - void DrawStick(); + void DrawReshapableInput(ControllerEmu::ReshapableInput& stick); void DrawMixedTriggers(); void paintEvent(QPaintEvent*) override; diff --git a/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.cpp b/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.cpp index 9db29909fd..e072f6b7bd 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.cpp +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.cpp @@ -24,22 +24,17 @@ AnalogStick::AnalogStick(const char* const name_, std::unique_ptr&& s AnalogStick::AnalogStick(const char* const name_, const char* const ui_name_, std::unique_ptr&& stick_gate) - : ControlGroup(name_, ui_name_, GroupType::Stick), m_stick_gate(std::move(stick_gate)) + : ReshapableInput(name_, ui_name_, GroupType::Stick), m_stick_gate(std::move(stick_gate)) { for (auto& named_direction : named_directions) controls.emplace_back(std::make_unique(Translate, named_direction)); controls.emplace_back(std::make_unique(Translate, _trans("Modifier"))); - // Set default input radius to that of the gate radius (no resizing) - // Allow radius greater than 1.0 for definitions of rounded squares - // This is ideal for Xbox controllers (and probably others) - numeric_settings.emplace_back( - std::make_unique(_trans("Input Radius"), GetGateRadiusAtAngle(0.0), 0, 140)); - // Set default input shape to an octagon (no reshaping) - numeric_settings.emplace_back( - std::make_unique(_trans("Input Shape"), 0.0, 0, 50)); - numeric_settings.emplace_back(std::make_unique(_trans("Dead Zone"), 0, 0, 50)); + // Default input radius to that of the gate radius (no resizing) + // Default input shape to an octagon (no reshaping) + // Max deadzone to 50% + AddReshapingSettings(GetGateRadiusAtAngle(0.0), 0.0, 50); } AnalogStick::StateData AnalogStick::GetState(bool adjusted) @@ -51,40 +46,9 @@ AnalogStick::StateData AnalogStick::GetState(bool adjusted) if (!adjusted) return {x, y}; - // TODO: make the AtAngle functions work with negative angles: - const ControlState ang = std::atan2(y, x) + MathUtil::TAU; - - const ControlState gate_max_dist = GetGateRadiusAtAngle(ang); - const ControlState input_max_dist = GetInputRadiusAtAngle(ang); - - // If input radius is zero we apply no scaling. - // This is useful when mapping native controllers without knowing intimate radius details. - const ControlState max_dist = input_max_dist ? input_max_dist : gate_max_dist; - - ControlState dist = std::sqrt(x * x + y * y) / max_dist; - - // If the modifier is pressed, scale the distance by the modifier's value. - // This is affected by the modifier's "range" setting which defaults to 50%. const ControlState modifier = controls[4]->control_ref->State(); - if (modifier) - { - // TODO: Modifier's range setting gets reset to 100% when the clear button is clicked. - // This causes the modifier to not behave how a user might suspect. - // Retaining the old scale-by-50% behavior until range is fixed to clear to 50%. - dist *= 0.5; - // dist *= modifier; - } - // Apply deadzone as a percentage of the user-defined radius/shape: - const ControlState deadzone = GetDeadzoneRadiusAtAngle(ang); - dist = std::max(0.0, dist - deadzone) / (1.0 - deadzone); - - // Scale to the gate shape/radius: - dist = dist *= gate_max_dist; - - x = MathUtil::Clamp(std::cos(ang) * dist, -1.0, 1.0); - y = MathUtil::Clamp(std::sin(ang) * dist, -1.0, 1.0); - return {x, y}; + return Reshape(x, y, modifier); } ControlState AnalogStick::GetGateRadiusAtAngle(double ang) const @@ -92,37 +56,6 @@ ControlState AnalogStick::GetGateRadiusAtAngle(double ang) const return m_stick_gate->GetRadiusAtAngle(ang); } -ControlState AnalogStick::GetDeadzoneRadiusAtAngle(double ang) const -{ - return CalculateInputShapeRadiusAtAngle(ang) * numeric_settings[SETTING_DEADZONE]->GetValue(); -} - -ControlState AnalogStick::GetInputRadiusAtAngle(double ang) const -{ - const ControlState radius = - CalculateInputShapeRadiusAtAngle(ang) * numeric_settings[SETTING_INPUT_RADIUS]->GetValue(); - // Clamp within the -1 to +1 square as input radius may be greater than 1.0: - return std::min(radius, SquareStickGate(1).GetRadiusAtAngle(ang)); -} - -ControlState AnalogStick::CalculateInputShapeRadiusAtAngle(double ang) const -{ - const auto shape = numeric_settings[SETTING_INPUT_SHAPE]->GetValue() * 4.0; - - if (shape < 1.0) - { - // Between 0 and 25 return a shape between octagon and circle - const auto amt = shape; - return OctagonStickGate(1).GetRadiusAtAngle(ang) * (1 - amt) + amt; - } - else - { - // Between 25 and 50 return a shape between circle and square - const auto amt = shape - 1.0; - return (1 - amt) + SquareStickGate(1).GetRadiusAtAngle(ang) * amt; - } -} - OctagonAnalogStick::OctagonAnalogStick(const char* name, ControlState gate_radius) : OctagonAnalogStick(name, name, gate_radius) { diff --git a/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.h b/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.h index 2351189f74..25de898331 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.h +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.h @@ -10,35 +10,17 @@ namespace ControllerEmu { -class AnalogStick : public ControlGroup +class AnalogStick : public ReshapableInput { public: - enum - { - SETTING_INPUT_RADIUS, - SETTING_INPUT_SHAPE, - SETTING_DEADZONE, - }; - - struct StateData - { - ControlState x{}; - ControlState y{}; - }; - AnalogStick(const char* name, std::unique_ptr&& stick_gate); AnalogStick(const char* name, const char* ui_name, std::unique_ptr&& stick_gate); - StateData GetState(bool adjusted = true); + StateData GetState(bool adjusted = true) override; - // Angle is in radians and should be non-negative - ControlState GetGateRadiusAtAngle(double ang) const; - ControlState GetDeadzoneRadiusAtAngle(double ang) const; - ControlState GetInputRadiusAtAngle(double ang) const; + ControlState GetGateRadiusAtAngle(double ang) const override; private: - ControlState CalculateInputShapeRadiusAtAngle(double ang) const; - std::unique_ptr m_stick_gate; }; diff --git a/Source/Core/InputCommon/ControllerEmu/ControlGroup/Tilt.cpp b/Source/Core/InputCommon/ControllerEmu/ControlGroup/Tilt.cpp index 9221b90243..b00ae5a4c9 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/Tilt.cpp +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/Tilt.cpp @@ -17,7 +17,8 @@ namespace ControllerEmu { -Tilt::Tilt(const std::string& name_) : ControlGroup(name_, GroupType::Tilt) +Tilt::Tilt(const std::string& name_) + : ReshapableInput(name_, name_, GroupType::Tilt), m_last_update(Clock::now()) { controls.emplace_back(std::make_unique(Translate, _trans("Forward"))); controls.emplace_back(std::make_unique(Translate, _trans("Backward"))); @@ -26,71 +27,52 @@ Tilt::Tilt(const std::string& name_) : ControlGroup(name_, GroupType::Tilt) controls.emplace_back(std::make_unique(Translate, _trans("Modifier"))); - numeric_settings.emplace_back(std::make_unique(_trans("Dead Zone"), 0, 0, 50)); - numeric_settings.emplace_back(std::make_unique(_trans("Circle Stick"), 0)); + // Set default input radius to the full 1.0 (no resizing) + // Set default input shape to a square (no reshaping) + // Max deadzone to 50% + AddReshapingSettings(1.0, 0.5, 50); + numeric_settings.emplace_back(std::make_unique(_trans("Angle"), 0.9, 0, 180)); } -Tilt::StateData Tilt::GetState(const bool step) +Tilt::StateData Tilt::GetState(bool adjusted) { - // this is all a mess + ControlState y = controls[0]->control_ref->State() - controls[1]->control_ref->State(); + ControlState x = controls[3]->control_ref->State() - controls[2]->control_ref->State(); - ControlState yy = controls[0]->control_ref->State() - controls[1]->control_ref->State(); - ControlState xx = controls[3]->control_ref->State() - controls[2]->control_ref->State(); + // Return raw values. (used in UI) + if (!adjusted) + return {x, y}; - ControlState deadzone = numeric_settings[0]->GetValue(); - ControlState circle = numeric_settings[1]->GetValue(); - auto const angle = numeric_settings[2]->GetValue() / 1.8; - ControlState m = controls[4]->control_ref->State(); + const ControlState modifier = controls[4]->control_ref->State(); - // deadzone / circle stick code - // this section might be all wrong, but its working good enough, I think + // Compute desired tilt: + StateData target = Reshape(x, y, modifier); - ControlState ang = atan2(yy, xx); - ControlState ang_sin = sin(ang); - ControlState ang_cos = cos(ang); + // Step the simulation. This is pretty ugly being here. + const auto now = Clock::now(); + const auto ms_since_update = + std::chrono::duration_cast(now - m_last_update).count(); + m_last_update = now; - // the amt a full square stick would have at current angle - ControlState square_full = - std::min(ang_sin ? 1 / fabs(ang_sin) : 2, ang_cos ? 1 / fabs(ang_cos) : 2); + constexpr int MAX_DEG_PER_SEC = 360 * 2; + const double MAX_STEP = MAX_DEG_PER_SEC / 180.0 * ms_since_update / 1000; - // the amt a full stick would have that was (user setting circular) at current angle - // I think this is more like a pointed circle rather than a rounded square like it should be - ControlState stick_full = (square_full * (1 - circle)) + (circle); + // TODO: Allow wrap around from 1.0 to -1.0 + // (take the fastest route to target) - ControlState dist = sqrt(xx * xx + yy * yy); + const double diff_x = (target.x - m_tilt.x); + m_tilt.x += std::min(MAX_STEP, std::abs(diff_x)) * ((diff_x < 0) ? -1 : 1); + const double diff_y = (target.y - m_tilt.y); + m_tilt.y += std::min(MAX_STEP, std::abs(diff_y)) * ((diff_y < 0) ? -1 : 1); - // dead zone code - dist = std::max(0.0, dist - deadzone * stick_full); - dist /= (1 - deadzone); - - // circle stick code - ControlState amt = dist / stick_full; - dist += (square_full - 1) * amt * circle; - - if (m) - dist *= 0.5; - - yy = std::max(-1.0, std::min(1.0, ang_sin * dist)); - xx = std::max(-1.0, std::min(1.0, ang_cos * dist)); - - // this is kinda silly here - // gui being open will make this happen 2x as fast, o well - - // silly - if (step) - { - if (xx > m_tilt.x) - m_tilt.x = std::min(m_tilt.x + 0.1, xx); - else if (xx < m_tilt.x) - m_tilt.x = std::max(m_tilt.x - 0.1, xx); - - if (yy > m_tilt.y) - m_tilt.y = std::min(m_tilt.y + 0.1, yy); - else if (yy < m_tilt.y) - m_tilt.y = std::max(m_tilt.y - 0.1, yy); - } - - return {m_tilt.x * angle, m_tilt.y * angle}; + return m_tilt; } + +ControlState Tilt::GetGateRadiusAtAngle(double ang) const +{ + const ControlState max_tilt_angle = numeric_settings[SETTING_MAX_ANGLE]->GetValue() / 1.8; + return SquareStickGate(max_tilt_angle).GetRadiusAtAngle(ang); +} + } // namespace ControllerEmu diff --git a/Source/Core/InputCommon/ControllerEmu/ControlGroup/Tilt.h b/Source/Core/InputCommon/ControllerEmu/ControlGroup/Tilt.h index b03dc5a8b1..9515ff2c2a 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/Tilt.h +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/Tilt.h @@ -4,26 +4,32 @@ #pragma once +#include #include -#include "InputCommon/ControllerEmu/ControlGroup/ControlGroup.h" + +#include "InputCommon/ControllerEmu/StickGate.h" #include "InputCommon/ControllerInterface/Device.h" namespace ControllerEmu { -class Tilt : public ControlGroup +class Tilt : public ReshapableInput { public: - struct StateData + enum { - ControlState x{}; - ControlState y{}; + SETTING_MAX_ANGLE = ReshapableInput::SETTING_COUNT, }; explicit Tilt(const std::string& name); - StateData GetState(bool step = true); + StateData GetState(bool adjusted = true); + + ControlState GetGateRadiusAtAngle(double ang) const override; private: + typedef std::chrono::steady_clock Clock; + StateData m_tilt; + Clock::time_point m_last_update; }; } // namespace ControllerEmu diff --git a/Source/Core/InputCommon/ControllerEmu/StickGate.cpp b/Source/Core/InputCommon/ControllerEmu/StickGate.cpp index c0c2a668a3..21d05184c2 100644 --- a/Source/Core/InputCommon/ControllerEmu/StickGate.cpp +++ b/Source/Core/InputCommon/ControllerEmu/StickGate.cpp @@ -6,7 +6,9 @@ #include +#include "Common/Common.h" #include "Common/MathUtil.h" +#include "InputCommon/ControllerEmu/Setting/NumericSetting.h" namespace ControllerEmu { @@ -44,4 +46,90 @@ ControlState SquareStickGate::GetRadiusAtAngle(double ang) const return m_half_width / std::cos(std::fmod(ang + section_ang / 2, section_ang) - section_ang / 2); } +ReshapableInput::ReshapableInput(std::string name, std::string ui_name, GroupType type) + : ControlGroup(std::move(name), std::move(ui_name), type) +{ +} + +ControlState ReshapableInput::GetDeadzoneRadiusAtAngle(double ang) const +{ + return CalculateInputShapeRadiusAtAngle(ang) * numeric_settings[SETTING_DEADZONE]->GetValue(); +} + +ControlState ReshapableInput::GetInputRadiusAtAngle(double ang) const +{ + const ControlState radius = + CalculateInputShapeRadiusAtAngle(ang) * numeric_settings[SETTING_INPUT_RADIUS]->GetValue(); + // Clamp within the -1 to +1 square as input radius may be greater than 1.0: + return std::min(radius, SquareStickGate(1).GetRadiusAtAngle(ang)); +} + +void ReshapableInput::AddReshapingSettings(ControlState default_radius, ControlState default_shape, + int max_deadzone) +{ + // Allow radius greater than 1.0 for definitions of rounded squares + // This is ideal for Xbox controllers (and probably others) + numeric_settings.emplace_back( + std::make_unique(_trans("Input Radius"), default_radius, 0, 140)); + numeric_settings.emplace_back( + std::make_unique(_trans("Input Shape"), default_shape, 0, 50)); + numeric_settings.emplace_back(std::make_unique(_trans("Dead Zone"), 0, 0, 50)); +} + +ReshapableInput::StateData ReshapableInput::Reshape(ControlState x, ControlState y, + ControlState modifier) +{ + // TODO: make the AtAngle functions work with negative angles: + const ControlState ang = std::atan2(y, x) + MathUtil::TAU; + + const ControlState gate_max_dist = GetGateRadiusAtAngle(ang); + const ControlState input_max_dist = GetInputRadiusAtAngle(ang); + + // If input radius is zero we apply no scaling. + // This is useful when mapping native controllers without knowing intimate radius details. + const ControlState max_dist = input_max_dist ? input_max_dist : gate_max_dist; + + ControlState dist = std::sqrt(x * x + y * y) / max_dist; + + // If the modifier is pressed, scale the distance by the modifier's value. + // This is affected by the modifier's "range" setting which defaults to 50%. + if (modifier) + { + // TODO: Modifier's range setting gets reset to 100% when the clear button is clicked. + // This causes the modifier to not behave how a user might suspect. + // Retaining the old scale-by-50% behavior until range is fixed to clear to 50%. + dist *= 0.5; + // dist *= modifier; + } + + // Apply deadzone as a percentage of the user-defined radius/shape: + const ControlState deadzone = GetDeadzoneRadiusAtAngle(ang); + dist = std::max(0.0, dist - deadzone) / (1.0 - deadzone); + + // Scale to the gate shape/radius: + dist = dist *= gate_max_dist; + + x = MathUtil::Clamp(std::cos(ang) * dist, -1.0, 1.0); + y = MathUtil::Clamp(std::sin(ang) * dist, -1.0, 1.0); + return {x, y}; +} + +ControlState ReshapableInput::CalculateInputShapeRadiusAtAngle(double ang) const +{ + const auto shape = numeric_settings[SETTING_INPUT_SHAPE]->GetValue() * 4.0; + + if (shape < 1.0) + { + // Between 0 and 25 return a shape between octagon and circle + const auto amt = shape; + return OctagonStickGate(1).GetRadiusAtAngle(ang) * (1 - amt) + amt; + } + else + { + // Between 25 and 50 return a shape between circle and square + const auto amt = shape - 1.0; + return (1 - amt) + SquareStickGate(1).GetRadiusAtAngle(ang) * amt; + } +} + } // namespace ControllerEmu diff --git a/Source/Core/InputCommon/ControllerEmu/StickGate.h b/Source/Core/InputCommon/ControllerEmu/StickGate.h index decb15d234..f0c02e05e3 100644 --- a/Source/Core/InputCommon/ControllerEmu/StickGate.h +++ b/Source/Core/InputCommon/ControllerEmu/StickGate.h @@ -5,6 +5,7 @@ #pragma once #include "InputCommon/ControlReference/ControlReference.h" +#include "InputCommon/ControllerEmu/ControlGroup/ControlGroup.h" namespace ControllerEmu { @@ -52,4 +53,40 @@ private: const ControlState m_half_width; }; +class ReshapableInput : public ControlGroup +{ +public: + ReshapableInput(std::string name, std::string ui_name, GroupType type); + + struct StateData + { + ControlState x{}; + ControlState y{}; + }; + + enum + { + SETTING_INPUT_RADIUS, + SETTING_INPUT_SHAPE, + SETTING_DEADZONE, + SETTING_COUNT, + }; + + // Angle is in radians and should be non-negative + ControlState GetDeadzoneRadiusAtAngle(double ang) const; + ControlState GetInputRadiusAtAngle(double ang) const; + + virtual ControlState GetGateRadiusAtAngle(double ang) const = 0; + virtual StateData GetState(bool adjusted = true) = 0; + +protected: + void AddReshapingSettings(ControlState default_radius, ControlState default_shape, + int max_deadzone); + + StateData Reshape(ControlState x, ControlState y, ControlState modifier = 0.0); + +private: + ControlState CalculateInputShapeRadiusAtAngle(double ang) const; +}; + } // namespace ControllerEmu