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:
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
// ...
}
};
Menu Navigation Example¶
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
deltaTimefor movement - Check that input is read in
update(), notdraw() - Verify framerate is stable
Multiple Triggers¶
- Use
isButtonPressed()instead ofisButtonDown()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