From ca9830b978a8e6f40d2c4c640bce3ac654db7607 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Mon, 5 Jan 2026 06:55:13 +0000 Subject: [PATCH] feat: Add input bindings configuration for keyboard and gamepad actions --- README.md | 42 +++ config/seed_runtime.json | 33 +++ scripts/cube_logic.lua | 10 +- src/app/service_based_app.cpp | 1 + src/services/impl/json_config_service.cpp | 67 +++++ src/services/impl/json_config_service.hpp | 6 + .../impl/json_config_writer_service.cpp | 39 +++ src/services/impl/sdl_input_service.cpp | 246 +++++++++++++++--- src/services/impl/sdl_input_service.hpp | 23 +- src/services/interfaces/config_types.hpp | 39 +++ src/services/interfaces/i_config_service.hpp | 7 + 11 files changed, 476 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 44a8313..02bc2e0 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,48 @@ SDL3 + Vulkan demo app with Lua-driven runtime configuration, audio playback, an - `sdl3_app --create-seed-json config/seed_runtime.json` writes a starter file assuming `scripts/cube_logic.lua` sits beside the binary. - `sdl3_app --set-default-json [path]` stores or overrides the runtime JSON; Windows writes `%APPDATA%/sdl3cpp`, other OSes use `$XDG_CONFIG_HOME/sdl3cpp/default_runtime.json` (fallback `~/.config/sdl3cpp`). +### Input bindings +`config/seed_runtime.json` includes an `input_bindings` section that maps keyboard keys and gamepad inputs to action names consumed by the Lua script (see `scripts/cube_logic.lua`). + +Keyboard bindings use SDL key names (e.g. `W`, `Space`, `Left Shift`). Gamepad bindings use SDL gamepad names (e.g. `start`, `south`, `dpad_up`, `leftx`). + +Example: +``` +"input_bindings": { + "move_forward": "W", + "move_back": "S", + "move_left": "A", + "move_right": "D", + "music_toggle": "M", + "music_toggle_gamepad": "start", + "gamepad_move_x_axis": "leftx", + "gamepad_move_y_axis": "lefty", + "gamepad_look_x_axis": "rightx", + "gamepad_look_y_axis": "righty", + "gamepad_dpad_up": "dpad_up", + "gamepad_dpad_down": "dpad_down", + "gamepad_dpad_left": "dpad_left", + "gamepad_dpad_right": "dpad_right", + "gamepad_button_actions": { + "south": "gamepad_a", + "east": "gamepad_b", + "west": "gamepad_x", + "north": "gamepad_y", + "left_shoulder": "gamepad_lb", + "right_shoulder": "gamepad_rb", + "left_stick": "gamepad_ls", + "right_stick": "gamepad_rs", + "back": "gamepad_back", + "start": "gamepad_start" + }, + "gamepad_axis_actions": { + "left_trigger": "gamepad_lt", + "right_trigger": "gamepad_rt" + }, + "gamepad_axis_action_threshold": 0.5 +} +``` + ## GUI demo `scripts/gui_demo.lua` paints the Lua GUI framework on top of the Vulkan scene. Launch it as `python scripts/dev_commands.py run -- --json-file-in config/gui_runtime.json` or register that config via `sdl3_app --set-default-json`. diff --git a/config/seed_runtime.json b/config/seed_runtime.json index 366fd22..8dce457 100644 --- a/config/seed_runtime.json +++ b/config/seed_runtime.json @@ -3,6 +3,39 @@ "window_height": 768, "lua_script": "scripts/cube_logic.lua", "scripts_directory": "scripts", + "input_bindings": { + "move_forward": "W", + "move_back": "S", + "move_left": "A", + "move_right": "D", + "music_toggle": "M", + "music_toggle_gamepad": "start", + "gamepad_move_x_axis": "leftx", + "gamepad_move_y_axis": "lefty", + "gamepad_look_x_axis": "rightx", + "gamepad_look_y_axis": "righty", + "gamepad_dpad_up": "dpad_up", + "gamepad_dpad_down": "dpad_down", + "gamepad_dpad_left": "dpad_left", + "gamepad_dpad_right": "dpad_right", + "gamepad_button_actions": { + "south": "gamepad_a", + "east": "gamepad_b", + "west": "gamepad_x", + "north": "gamepad_y", + "left_shoulder": "gamepad_lb", + "right_shoulder": "gamepad_rb", + "left_stick": "gamepad_ls", + "right_stick": "gamepad_rs", + "back": "gamepad_back", + "start": "gamepad_start" + }, + "gamepad_axis_actions": { + "left_trigger": "gamepad_lt", + "right_trigger": "gamepad_rt" + }, + "gamepad_axis_action_threshold": 0.5 + }, "project_root": "../", "shaders_directory": "shaders", "device_extensions": [ diff --git a/scripts/cube_logic.lua b/scripts/cube_logic.lua index 890bbfd..9f6d664 100644 --- a/scripts/cube_logic.lua +++ b/scripts/cube_logic.lua @@ -257,16 +257,16 @@ local function update_camera(dt) local move_x = 0.0 local move_z = 0.0 - if gui_input.keyStates["w"] then + if gui_input.keyStates["move_forward"] then move_z = move_z + 1.0 end - if gui_input.keyStates["s"] then + if gui_input.keyStates["move_back"] then move_z = move_z - 1.0 end - if gui_input.keyStates["d"] then + if gui_input.keyStates["move_right"] then move_x = move_x + 1.0 end - if gui_input.keyStates["a"] then + if gui_input.keyStates["move_left"] then move_x = move_x - 1.0 end @@ -295,7 +295,7 @@ local function update_audio_controls() end local pad = gui_input.gamepad - local toggle_pressed = gui_input.keyStates["m"] + local toggle_pressed = gui_input.keyStates["music_toggle"] if pad and pad.connected and pad.togglePressed then toggle_pressed = true end diff --git a/src/app/service_based_app.cpp b/src/app/service_based_app.cpp index 5b8c1dc..811580e 100644 --- a/src/app/service_based_app.cpp +++ b/src/app/service_based_app.cpp @@ -224,6 +224,7 @@ void ServiceBasedApp::RegisterServices() { // Input service registry_.RegisterService( registry_.GetService(), + registry_.GetService(), registry_.GetService()); // Audio service (needed before script bindings execute) diff --git a/src/services/impl/json_config_service.cpp b/src/services/impl/json_config_service.cpp index c2f9ce4..6dd089d 100644 --- a/src/services/impl/json_config_service.cpp +++ b/src/services/impl/json_config_service.cpp @@ -176,6 +176,73 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr logger, c config.windowTitle = value.GetString(); } + if (document.HasMember("input_bindings")) { + const auto& bindingsValue = document["input_bindings"]; + if (!bindingsValue.IsObject()) { + throw std::runtime_error("JSON member 'input_bindings' must be an object"); + } + auto readBinding = [&](const char* name, std::string& target) { + if (!bindingsValue.HasMember(name)) { + return; + } + const auto& value = bindingsValue[name]; + if (!value.IsString()) { + throw std::runtime_error("JSON member 'input_bindings." + std::string(name) + "' must be a string"); + } + target = value.GetString(); + }; + + readBinding("move_forward", config.inputBindings.moveForwardKey); + readBinding("move_back", config.inputBindings.moveBackKey); + readBinding("move_left", config.inputBindings.moveLeftKey); + readBinding("move_right", config.inputBindings.moveRightKey); + readBinding("music_toggle", config.inputBindings.musicToggleKey); + readBinding("music_toggle_gamepad", config.inputBindings.musicToggleGamepadButton); + readBinding("gamepad_move_x_axis", config.inputBindings.gamepadMoveXAxis); + readBinding("gamepad_move_y_axis", config.inputBindings.gamepadMoveYAxis); + readBinding("gamepad_look_x_axis", config.inputBindings.gamepadLookXAxis); + readBinding("gamepad_look_y_axis", config.inputBindings.gamepadLookYAxis); + readBinding("gamepad_dpad_up", config.inputBindings.gamepadDpadUpButton); + readBinding("gamepad_dpad_down", config.inputBindings.gamepadDpadDownButton); + readBinding("gamepad_dpad_left", config.inputBindings.gamepadDpadLeftButton); + readBinding("gamepad_dpad_right", config.inputBindings.gamepadDpadRightButton); + + auto readMapping = [&](const char* name, + std::unordered_map& target) { + if (!bindingsValue.HasMember(name)) { + return; + } + const auto& mappingValue = bindingsValue[name]; + if (!mappingValue.IsObject()) { + throw std::runtime_error("JSON member 'input_bindings." + std::string(name) + "' must be an object"); + } + for (auto it = mappingValue.MemberBegin(); it != mappingValue.MemberEnd(); ++it) { + if (!it->name.IsString() || !it->value.IsString()) { + throw std::runtime_error("JSON member 'input_bindings." + std::string(name) + + "' must contain string pairs"); + } + std::string key = it->name.GetString(); + std::string value = it->value.GetString(); + if (value.empty()) { + target.erase(key); + } else { + target[key] = value; + } + } + }; + + readMapping("gamepad_button_actions", config.inputBindings.gamepadButtonActions); + readMapping("gamepad_axis_actions", config.inputBindings.gamepadAxisActions); + + if (bindingsValue.HasMember("gamepad_axis_action_threshold")) { + const auto& value = bindingsValue["gamepad_axis_action_threshold"]; + if (!value.IsNumber()) { + throw std::runtime_error("JSON member 'input_bindings.gamepad_axis_action_threshold' must be a number"); + } + config.inputBindings.gamepadAxisActionThreshold = static_cast(value.GetDouble()); + } + } + return config; } diff --git a/src/services/impl/json_config_service.hpp b/src/services/impl/json_config_service.hpp index 81a07be..f31146e 100644 --- a/src/services/impl/json_config_service.hpp +++ b/src/services/impl/json_config_service.hpp @@ -76,6 +76,12 @@ public: return config_.windowTitle; } std::vector GetDeviceExtensions() const override; + const InputBindings& GetInputBindings() const override { + if (logger_) { + logger_->Trace("JsonConfigService", "GetInputBindings"); + } + return config_.inputBindings; + } /** * @brief Get the full runtime configuration. diff --git a/src/services/impl/json_config_writer_service.cpp b/src/services/impl/json_config_writer_service.cpp index bb2e8d9..a525e4f 100644 --- a/src/services/impl/json_config_writer_service.cpp +++ b/src/services/impl/json_config_writer_service.cpp @@ -49,6 +49,45 @@ void JsonConfigWriterService::WriteConfig(const RuntimeConfig& config, const std std::filesystem::path scriptsDir = config.scriptPath.parent_path(); addStringMember("scripts_directory", scriptsDir.string()); + rapidjson::Value bindingsObject(rapidjson::kObjectType); + auto addBindingMember = [&](const char* name, const std::string& value) { + rapidjson::Value nameValue(name, allocator); + rapidjson::Value stringValue(value.c_str(), allocator); + bindingsObject.AddMember(nameValue, stringValue, allocator); + }; + addBindingMember("move_forward", config.inputBindings.moveForwardKey); + addBindingMember("move_back", config.inputBindings.moveBackKey); + addBindingMember("move_left", config.inputBindings.moveLeftKey); + addBindingMember("move_right", config.inputBindings.moveRightKey); + addBindingMember("music_toggle", config.inputBindings.musicToggleKey); + addBindingMember("music_toggle_gamepad", config.inputBindings.musicToggleGamepadButton); + addBindingMember("gamepad_move_x_axis", config.inputBindings.gamepadMoveXAxis); + addBindingMember("gamepad_move_y_axis", config.inputBindings.gamepadMoveYAxis); + addBindingMember("gamepad_look_x_axis", config.inputBindings.gamepadLookXAxis); + addBindingMember("gamepad_look_y_axis", config.inputBindings.gamepadLookYAxis); + addBindingMember("gamepad_dpad_up", config.inputBindings.gamepadDpadUpButton); + addBindingMember("gamepad_dpad_down", config.inputBindings.gamepadDpadDownButton); + addBindingMember("gamepad_dpad_left", config.inputBindings.gamepadDpadLeftButton); + addBindingMember("gamepad_dpad_right", config.inputBindings.gamepadDpadRightButton); + + auto addMappingObject = [&](const char* name, + const std::unordered_map& mappings, + rapidjson::Value& target) { + rapidjson::Value mappingObject(rapidjson::kObjectType); + for (const auto& [key, value] : mappings) { + rapidjson::Value keyValue(key.c_str(), allocator); + rapidjson::Value stringValue(value.c_str(), allocator); + mappingObject.AddMember(keyValue, stringValue, allocator); + } + target.AddMember(rapidjson::Value(name, allocator), mappingObject, allocator); + }; + + addMappingObject("gamepad_button_actions", config.inputBindings.gamepadButtonActions, bindingsObject); + addMappingObject("gamepad_axis_actions", config.inputBindings.gamepadAxisActions, bindingsObject); + bindingsObject.AddMember("gamepad_axis_action_threshold", + config.inputBindings.gamepadAxisActionThreshold, allocator); + document.AddMember("input_bindings", bindingsObject, allocator); + std::filesystem::path projectRoot = scriptsDir.parent_path(); if (!projectRoot.empty()) { addStringMember("project_root", projectRoot.string()); diff --git a/src/services/impl/sdl_input_service.cpp b/src/services/impl/sdl_input_service.cpp index d60f609..97d0029 100644 --- a/src/services/impl/sdl_input_service.cpp +++ b/src/services/impl/sdl_input_service.cpp @@ -1,5 +1,9 @@ #include "sdl_input_service.hpp" +#include +#include +#include + namespace { constexpr float kAxisPositiveMax = static_cast(SDL_JOYSTICK_AXIS_MAX); constexpr float kAxisNegativeMax = static_cast(-SDL_JOYSTICK_AXIS_MIN); @@ -23,12 +27,15 @@ const std::unordered_map SdlInputService::kGuiKeyNames {SDLK_DELETE, "delete"}, {SDLK_RETURN, "return"}, {SDLK_TAB, "tab"}, {SDLK_ESCAPE, "escape"}, {SDLK_LCTRL, "lctrl"}, {SDLK_RCTRL, "rctrl"}, {SDLK_LSHIFT, "lshift"}, {SDLK_RSHIFT, "rshift"}, {SDLK_LALT, "lalt"}, - {SDLK_RALT, "ralt"}, {SDLK_w, "w"}, {SDLK_a, "a"}, {SDLK_s, "s"}, - {SDLK_d, "d"}, {SDLK_m, "m"} + {SDLK_RALT, "ralt"} }; -SdlInputService::SdlInputService(std::shared_ptr eventBus, std::shared_ptr logger) - : eventBus_(std::move(eventBus)), logger_(logger) { +SdlInputService::SdlInputService(std::shared_ptr eventBus, + std::shared_ptr configService, + std::shared_ptr logger) + : eventBus_(std::move(eventBus)), + configService_(std::move(configService)), + logger_(logger) { // Subscribe to input events eventBus_->Subscribe(events::EventType::KeyPressed, [this](const events::Event& e) { @@ -63,6 +70,7 @@ SdlInputService::SdlInputService(std::shared_ptr eventBus, st logger_->Trace("SdlInputService", "SdlInputService", "eventBus=" + std::string(eventBus_ ? "set" : "null")); } + BuildActionKeyMapping(); EnsureGamepadSubsystem(); } @@ -80,24 +88,12 @@ void SdlInputService::ProcessEvent(const SDL_Event& event) { switch (event.type) { case SDL_EVENT_KEY_DOWN: state_.keysPressed.insert(event.key.key); - // GUI input processing - { - auto it = kGuiKeyNames.find(event.key.key); - if (it != kGuiKeyNames.end()) { - guiInputSnapshot_.keyStates[it->second] = true; - } - } + ApplyKeyMapping(event.key.key, true); break; case SDL_EVENT_KEY_UP: state_.keysPressed.erase(event.key.key); - // GUI input processing - { - auto it = kGuiKeyNames.find(event.key.key); - if (it != kGuiKeyNames.end()) { - guiInputSnapshot_.keyStates[it->second] = false; - } - } + ApplyKeyMapping(event.key.key, false); break; case SDL_EVENT_MOUSE_MOTION: @@ -189,10 +185,7 @@ void SdlInputService::OnKeyPressed(const events::Event& event) { ", repeat=" + std::string(keyEvent.repeat ? "true" : "false")); } state_.keysPressed.insert(keyEvent.key); - auto it = kGuiKeyNames.find(keyEvent.key); - if (it != kGuiKeyNames.end()) { - guiInputSnapshot_.keyStates[it->second] = true; - } + ApplyKeyMapping(keyEvent.key, true); } void SdlInputService::OnKeyReleased(const events::Event& event) { @@ -205,10 +198,7 @@ void SdlInputService::OnKeyReleased(const events::Event& event) { ", repeat=" + std::string(keyEvent.repeat ? "true" : "false")); } state_.keysPressed.erase(keyEvent.key); - auto it = kGuiKeyNames.find(keyEvent.key); - if (it != kGuiKeyNames.end()) { - guiInputSnapshot_.keyStates[it->second] = false; - } + ApplyKeyMapping(keyEvent.key, false); } void SdlInputService::OnMouseMoved(const events::Event& event) { @@ -279,6 +269,167 @@ void SdlInputService::OnTextInput(const events::Event& event) { guiInputSnapshot_.textInput += textEvent.text; } +void SdlInputService::BuildActionKeyMapping() { + actionKeyNames_.clear(); + gamepadButtonActions_.clear(); + gamepadAxisActions_.clear(); + if (!configService_) { + if (logger_) { + logger_->Trace("SdlInputService", "BuildActionKeyMapping", "configService=null"); + } + return; + } + + const auto& bindings = configService_->GetInputBindings(); + auto addKey = [&](const char* actionName, const std::string& keyName) { + if (keyName.empty()) { + return; + } + SDL_Keycode key = SDL_GetKeyFromName(keyName.c_str()); + if (key == SDLK_UNKNOWN) { + if (logger_) { + logger_->Error("SdlInputService: unknown key binding for " + std::string(actionName) + + " -> " + keyName); + } + return; + } + actionKeyNames_[key] = actionName; + if (logger_) { + logger_->Trace("SdlInputService", "BuildActionKeyMapping", + "action=" + std::string(actionName) + + ", keyName=" + keyName + + ", keyCode=" + std::to_string(static_cast(key))); + } + }; + + addKey("move_forward", bindings.moveForwardKey); + addKey("move_back", bindings.moveBackKey); + addKey("move_left", bindings.moveLeftKey); + addKey("move_right", bindings.moveRightKey); + addKey("music_toggle", bindings.musicToggleKey); + + auto toLower = [](std::string value) { + std::transform(value.begin(), value.end(), value.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return value; + }; + + auto setButton = [&](const char* bindingName, const std::string& buttonValue, SDL_GamepadButton& target) { + if (buttonValue.empty()) { + return; + } + std::string normalized = toLower(buttonValue); + SDL_GamepadButton button = SDL_GetGamepadButtonFromString(normalized.c_str()); + if (button != SDL_GAMEPAD_BUTTON_INVALID) { + target = button; + } else if (logger_) { + logger_->Error("SdlInputService: unknown gamepad button binding for " + + std::string(bindingName) + " -> " + buttonValue); + } + }; + + setButton("music_toggle_gamepad", bindings.musicToggleGamepadButton, musicToggleButton_); + setButton("gamepad_dpad_up", bindings.gamepadDpadUpButton, dpadUpButton_); + setButton("gamepad_dpad_down", bindings.gamepadDpadDownButton, dpadDownButton_); + setButton("gamepad_dpad_left", bindings.gamepadDpadLeftButton, dpadLeftButton_); + setButton("gamepad_dpad_right", bindings.gamepadDpadRightButton, dpadRightButton_); + + auto setAxis = [&](const char* axisName, const std::string& axisValue, SDL_GamepadAxis& target) { + if (axisValue.empty()) { + return; + } + std::string normalized = toLower(axisValue); + SDL_GamepadAxis axis = SDL_GetGamepadAxisFromString(normalized.c_str()); + if (axis != SDL_GAMEPAD_AXIS_INVALID) { + target = axis; + } else if (logger_) { + logger_->Error("SdlInputService: unknown gamepad axis binding for " + + std::string(axisName) + " -> " + axisValue); + } + }; + + setAxis("gamepad_move_x_axis", bindings.gamepadMoveXAxis, moveXAxis_); + setAxis("gamepad_move_y_axis", bindings.gamepadMoveYAxis, moveYAxis_); + setAxis("gamepad_look_x_axis", bindings.gamepadLookXAxis, lookXAxis_); + setAxis("gamepad_look_y_axis", bindings.gamepadLookYAxis, lookYAxis_); + + for (const auto& [buttonName, actionName] : bindings.gamepadButtonActions) { + if (buttonName.empty() || actionName.empty()) { + continue; + } + std::string normalized = toLower(buttonName); + SDL_GamepadButton button = SDL_GetGamepadButtonFromString(normalized.c_str()); + if (button == SDL_GAMEPAD_BUTTON_INVALID) { + if (logger_) { + logger_->Error("SdlInputService: unknown gamepad button mapping " + + buttonName + " -> " + actionName); + } + continue; + } + gamepadButtonActions_[button] = actionName; + } + + for (const auto& [axisName, actionName] : bindings.gamepadAxisActions) { + if (axisName.empty() || actionName.empty()) { + continue; + } + std::string normalized = toLower(axisName); + SDL_GamepadAxis axis = SDL_GetGamepadAxisFromString(normalized.c_str()); + if (axis == SDL_GAMEPAD_AXIS_INVALID) { + if (logger_) { + logger_->Error("SdlInputService: unknown gamepad axis mapping " + + axisName + " -> " + actionName); + } + continue; + } + gamepadAxisActions_[axis] = actionName; + } + + gamepadAxisActionThreshold_ = bindings.gamepadAxisActionThreshold; + if (gamepadAxisActionThreshold_ < 0.0f) { + gamepadAxisActionThreshold_ = 0.0f; + } else if (gamepadAxisActionThreshold_ > 1.0f) { + gamepadAxisActionThreshold_ = 1.0f; + } + + if (logger_) { + logger_->Trace("SdlInputService", "BuildActionKeyMapping", + "musicToggleButton=" + std::to_string(static_cast(musicToggleButton_)) + + ", dpadUpButton=" + std::to_string(static_cast(dpadUpButton_)) + + ", dpadDownButton=" + std::to_string(static_cast(dpadDownButton_)) + + ", dpadLeftButton=" + std::to_string(static_cast(dpadLeftButton_)) + + ", dpadRightButton=" + std::to_string(static_cast(dpadRightButton_)) + + ", moveXAxis=" + std::to_string(static_cast(moveXAxis_)) + + ", moveYAxis=" + std::to_string(static_cast(moveYAxis_)) + + ", lookXAxis=" + std::to_string(static_cast(lookXAxis_)) + + ", lookYAxis=" + std::to_string(static_cast(lookYAxis_)) + + ", buttonActions=" + std::to_string(gamepadButtonActions_.size()) + + ", axisActions=" + std::to_string(gamepadAxisActions_.size()) + + ", axisThreshold=" + std::to_string(gamepadAxisActionThreshold_)); + } +} + +void SdlInputService::ApplyKeyMapping(SDL_Keycode key, bool isDown) { + auto actionIt = actionKeyNames_.find(key); + if (actionIt != actionKeyNames_.end()) { + guiInputSnapshot_.keyStates[actionIt->second] = isDown; + } + auto guiIt = kGuiKeyNames.find(key); + if (guiIt != kGuiKeyNames.end()) { + guiInputSnapshot_.keyStates[guiIt->second] = isDown; + } +} + +bool SdlInputService::IsActionKeyPressed(const std::string& action) const { + for (const auto& key : state_.keysPressed) { + auto it = actionKeyNames_.find(key); + if (it != actionKeyNames_.end() && it->second == action) { + return true; + } + } + return false; +} + void SdlInputService::EnsureGamepadSubsystem() { uint32_t initialized = SDL_WasInit(0); if ((initialized & SDL_INIT_GAMEPAD) != 0) { @@ -355,12 +506,45 @@ void SdlInputService::UpdateGamepadSnapshot() { } guiInputSnapshot_.gamepadConnected = true; - guiInputSnapshot_.gamepadLeftX = NormalizeAxis(SDL_GetGamepadAxis(gamepad_, SDL_GAMEPAD_AXIS_LEFTX)); - guiInputSnapshot_.gamepadLeftY = NormalizeAxis(SDL_GetGamepadAxis(gamepad_, SDL_GAMEPAD_AXIS_LEFTY)); - guiInputSnapshot_.gamepadRightX = NormalizeAxis(SDL_GetGamepadAxis(gamepad_, SDL_GAMEPAD_AXIS_RIGHTX)); - guiInputSnapshot_.gamepadRightY = NormalizeAxis(SDL_GetGamepadAxis(gamepad_, SDL_GAMEPAD_AXIS_RIGHTY)); + guiInputSnapshot_.gamepadLeftX = 0.0f; + guiInputSnapshot_.gamepadLeftY = 0.0f; + guiInputSnapshot_.gamepadRightX = 0.0f; + guiInputSnapshot_.gamepadRightY = 0.0f; + if (moveXAxis_ != SDL_GAMEPAD_AXIS_INVALID) { + guiInputSnapshot_.gamepadLeftX = NormalizeAxis(SDL_GetGamepadAxis(gamepad_, moveXAxis_)); + } + if (moveYAxis_ != SDL_GAMEPAD_AXIS_INVALID) { + guiInputSnapshot_.gamepadLeftY = NormalizeAxis(SDL_GetGamepadAxis(gamepad_, moveYAxis_)); + } + if (lookXAxis_ != SDL_GAMEPAD_AXIS_INVALID) { + guiInputSnapshot_.gamepadRightX = NormalizeAxis(SDL_GetGamepadAxis(gamepad_, lookXAxis_)); + } + if (lookYAxis_ != SDL_GAMEPAD_AXIS_INVALID) { + guiInputSnapshot_.gamepadRightY = NormalizeAxis(SDL_GetGamepadAxis(gamepad_, lookYAxis_)); + } guiInputSnapshot_.gamepadTogglePressed = - SDL_GetGamepadButton(gamepad_, SDL_GAMEPAD_BUTTON_START); + SDL_GetGamepadButton(gamepad_, musicToggleButton_); + + auto updateActionState = [&](const std::string& actionName, bool gamepadPressed) { + bool keyboardPressed = IsActionKeyPressed(actionName); + bool current = guiInputSnapshot_.keyStates[actionName]; + guiInputSnapshot_.keyStates[actionName] = current || keyboardPressed || gamepadPressed; + }; + + updateActionState("move_forward", SDL_GetGamepadButton(gamepad_, dpadUpButton_)); + updateActionState("move_back", SDL_GetGamepadButton(gamepad_, dpadDownButton_)); + updateActionState("move_left", SDL_GetGamepadButton(gamepad_, dpadLeftButton_)); + updateActionState("move_right", SDL_GetGamepadButton(gamepad_, dpadRightButton_)); + + for (const auto& [button, actionName] : gamepadButtonActions_) { + updateActionState(actionName, SDL_GetGamepadButton(gamepad_, button)); + } + + for (const auto& [axis, actionName] : gamepadAxisActions_) { + float value = NormalizeAxis(SDL_GetGamepadAxis(gamepad_, axis)); + bool pressed = std::fabs(value) >= gamepadAxisActionThreshold_; + updateActionState(actionName, pressed); + } } void SdlInputService::SetGuiScriptService(IGuiScriptService* guiScriptService) { diff --git a/src/services/impl/sdl_input_service.hpp b/src/services/impl/sdl_input_service.hpp index b80f8c1..7050a1a 100644 --- a/src/services/impl/sdl_input_service.hpp +++ b/src/services/impl/sdl_input_service.hpp @@ -3,8 +3,10 @@ #include "../interfaces/i_input_service.hpp" #include "../interfaces/i_gui_script_service.hpp" #include "../interfaces/i_logger.hpp" +#include "../interfaces/i_config_service.hpp" #include "../../events/i_event_bus.hpp" #include +#include namespace sdl3cpp::services::impl { @@ -23,7 +25,9 @@ public: * * @param eventBus Event bus to subscribe to */ - explicit SdlInputService(std::shared_ptr eventBus, std::shared_ptr logger); + explicit SdlInputService(std::shared_ptr eventBus, + std::shared_ptr configService, + std::shared_ptr logger); ~SdlInputService() override; // IInputService interface @@ -43,11 +47,25 @@ public: private: std::shared_ptr eventBus_; + std::shared_ptr configService_; std::shared_ptr logger_; InputState state_; GuiInputSnapshot guiInputSnapshot_; IGuiScriptService* guiScriptService_ = nullptr; SDL_Gamepad* gamepad_ = nullptr; + SDL_GamepadButton musicToggleButton_ = SDL_GAMEPAD_BUTTON_START; + SDL_GamepadButton dpadUpButton_ = SDL_GAMEPAD_BUTTON_DPAD_UP; + SDL_GamepadButton dpadDownButton_ = SDL_GAMEPAD_BUTTON_DPAD_DOWN; + SDL_GamepadButton dpadLeftButton_ = SDL_GAMEPAD_BUTTON_DPAD_LEFT; + SDL_GamepadButton dpadRightButton_ = SDL_GAMEPAD_BUTTON_DPAD_RIGHT; + SDL_GamepadAxis moveXAxis_ = SDL_GAMEPAD_AXIS_LEFTX; + SDL_GamepadAxis moveYAxis_ = SDL_GAMEPAD_AXIS_LEFTY; + SDL_GamepadAxis lookXAxis_ = SDL_GAMEPAD_AXIS_RIGHTX; + SDL_GamepadAxis lookYAxis_ = SDL_GAMEPAD_AXIS_RIGHTY; + std::unordered_map gamepadButtonActions_; + std::unordered_map gamepadAxisActions_; + float gamepadAxisActionThreshold_ = 0.5f; + std::unordered_map actionKeyNames_; // Event bus listeners void OnKeyPressed(const events::Event& event); @@ -61,6 +79,9 @@ private: void TryOpenGamepad(); void CloseGamepad(); void UpdateGamepadSnapshot(); + void BuildActionKeyMapping(); + void ApplyKeyMapping(SDL_Keycode key, bool isDown); + bool IsActionKeyPressed(const std::string& action) const; // GUI key mapping (extracted from old Sdl3App) static const std::unordered_map kGuiKeyNames; diff --git a/src/services/interfaces/config_types.hpp b/src/services/interfaces/config_types.hpp index dca1b72..163b047 100644 --- a/src/services/interfaces/config_types.hpp +++ b/src/services/interfaces/config_types.hpp @@ -3,9 +3,47 @@ #include #include #include +#include namespace sdl3cpp::services { +/** + * @brief Input bindings for game and UI actions. + */ +struct InputBindings { + std::string moveForwardKey = "W"; + std::string moveBackKey = "S"; + std::string moveLeftKey = "A"; + std::string moveRightKey = "D"; + std::string musicToggleKey = "M"; + std::string musicToggleGamepadButton = "start"; + std::string gamepadMoveXAxis = "leftx"; + std::string gamepadMoveYAxis = "lefty"; + std::string gamepadLookXAxis = "rightx"; + std::string gamepadLookYAxis = "righty"; + std::string gamepadDpadUpButton = "dpad_up"; + std::string gamepadDpadDownButton = "dpad_down"; + std::string gamepadDpadLeftButton = "dpad_left"; + std::string gamepadDpadRightButton = "dpad_right"; + std::unordered_map gamepadButtonActions = { + {"south", "gamepad_a"}, + {"east", "gamepad_b"}, + {"west", "gamepad_x"}, + {"north", "gamepad_y"}, + {"left_shoulder", "gamepad_lb"}, + {"right_shoulder", "gamepad_rb"}, + {"left_stick", "gamepad_ls"}, + {"right_stick", "gamepad_rs"}, + {"back", "gamepad_back"}, + {"start", "gamepad_start"} + }; + std::unordered_map gamepadAxisActions = { + {"left_trigger", "gamepad_lt"}, + {"right_trigger", "gamepad_rt"} + }; + float gamepadAxisActionThreshold = 0.5f; +}; + /** * @brief Runtime configuration values used across services. */ @@ -15,6 +53,7 @@ struct RuntimeConfig { std::filesystem::path scriptPath; bool luaDebug = false; std::string windowTitle = "SDL3 Vulkan Demo"; + InputBindings inputBindings{}; }; } // namespace sdl3cpp::services diff --git a/src/services/interfaces/i_config_service.hpp b/src/services/interfaces/i_config_service.hpp index b6bdbc7..2a2bda5 100644 --- a/src/services/interfaces/i_config_service.hpp +++ b/src/services/interfaces/i_config_service.hpp @@ -1,5 +1,6 @@ #pragma once +#include "config_types.hpp" #include #include #include @@ -52,6 +53,12 @@ public: * @return List of extension names */ virtual std::vector GetDeviceExtensions() const = 0; + + /** + * @brief Get configured input bindings. + * @return Input bindings structure + */ + virtual const InputBindings& GetInputBindings() const = 0; }; } // namespace sdl3cpp::services