GroveEngine/plans/PLAN_INPUT_MODULE.md
2025-12-04 20:15:53 +08:00

21 KiB

InputModule - Plan d'implémentation

Vue d'ensemble

Module de capture et conversion d'événements d'entrée (clavier, souris, gamepad) vers le système IIO de GroveEngine. Permet un découplage complet entre la source d'input (SDL, GLFW, Windows, etc.) et les modules consommateurs (UI, Game Logic, etc.).

Objectifs

  • Découplage - Séparer la capture d'events de leur consommation
  • Réutilisabilité - Utilisable pour tests ET production
  • Hot-reload - Supporte le rechargement dynamique avec préservation de l'état
  • Multi-backend - Support SDL d'abord, extensible à GLFW/Win32/etc.
  • Thread-safe - Injection d'events depuis la main loop, traitement dans process()
  • Production-ready - Performance, logging, monitoring

Architecture

modules/InputModule/
├── InputModule.cpp/h           # Module principal IModule
├── Core/
│   ├── InputState.cpp/h        # État des inputs (touches pressées, position souris)
│   └── InputConverter.cpp/h    # Conversion events natifs → IIO messages
└── Backends/
    └── SDLBackend.cpp/h        # Backend SDL (SDL_Event → InputEvent)

Topics IIO publiés

Input Mouse

Topic Payload Description
input:mouse:move {x, y} Position souris (coordonnées écran)
input:mouse:button {button, pressed, x, y} Click souris (button: 0=left, 1=middle, 2=right)
input:mouse:wheel {delta} Molette souris (delta: + = haut, - = bas)

Input Keyboard

Topic Payload Description
input:keyboard:key {key, pressed, repeat, modifiers} Touche clavier (scancode)
input:keyboard:text {text} Saisie texte UTF-8 (pour TextInput)

Input Gamepad (Phase 2)

Topic Payload Description
input:gamepad:button {id, button, pressed} Bouton gamepad
input:gamepad:axis {id, axis, value} Axe analogique (-1.0 à 1.0)
input:gamepad:connected {id, name} Gamepad connecté/déconnecté

Phases d'implémentation

Phase 1: Core InputModule + SDL Backend

Objectif: Module fonctionnel avec support souris + clavier via SDL

1.1 Structure de base

Fichiers à créer:

// InputModule.h/cpp - IModule principal
class InputModule : public IModule {
public:
    InputModule();
    ~InputModule() override;

    // IModule interface
    void setConfiguration(const IDataNode& config, IIO* io, ITaskScheduler* scheduler) override;
    void process(const IDataNode& input) override;
    void shutdown() override;

    std::unique_ptr<IDataNode> getState() override;
    void setState(const IDataNode& state) override;
    const IDataNode& getConfiguration() override;
    std::unique_ptr<IDataNode> getHealthStatus() override;

    std::string getType() const override { return "input_module"; }
    bool isIdle() const override { return true; }

    // API spécifique InputModule
    void feedEvent(const void* nativeEvent);  // Injection depuis main loop

private:
    IIO* m_io = nullptr;
    std::unique_ptr<InputState> m_state;
    std::unique_ptr<InputConverter> m_converter;
    std::unique_ptr<SDLBackend> m_backend;

    // Event buffer (thread-safe)
    std::vector<SDL_Event> m_eventBuffer;
    std::mutex m_bufferMutex;

    // Config
    std::string m_backend = "sdl";  // "sdl", "glfw", "win32", etc.
    bool m_enableMouse = true;
    bool m_enableKeyboard = true;
    bool m_enableGamepad = false;

    // Stats
    uint64_t m_frameCount = 0;
    uint64_t m_eventsProcessed = 0;
};

Topics IIO:

  • Publish: input:mouse:move, input:mouse:button, input:keyboard:key, input:keyboard:text
  • Subscribe: (aucun pour Phase 1)

1.2 InputState - État des inputs

// InputState.h/cpp - État courant des inputs
class InputState {
public:
    // Mouse state
    int mouseX = 0;
    int mouseY = 0;
    bool mouseButtons[3] = {false, false, false};  // L, M, R

    // Keyboard state
    std::unordered_set<int> keysPressed;  // Scancodes pressés

    // Modifiers
    struct Modifiers {
        bool shift = false;
        bool ctrl = false;
        bool alt = false;
    } modifiers;

    // Methods
    void setMousePosition(int x, int y);
    void setMouseButton(int button, bool pressed);
    void setKey(int scancode, bool pressed);
    void updateModifiers(bool shift, bool ctrl, bool alt);

    // Query
    bool isMouseButtonPressed(int button) const;
    bool isKeyPressed(int scancode) const;
};

1.3 SDLBackend - Conversion SDL → Generic

// SDLBackend.h/cpp - Convertit SDL_Event en événements génériques
class SDLBackend {
public:
    struct InputEvent {
        enum Type {
            MouseMove,
            MouseButton,
            MouseWheel,
            KeyboardKey,
            KeyboardText
        };

        Type type;

        // Mouse data
        int mouseX, mouseY;
        int button;  // 0=left, 1=middle, 2=right
        bool pressed;
        float wheelDelta;

        // Keyboard data
        int scancode;
        bool repeat;
        std::string text;  // UTF-8

        // Modifiers
        bool shift, ctrl, alt;
    };

    // Convertit SDL_Event → InputEvent
    static bool convert(const SDL_Event& sdlEvent, InputEvent& outEvent);
};

Conversion SDL → Generic:

bool SDLBackend::convert(const SDL_Event& sdlEvent, InputEvent& outEvent) {
    switch (sdlEvent.type) {
        case SDL_MOUSEMOTION:
            outEvent.type = InputEvent::MouseMove;
            outEvent.mouseX = sdlEvent.motion.x;
            outEvent.mouseY = sdlEvent.motion.y;
            return true;

        case SDL_MOUSEBUTTONDOWN:
        case SDL_MOUSEBUTTONUP:
            outEvent.type = InputEvent::MouseButton;
            outEvent.button = sdlEvent.button.button - 1;  // SDL: 1-based
            outEvent.pressed = (sdlEvent.type == SDL_MOUSEBUTTONDOWN);
            outEvent.mouseX = sdlEvent.button.x;
            outEvent.mouseY = sdlEvent.button.y;
            return true;

        case SDL_MOUSEWHEEL:
            outEvent.type = InputEvent::MouseWheel;
            outEvent.wheelDelta = static_cast<float>(sdlEvent.wheel.y);
            return true;

        case SDL_KEYDOWN:
        case SDL_KEYUP:
            outEvent.type = InputEvent::KeyboardKey;
            outEvent.scancode = sdlEvent.key.keysym.scancode;
            outEvent.pressed = (sdlEvent.type == SDL_KEYDOWN);
            outEvent.repeat = (sdlEvent.key.repeat != 0);
            outEvent.shift = (sdlEvent.key.keysym.mod & KMOD_SHIFT) != 0;
            outEvent.ctrl = (sdlEvent.key.keysym.mod & KMOD_CTRL) != 0;
            outEvent.alt = (sdlEvent.key.keysym.mod & KMOD_ALT) != 0;
            return true;

        case SDL_TEXTINPUT:
            outEvent.type = InputEvent::KeyboardText;
            outEvent.text = sdlEvent.text.text;
            return true;

        default:
            return false;  // Event non supporté
    }
}

1.4 InputConverter - Generic → IIO

// InputConverter.h/cpp - Convertit InputEvent → IIO messages
class InputConverter {
public:
    InputConverter(IIO* io);

    void publishMouseMove(int x, int y);
    void publishMouseButton(int button, bool pressed, int x, int y);
    void publishMouseWheel(float delta);
    void publishKeyboardKey(int scancode, bool pressed, bool repeat, bool shift, bool ctrl, bool alt);
    void publishKeyboardText(const std::string& text);

private:
    IIO* m_io;
};

Implémentation:

void InputConverter::publishMouseMove(int x, int y) {
    auto msg = std::make_unique<JsonDataNode>("mouse_move");
    msg->setInt("x", x);
    msg->setInt("y", y);
    m_io->publish("input:mouse:move", std::move(msg));
}

void InputConverter::publishMouseButton(int button, bool pressed, int x, int y) {
    auto msg = std::make_unique<JsonDataNode>("mouse_button");
    msg->setInt("button", button);
    msg->setBool("pressed", pressed);
    msg->setInt("x", x);
    msg->setInt("y", y);
    m_io->publish("input:mouse:button", std::move(msg));
}

void InputConverter::publishKeyboardKey(int scancode, bool pressed, bool repeat,
                                        bool shift, bool ctrl, bool alt) {
    auto msg = std::make_unique<JsonDataNode>("keyboard_key");
    msg->setInt("scancode", scancode);
    msg->setBool("pressed", pressed);
    msg->setBool("repeat", repeat);
    msg->setBool("shift", shift);
    msg->setBool("ctrl", ctrl);
    msg->setBool("alt", alt);
    m_io->publish("input:keyboard:key", std::move(msg));
}

void InputConverter::publishKeyboardText(const std::string& text) {
    auto msg = std::make_unique<JsonDataNode>("keyboard_text");
    msg->setString("text", text);
    m_io->publish("input:keyboard:text", std::move(msg));
}

1.5 InputModule::process() - Pipeline complet

void InputModule::process(const IDataNode& input) {
    m_frameCount++;

    // 1. Lock et récupère les events du buffer
    std::vector<SDL_Event> events;
    {
        std::lock_guard<std::mutex> lock(m_bufferMutex);
        events = std::move(m_eventBuffer);
        m_eventBuffer.clear();
    }

    // 2. Convertit SDL → Generic → IIO
    for (const auto& sdlEvent : events) {
        SDLBackend::InputEvent genericEvent;

        if (!SDLBackend::convert(sdlEvent, genericEvent)) {
            continue;  // Event non supporté, skip
        }

        // 3. Update state
        switch (genericEvent.type) {
            case SDLBackend::InputEvent::MouseMove:
                m_state->setMousePosition(genericEvent.mouseX, genericEvent.mouseY);
                m_converter->publishMouseMove(genericEvent.mouseX, genericEvent.mouseY);
                break;

            case SDLBackend::InputEvent::MouseButton:
                m_state->setMouseButton(genericEvent.button, genericEvent.pressed);
                m_converter->publishMouseButton(genericEvent.button, genericEvent.pressed,
                                                 genericEvent.mouseX, genericEvent.mouseY);
                break;

            case SDLBackend::InputEvent::MouseWheel:
                m_converter->publishMouseWheel(genericEvent.wheelDelta);
                break;

            case SDLBackend::InputEvent::KeyboardKey:
                m_state->setKey(genericEvent.scancode, genericEvent.pressed);
                m_state->updateModifiers(genericEvent.shift, genericEvent.ctrl, genericEvent.alt);
                m_converter->publishKeyboardKey(genericEvent.scancode, genericEvent.pressed,
                                                 genericEvent.repeat, genericEvent.shift,
                                                 genericEvent.ctrl, genericEvent.alt);
                break;

            case SDLBackend::InputEvent::KeyboardText:
                m_converter->publishKeyboardText(genericEvent.text);
                break;
        }

        m_eventsProcessed++;
    }
}

1.6 feedEvent() - Injection thread-safe

void InputModule::feedEvent(const void* nativeEvent) {
    const SDL_Event* sdlEvent = static_cast<const SDL_Event*>(nativeEvent);

    std::lock_guard<std::mutex> lock(m_bufferMutex);
    m_eventBuffer.push_back(*sdlEvent);
}

1.7 Configuration JSON

{
  "backend": "sdl",
  "enableMouse": true,
  "enableKeyboard": true,
  "enableGamepad": false,
  "logLevel": "info"
}

1.8 CMakeLists.txt

# modules/InputModule/CMakeLists.txt

add_library(InputModule SHARED
    InputModule.cpp
    Core/InputState.cpp
    Core/InputConverter.cpp
    Backends/SDLBackend.cpp
)

target_include_directories(InputModule
    PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
    PRIVATE ${CMAKE_SOURCE_DIR}/include
)

target_link_libraries(InputModule
    PRIVATE
        GroveEngine::impl
        SDL2::SDL2
        nlohmann_json::nlohmann_json
        spdlog::spdlog
)

# Install
install(TARGETS InputModule
    LIBRARY DESTINATION modules
    RUNTIME DESTINATION modules
)

1.9 Test Phase 1

Créer: tests/visual/test_30_input_module.cpp

// Test basique : Afficher les events dans la console
int main() {
    // Setup SDL + modules
    SDL_Init(SDL_INIT_VIDEO);
    SDL_Window* window = SDL_CreateWindow(...);

    auto& ioManager = IntraIOManager::getInstance();
    auto inputIO = ioManager.createInstance("input_module");
    auto testIO = ioManager.createInstance("test_controller");

    // Load InputModule
    ModuleLoader inputLoader;
    auto inputModule = inputLoader.load("../modules/InputModule.dll", "input_module");

    JsonDataNode config("config");
    config.setString("backend", "sdl");
    inputModule->setConfiguration(config, inputIO.get(), nullptr);

    // Subscribe to all input events
    testIO->subscribe("input:mouse:move");
    testIO->subscribe("input:mouse:button");
    testIO->subscribe("input:keyboard:key");

    // Main loop
    bool running = true;
    while (running) {
        // 1. Poll SDL events
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) running = false;

            // 2. Feed to InputModule
            inputModule->feedEvent(&event);  // ← API spéciale
        }

        // 3. Process InputModule
        JsonDataNode input("input");
        inputModule->process(input);

        // 4. Check IIO messages
        while (testIO->hasMessages() > 0) {
            auto msg = testIO->pullMessage();
            std::cout << "Event: " << msg.topic << "\n";

            if (msg.topic == "input:mouse:move") {
                int x = msg.data->getInt("x", 0);
                int y = msg.data->getInt("y", 0);
                std::cout << "  Mouse: " << x << ", " << y << "\n";
            }
        }

        SDL_Delay(16);  // ~60fps
    }

    inputModule->shutdown();
    SDL_DestroyWindow(window);
    SDL_Quit();
}

Résultat attendu:

Event: input:mouse:move
  Mouse: 320, 240
Event: input:mouse:button
  Button: 0, Pressed: true
Event: input:keyboard:key
  Scancode: 44 (Space), Pressed: true

Phase 2: Gamepad Support (Optionnel)

Fichiers:

  • Backends/SDLGamepadBackend.cpp/h

Topics:

  • input:gamepad:button
  • input:gamepad:axis
  • input:gamepad:connected

Test: test_31_input_gamepad.cpp


Phase 3: Integration avec UIModule

Test: tests/integration/IT_015_input_ui_integration.cpp

Objectif: Valider l'intégration complète de la chaîne input → UI → render

Pipeline testé:

SDL_Event → InputModule → IIO → UIModule → IIO → BgfxRenderer

Scénarios de test:

  1. Mouse Input Flow

    • Simule SDL_MOUSEMOTION → Vérifie input:mouse:move publié
    • Simule SDL_MOUSEBUTTONDOWN/UP → Vérifie input:mouse:button publié
    • Vérifie que UIModule détecte le hover (ui:hover)
    • Vérifie que UIModule détecte le click (ui:click, ui:action)
  2. Keyboard Input Flow

    • Simule SDL_KEYDOWN/UP → Vérifie input:keyboard:key publié
    • Vérifie que UIModule peut recevoir les événements clavier
  3. End-to-End Verification

    • InputModule publie correctement les events IIO
    • UIModule consomme les events et génère des events UI
    • BgfxRenderer (mode headless) reçoit les commandes de rendu
    • Pas de perte d'événements dans le pipeline

Métriques vérifiées:

  • Nombre d'événements input publiés (mouse moves, clicks, keys)
  • Nombre d'événements UI générés (clicks, hovers, actions)
  • Health status de l'InputModule (events processed, frames)

Usage:

# Run integration test
cd build
ctest -R InputUIIntegration --output-on-failure

# Or run directly
./IT_015_input_ui_integration

Résultat attendu:

✅ InputModule correctly published input events
✅ UIModule correctly processed input events
✅ IT_015: Integration test PASSED

Dépendances

  • GroveEngine Core - IModule, IIO, IDataNode
  • SDL2 - Pour la Phase 1 (backend SDL)
  • nlohmann/json - Parsing JSON config
  • spdlog - Logging

Tests

Test Description Phase
test_30_input_module Test basique InputModule seul 1
test_31_input_gamepad Test gamepad 2
IT_015_input_ui_integration InputModule + UIModule + BgfxRenderer 3

Hot-Reload Support

getState()

std::unique_ptr<IDataNode> InputModule::getState() {
    auto state = std::make_unique<JsonDataNode>("state");

    // Mouse state
    state->setInt("mouseX", m_state->mouseX);
    state->setInt("mouseY", m_state->mouseY);

    // Buffered events (important pour pas perdre des events pendant reload)
    std::lock_guard<std::mutex> lock(m_bufferMutex);
    state->setInt("bufferedEventCount", m_eventBuffer.size());

    return state;
}

setState()

void InputModule::setState(const IDataNode& state) {
    m_state->mouseX = state.getInt("mouseX", 0);
    m_state->mouseY = state.getInt("mouseY", 0);

    // Note: On ne peut pas restaurer le buffer d'events (SDL_Event non sérialisable)
    // C'est acceptable car on perd au max 1 frame d'events
}

Performance

Objectifs:

  • < 0.1ms par frame pour process() (100 events/frame max)
  • 0 allocation dynamique dans process() (sauf IIO messages)
  • Thread-safe feedEvent() avec lock minimal

Profiling:

std::unique_ptr<IDataNode> InputModule::getHealthStatus() {
    auto health = std::make_unique<JsonDataNode>("health");
    health->setString("status", "healthy");
    health->setInt("frameCount", m_frameCount);
    health->setInt("eventsProcessed", m_eventsProcessed);
    health->setDouble("eventsPerFrame", m_eventsProcessed / (double)m_frameCount);
    return health;
}

Usage dans un vrai jeu

// Game main.cpp
int main() {
    // Setup modules
    auto moduleSystem = ModuleSystemFactory::create("sequential");
    auto& ioManager = IntraIOManager::getInstance();

    // Load modules
    auto inputModule = loadModule("InputModule.dll");
    auto uiModule = loadModule("UIModule.dll");
    auto gameModule = loadModule("MyGameLogic.dll");
    auto rendererModule = loadModule("BgfxRenderer.dll");

    // Register (ordre important!)
    moduleSystem->registerModule("input", std::move(inputModule));    // 1er
    moduleSystem->registerModule("ui", std::move(uiModule));          // 2ème
    moduleSystem->registerModule("game", std::move(gameModule));      // 3ème
    moduleSystem->registerModule("renderer", std::move(rendererModule)); // 4ème

    // Get raw pointer to InputModule (pour feedEvent)
    InputModule* inputModulePtr = /* ... via queryModule ou autre ... */;

    // Main loop
    while (running) {
        // 1. Poll inputs
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) running = false;
            inputModulePtr->feedEvent(&event);
        }

        // 2. Process all modules (ordre garanti)
        moduleSystem->processModules(deltaTime);

        // InputModule publie → UIModule consomme → Renderer affiche
    }
}

Fichiers à créer

modules/InputModule/
├── CMakeLists.txt                 # Build configuration
├── InputModule.h                  # Module principal header
├── InputModule.cpp                # Module principal implementation
├── Core/
│   ├── InputState.h               # État des inputs
│   ├── InputState.cpp
│   ├── InputConverter.h           # Generic → IIO
│   └── InputConverter.cpp
└── Backends/
    ├── SDLBackend.h               # SDL → Generic
    └── SDLBackend.cpp

tests/visual/
└── test_30_input_module.cpp       # Test basique

tests/integration/
└── IT_015_input_ui_integration.cpp  # Test avec UIModule

Estimation

Phase Complexité Temps estimé
1.1-1.3 Moyenne 2-3h (structure + backend)
1.4-1.5 Facile 1-2h (converter + process)
1.6-1.9 Facile 1-2h (config + test)
Total Phase 1 4-7h InputModule production-ready
Phase 2 Moyenne 2-3h (gamepad)
Phase 3 Facile 1h (integration test)

Ordre recommandé

  1. Créer structure (CMakeLists, headers vides)
  2. InputState (simple, pas de dépendances)
  3. SDLBackend (conversion SDL → Generic)
  4. InputConverter (conversion Generic → IIO)
  5. InputModule::process() (pipeline complet)
  6. InputModule::feedEvent() (thread-safe buffer)
  7. Test basique (test_30_input_module.cpp)
  8. Test integration (avec UIModule)

On commence ?

Prêt à implémenter la Phase 1 ! 🚀