diff --git a/Source/Core/Common/MathUtil.h b/Source/Core/Common/MathUtil.h index bc0162b9ee..01a39db5b1 100644 --- a/Source/Core/Common/MathUtil.h +++ b/Source/Core/Common/MathUtil.h @@ -17,6 +17,9 @@ namespace MathUtil { +constexpr double TAU = 6.2831853071795865; +constexpr double PI = TAU / 2; + template constexpr T Clamp(const T val, const T& min, const T& max) { diff --git a/Source/Core/Core/HW/GCPadEmu.cpp b/Source/Core/Core/HW/GCPadEmu.cpp index 8f94fb7519..c0d6836f7d 100644 --- a/Source/Core/Core/HW/GCPadEmu.cpp +++ b/Source/Core/Core/HW/GCPadEmu.cpp @@ -63,10 +63,14 @@ GCPad::GCPad(const unsigned int index) : m_index(index) } // sticks - groups.emplace_back(m_main_stick = new ControllerEmu::AnalogStick( - "Main Stick", _trans("Control Stick"), DEFAULT_PAD_STICK_RADIUS)); - groups.emplace_back(m_c_stick = new ControllerEmu::AnalogStick("C-Stick", _trans("C Stick"), - DEFAULT_PAD_STICK_RADIUS)); + constexpr auto main_gate_radius = + ControlState(MAIN_STICK_GATE_RADIUS) / GCPadStatus::MAIN_STICK_RADIUS; + groups.emplace_back(m_main_stick = new ControllerEmu::OctagonAnalogStick( + "Main Stick", _trans("Control Stick"), main_gate_radius)); + + constexpr auto c_gate_radius = ControlState(C_STICK_GATE_RADIUS) / GCPadStatus::MAIN_STICK_RADIUS; + groups.emplace_back(m_c_stick = new ControllerEmu::OctagonAnalogStick( + "C-Stick", _trans("C Stick"), c_gate_radius)); // triggers groups.emplace_back(m_triggers = new ControllerEmu::MixedTriggers(_trans("Triggers"))); @@ -226,7 +230,8 @@ void GCPad::LoadDefaults(const ControllerInterface& ciface) m_main_stick->SetControlExpression(4, "LSHIFT"); // Modifier #elif __APPLE__ - m_c_stick->SetControlExpression(4, "Left Control"); // Modifier + // Modifier + m_c_stick->SetControlExpression(4, "Left Control"); // Control Stick m_main_stick->SetControlExpression(0, "Up Arrow"); // Up diff --git a/Source/Core/Core/HW/GCPadEmu.h b/Source/Core/Core/HW/GCPadEmu.h index c5a10acfa6..58272dc74a 100644 --- a/Source/Core/Core/HW/GCPadEmu.h +++ b/Source/Core/Core/HW/GCPadEmu.h @@ -16,7 +16,7 @@ namespace ControllerEmu class AnalogStick; class Buttons; class MixedTriggers; -} +} // namespace ControllerEmu enum class PadGroup { @@ -45,6 +45,9 @@ public: void LoadDefaults(const ControllerInterface& ciface) override; + static const u8 MAIN_STICK_GATE_RADIUS = 87; + static const u8 C_STICK_GATE_RADIUS = 74; + private: ControllerEmu::Buttons* m_buttons; ControllerEmu::AnalogStick* m_main_stick; @@ -57,7 +60,4 @@ private: ControllerEmu::BooleanSetting* m_always_connected; const unsigned int m_index; - - // Default analog stick radius for GameCube controllers. - static constexpr ControlState DEFAULT_PAD_STICK_RADIUS = 1.0; }; diff --git a/Source/Core/Core/HW/WiimoteEmu/Attachment/Attachment.h b/Source/Core/Core/HW/WiimoteEmu/Attachment/Attachment.h index dca1d23972..b1bae12086 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Attachment/Attachment.h +++ b/Source/Core/Core/HW/WiimoteEmu/Attachment/Attachment.h @@ -25,9 +25,6 @@ public: std::string GetName() const override; protected: - // Default radius for attachment analog sticks. - static constexpr ControlState DEFAULT_ATTACHMENT_STICK_RADIUS = 1.0; - std::array m_id{}; std::array m_calibration{}; diff --git a/Source/Core/Core/HW/WiimoteEmu/Attachment/Classic.cpp b/Source/Core/Core/HW/WiimoteEmu/Attachment/Classic.cpp index bf2b41fc6b..1cabaf60c4 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Attachment/Classic.cpp +++ b/Source/Core/Core/HW/WiimoteEmu/Attachment/Classic.cpp @@ -83,10 +83,11 @@ Classic::Classic(ExtensionReg& reg) : Attachment(_trans("Classic"), reg) } // sticks - groups.emplace_back(m_left_stick = new ControllerEmu::AnalogStick( - _trans("Left Stick"), DEFAULT_ATTACHMENT_STICK_RADIUS)); - groups.emplace_back(m_right_stick = new ControllerEmu::AnalogStick( - _trans("Right Stick"), DEFAULT_ATTACHMENT_STICK_RADIUS)); + constexpr auto gate_radius = ControlState(STICK_GATE_RADIUS) / LEFT_STICK_RADIUS; + groups.emplace_back(m_left_stick = + new ControllerEmu::OctagonAnalogStick(_trans("Left Stick"), gate_radius)); + groups.emplace_back( + m_right_stick = new ControllerEmu::OctagonAnalogStick(_trans("Right Stick"), gate_radius)); // triggers groups.emplace_back(m_triggers = new ControllerEmu::MixedTriggers(_trans("Triggers"))); diff --git a/Source/Core/Core/HW/WiimoteEmu/Attachment/Classic.h b/Source/Core/Core/HW/WiimoteEmu/Attachment/Classic.h index a914569516..357ad9a1cc 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Attachment/Classic.h +++ b/Source/Core/Core/HW/WiimoteEmu/Attachment/Classic.h @@ -68,6 +68,8 @@ public: RIGHT_TRIGGER_RANGE = 0x1F, }; + static const u8 STICK_GATE_RADIUS = 0x16; + private: ControllerEmu::Buttons* m_buttons; ControllerEmu::MixedTriggers* m_triggers; diff --git a/Source/Core/Core/HW/WiimoteEmu/Attachment/Drums.cpp b/Source/Core/Core/HW/WiimoteEmu/Attachment/Drums.cpp index 726a20739b..b32b767b01 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Attachment/Drums.cpp +++ b/Source/Core/Core/HW/WiimoteEmu/Attachment/Drums.cpp @@ -54,8 +54,9 @@ Drums::Drums(ExtensionReg& reg) : Attachment(_trans("Drums"), reg) } // stick - groups.emplace_back( - m_stick = new ControllerEmu::AnalogStick(_trans("Stick"), DEFAULT_ATTACHMENT_STICK_RADIUS)); + constexpr auto gate_radius = ControlState(STICK_GATE_RADIUS) / STICK_RADIUS; + groups.emplace_back(m_stick = + new ControllerEmu::OctagonAnalogStick(_trans("Stick"), gate_radius)); // buttons groups.emplace_back(m_buttons = new ControllerEmu::Buttons(_trans("Buttons"))); @@ -76,8 +77,8 @@ void Drums::GetState(u8* const data) { const ControllerEmu::AnalogStick::StateData stick_state = m_stick->GetState(); - drum_data.sx = static_cast((stick_state.x * 0x1F) + 0x20); - drum_data.sy = static_cast((stick_state.y * 0x1F) + 0x20); + drum_data.sx = static_cast((stick_state.x * STICK_RADIUS) + STICK_CENTER); + drum_data.sy = static_cast((stick_state.y * STICK_RADIUS) + STICK_CENTER); } // TODO: softness maybe @@ -119,4 +120,4 @@ ControllerEmu::ControlGroup* Drums::GetGroup(DrumsGroup group) return nullptr; } } -} +} // namespace WiimoteEmu diff --git a/Source/Core/Core/HW/WiimoteEmu/Attachment/Drums.h b/Source/Core/Core/HW/WiimoteEmu/Attachment/Drums.h index 83caa78814..ea36869cb5 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Attachment/Drums.h +++ b/Source/Core/Core/HW/WiimoteEmu/Attachment/Drums.h @@ -40,6 +40,12 @@ public: PAD_ORANGE = 0x8000, }; + static const u8 STICK_CENTER = 0x20; + static const u8 STICK_RADIUS = 0x1f; + + // TODO: Test real hardware. Is this accurate? + static const u8 STICK_GATE_RADIUS = 0x16; + private: ControllerEmu::Buttons* m_buttons; ControllerEmu::Buttons* m_pads; diff --git a/Source/Core/Core/HW/WiimoteEmu/Attachment/Guitar.cpp b/Source/Core/Core/HW/WiimoteEmu/Attachment/Guitar.cpp index cb2d59a885..8b3d447376 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Attachment/Guitar.cpp +++ b/Source/Core/Core/HW/WiimoteEmu/Attachment/Guitar.cpp @@ -83,8 +83,9 @@ Guitar::Guitar(ExtensionReg& reg) : Attachment(_trans("Guitar"), reg) m_buttons->controls.emplace_back(new ControllerEmu::Input(ControllerEmu::DoNotTranslate, "+")); // stick - groups.emplace_back( - m_stick = new ControllerEmu::AnalogStick(_trans("Stick"), DEFAULT_ATTACHMENT_STICK_RADIUS)); + constexpr auto gate_radius = ControlState(STICK_GATE_RADIUS) / STICK_RADIUS; + groups.emplace_back(m_stick = + new ControllerEmu::OctagonAnalogStick(_trans("Stick"), gate_radius)); // whammy groups.emplace_back(m_whammy = new ControllerEmu::Triggers(_trans("Whammy"))); @@ -108,8 +109,8 @@ void Guitar::GetState(u8* const data) { const ControllerEmu::AnalogStick::StateData stick_state = m_stick->GetState(); - guitar_data.sx = static_cast((stick_state.x * 0x1F) + 0x20); - guitar_data.sy = static_cast((stick_state.y * 0x1F) + 0x20); + guitar_data.sx = static_cast((stick_state.x * STICK_RADIUS) + STICK_CENTER); + guitar_data.sy = static_cast((stick_state.y * STICK_RADIUS) + STICK_CENTER); } // slider bar @@ -174,4 +175,4 @@ ControllerEmu::ControlGroup* Guitar::GetGroup(GuitarGroup group) return nullptr; } } -} +} // namespace WiimoteEmu diff --git a/Source/Core/Core/HW/WiimoteEmu/Attachment/Guitar.h b/Source/Core/Core/HW/WiimoteEmu/Attachment/Guitar.h index 36fd809af8..8964221511 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Attachment/Guitar.h +++ b/Source/Core/Core/HW/WiimoteEmu/Attachment/Guitar.h @@ -43,6 +43,12 @@ public: FRET_ORANGE = 0x8000, }; + static const u8 STICK_CENTER = 0x20; + static const u8 STICK_RADIUS = 0x1f; + + // TODO: Test real hardware. Is this accurate? + static const u8 STICK_GATE_RADIUS = 0x16; + private: ControllerEmu::Buttons* m_buttons; ControllerEmu::Buttons* m_frets; diff --git a/Source/Core/Core/HW/WiimoteEmu/Attachment/Nunchuk.cpp b/Source/Core/Core/HW/WiimoteEmu/Attachment/Nunchuk.cpp index d280272e6b..accc64bc74 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Attachment/Nunchuk.cpp +++ b/Source/Core/Core/HW/WiimoteEmu/Attachment/Nunchuk.cpp @@ -38,8 +38,9 @@ Nunchuk::Nunchuk(ExtensionReg& reg) : Attachment(_trans("Nunchuk"), reg) m_buttons->controls.emplace_back(new ControllerEmu::Input(ControllerEmu::DoNotTranslate, "Z")); // stick - groups.emplace_back( - m_stick = new ControllerEmu::AnalogStick(_trans("Stick"), DEFAULT_ATTACHMENT_STICK_RADIUS)); + constexpr auto gate_radius = ControlState(STICK_GATE_RADIUS) / STICK_RADIUS; + groups.emplace_back(m_stick = + new ControllerEmu::OctagonAnalogStick(_trans("Stick"), gate_radius)); // swing groups.emplace_back(m_swing = new ControllerEmu::Force(_trans("Swing"))); diff --git a/Source/Core/Core/HW/WiimoteEmu/Attachment/Nunchuk.h b/Source/Core/Core/HW/WiimoteEmu/Attachment/Nunchuk.h index b16df713d7..9b36cac1b2 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Attachment/Nunchuk.h +++ b/Source/Core/Core/HW/WiimoteEmu/Attachment/Nunchuk.h @@ -48,6 +48,7 @@ public: { STICK_CENTER = 0x80, STICK_RADIUS = 0x7F, + STICK_GATE_RADIUS = 0x52, }; void LoadDefaults(const ControllerInterface& ciface) override; diff --git a/Source/Core/Core/HW/WiimoteEmu/Attachment/Turntable.cpp b/Source/Core/Core/HW/WiimoteEmu/Attachment/Turntable.cpp index eb73093e2f..76eef2ab2a 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Attachment/Turntable.cpp +++ b/Source/Core/Core/HW/WiimoteEmu/Attachment/Turntable.cpp @@ -69,8 +69,9 @@ Turntable::Turntable(ExtensionReg& reg) : Attachment(_trans("Turntable"), reg) new ControllerEmu::Slider("Table Right", _trans("Right Table"))); // stick - groups.emplace_back( - m_stick = new ControllerEmu::AnalogStick(_trans("Stick"), DEFAULT_ATTACHMENT_STICK_RADIUS)); + constexpr auto gate_radius = ControlState(STICK_GATE_RADIUS) / STICK_RADIUS; + groups.emplace_back(m_stick = + new ControllerEmu::OctagonAnalogStick(_trans("Stick"), gate_radius)); // effect dial groups.emplace_back(m_effect_dial = new ControllerEmu::Triggers(_trans("Effect"))); @@ -92,8 +93,8 @@ void Turntable::GetState(u8* const data) { const ControllerEmu::AnalogStick::StateData stick_state = m_stick->GetState(); - tt_data.sx = static_cast((stick_state.x * 0x1F) + 0x20); - tt_data.sy = static_cast((stick_state.y * 0x1F) + 0x20); + tt_data.sx = static_cast((stick_state.x * STICK_RADIUS) + STICK_CENTER); + tt_data.sy = static_cast((stick_state.y * STICK_RADIUS) + STICK_CENTER); } // left table @@ -170,4 +171,4 @@ ControllerEmu::ControlGroup* Turntable::GetGroup(TurntableGroup group) return nullptr; } } -} +} // namespace WiimoteEmu diff --git a/Source/Core/Core/HW/WiimoteEmu/Attachment/Turntable.h b/Source/Core/Core/HW/WiimoteEmu/Attachment/Turntable.h index c97b68ccad..8d2a0b0988 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Attachment/Turntable.h +++ b/Source/Core/Core/HW/WiimoteEmu/Attachment/Turntable.h @@ -45,6 +45,12 @@ public: BUTTON_PLUS = 0x04, }; + static const u8 STICK_CENTER = 0x20; + static const u8 STICK_RADIUS = 0x1f; + + // TODO: Test real hardware. Is this accurate? + static const u8 STICK_GATE_RADIUS = 0x16; + private: ControllerEmu::Buttons* m_buttons; ControllerEmu::AnalogStick* m_stick; diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp index 5416b4fd2f..538e67bc5c 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp @@ -10,9 +10,11 @@ #include #include +#include "Common/MathUtil.h" + #include "InputCommon/ControlReference/ControlReference.h" #include "InputCommon/ControllerEmu/Control/Control.h" -#include "InputCommon/ControllerEmu/ControlGroup/ControlGroup.h" +#include "InputCommon/ControllerEmu/ControlGroup/AnalogStick.h" #include "InputCommon/ControllerEmu/Setting/NumericSetting.h" #include "InputCommon/ControllerInterface/Device.h" @@ -28,7 +30,7 @@ MappingIndicator::MappingIndicator(ControllerEmu::ControlGroup* group) : m_group BindCursorControls(false); break; case ControllerEmu::GroupType::Stick: - BindStickControls(); + // Nothing needed: break; case ControllerEmu::GroupType::Tilt: BindCursorControls(true); @@ -68,18 +70,6 @@ void MappingIndicator::BindCursorControls(bool tilt) } } -void MappingIndicator::BindStickControls() -{ - m_stick_up = m_group->controls[0]->control_ref.get(); - m_stick_down = m_group->controls[1]->control_ref.get(); - m_stick_left = m_group->controls[2]->control_ref.get(); - m_stick_right = m_group->controls[3]->control_ref.get(); - m_stick_modifier = m_group->controls[4]->control_ref.get(); - - m_stick_radius = m_group->numeric_settings[0].get(); - m_stick_deadzone = m_group->numeric_settings[1].get(); -} - void MappingIndicator::BindMixedTriggersControls() { m_mixed_triggers_l_button = m_group->controls[0]->control_ref.get(); @@ -144,83 +134,92 @@ void MappingIndicator::DrawCursor(bool tilt) p.fillRect(curx, cury, 8, 8, Qt::red); } +// Constructs a polygon by querying a radius at varying angles: +template +QPolygonF GetPolygonFromRadiusGetter(F&& radius_getter, double scale) +{ + // 24 is a multiple of 8 (octagon) and enough points to be visibly pleasing: + constexpr int shape_point_count = 24; + QPolygonF shape{shape_point_count}; + + int p = 0; + for (auto& point : shape) + { + const double angle = MathUtil::TAU * p / shape.size(); + const double radius = radius_getter(angle) * scale; + + point = {std::cos(angle) * radius, std::sin(angle) * radius}; + ++p; + } + + return shape; +} + void MappingIndicator::DrawStick() { - float centerx = width() / 2., centery = height() / 2.; + // Make the c-stick yellow: + const bool is_c_stick = m_group->name == "C-Stick"; + const QColor gate_color = is_c_stick ? Qt::yellow : Qt::lightGray; - bool c_stick = m_group->name == "C-Stick"; - bool classic_controller = m_group->name == "Left Stick" || m_group->name == "Right Stick"; + auto& stick = *static_cast(m_group); - float ratio = 1; + // TODO: This SetControllerStateNeeded interface leaks input into the game + // We should probably hold the mutex for UI updates. + Settings::Instance().SetControllerStateNeeded(true); + const auto raw_coord = stick.GetState(false); + const auto adj_coord = stick.GetState(true); + Settings::Instance().SetControllerStateNeeded(false); - if (c_stick) - ratio = 1.; - else if (classic_controller) - ratio = 0.9f; + // Bounding box size: + const double scale = height() / 2.5; - // Polled values - float mod = PollControlState(m_stick_modifier) ? 0.5 : 1; - float radius = m_stick_radius->GetValue(); - float curx = -PollControlState(m_stick_left) + PollControlState(m_stick_right), - cury = -PollControlState(m_stick_up) + PollControlState(m_stick_down); - // The maximum deadzone value covers 50% of the stick area - float deadzone = m_stick_deadzone->GetValue() / 2.; - - // Size parameters - float max_size = (height() / 2.5) / ratio; - float stick_size = (height() / 3.) / ratio; - - // Emulated cursor position - float virt_curx, virt_cury; - - if (std::abs(curx) < deadzone && std::abs(cury) < deadzone) - { - virt_curx = virt_cury = 0; - } - else - { - virt_curx = curx * mod; - virt_cury = cury * mod; - } - - // Coordinates for an octagon - std::array radius_octagon = {{ - QPointF(centerx, centery + stick_size), // Bottom - QPointF(centerx + stick_size / sqrt(2), centery + stick_size / sqrt(2)), // Bottom Right - QPointF(centerx + stick_size, centery), // Right - QPointF(centerx + stick_size / sqrt(2), centery - stick_size / sqrt(2)), // Top Right - QPointF(centerx, centery - stick_size), // Top - QPointF(centerx - stick_size / sqrt(2), centery - stick_size / sqrt(2)), // Top Left - QPointF(centerx - stick_size, centery), // Left - QPointF(centerx - stick_size / sqrt(2), centery + stick_size / sqrt(2)) // Bottom Left - }}; + const float dot_radius = 2; QPainter p(this); + p.translate(width() / 2, height() / 2); + + // Bounding box. + p.setBrush(Qt::white); + p.setPen(Qt::gray); + p.drawRect(-scale - 1, -scale - 1, scale * 2 + 1, scale * 2 + 1); + + // UI y-axis is opposite that of stick. + p.scale(1.0, -1.0); + + // Enable AA after drawing bounding box. p.setRenderHint(QPainter::Antialiasing, true); p.setRenderHint(QPainter::SmoothPixmapTransform, true); - // Draw maximum values - p.setBrush(Qt::white); - p.setPen(Qt::black); - p.drawRect(centerx - max_size, centery - max_size, max_size * 2, max_size * 2); + // Input gate. (i.e. the octagon shape) + p.setPen(Qt::darkGray); + p.setBrush(gate_color); + p.drawPolygon(GetPolygonFromRadiusGetter( + [&stick](double ang) { return stick.GetGateRadiusAtAngle(ang); }, scale)); - // Draw radius - p.setBrush(c_stick ? Qt::yellow : Qt::darkGray); - p.drawPolygon(radius_octagon.data(), static_cast(radius_octagon.size())); + // Deadzone. + p.setPen(Qt::darkGray); + p.setBrush(QBrush(Qt::darkGray, Qt::BDiagPattern)); + p.drawPolygon(GetPolygonFromRadiusGetter( + [&stick](double ang) { return stick.GetDeadzoneRadiusAtAngle(ang); }, scale)); - // Draw deadzone - p.setBrush(c_stick ? Qt::darkYellow : Qt::lightGray); - p.drawEllipse(centerx - deadzone * stick_size, centery - deadzone * stick_size, - deadzone * stick_size * 2, deadzone * stick_size * 2); + // Input shape. + p.setPen(QPen(Qt::darkGray, 1.0, Qt::DashLine)); + p.setBrush(Qt::NoBrush); + p.drawPolygon(GetPolygonFromRadiusGetter( + [&stick](double ang) { return stick.GetInputRadiusAtAngle(ang); }, scale)); - // Draw stick - p.setBrush(Qt::black); - p.drawEllipse(centerx - 4 + curx * max_size, centery - 4 + cury * max_size, 8, 8); + // Raw stick position. + p.setPen(Qt::NoPen); + p.setBrush(Qt::darkGray); + p.drawEllipse(QPointF{raw_coord.x, raw_coord.y} * scale, dot_radius, dot_radius); - // Draw virtual stick - p.setBrush(Qt::red); - p.drawEllipse(centerx - 4 + virt_curx * max_size * radius, - centery - 4 + virt_cury * max_size * radius, 8, 8); + // Adjusted stick position. + if (adj_coord.x || adj_coord.y) + { + p.setPen(Qt::NoPen); + p.setBrush(Qt::red); + p.drawEllipse(QPointF{adj_coord.x, adj_coord.y} * scale, dot_radius, dot_radius); + } } void MappingIndicator::DrawMixedTriggers() diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h index 62baf47250..f3bff86798 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h +++ b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h @@ -25,7 +25,6 @@ public: private: void BindCursorControls(bool tilt); - void BindStickControls(); void BindMixedTriggersControls(); void DrawCursor(bool tilt); @@ -35,16 +34,6 @@ private: void paintEvent(QPaintEvent*) override; ControllerEmu::ControlGroup* m_group; - // Stick settings - ControlReference* m_stick_up; - ControlReference* m_stick_down; - ControlReference* m_stick_left; - ControlReference* m_stick_right; - ControlReference* m_stick_modifier; - - ControllerEmu::NumericSetting* m_stick_radius; - ControllerEmu::NumericSetting* m_stick_deadzone; - // Cursor settings ControlReference* m_cursor_up; ControlReference* m_cursor_down; diff --git a/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.cpp b/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.cpp index c38e5f1d3a..5368c6b7ba 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.cpp +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.cpp @@ -9,6 +9,7 @@ #include #include "Common/Common.h" +#include "Common/MathUtil.h" #include "InputCommon/ControlReference/ControlReference.h" #include "InputCommon/ControllerEmu/Control/Control.h" @@ -18,54 +19,158 @@ namespace ControllerEmu { -AnalogStick::AnalogStick(const char* const name_, ControlState default_radius) - : AnalogStick(name_, name_, default_radius) +AnalogStick::AnalogStick(const char* const name_, std::unique_ptr&& stick_gate) + : AnalogStick(name_, name_, std::move(stick_gate)) { } AnalogStick::AnalogStick(const char* const name_, const char* const ui_name_, - ControlState default_radius) - : ControlGroup(name_, ui_name_, GroupType::Stick) + std::unique_ptr&& stick_gate) + : ControlGroup(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"))); + numeric_settings.emplace_back( - std::make_unique(_trans("Radius"), default_radius, 0, 100)); + std::make_unique(_trans("Input Radius"), 1.0, 0, 100)); + 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)); } -AnalogStick::StateData AnalogStick::GetState() +AnalogStick::StateData AnalogStick::GetState(bool adjusted) { ControlState y = controls[0]->control_ref->State() - controls[1]->control_ref->State(); ControlState x = controls[3]->control_ref->State() - controls[2]->control_ref->State(); - const ControlState radius = numeric_settings[SETTING_RADIUS]->GetValue(); - const ControlState deadzone = numeric_settings[SETTING_DEADZONE]->GetValue(); - const ControlState m = controls[4]->control_ref->State(); + // Return raw values. (used in UI) + if (!adjusted) + return {x, y}; - const ControlState ang = atan2(y, x); + // TODO: make the AtAngle functions work with negative angles: + const ControlState ang = atan2(y, x) + MathUtil::TAU; const ControlState ang_sin = sin(ang); const ControlState ang_cos = cos(ang); - ControlState dist = sqrt(x * x + y * y); + const ControlState gate_max_dist = GetGateRadiusAtAngle(ang); + const ControlState input_max_dist = GetInputRadiusAtAngle(ang); - // dead zone code - dist = std::max(0.0, dist - deadzone); - dist /= (1 - deadzone); + // 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; - // radius - dist *= radius; + ControlState dist = std::sqrt(x * x + y * y) / max_dist; - // The modifier halves the distance by 50%, which is useful - // for keyboard controls. - if (m) + // 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; y = std::max(-1.0, std::min(1.0, ang_sin * dist)); x = std::max(-1.0, std::min(1.0, ang_cos * dist)); return {x, y}; } + +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 +{ + return CalculateInputShapeRadiusAtAngle(ang) * numeric_settings[SETTING_INPUT_RADIUS]->GetValue(); +} + +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::ComputeRadiusAtAngle(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::ComputeRadiusAtAngle(ang) * amt; + } +} + +OctagonStickGate::OctagonStickGate(ControlState radius) : m_radius(radius) +{ +} + +ControlState OctagonStickGate::GetRadiusAtAngle(double ang) const +{ + return ComputeRadiusAtAngle(ang) * m_radius; +} + +ControlState OctagonStickGate::ComputeRadiusAtAngle(double ang) +{ + // Ratio of octagon circumcircle to incircle: + const double incircle_radius = 0.923879532511287; + const double section_ang = MathUtil::TAU / 8; + return incircle_radius / std::cos(std::fmod(ang, section_ang) - section_ang / 2); +} + +RoundStickGate::RoundStickGate(ControlState radius) : m_radius(radius) +{ +} + +ControlState RoundStickGate::GetRadiusAtAngle(double) const +{ + return m_radius; +} + +SquareStickGate::SquareStickGate(ControlState half_width) : m_half_width(half_width) +{ +} + +ControlState SquareStickGate::GetRadiusAtAngle(double ang) const +{ + return ComputeRadiusAtAngle(ang) * m_half_width; +} + +ControlState SquareStickGate::ComputeRadiusAtAngle(double ang) +{ + const double section_ang = MathUtil::TAU / 4; + return 1 / std::cos(std::fmod(ang + section_ang / 2, section_ang) - section_ang / 2); +} + +OctagonAnalogStick::OctagonAnalogStick(const char* name, ControlState gate_radius) + : OctagonAnalogStick(name, name, gate_radius) +{ +} + +OctagonAnalogStick::OctagonAnalogStick(const char* name, const char* ui_name, + ControlState gate_radius) + : AnalogStick(name, ui_name, std::make_unique(gate_radius)) +{ +} + } // namespace ControllerEmu diff --git a/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.h b/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.h index d8d8e4ddea..336669124a 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.h +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/AnalogStick.h @@ -9,12 +9,57 @@ namespace ControllerEmu { +// An abstract class representing the plastic shell that limits an analog stick's movement. +class StickGate +{ +public: + // Angle is in radians and should be non-negative + virtual ControlState GetRadiusAtAngle(double ang) const = 0; +}; + +// An octagon-shaped stick gate is found on most Nintendo GC/Wii analog sticks. +class OctagonStickGate : public StickGate +{ +public: + explicit OctagonStickGate(ControlState radius); + ControlState GetRadiusAtAngle(double ang) const override; + + static ControlState ComputeRadiusAtAngle(double ang); + + const ControlState m_radius; +}; + +// A round-shaped stick gate. Possibly found on 3rd-party accessories. +class RoundStickGate : public StickGate +{ +public: + explicit RoundStickGate(ControlState radius); + ControlState GetRadiusAtAngle(double ang) const override; + +private: + const ControlState m_radius; +}; + +// A square-shaped stick gate. e.g. keyboard input. +class SquareStickGate : public StickGate +{ +public: + explicit SquareStickGate(ControlState half_width); + ControlState GetRadiusAtAngle(double ang) const override; + + static ControlState ComputeRadiusAtAngle(double ang); + +private: + const ControlState m_half_width; +}; + class AnalogStick : public ControlGroup { public: enum { - SETTING_RADIUS, + SETTING_INPUT_RADIUS, + SETTING_INPUT_SHAPE, SETTING_DEADZONE, }; @@ -24,10 +69,28 @@ public: ControlState y{}; }; - // The GameCube controller and Wiimote attachments have a different default radius - AnalogStick(const char* name, ControlState default_radius); - AnalogStick(const char* name, const char* ui_name, ControlState default_radius); + AnalogStick(const char* name, std::unique_ptr&& stick_gate); + AnalogStick(const char* name, const char* ui_name, std::unique_ptr&& stick_gate); - StateData GetState(); + StateData GetState(bool adjusted = true); + + // Angle is in radians and should be non-negative + ControlState GetGateRadiusAtAngle(double ang) const; + ControlState GetDeadzoneRadiusAtAngle(double ang) const; + ControlState GetInputRadiusAtAngle(double ang) const; + +private: + ControlState CalculateInputShapeRadiusAtAngle(double ang) const; + + std::unique_ptr m_stick_gate; }; + +// An AnalogStick with an OctagonStickGate +class OctagonAnalogStick : public AnalogStick +{ +public: + OctagonAnalogStick(const char* name, ControlState gate_radius); + OctagonAnalogStick(const char* name, const char* ui_name, ControlState gate_radius); +}; + } // namespace ControllerEmu