#include #include #include static float NormalizeAxis(Sint16 v, int deadzone) { // Deadzone in raw units. Return [-1..1] const int iv = (int)v; if (std::abs(iv) <= deadzone) return 0.0f; // Normalize to [-1..1] while preserving sign. // SDL axis range is roughly [-32768..32767] const float denom = (iv < 0) ? 32768.0f : 32767.0f; float f = (float)iv / denom; // Optional: re-scale so that value starts at 0 outside deadzone // (nice for sticks; comment out if you prefer raw normalize) const float dz = (float)deadzone / 32767.0f; const float sign = (f < 0.0f) ? -1.0f : 1.0f; const float af = std::abs(f); const float scaled = (af - dz) / (1.0f - dz); return sign * std::clamp(scaled, 0.0f, 1.0f); } void InputManager::Init() { // Make sure gamecontroller subsystem is active (safe even if already) SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER); // Open any controllers already connected const int num = SDL_NumJoysticks(); for (int i = 0; i < num; ++i) { if (SDL_IsGameController(i)) { AddController(i); } } } void InputManager::BeginFrame() { m_keysPressed.clear(); m_keysReleased.clear(); m_mousePressed.clear(); m_mouseReleased.clear(); m_wheelX = 0; m_wheelY = 0; m_textInput.clear(); // Relative mouse accumulators are filled during SDL event processing // and consumed by game code each frame. Reset them here (before events // are processed) so this frame accumulates fresh deltas. m_mouseRelDX = 0; m_mouseRelDY = 0; // Clear controller "edge" sets + snapshot axes for (auto& [id, pad]: m_pads) { pad.buttonsPressed.clear(); pad.buttonsReleased.clear(); pad.prevAxes = pad.axes; } } void InputManager::SetMouseCaptured(bool captured) { if (captured == m_MouseCaptured) return; m_MouseCaptured = captured; SDL_SetRelativeMouseMode(captured ? SDL_TRUE : SDL_FALSE); } void InputManager::AddController(int deviceIndex) { if (!SDL_IsGameController(deviceIndex)) return; SDL_GameController* gc = SDL_GameControllerOpen(deviceIndex); if (!gc) return; SDL_Joystick* joy = SDL_GameControllerGetJoystick(gc); SDL_JoystickID instanceId = SDL_JoystickInstanceID(joy); PadState ps; ps.controller = gc; ps.instanceId = instanceId; ps.axes.fill(0); ps.prevAxes.fill(0); m_pads[instanceId] = ps; // Track order (padIndex mapping) m_padOrder.push_back(instanceId); } void InputManager::RemoveController(SDL_JoystickID instanceId) { auto it = m_pads.find(instanceId); if (it != m_pads.end()) { if (it->second.controller) { SDL_GameControllerClose(it->second.controller); } m_pads.erase(it); } // Remove from order list m_padOrder.erase( std::remove(m_padOrder.begin(), m_padOrder.end(), instanceId), m_padOrder.end() ); } bool InputManager::ProcessEvent(const SDL_Event& e) { switch (e.type) { case SDL_QUIT: { return true; } case SDL_KEYDOWN: { if (e.key.repeat) break; SDL_Scancode sc = e.key.keysym.scancode; m_keysDown.insert(sc); m_keysPressed.insert(sc); } break; case SDL_KEYUP: { SDL_Scancode sc = e.key.keysym.scancode; m_keysDown.erase(sc); m_keysReleased.insert(sc); } break; case SDL_MOUSEBUTTONDOWN: { Uint8 b = e.button.button; m_mouseDown.insert(b); m_mousePressed.insert(b); } break; case SDL_MOUSEBUTTONUP: { Uint8 b = e.button.button; m_mouseDown.erase(b); m_mouseReleased.insert(b); } break; case SDL_MOUSEMOTION: { m_mouseX = e.motion.x; m_mouseY = e.motion.y; m_mouseRelDX += e.motion.xrel; m_mouseRelDY += e.motion.yrel; } break; case SDL_MOUSEWHEEL: { m_wheelX += e.wheel.x; m_wheelY += e.wheel.y; } break; case SDL_TEXTINPUT: { m_textInput += e.text.text; } break; // ---- Controller hotplug ---- case SDL_CONTROLLERDEVICEADDED: { // e.cdevice.which is the device index here AddController(e.cdevice.which); } break; case SDL_CONTROLLERDEVICEREMOVED: { // e.cdevice.which is instanceId here RemoveController((SDL_JoystickID)e.cdevice.which); } break; // ---- Controller buttons ---- case SDL_CONTROLLERBUTTONDOWN: { SDL_JoystickID id = (SDL_JoystickID)e.cbutton.which; auto it = m_pads.find(id); if (it == m_pads.end()) break; uint8_t btn = (uint8_t)e.cbutton.button; it->second.buttonsDown.insert(btn); it->second.buttonsPressed.insert(btn); } break; case SDL_CONTROLLERBUTTONUP: { SDL_JoystickID id = (SDL_JoystickID)e.cbutton.which; auto it = m_pads.find(id); if (it == m_pads.end()) break; uint8_t btn = (uint8_t)e.cbutton.button; it->second.buttonsDown.erase(btn); it->second.buttonsReleased.insert(btn); } break; // ---- Controller axes ---- case SDL_CONTROLLERAXISMOTION: { SDL_JoystickID id = (SDL_JoystickID)e.caxis.which; auto it = m_pads.find(id); if (it == m_pads.end()) break; const int axis = (int)e.caxis.axis; if (axis >= 0 && axis < SDL_CONTROLLER_AXIS_MAX) { it->second.axes[(size_t)axis] = e.caxis.value; } } break; default: break; } return false; } bool InputManager::IsKeyDown(SDL_Scancode sc) const { return m_keysDown.count(sc) != 0; } bool InputManager::WasKeyPressed(SDL_Scancode sc) const { return m_keysPressed.count(sc) != 0; } bool InputManager::WasKeyReleased(SDL_Scancode sc) const { return m_keysReleased.count(sc) != 0; } bool InputManager::IsMouseDown(Uint8 button) const { return m_mouseDown.count(button) != 0; } bool InputManager::WasMousePressed(Uint8 button) const { return m_mousePressed.count(button) != 0; } bool InputManager::WasMouseReleased(Uint8 button) const { return m_mouseReleased.count(button) != 0; } void InputManager::StartTextInput() { if (!m_textInputActive) { SDL_StartTextInput(); m_textInputActive = true; } } void InputManager::StopTextInput() { if (m_textInputActive) { SDL_StopTextInput(); m_textInputActive = false; } } // ---- Controller direct queries ---- SDL_JoystickID InputManager::GetPadInstanceId(uint8_t padIndex) const { if (padIndex >= m_padOrder.size()) return -1; return m_padOrder[padIndex]; } bool InputManager::IsPadButtonDown(SDL_JoystickID id, SDL_GameControllerButton btn) const { auto it = m_pads.find(id); if (it == m_pads.end()) return false; return it->second.buttonsDown.count((uint8_t)btn) != 0; } bool InputManager::WasPadButtonPressed(SDL_JoystickID id, SDL_GameControllerButton btn) const { auto it = m_pads.find(id); if (it == m_pads.end()) return false; return it->second.buttonsPressed.count((uint8_t)btn) != 0; } bool InputManager::WasPadButtonReleased(SDL_JoystickID id, SDL_GameControllerButton btn) const { auto it = m_pads.find(id); if (it == m_pads.end()) return false; return it->second.buttonsReleased.count((uint8_t)btn) != 0; } float InputManager::GetPadAxis(SDL_JoystickID id, SDL_GameControllerAxis axis) const { auto it = m_pads.find(id); if (it == m_pads.end()) return 0.0f; const int a = (int)axis; if (a < 0 || a >= SDL_CONTROLLER_AXIS_MAX) return 0.0f; return NormalizeAxis(it->second.axes[(size_t)a], m_axisDeadzone); } // ---- Binding helpers ---- InputManager::Binding InputManager::PadButton(uint8_t padIndex, SDL_GameControllerButton btn) { Binding b; b.device = Device::GamepadButton; b.code = ((int)padIndex << 8) | ((int)btn & 0xFF); return b; } InputManager::Binding InputManager::PadAxis(uint8_t padIndex, SDL_GameControllerAxis axis, int sign, float threshold) { Binding b; b.device = Device::GamepadAxis; b.code = ((int)padIndex << 8) | ((int)axis & 0xFF); b.sign = (sign < 0) ? -1 : +1; b.threshold = threshold; return b; } // ---- Action mapping ---- void InputManager::BindAction(const std::string& action, const Binding& binding) { m_bindings[action].push_back(binding); } bool InputManager::QueryBinding(const Binding& b, ButtonState state) const { switch (b.device) { case Device::Keyboard: { auto sc = static_cast(b.code); if (state == ButtonState::Down) return IsKeyDown(sc); if (state == ButtonState::Pressed) return WasKeyPressed(sc); if (state == ButtonState::Released) return WasKeyReleased(sc); } break; case Device::MouseButton: { auto mb = static_cast(b.code); if (state == ButtonState::Down) return IsMouseDown(mb); if (state == ButtonState::Pressed) return WasMousePressed(mb); if (state == ButtonState::Released) return WasMouseReleased(mb); } break; case Device::MouseWheel: { const int c = b.code; if (state != ButtonState::Pressed) return false; if (c == 1) return m_wheelY > 0; if (c == -1) return m_wheelY < 0; if (c == 2) return m_wheelX > 0; if (c == -2) return m_wheelX < 0; } break; case Device::GamepadButton: { const uint8_t padIndex = GetPadIndexFromCode(b.code); const uint8_t btn = GetLow8FromCode(b.code); const SDL_JoystickID id = GetPadInstanceId(padIndex); if (id < 0) return false; if (state == ButtonState::Down) return IsPadButtonDown(id, (SDL_GameControllerButton)btn); if (state == ButtonState::Pressed) return WasPadButtonPressed(id, (SDL_GameControllerButton)btn); if (state == ButtonState::Released) return WasPadButtonReleased(id, (SDL_GameControllerButton)btn); } break; case Device::GamepadAxis: { // Axis bindings are treated as "digital" by thresholding const uint8_t padIndex = GetPadIndexFromCode(b.code); const uint8_t axis = GetLow8FromCode(b.code); const SDL_JoystickID id = GetPadInstanceId(padIndex); if (id < 0) return false; auto it = m_pads.find(id); if (it == m_pads.end()) return false; if (axis >= SDL_CONTROLLER_AXIS_MAX) return false; const float cur = NormalizeAxis(it->second.axes[axis], m_axisDeadzone) * (float)b.sign; const float prev = NormalizeAxis(it->second.prevAxes[axis], m_axisDeadzone) * (float)b.sign; const bool curOn = cur >= b.threshold; const bool prevOn = prev >= b.threshold; if (state == ButtonState::Down) return curOn; if (state == ButtonState::Pressed) return (curOn && !prevOn); if (state == ButtonState::Released) return (!curOn && prevOn); } break; } return false; } bool InputManager::GetAction(const std::string& action, ButtonState state) const { auto it = m_bindings.find(action); if (it == m_bindings.end()) return false; for (const auto& b: it->second) { if (QueryBinding(b, state)) return true; } return false; }