Skip to content

Input and Control

Handling user input is essential for any interactive game. This guide covers how to read and process input from buttons (ESP32) or keyboard (Native) in PixelRoot32.

Input Configuration

Before you can read input, you need to configure the InputManager. This is done when creating the Engine:

ESP32 Configuration

#include <input/InputConfig.h>

// InputConfig(buttonCount, UP, DOWN, LEFT, RIGHT, A, B)
pr32::input::InputConfig inputConfig(
    6,      // Total number of buttons
    32,     // UP button GPIO pin
    27,     // DOWN button GPIO pin
    33,     // LEFT button GPIO pin
    14,     // RIGHT button GPIO pin
    13,     // A button GPIO pin
    12      // B button GPIO pin
);

Native (PC) Configuration

#include <SDL2/SDL.h>
#include <input/InputConfig.h>

// InputConfig(buttonCount, UP, DOWN, LEFT, RIGHT, A, B)
pr32::input::InputConfig inputConfig(
    6,                      // Total number of buttons
    SDL_SCANCODE_UP,        // UP key
    SDL_SCANCODE_DOWN,      // DOWN key
    SDL_SCANCODE_LEFT,      // LEFT key
    SDL_SCANCODE_RIGHT,     // RIGHT key
    SDL_SCANCODE_SPACE,     // A button (Space)
    SDL_SCANCODE_RETURN     // B button (Enter)
);

Reading Input

Access the InputManager through the Engine:

auto& input = engine.getInputManager();

Input States

The InputManager provides four different ways to check button state:

1. isButtonPressed()

Returns true only on the frame when the button was just pressed:

if (input.isButtonPressed(4)) { // Button A (index 4)
    // This code runs only once when button is first pressed
    jump();
}

Use for: Actions that should trigger once per press (jump, shoot, select menu item).

2. isButtonReleased()

Returns true only on the frame when the button was just released:

if (input.isButtonReleased(4)) {
    // This code runs only once when button is released
    stopCharging();
}

Use for: Actions that trigger on release (charge attacks, menu confirmation).

3. isButtonDown()

Returns true while the button is currently held down:

if (input.isButtonDown(2)) { // LEFT button (index 2)
    // This code runs every frame while button is held
    playerX -= speed * (deltaTime * 0.001f);
}

Use for: Continuous actions (movement, holding, charging).

4. isButtonClicked()

Returns true when the button was pressed and then released:

if (input.isButtonClicked(4)) {
    // This code runs once per click (press + release cycle)
    toggleMenu();
}

Use for: Toggle actions, menu selections, click interactions.

Button Indices

The button indices correspond to the order in InputConfig:

  • Index 0: UP
  • Index 1: DOWN
  • Index 2: LEFT
  • Index 3: RIGHT
  • Index 4: A button
  • Index 5: B button

For convenience, you can define constants:

namespace Buttons {
    constexpr uint8_t UP = 0;
    constexpr uint8_t DOWN = 1;
    constexpr uint8_t LEFT = 2;
    constexpr uint8_t RIGHT = 3;
    constexpr uint8_t A = 4;
    constexpr uint8_t B = 5;
}

// Usage
if (input.isButtonPressed(Buttons::A)) {
    // ...
}

Character Control Example

Here's a complete example of character movement:

#include <core/Actor.h>
#include <graphics/Renderer.h>
#include <graphics/Color.h>

class PlayerActor : public pixelroot32::core::Actor {
public:
    float speed = 100.0f; // pixels per second

    PlayerActor(float x, float y)
        : Actor(x, y, 16, 16) {
        setRenderLayer(1);
    }

    void update(unsigned long deltaTime) override {
        auto& input = engine.getInputManager();
        float dt = deltaTime * 0.001f; // Convert to seconds

        // Horizontal movement
        if (input.isButtonDown(Buttons::LEFT)) {
            x -= speed * dt;
        }
        if (input.isButtonDown(Buttons::RIGHT)) {
            x += speed * dt;
        }

        // Vertical movement
        if (input.isButtonDown(Buttons::UP)) {
            y -= speed * dt;
        }
        if (input.isButtonDown(Buttons::DOWN)) {
            y += speed * dt;
        }

        // Keep player on screen
        if (x < 0) x = 0;
        if (x > 224) x = 224;
        if (y < 0) y = 0;
        if (y > 224) y = 224;
    }

    void draw(pixelroot32::graphics::Renderer& renderer) override {
        renderer.drawFilledRectangle(
            static_cast<int>(x),
            static_cast<int>(y),
            width,
            height,
            pixelroot32::graphics::Color::Cyan
        );
    }

    pixelroot32::core::Rect getHitBox() override {
        return {x, y, width, height};
    }

    void onCollision(pixelroot32::core::Actor* other) override {
        // Handle collisions
    }
};

Jumping Example

For platformer-style jumping:

class PlatformerPlayer : public pixelroot32::core::PhysicsActor {
public:
    bool canJump = true;
    float jumpForce = 200.0f;

    void update(unsigned long deltaTime) override {
        auto& input = engine.getInputManager();
        float dt = deltaTime * 0.001f;

        // Horizontal movement
        float moveSpeed = 80.0f;
        if (input.isButtonDown(Buttons::LEFT)) {
            setVelocity(-moveSpeed, vy);
        } else if (input.isButtonDown(Buttons::RIGHT)) {
            setVelocity(moveSpeed, vy);
        } else {
            setVelocity(0, vy);
        }

        // Jump (only on press, and only if on ground)
        if (input.isButtonPressed(Buttons::A) && canJump) {
            setVelocity(vx, -jumpForce);
            canJump = false;
        }

        // Check if on ground (for jump reset)
        auto collisionInfo = getWorldCollisionInfo();
        if (collisionInfo.bottom) {
            canJump = true;
        }

        PhysicsActor::update(deltaTime);
    }
};

Shooting Example

For shooting projectiles:

class ShooterActor : public pixelroot32::core::Actor {
private:
    bool fireInputReady = true;

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

        // Shooting (with cooldown)
        if (input.isButtonPressed(Buttons::A) && fireInputReady) {
            shoot();
            fireInputReady = false;
        }

        // Reset fire input when button is released
        if (input.isButtonReleased(Buttons::A)) {
            fireInputReady = true;
        }
    }

    void shoot() {
        // Create projectile
        // ...
    }
};

For menu navigation:

class MenuScene : public pixelroot32::core::Scene {
private:
    int selectedIndex = 0;
    static const int MENU_ITEMS = 3;

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

        // Navigate menu
        if (input.isButtonPressed(Buttons::UP)) {
            selectedIndex--;
            if (selectedIndex < 0) selectedIndex = MENU_ITEMS - 1;
        }

        if (input.isButtonPressed(Buttons::DOWN)) {
            selectedIndex++;
            if (selectedIndex >= MENU_ITEMS) selectedIndex = 0;
        }

        // Select menu item
        if (input.isButtonPressed(Buttons::A)) {
            selectMenuItem(selectedIndex);
        }

        Scene::update(deltaTime);
    }
};

Best Practices

Frame-Rate Independence

Always multiply movement by delta time:

// ✅ GOOD: Framerate-independent
x += speed * (deltaTime * 0.001f);

// ❌ BAD: Framerate-dependent
x += speed;

Input Debouncing

For actions that should only trigger once:

// Use isButtonPressed() instead of isButtonDown()
if (input.isButtonPressed(Buttons::A)) {
    // Triggers once per press
}

Input Buffering

For responsive controls, you can buffer input:

class InputBuffer {
    uint8_t bufferedInput = 0;

public:
    void update(const InputManager& input) {
        if (input.isButtonPressed(Buttons::A)) {
            bufferedInput = Buttons::A;
        }
    }

    bool hasBufferedInput() const { return bufferedInput != 0; }
    uint8_t consumeInput() {
        uint8_t result = bufferedInput;
        bufferedInput = 0;
        return result;
    }
};

Multiple Input Methods

Support both button presses and held buttons:

// Allow both tap and hold for rapid fire
if (input.isButtonPressed(Buttons::A) || 
    (input.isButtonDown(Buttons::A) && rapidFireTimer <= 0)) {
    shoot();
    rapidFireTimer = RAPID_FIRE_DELAY;
}

Common Patterns

Directional Input

float getHorizontalInput() {
    auto& input = engine.getInputManager();
    float dir = 0.0f;
    if (input.isButtonDown(Buttons::LEFT)) dir -= 1.0f;
    if (input.isButtonDown(Buttons::RIGHT)) dir += 1.0f;
    return dir;
}

float getVerticalInput() {
    auto& input = engine.getInputManager();
    float dir = 0.0f;
    if (input.isButtonDown(Buttons::UP)) dir -= 1.0f;
    if (input.isButtonDown(Buttons::DOWN)) dir += 1.0f;
    return dir;
}

Input State Machine

For complex input handling:

enum class InputState {
    IDLE,
    PRESSED,
    HELD,
    RELEASED
};

InputState getButtonState(const InputManager& input, uint8_t button) {
    if (input.isButtonPressed(button)) return InputState::PRESSED;
    if (input.isButtonDown(button)) return InputState::HELD;
    if (input.isButtonReleased(button)) return InputState::RELEASED;
    return InputState::IDLE;
}

Troubleshooting

Button Not Responding

  • Check button indices match your InputConfig
  • Verify GPIO pins (ESP32) or scancodes (Native) are correct
  • Ensure InputManager is being updated (happens automatically in Engine)

Input Feels Laggy

  • Ensure you're using deltaTime for movement
  • Check that input is read in update(), not draw()
  • Verify framerate is stable

Multiple Triggers

  • Use isButtonPressed() instead of isButtonDown() for one-time actions
  • Implement input buffering or cooldown timers

Next Steps

Now that you can handle input, learn about: - Audio - Add sound effects and music - Physics and Collisions - Make objects interact - User Interface - Create menus and HUDs


See also: - API Reference - InputManager - API Reference - InputConfig - Manual - Input Overview