Merge branch 'feat/gamepad'

This commit is contained in:
JailDesigner committed 2026-04-22 19:44:42 +02:00
commit a2b01ba9d5
13 files changed
+459 -28

No files matched your search

+1
View File
@@ -59,6 +59,7 @@ set(APP_SOURCES
source/core/input/key_config.cpp source/core/input/key_config.cpp
source/core/input/global_inputs.cpp source/core/input/global_inputs.cpp
source/core/input/input_state.cpp source/core/input/input_state.cpp
source/core/input/gamepad_manager.cpp
source/core/rendering/screen.cpp source/core/rendering/screen.cpp
source/core/rendering/canvas.cpp source/core/rendering/canvas.cpp
source/core/rendering/notifier.cpp source/core/rendering/notifier.cpp
+2
View File
@@ -3,6 +3,8 @@
notification: notification:
test: "notificacio de prova" test: "notificacio de prova"
gamepad_connected: "connectat"
gamepad_disconnected: "desconnectat"
input: input:
zoom: "zoom" zoom: "zoom"
+2
View File
@@ -3,6 +3,8 @@
notification: notification:
test: "test notification" test: "test notification"
gamepad_connected: "connected"
gamepad_disconnected: "disconnected"
input: input:
zoom: "zoom" zoom: "zoom"
+164
View File
@@ -0,0 +1,164 @@
#include "core/input/gamepad_manager.hpp"
#include <algorithm>
#include <iostream>
#include <string>
#include <utility>
#include "core/locale/locale.hpp"
#include "core/rendering/notification_presets.hpp"
namespace {
#ifdef __EMSCRIPTEN__
// A Emscripten, SDL 3.4+ no casa el GUID dels mandos web: Chrome/Android
// no aporten Vendor/Product ID i el parser de SDL extreu dades brutes.
// El W3C Gamepad API, però, garanteix un layout estàndard quan el mapping
// reporta "standard", així que injectem un mapping SDL explícit amb eixe
// layout per al GUID del joystick abans que SDL intenti obrir-lo com a
// gamepad. Fora d'Emscripten és un no-op.
void installWebStandardMapping(SDL_JoystickID jid) {
SDL_GUID guid = SDL_GetJoystickGUIDForID(jid);
char guid_str[33];
SDL_GUIDToString(guid, guid_str, sizeof(guid_str));
const char* name = SDL_GetJoystickNameForID(jid);
if (name == nullptr || *name == '\0') { name = "Standard Gamepad"; }
char mapping[512];
SDL_snprintf(mapping, sizeof(mapping),
"%s,%s,"
"a:b0,b:b1,x:b2,y:b3,"
"leftshoulder:b4,rightshoulder:b5,"
"lefttrigger:b6,righttrigger:b7,"
"back:b8,start:b9,"
"leftstick:b10,rightstick:b11,"
"dpup:b12,dpdown:b13,dpleft:b14,dpright:b15,"
"guide:b16,"
"leftx:a0,lefty:a1,rightx:a2,righty:a3,"
"platform:Emscripten",
guid_str,
name);
SDL_AddGamepadMapping(mapping);
}
#else
void installWebStandardMapping(SDL_JoystickID /*jid*/) {}
#endif
} // namespace
auto GamepadManager::get() -> GamepadManager& {
static GamepadManager instance_;
return instance_;
}
auto GamepadManager::prettyName(const char* raw) -> std::string {
std::string name = (raw != nullptr && *raw != '\0') ? std::string(raw) : std::string("Gamepad");
const auto POS = name.find_first_of("([");
if (POS != std::string::npos) { name.erase(POS); }
while (!name.empty() && name.back() == ' ') { name.pop_back(); }
if (name.size() > 25) { name.resize(25); }
if (name.empty()) { name = "Gamepad"; }
return name;
}
auto GamepadManager::uniqueName(const std::string& base) const -> std::string {
const bool COLLISION = std::ranges::any_of(
entries_,
[&](const Entry& e) { return e.pretty_name == base; });
if (!COLLISION) { return base; }
// Format " #N" començant a #2 — la primera ocurrència manté el nom net.
for (int i = 2; i < 100; ++i) {
std::string candidate = base + " #" + std::to_string(i);
const bool MATCH = std::ranges::any_of(
entries_,
[&](const Entry& e) { return e.pretty_name == candidate; });
if (!MATCH) { return candidate; }
}
return base; // cas ultra-improbable: 99 mandos idèntics connectats
}
void GamepadManager::addGamepad(SDL_JoystickID jid, bool notify) {
installWebStandardMapping(jid);
if (!SDL_IsGamepad(jid)) { return; }
SDL_Gamepad* pad = SDL_OpenGamepad(jid);
if (pad == nullptr) {
std::cout << "GamepadManager: SDL_OpenGamepad(" << jid
<< ") ha fallat: " << SDL_GetError() << '\n';
return;
}
std::string name = uniqueName(prettyName(SDL_GetGamepadName(pad)));
std::cout << "GamepadManager: connectat " << name << '\n';
entries_.push_back(Entry{.pad = pad, .instance_id = jid, .pretty_name = name});
if (notify) {
Notification::notify(name + ' ' + Locale::get().text("notification.gamepad_connected"));
}
}
void GamepadManager::removeGamepad(SDL_JoystickID jid) {
auto it = std::ranges::find_if(
entries_,
[jid](const Entry& e) { return e.instance_id == jid; });
if (it == entries_.end()) { return; }
std::string name = std::move(it->pretty_name);
if (it->pad != nullptr) { SDL_CloseGamepad(it->pad); }
std::cout << "GamepadManager: desconnectat " << name << '\n';
entries_.erase(it);
Notification::notify(name + ' ' + Locale::get().text("notification.gamepad_disconnected"));
}
void GamepadManager::init() {
if (initialized_) { return; }
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
std::cout << "GamepadManager: SDL_INIT_GAMEPAD ha fallat: "
<< SDL_GetError() << '\n';
return;
}
// `gamecontrollerdb.txt` és opcional. Si no existeix, SDL retorna -1 i
// continuem amb les definicions internes del sistema.
const int ADDED = SDL_AddGamepadMappingsFromFile("data/input/gamecontrollerdb.txt");
if (ADDED > 0) {
std::cout << "GamepadManager: " << ADDED << " mappings de gamecontrollerdb.txt\n";
}
int count = 0;
SDL_JoystickID* ids = SDL_GetJoysticks(&count);
if (ids != nullptr) {
for (int i = 0; i < count; ++i) {
addGamepad(ids[i], /*notify=*/false);
}
SDL_free(ids);
}
initialized_ = true;
}
void GamepadManager::shutdown() {
for (auto& entry : entries_) {
if (entry.pad != nullptr) { SDL_CloseGamepad(entry.pad); }
}
entries_.clear();
if (initialized_) {
SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
initialized_ = false;
}
}
void GamepadManager::handleEvent(const SDL_Event& ev) {
if (ev.type == SDL_EVENT_GAMEPAD_ADDED) {
addGamepad(ev.gdevice.which, /*notify=*/true);
} else if (ev.type == SDL_EVENT_GAMEPAD_REMOVED) {
removeGamepad(ev.gdevice.which);
}
}
auto GamepadManager::getPrimary() const -> SDL_Gamepad* {
return entries_.empty() ? nullptr : entries_.front().pad;
}
auto GamepadManager::getCount() const -> std::size_t {
return entries_.size();
}
auto GamepadManager::getByPlayerIndex(std::size_t idx) const -> SDL_Gamepad* {
return idx < entries_.size() ? entries_[idx].pad : nullptr;
}
+77
View File
@@ -0,0 +1,77 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstddef>
#include <string>
#include <vector>
// Gestor global de gamepads. Descobreix els mandos presents a `init()`
// sense emetre notificacions (el jugador ja els té posats al llançar el
// joc). Durant el runtime, `handleEvent()` processa els events de hot-plug
// i emet notificacions visibles via `Notification::notify`.
//
// El polling real de botons i eixos NO es fa a través d'aquesta classe:
// els consumidors obtenen el `SDL_Gamepad*` primari (o l'enèsim per a
// multi-jugador futur) i el passen a `InputState::getGamepadButton/Axis`,
// que ho filtra pel flag de bloqueig de la UI (mateixa estratègia que
// amb el teclat).
class GamepadManager {
public:
static auto get() -> GamepadManager&;
GamepadManager(const GamepadManager&) = delete;
GamepadManager(GamepadManager&&) = delete;
auto operator=(const GamepadManager&) -> GamepadManager& = delete;
auto operator=(GamepadManager&&) -> GamepadManager& = delete;
// Inicialitza el subsistema SDL de gamepad, intenta carregar un
// `gamecontrollerdb.txt` opcional i obri silenciosament tots els
// mandos ja connectats. Cridat una sola vegada des de Director.
void init();
// Tanca tots els gamepads oberts i treu el subsistema SDL. Cridat
// des de `~Director` abans de `SDL_Quit` per evitar handles orfes.
void shutdown();
// Processa SDL_EVENT_GAMEPAD_ADDED i SDL_EVENT_GAMEPAD_REMOVED
// emesos durant el runtime. Emet notificacions "<nom> connectat" o
// "<nom> desconnectat". El Director decideix quins events li arriben
// via `processGamepadEvents()`.
void handleEvent(const SDL_Event& ev);
// Primary = primer gamepad al vector. `nullptr` si no n'hi ha cap.
// Ús típic: Player 1 i navegació del ServiceMenu.
[[nodiscard]] auto getPrimary() const -> SDL_Gamepad*;
[[nodiscard]] auto getCount() const -> std::size_t;
// Retorna el mand del jugador `idx`-èsim (0-indexed) o `nullptr` si
// encara no hi ha prou mandos connectats. Reservat per a multi-jugador.
[[nodiscard]] auto getByPlayerIndex(std::size_t idx) const -> SDL_Gamepad*;
private:
struct Entry {
SDL_Gamepad* pad{nullptr};
SDL_JoystickID instance_id{0};
std::string pretty_name;
};
GamepadManager() = default;
~GamepadManager() = default;
// Retalla `raw` al primer '(' o '[', elimina espais finals i talla
// a 25 caràcters. Fallback "Gamepad" si queda buit.
[[nodiscard]] static auto prettyName(const char* raw) -> std::string;
// Si `base` ja existeix al vector, afegeix " #2", " #3", ... fins que
// trobi un nom lliure. Així dos mandos idèntics es distingeixen a les
// notificacions.
[[nodiscard]] auto uniqueName(const std::string& base) const -> std::string;
void addGamepad(SDL_JoystickID jid, bool notify);
void removeGamepad(SDL_JoystickID jid);
std::vector<Entry> entries_;
bool initialized_{false};
};
+8
View File
@@ -28,4 +28,12 @@ namespace InputState {
return blocked ? zeroKeyboard() : SDL_GetKeyboardState(nullptr); return blocked ? zeroKeyboard() : SDL_GetKeyboardState(nullptr);
} }
auto getGamepadButton(SDL_Gamepad* gp, SDL_GamepadButton b) -> bool {
return (blocked || gp == nullptr) ? false : SDL_GetGamepadButton(gp, b);
}
auto getGamepadAxis(SDL_Gamepad* gp, SDL_GamepadAxis ax) -> Sint16 {
return (blocked || gp == nullptr) ? static_cast<Sint16>(0) : SDL_GetGamepadAxis(gp, ax);
}
} // namespace InputState } // namespace InputState
+11 -11
View File
@@ -14,17 +14,9 @@
// que l'escena els vegi, així que la seva navegació segueix funcionant per // que l'escena els vegi, així que la seva navegació segueix funcionant per
// molt que el flag estigui posat. // molt que el flag estigui posat.
// //
// --- Nota per a l'extensió a gamepad (pròxima iteració) --- // El mateix patró aplica al gamepad: `getGamepadButton`/`getGamepadAxis`
// Quan s'afegeixi suport de comandament, el jugador haurà de llegir eixos // retornen zero/false quan el bloqueig és actiu. Els mandos concrets els
// i botons via polling (p.ex. `SDL_GetGamepadAxis`/`SDL_GetGamepadButton`). // obtén el caller via `GamepadManager::get().getPrimary()` o similars.
// Aquest mòdul s'ha d'ampliar amb:
// [[nodiscard]] auto getGamepadAxis(SDL_Gamepad* gp, SDL_GamepadAxis ax)
// -> Sint16; // 0 si bloquejat
// [[nodiscard]] auto getGamepadButton(SDL_Gamepad* gp, SDL_GamepadButton b)
// -> bool; // false si bloquejat
// Mateix patró: zero/false quan `blocked_`, pass-through altrament. Així
// Player (i el que sigui) només ha de canviar el nom de la crida; el menú
// no ha de saber res del gamepad més enllà de processar els seus events.
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
@@ -43,4 +35,12 @@ namespace InputState {
// guardar-lo frame a frame. // guardar-lo frame a frame.
[[nodiscard]] auto getKeyboardState() -> const bool*; [[nodiscard]] auto getKeyboardState() -> const bool*;
// Versió filtrada de `SDL_GetGamepadButton`. Retorna false si el bloqueig
// és actiu o si `gp` és nullptr (cas habitual: no hi ha cap mand).
[[nodiscard]] auto getGamepadButton(SDL_Gamepad* gp, SDL_GamepadButton b) -> bool;
// Versió filtrada de `SDL_GetGamepadAxis`. Retorna 0 si el bloqueig
// és actiu o si `gp` és nullptr.
[[nodiscard]] auto getGamepadAxis(SDL_Gamepad* gp, SDL_GamepadAxis ax) -> Sint16;
} // namespace InputState } // namespace InputState
+72 -10
View File
@@ -1,11 +1,59 @@
#include "core/rendering/notifier.hpp" #include "core/rendering/notifier.hpp"
#include <algorithm>
#include <numeric>
#include <utility> #include <utility>
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/text.hpp" #include "core/rendering/text.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
namespace {
// Parteix `text` en una llista de línies que caben cadascuna dins
// `max_width`. Algoritme de projecte_2026: tall per espais, la paraula
// que no cap passa a la línia següent. Les paraules aïllades més
// llargues que `max_width` es queden a la seva pròpia línia sense
// trencar-les (cas improbable amb els pretty_names de gamepad retallats
// a ~25 caràcters).
auto wrapToWidth(const std::string& text, int max_width, const Text& font, int kerning) -> std::vector<std::string> {
std::vector<std::string> lines;
if (text.empty()) {
lines.emplace_back();
return lines;
}
std::string current;
std::size_t i = 0;
while (i < text.size()) {
const std::size_t SPACE_POS = text.find(' ', i);
const bool LAST = (SPACE_POS == std::string::npos);
const std::string WORD = text.substr(i, LAST ? std::string::npos : SPACE_POS - i);
if (current.empty()) {
current = WORD;
} else {
std::string candidate = current;
candidate += ' ';
candidate += WORD;
if (font.length(candidate, kerning) <= max_width) {
current = std::move(candidate);
} else {
lines.push_back(current);
current = WORD;
}
}
if (LAST) { break; }
i = SPACE_POS + 1;
}
lines.push_back(current);
return lines;
}
} // namespace
auto Notifier::get() -> Notifier& { auto Notifier::get() -> Notifier& {
static Notifier instance_; static Notifier instance_;
return instance_; return instance_;
@@ -34,8 +82,8 @@ void Notifier::update(int delta_ms) {
break; break;
case Status::VANISHING: case Status::VANISHING:
y_ -= SLIDE_SPEED * DT; y_ -= SLIDE_SPEED * DT;
if (y_ <= static_cast<float>(-HEIGHT)) { if (y_ <= static_cast<float>(-current_box_h_)) {
y_ = static_cast<float>(-HEIGHT - MARGIN_TOP); y_ = static_cast<float>(-current_box_h_ - MARGIN_TOP);
status_ = Status::HIDDEN; status_ = Status::HIDDEN;
} }
break; break;
@@ -45,31 +93,45 @@ void Notifier::update(int delta_ms) {
} }
void Notifier::render() { void Notifier::render() {
if (status_ == Status::HIDDEN || text_ == nullptr || current_text_.empty()) { return; } if (status_ == Status::HIDDEN || text_ == nullptr || current_lines_.empty()) { return; }
const int TEXT_W = text_->length(current_text_, 1); const int BOX_X = (GameCanvas::WIDTH - current_box_w_) / 2;
const int BOX_W = TEXT_W + (2 * PADDING_H);
const int BOX_X = (GameCanvas::WIDTH - BOX_W) / 2;
const int BOX_Y = static_cast<int>(y_); const int BOX_Y = static_cast<int>(y_);
// Fons sòlid amb la paleta UI que ens ha injectat el caller a init() // Fons sòlid amb la paleta UI que ens ha injectat el caller a init()
// (ARGB directe al canvas, sense tocar cap surface paletada). // (ARGB directe al canvas, sense tocar cap surface paletada).
Screen::get().fillRect(BOX_X, BOX_Y, BOX_W, HEIGHT, ui_palette_[current_bg_color_]); Screen::get().fillRect(BOX_X, BOX_Y, current_box_w_, current_box_h_, ui_palette_[current_bg_color_]);
const int LINE_STEP = Rendering::Hud::FONT_SIZE_PX + LINE_GAP;
int line_y = BOX_Y + PADDING_V;
for (const auto& line : current_lines_) {
text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
GameCanvas::CENTER_X, GameCanvas::CENTER_X,
BOX_Y + PADDING_V, line_y,
current_text_, line,
1, 1,
current_text_color_); current_text_color_);
line_y += LINE_STEP;
}
} }
void Notifier::notify(const std::string& text, Uint8 text_color, Uint8 bg_color, int time_ms) { void Notifier::notify(const std::string& text, Uint8 text_color, Uint8 bg_color, int time_ms) {
// Política simple: la nova notificació reemplaça la que hi havia. Per // Política simple: la nova notificació reemplaça la que hi havia. Per
// gestionar-ne una cua, canviar ací per una FIFO quan calga. // gestionar-ne una cua, canviar ací per una FIFO quan calga.
current_text_ = text; if (text_ == nullptr) { return; }
current_lines_ = wrapToWidth(text, MAX_TEXT_WIDTH, *text_, 1);
// Amplada = max de les línies + padding. Alçada = N*glif + (N-1)*gap + padding.
const int MAX_LINE_W = std::accumulate(current_lines_.begin(), current_lines_.end(), 0, [this](int acc, const std::string& s) { return std::max(acc, text_->length(s, 1)); });
current_box_w_ = MAX_LINE_W + (2 * PADDING_H);
const int N = static_cast<int>(current_lines_.size());
current_box_h_ = (N * Rendering::Hud::FONT_SIZE_PX) + ((N - 1) * LINE_GAP) + (2 * PADDING_V);
current_text_color_ = text_color; current_text_color_ = text_color;
current_bg_color_ = bg_color; current_bg_color_ = bg_color;
remaining_ms_ = time_ms; remaining_ms_ = time_ms;
y_ = static_cast<float>(-current_box_h_ - MARGIN_TOP);
status_ = Status::RISING; status_ = Status::RISING;
} }
+15 -3
View File
@@ -5,9 +5,11 @@
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector>
#include "core/rendering/hud_constants.hpp" #include "core/rendering/hud_constants.hpp"
#include "core/rendering/surface.hpp" #include "core/rendering/surface.hpp"
#include "utils/defines.hpp"
class Text; class Text;
@@ -19,6 +21,10 @@ class Text;
// API crua: Notifier::get().notify(text, text_color, bg_color, time_ms). // API crua: Notifier::get().notify(text, text_color, bg_color, time_ms).
// Per a treballar amb presets pre-definits, vegeu Notification::notify() a // Per a treballar amb presets pre-definits, vegeu Notification::notify() a
// core/rendering/notification_presets.hpp. // core/rendering/notification_presets.hpp.
//
// Multilinia: si el text passa de l'amplada màxima (`MAX_TEXT_WIDTH`), es
// parteix per espais i la caixa creix en alçada automàticament. Els textos
// curts (cas habitual) ocupen una sola línia i no canvien d'aspecte.
class Notifier { class Notifier {
public: public:
static auto get() -> Notifier&; static auto get() -> Notifier&;
@@ -39,9 +45,13 @@ class Notifier {
static constexpr int PADDING_H = 6; static constexpr int PADDING_H = 6;
static constexpr int PADDING_V = 3; static constexpr int PADDING_V = 3;
static constexpr int HEIGHT = Rendering::Hud::FONT_SIZE_PX + (2 * PADDING_V); static constexpr int LINE_GAP = 1; // Separació vertical entre línies multilinia.
static constexpr int MARGIN_TOP = 2; // Separació amb la vora superior del canvas. static constexpr int MARGIN_TOP = 2; // Separació amb la vora superior del canvas.
static constexpr float SLIDE_SPEED = 120.0F; static constexpr float SLIDE_SPEED = 120.0F;
// Amplada màxima (sense comptar padding) que pot ocupar el text abans que
// `wrapToWidth` el parteixi en línies. Deixem marge respecte a la vora del
// canvas perquè la caixa no toqui els laterals.
static constexpr int MAX_TEXT_WIDTH = GameCanvas::WIDTH - 20 - (2 * PADDING_H);
private: private:
enum class Status : std::uint8_t { HIDDEN, enum class Status : std::uint8_t { HIDDEN,
@@ -55,8 +65,10 @@ class Notifier {
std::shared_ptr<Text> text_; std::shared_ptr<Text> text_;
Palette ui_palette_{}; Palette ui_palette_{};
Status status_{Status::HIDDEN}; Status status_{Status::HIDDEN};
float y_{static_cast<float>(-HEIGHT - MARGIN_TOP)}; float y_{0.0F};
std::string current_text_; std::vector<std::string> current_lines_;
int current_box_w_{0};
int current_box_h_{0};
Uint8 current_text_color_{0}; Uint8 current_text_color_{0};
Uint8 current_bg_color_{0}; Uint8 current_bg_color_{0};
int remaining_ms_{0}; int remaining_ms_{0};
+39 -4
View File
@@ -12,6 +12,7 @@
#include <utility> #include <utility>
#include "core/audio/audio.hpp" #include "core/audio/audio.hpp"
#include "core/input/gamepad_manager.hpp"
#include "core/input/key_config.hpp" #include "core/input/key_config.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/rendering/notifier.hpp" #include "core/rendering/notifier.hpp"
@@ -102,6 +103,11 @@ Director::Director() {
// JA_Music_t que continga, així que no queden streams orfes. // JA_Music_t que continga, així que no queden streams orfes.
Audio::init(Options::buildAudioConfig()); Audio::init(Options::buildAudioConfig());
// Gamepad: descobreix mandos presents silenciosament. A partir d'ara
// els events SDL_EVENT_GAMEPAD_ADDED/REMOVED arribaran via
// `processGamepadEvents()` i dispararan notificacions.
GamepadManager::get().init();
// Font compartida pels overlays: construïm una sola instància de Text // Font compartida pels overlays: construïm una sola instància de Text
// i la injectem als consumidors (Notifier, RenderInfo) per a que cap // i la injectem als consumidors (Notifier, RenderInfo) per a que cap
// d'ells hagi de conèixer els noms dels recursos concrets. // d'ells hagi de conèixer els noms dels recursos concrets.
@@ -200,6 +206,7 @@ Director::~Director() {
// desfà les llibreries abans que corren els destructors estàtics. // desfà les llibreries abans que corren els destructors estàtics.
current_scene_.reset(); current_scene_.reset();
Resource::Cache::get().clearAudio(); Resource::Cache::get().clearAudio();
GamepadManager::get().shutdown();
Audio::destroy(); Audio::destroy();
Screen::get().shutdown(); Screen::get().shutdown();
SDL_Quit(); SDL_Quit();
@@ -235,6 +242,32 @@ void Director::processEvents() {
if (QUIT_REQUESTED) { current_scene_.reset(); } if (QUIT_REQUESTED) { current_scene_.reset(); }
} }
void Director::processGamepadEvents() {
// Dues responsabilitats: (1) passar ADDED/REMOVED al GamepadManager
// perquè obri/tanqui mandos i emeti les notificacions hot-plug; (2)
// tractar el botó BACK com a toggle del menú quan està tancat (el
// paral·lel de F9 per al teclat). Quan el menú està obert, el BACK
// passa pel pipeline del `ServiceMenu::handleEvent()` via
// `processMenuInput()` i allà es converteix en `close()`.
for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) {
bool consumed = false;
if (it->type == SDL_EVENT_GAMEPAD_ADDED || it->type == SDL_EVENT_GAMEPAD_REMOVED) {
GamepadManager::get().handleEvent(*it);
consumed = true;
} else if (it->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN &&
it->gbutton.button == SDL_GAMEPAD_BUTTON_BACK &&
!ServiceMenu::get().isOpen()) {
ServiceMenu::get().toggle();
consumed = true;
}
if (consumed) {
it = EventBuffer::events.erase(it);
} else {
++it;
}
}
}
void Director::processMenuInput() { void Director::processMenuInput() {
// Intercepta input abans que l'escena vegi els events. Si el menú està // Intercepta input abans que l'escena vegi els events. Si el menú està
// obert, consumeix TOTS els KEY_DOWN perquè ni GlobalInputs (F1–F8, // obert, consumeix TOTS els KEY_DOWN perquè ni GlobalInputs (F1–F8,
@@ -263,10 +296,12 @@ void Director::switchSceneIfRequested() {
} }
auto Director::iterate() -> SDL_AppResult { auto Director::iterate() -> SDL_AppResult {
// Ordre important: primer el menú (pot consumir tecles i INJECTAR un // Ordre important: primer els events de gamepad (hot-plug + BACK que
// SDL_EVENT_QUIT des de l'acció "Eixir del joc"), després processEvents // obre el menú quan està tancat), després el menú (pot consumir tecles
// perquè vegi el QUIT al mateix frame i tanqui l'escena — altrament el // i INJECTAR un SDL_EVENT_QUIT des de l'acció "Eixir del joc"), i
// QUIT injectat es perdria quan es netegi EventBuffer al final d'iterate. // finalment processEvents perquè vegi el QUIT al mateix frame — altrament
// el QUIT injectat es perdria quan es netegi EventBuffer al final.
processGamepadEvents();
processMenuInput(); processMenuInput();
processEvents(); processEvents();
+1
View File
@@ -22,6 +22,7 @@ class Director {
void createSystemFolder(); void createSystemFolder();
void processEvents(); void processEvents();
static void processGamepadEvents();
static void processMenuInput(); static void processMenuInput();
void switchSceneIfRequested(); void switchSceneIfRequested();
}; };
+32
View File
@@ -8,6 +8,7 @@
#include <stdexcept> #include <stdexcept>
#include <utility> #include <utility>
#include "core/input/gamepad_manager.hpp"
#include "core/input/input_state.hpp" #include "core/input/input_state.hpp"
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
@@ -20,6 +21,29 @@ namespace {
// Alias local per reduir soroll; la constant viu a Defaults::Player. // Alias local per reduir soroll; la constant viu a Defaults::Player.
constexpr float SPEED_PX_PER_S = Defaults::Player::SPEED_PX_PER_S; constexpr float SPEED_PX_PER_S = Defaults::Player::SPEED_PX_PER_S;
// Dead-zone del stick analògic esquerre (rang Sint16: -32768..32767).
// Converteix eix analògic en direcció digital; per sota, es considera
// repòs i no contribueix a la velocitat.
constexpr Sint16 STICK_DEADZONE = 12000;
// Acumula la contribució del primer gamepad (cruceta + stick esquerre)
// sobre els acumuladors de velocitat. Extracció d'`update()` per a
// mantenir la complexitat cognitiva sota llindar (threshold 25).
void accumulateGamepadMovement(float& vx, float& vy) {
SDL_Gamepad* pad = GamepadManager::get().getPrimary();
if (pad == nullptr) { return; }
if (InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_LEFT)) { vx -= SPEED_PX_PER_S; }
if (InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT)) { vx += SPEED_PX_PER_S; }
if (InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_UP)) { vy -= SPEED_PX_PER_S; }
if (InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_DOWN)) { vy += SPEED_PX_PER_S; }
const Sint16 LX = InputState::getGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTX);
const Sint16 LY = InputState::getGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTY);
if (LX < -STICK_DEADZONE) { vx -= SPEED_PX_PER_S; }
if (LX > STICK_DEADZONE) { vx += SPEED_PX_PER_S; }
if (LY < -STICK_DEADZONE) { vy -= SPEED_PX_PER_S; }
if (LY > STICK_DEADZONE) { vy += SPEED_PX_PER_S; }
}
} // namespace } // namespace
void Player::loadFrom(const std::string& yaml_path) { void Player::loadFrom(const std::string& yaml_path) {
@@ -70,6 +94,14 @@ void Player::update(float delta_time) {
if (keys[controls_.up]) { vy -= SPEED_PX_PER_S; } if (keys[controls_.up]) { vy -= SPEED_PX_PER_S; }
if (keys[controls_.down]) { vy += SPEED_PX_PER_S; } if (keys[controls_.down]) { vy += SPEED_PX_PER_S; }
// Cruceta i stick esquerre del primer gamepad (si n'hi ha). També
// respecten el bloqueig d'`InputState`, així que amb el menú obert
// els botons tornen false/0 i no contribueixen a la velocitat.
// Direccions oposades (teclat left + d-pad right) es cancel·len de
// forma natural; premuda simultània en la mateixa direcció no duplica
// la velocitat perquè cada branca només resta/suma una vegada.
accumulateGamepadMovement(vx, vy);
setVelX(vx); setVelX(vx);
setVelY(vy); setVelY(vy);
+35
View File
@@ -426,6 +426,41 @@ void ServiceMenu::activateCurrent(int direction) {
auto ServiceMenu::handleEvent(const SDL_Event& ev) -> bool { auto ServiceMenu::handleEvent(const SDL_Event& ev) -> bool {
if (!open_) { return false; } if (!open_) { return false; }
// Botons del gamepad: cruceta per navegar, EAST/NORTH com a OK,
// WEST/SOUTH com a BACK de pàgina, BACK tanca el menú sencer. Quan
// el menú està obert consumim TOTS els BUTTON_DOWN perquè cap botó
// arribi als consumidors de gameplay. Paral·lel al consum total de
// KEY_DOWN més avall.
if (ev.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
switch (ev.gbutton.button) {
case SDL_GAMEPAD_BUTTON_DPAD_UP:
moveCursor(-1);
break;
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
moveCursor(+1);
break;
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
activateCurrent(-1);
break;
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
case SDL_GAMEPAD_BUTTON_EAST:
case SDL_GAMEPAD_BUTTON_NORTH:
activateCurrent(+1);
break;
case SDL_GAMEPAD_BUTTON_WEST:
case SDL_GAMEPAD_BUTTON_SOUTH:
popPage();
break;
case SDL_GAMEPAD_BUTTON_BACK:
close();
break;
default:
break;
}
return true;
}
if (ev.type != SDL_EVENT_KEY_DOWN) { return false; } if (ev.type != SDL_EVENT_KEY_DOWN) { return false; }
const auto SC = ev.key.scancode; const auto SC = ev.key.scancode;