Skip to content

Input System Guide

Overview

The PixelRoot32 input system provides unified button handling across ESP32 and PC (native/SDL2) platforms. It abstracts hardware-specific input (GPIO pins on ESP32, keyboard on PC) into a consistent API for game development.

Architecture

The input system is built around the InputManager class which: - Polls button states every frame - Tracks button presses, releases, and hold states - Provides both polling and event-based interfaces - Works identically across all supported platforms

Button Mapping

PixelRoot32 uses a standard 6-button layout:

Button Index Name ESP32 (GPIO) PC (SDL Key)
0 UP Configurable SDL_SCANCODE_UP
1 DOWN Configurable SDL_SCANCODE_DOWN
2 LEFT Configurable SDL_SCANCODE_LEFT
3 RIGHT Configurable SDL_SCANCODE_RIGHT
4 A (Action) Configurable SDL_SCANCODE_SPACE
5 B (Action) Configurable SDL_SCANCODE_RETURN

Configuration

ESP32 Hardware Input

#include <input/InputConfig.h>

// Configure 6 buttons with specific GPIO pins
pixelroot32::input::InputConfig inputConfig(
    6,      // button count
    32,     // UP pin
    27,     // DOWN pin
    33,     // LEFT pin
    14,     // RIGHT pin
    13,     // A button pin
    12      // B button pin
);

// Create engine with input config
pixelroot32::core::Engine engine(displayConfig, inputConfig, audioConfig);

PC Native Input (SDL2)

#include <input/InputConfig.h>

// Configure with SDL scancodes
pixelroot32::input::InputConfig inputConfig(
    6,                      // button count
    SDL_SCANCODE_UP,        // UP
    SDL_SCANCODE_DOWN,      // DOWN
    SDL_SCANCODE_LEFT,      // LEFT
    SDL_SCANCODE_RIGHT,     // RIGHT
    SDL_SCANCODE_SPACE,     // A button
    SDL_SCANCODE_RETURN     // B button
);

Input Patterns

1. Polling (Every Frame)

Check button state in your update() method:

void MyScene::update(unsigned long deltaTime) {
    auto& input = engine.getInputManager();

    // Check if button is currently pressed
    if (input.isButtonPressed(0)) {  // UP
        player->move(0, -speed * deltaTime);
    }
    if (input.isButtonPressed(2)) {  // LEFT
        player->move(-speed * deltaTime, 0);
    }

    // Check for "just pressed" (single trigger)
    if (input.isButtonJustPressed(4)) {  // A button
        player->jump();
    }

    // Check for "just released"
    if (input.isButtonJustReleased(4)) {
        player->endJump();
    }
}

2. State-Based Input

Query specific button states:

auto& input = engine.getInputManager();

// Was button pressed this frame?
bool pressed = input.isButtonPressed(buttonIndex);

// Was button just pressed this frame (transition from released)?
bool justPressed = input.isButtonJustPressed(buttonIndex);

// Was button just released this frame?
bool justReleased = input.isButtonJustReleased(buttonIndex);

3. D-Pad Movement Helper

Common pattern for directional movement:

void updatePlayerMovement() {
    auto& input = engine.getInputManager();

    float dx = 0, dy = 0;
    float speed = 100.0f;  // pixels per second

    if (input.isButtonPressed(0)) dy -= 1;  // UP
    if (input.isButtonPressed(1)) dy += 1;  // DOWN
    if (input.isButtonPressed(2)) dx -= 1;  // LEFT
    if (input.isButtonPressed(3)) dx += 1;  // RIGHT

    // Normalize diagonal movement
    if (dx != 0 && dy != 0) {
        dx *= 0.707f;  // 1/sqrt(2)
        dy *= 0.707f;
    }

    player->velocity.x = dx * speed;
    player->velocity.y = dy * speed;
}

Platform Differences

ESP32

  • Debouncing: Handled automatically by the OneButton library
  • Pull-up: Configure your GPIO pins with internal pull-up resistors
  • Hardware: Physical buttons connected to specified GPIO pins
// platformio.ini - Define pins
build_flags = 
    -D BUTTON_UP=32
    -D BUTTON_DOWN=27

PC Native

  • Keyboard Focus: Input works when SDL window has focus
  • Key Repeat: Disabled by default (use isButtonPressed for repeat)
  • Multiple Keys: All 6 buttons can be pressed simultaneously

Best Practices

1. Use isButtonJustPressed for Actions

// ✅ GOOD: Single jump per press
if (input.isButtonJustPressed(4)) {
    player->jump();
}

// ❌ BAD: Would jump every frame while held
if (input.isButtonPressed(4)) {
    player->jump();
}

2. Normalize Diagonal Movement

// Always normalize to prevent faster diagonal movement
if (dx != 0 && dy != 0) {
    dx *= 0.707f;
    dy *= 0.707f;
}

3. Handle Menu Navigation

// Menu navigation with repeat delay
unsigned long lastNavTime = 0;
const unsigned long NAV_DELAY = 200;  // ms

void updateMenu() {
    auto& input = engine.getInputManager();
    unsigned long now = millis();

    if (now - lastNavTime > NAV_DELAY) {
        if (input.isButtonPressed(0)) {  // UP
            menu->moveSelection(-1);
            lastNavTime = now;
        }
        if (input.isButtonPressed(1)) {  // DOWN
            menu->moveSelection(1);
            lastNavTime = now;
        }
    }

    // Select with A button
    if (input.isButtonJustPressed(4)) {
        menu->select();
    }
}

4. Virtual Input (Code-Driven)

Create virtual button presses for AI or demo modes:

// Simulate button press programmatically
void triggerVirtualInput(int buttonIndex) {
    // Access internal state (advanced usage)
    // Useful for AI players or recorded replays
}

Advanced Features

Input Buffering

For fighting games or precise platformers, you may want to buffer inputs:

struct InputBuffer {
    static const int BUFFER_SIZE = 8;
    struct BufferedInput {
        int button;
        unsigned long time;
    };
    BufferedInput buffer[BUFFER_SIZE];
    int count = 0;

    void record(int button) {
        if (count < BUFFER_SIZE) {
            buffer[count++] = {button, millis()};
        }
    }

    bool checkCombo(const int* combo, int length, unsigned long window) {
        // Check if combo was entered within time window
        // ... implementation
    }
};

Axis-Based Input (Analog)

For analog sticks or variable pressure:

// Get intensity if using analog input (advanced)
float getAnalogValue(int buttonIndex) {
    // Returns 0.0 to 1.0 for analog buttons
    // Currently not implemented in base InputManager
    return input.isButtonPressed(buttonIndex) ? 1.0f : 0.0f;
}

Troubleshooting

Buttons Not Responding (ESP32)

  1. Check wiring: Verify buttons are connected to correct GPIO pins
  2. Pull-up resistors: Enable internal pull-ups or add external 10kΩ resistors
  3. Pin configuration: Double-check InputConfig pin numbers
  4. Ground connection: Ensure buttons connect to GND when pressed

Keys Not Working (PC)

  1. Window focus: Click on the game window to ensure it has focus
  2. SDL initialization: Verify SDL is properly initialized
  3. Scancode vs Keycode: Use SDL_SCANCODE_* not SDLK_*

Multiple Button Presses

If you need more than 6 buttons, extend the input system:

// Custom input configuration with more buttons
class ExtendedInputConfig : public InputConfig {
    // Add support for additional buttons
    // Requires modifying the engine's input polling
};

API Reference

See API Reference - InputManager for complete method documentation.

Examples

  • Pong: Basic D-pad control (2 players)
  • Space Invaders: Simple movement + fire button
  • Snake: Directional control with wrap-around

See also: