mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-24 21:55:09 +00:00
feat: Add input bindings configuration for keyboard and gamepad actions
This commit is contained in:
42
README.md
42
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`.
|
||||
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -224,6 +224,7 @@ void ServiceBasedApp::RegisterServices() {
|
||||
// Input service
|
||||
registry_.RegisterService<services::IInputService, services::impl::SdlInputService>(
|
||||
registry_.GetService<events::IEventBus>(),
|
||||
registry_.GetService<services::IConfigService>(),
|
||||
registry_.GetService<services::ILogger>());
|
||||
|
||||
// Audio service (needed before script bindings execute)
|
||||
|
||||
@@ -176,6 +176,73 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> 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<std::string, std::string>& 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<float>(value.GetDouble());
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,12 @@ public:
|
||||
return config_.windowTitle;
|
||||
}
|
||||
std::vector<const char*> GetDeviceExtensions() const override;
|
||||
const InputBindings& GetInputBindings() const override {
|
||||
if (logger_) {
|
||||
logger_->Trace("JsonConfigService", "GetInputBindings");
|
||||
}
|
||||
return config_.inputBindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the full runtime configuration.
|
||||
|
||||
@@ -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<std::string, std::string>& 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());
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#include "sdl_input_service.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
constexpr float kAxisPositiveMax = static_cast<float>(SDL_JOYSTICK_AXIS_MAX);
|
||||
constexpr float kAxisNegativeMax = static_cast<float>(-SDL_JOYSTICK_AXIS_MIN);
|
||||
@@ -23,12 +27,15 @@ const std::unordered_map<SDL_Keycode, std::string> 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<events::IEventBus> eventBus, std::shared_ptr<ILogger> logger)
|
||||
: eventBus_(std::move(eventBus)), logger_(logger) {
|
||||
SdlInputService::SdlInputService(std::shared_ptr<events::IEventBus> eventBus,
|
||||
std::shared_ptr<IConfigService> configService,
|
||||
std::shared_ptr<ILogger> 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<events::IEventBus> 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<int>(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<char>(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<int>(musicToggleButton_)) +
|
||||
", dpadUpButton=" + std::to_string(static_cast<int>(dpadUpButton_)) +
|
||||
", dpadDownButton=" + std::to_string(static_cast<int>(dpadDownButton_)) +
|
||||
", dpadLeftButton=" + std::to_string(static_cast<int>(dpadLeftButton_)) +
|
||||
", dpadRightButton=" + std::to_string(static_cast<int>(dpadRightButton_)) +
|
||||
", moveXAxis=" + std::to_string(static_cast<int>(moveXAxis_)) +
|
||||
", moveYAxis=" + std::to_string(static_cast<int>(moveYAxis_)) +
|
||||
", lookXAxis=" + std::to_string(static_cast<int>(lookXAxis_)) +
|
||||
", lookYAxis=" + std::to_string(static_cast<int>(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) {
|
||||
|
||||
@@ -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 <memory>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
@@ -23,7 +25,9 @@ public:
|
||||
*
|
||||
* @param eventBus Event bus to subscribe to
|
||||
*/
|
||||
explicit SdlInputService(std::shared_ptr<events::IEventBus> eventBus, std::shared_ptr<ILogger> logger);
|
||||
explicit SdlInputService(std::shared_ptr<events::IEventBus> eventBus,
|
||||
std::shared_ptr<IConfigService> configService,
|
||||
std::shared_ptr<ILogger> logger);
|
||||
~SdlInputService() override;
|
||||
|
||||
// IInputService interface
|
||||
@@ -43,11 +47,25 @@ public:
|
||||
|
||||
private:
|
||||
std::shared_ptr<events::IEventBus> eventBus_;
|
||||
std::shared_ptr<IConfigService> configService_;
|
||||
std::shared_ptr<ILogger> 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<SDL_GamepadButton, std::string> gamepadButtonActions_;
|
||||
std::unordered_map<SDL_GamepadAxis, std::string> gamepadAxisActions_;
|
||||
float gamepadAxisActionThreshold_ = 0.5f;
|
||||
std::unordered_map<SDL_Keycode, std::string> 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<SDL_Keycode, std::string> kGuiKeyNames;
|
||||
|
||||
@@ -3,9 +3,47 @@
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
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<std::string, std::string> 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<std::string, std::string> 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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "config_types.hpp"
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
@@ -52,6 +53,12 @@ public:
|
||||
* @return List of extension names
|
||||
*/
|
||||
virtual std::vector<const char*> GetDeviceExtensions() const = 0;
|
||||
|
||||
/**
|
||||
* @brief Get configured input bindings.
|
||||
* @return Input bindings structure
|
||||
*/
|
||||
virtual const InputBindings& GetInputBindings() const = 0;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services
|
||||
|
||||
Reference in New Issue
Block a user