Skip to content

Physics and Collisions

PixelRoot32 provides a physics system for moving objects and collision detection. This guide covers PhysicsActor for automatic physics and the collision system for detecting interactions between objects.

PhysicsActor

A PhysicsActor is an Actor that automatically handles physics: velocity, gravity, friction, and world boundary collisions.

Creating a PhysicsActor

#include <core/PhysicsActor.h>

class Ball : public pixelroot32::core::PhysicsActor {
public:
    Ball(float x, float y, float radius)
        : PhysicsActor(x, y, radius * 2, radius * 2) {
        setRenderLayer(1);

        // Set physics properties
        setRestitution(0.8f);  // Bounciness (0.8 = 80% bounce)
        setFriction(0.1f);     // Friction (0.1 = slight friction)

        // Set world boundaries
        setWorldSize(240, 240);
    }

    void update(unsigned long deltaTime) override {
        // Apply gravity
        // (PhysicsActor handles this automatically, but you can add custom forces)

        // Call parent update to apply physics
        PhysicsActor::update(deltaTime);
    }

    void draw(pixelroot32::graphics::Renderer& renderer) override {
        int radius = width / 2;
        renderer.drawFilledCircle(
            static_cast<int>(x + radius),
            static_cast<int>(y + radius),
            radius,
            pixelroot32::graphics::Color::White
        );
    }

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

    void onCollision(pixelroot32::core::Actor* other) override {
        // Handle collision with other actors
    }

    void onWorldCollision() override {
        // Called when hitting world boundaries
        // You can play a sound effect here, for example
    }
};

Physics Properties

Velocity

Set the velocity directly:

ball->setVelocity(100.0f, -50.0f); // Move right at 100 px/s, up at 50 px/s

Or modify existing velocity:

float vx = ball->vx; // Access velocity components (protected, use getter if needed)
ball->setVelocity(vx + 10.0f, ball->vy); // Accelerate horizontally

Restitution (Bounciness)

Controls how much energy is conserved in collisions:

ball->setRestitution(1.0f);  // Perfect bounce (no energy loss)
ball->setRestitution(0.5f);  // 50% energy loss
ball->setRestitution(0.0f);  // No bounce (stops on impact)

Friction

Applies gradual velocity reduction:

ball->setFriction(0.0f);  // No friction (slides forever)
ball->setFriction(0.5f);  // Moderate friction
ball->setFriction(1.0f);  // High friction (stops quickly)

World Boundaries

Set the playable area:

// Set world size (used as default boundaries)
ball->setWorldSize(240, 240);

// Or set custom boundaries
pixelroot32::core::LimitRect limits(10, 10, 230, 230); // Left, Top, Right, Bottom
ball->setLimits(limits);

World Collision Detection

Check if the actor hit world boundaries:

void update(unsigned long deltaTime) override {
    PhysicsActor::update(deltaTime);

    auto collisionInfo = getWorldCollisionInfo();

    if (collisionInfo.left || collisionInfo.right) {
        // Hit side walls
    }

    if (collisionInfo.top || collisionInfo.bottom) {
        // Hit top/bottom walls
    }
}

Collision System

The collision system detects when Actors overlap and triggers callbacks.

Collision Layers

Use bit flags to organize actors into groups:

// Define layers (typically in GameLayers.h)
namespace Layers {
    constexpr uint16_t PLAYER = 0x0001;    // Bit 0
    constexpr uint16_t ENEMY = 0x0002;     // Bit 1
    constexpr uint16_t PROJECTILE = 0x0004; // Bit 2
    constexpr uint16_t WALL = 0x0008;      // Bit 3
    constexpr uint16_t PICKUP = 0x0010;    // Bit 4
}

Setting Up Collisions

Configure each actor's collision layer and mask:

class PlayerActor : public pixelroot32::core::Actor {
public:
    PlayerActor(float x, float y)
        : Actor(x, y, 16, 16) {
        // This actor belongs to the PLAYER layer
        setCollisionLayer(Layers::PLAYER);

        // This actor can collide with ENEMY, WALL, and PICKUP
        setCollisionMask(Layers::ENEMY | Layers::WALL | Layers::PICKUP);
    }

    // ... rest of implementation
};

class EnemyActor : public pixelroot32::core::Actor {
public:
    EnemyActor(float x, float y)
        : Actor(x, y, 16, 16) {
        // This actor belongs to the ENEMY layer
        setCollisionLayer(Layers::ENEMY);

        // This actor can collide with PLAYER and PROJECTILE
        setCollisionMask(Layers::PLAYER | Layers::PROJECTILE);
    }

    // ... rest of implementation
};

Collision Detection

Collisions are automatically detected by the Scene's CollisionSystem. You handle collisions in onCollision():

void PlayerActor::onCollision(pixelroot32::core::Actor* other) override {
    // Check what we collided with
    if (other->isInLayer(Layers::ENEMY)) {
        // Hit an enemy - take damage
        takeDamage();
    } else if (other->isInLayer(Layers::PICKUP)) {
        // Hit a pickup - collect it
        collectPickup(other);
    } else if (other->isInLayer(Layers::WALL)) {
        // Hit a wall - stop movement
        stopMovement();
    }
}

Hitbox

Define the collision shape:

pixelroot32::core::Rect getHitBox() override {
    // Simple AABB (Axis-Aligned Bounding Box)
    return {x, y, width, height};

    // Or use a smaller hitbox for more forgiving collisions
    // return {x + 2, y + 2, width - 4, height - 4};
}

Complete Example: Bouncing Ball

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

class BouncingBall : public pixelroot32::core::PhysicsActor {
public:
    BouncingBall(float x, float y, float radius)
        : PhysicsActor(x, y, radius * 2, radius * 2) {
        setRenderLayer(1);

        // Physics setup
        setRestitution(0.9f);  // Very bouncy
        setFriction(0.05f);    // Low friction
        setWorldSize(240, 240); // World boundaries

        // Initial velocity
        setVelocity(50.0f, -30.0f);
    }

    void update(unsigned long deltaTime) override {
        // Apply gravity
        float gravity = 200.0f; // pixels per second squared
        float dt = deltaTime * 0.001f;
        setVelocity(vx, vy + gravity * dt);

        // Update physics
        PhysicsActor::update(deltaTime);
    }

    void draw(pixelroot32::graphics::Renderer& renderer) override {
        int radius = width / 2;
        renderer.drawFilledCircle(
            static_cast<int>(x + radius),
            static_cast<int>(y + radius),
            radius,
            pixelroot32::graphics::Color::Cyan
        );
    }

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

    void onCollision(pixelroot32::core::Actor* other) override {
        // Bounce off other objects
        // (PhysicsActor handles world boundaries automatically)
    }

    void onWorldCollision() override {
        // Play bounce sound when hitting walls
        // (Implementation depends on your audio setup)
    }
};

Complete Example: Platformer Player

class PlatformerPlayer : public pixelroot32::core::PhysicsActor {
private:
    bool onGround = false;
    float jumpForce = 250.0f;
    float moveSpeed = 100.0f;

public:
    PlatformerPlayer(float x, float y)
        : PhysicsActor(x, y, 16, 16) {
        setRenderLayer(1);
        setFriction(0.3f);  // Ground friction
        setWorldSize(240, 240);

        // Collision setup
        setCollisionLayer(Layers::PLAYER);
        setCollisionMask(Layers::ENEMY | Layers::PLATFORM);
    }

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

        // Horizontal movement
        float moveDir = 0.0f;
        if (input.isButtonDown(Buttons::LEFT)) moveDir -= 1.0f;
        if (input.isButtonDown(Buttons::RIGHT)) moveDir += 1.0f;

        setVelocity(moveDir * moveSpeed, vy);

        // Apply gravity
        float gravity = 300.0f;
        setVelocity(vx, vy + gravity * dt);

        // Jump
        if (input.isButtonPressed(Buttons::A) && onGround) {
            setVelocity(vx, -jumpForce);
            onGround = false;
        }

        // Update physics
        PhysicsActor::update(deltaTime);

        // Check if on ground
        auto collisionInfo = getWorldCollisionInfo();
        onGround = collisionInfo.bottom;

        // Also check collision with platforms
        // (This would be handled in onCollision)
    }

    void onCollision(pixelroot32::core::Actor* other) override {
        if (other->isInLayer(Layers::PLATFORM)) {
            // Land on platform
            auto platformRect = other->getHitBox();
            if (y + height <= platformRect.y + 5) { // Within 5 pixels of top
                y = platformRect.y - height;
                vy = 0;
                onGround = true;
            }
        } else if (other->isInLayer(Layers::ENEMY)) {
            // Hit enemy
            takeDamage();
        }
    }

    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};
    }
};

Sweep Tests

For fast-moving projectiles, use sweep tests to detect collisions between positions:

#include <physics/CollisionPrimitives.h>

bool checkProjectileHit(PhysicsActor* projectile, Actor* target) {
    // Get previous and current positions
    float prevX = projectile->x - (projectile->vx * deltaTime * 0.001f);
    float prevY = projectile->y - (projectile->vy * deltaTime * 0.001f);

    // Create circles for sweep test
    pixelroot32::physics::Circle startCircle;
    startCircle.x = prevX + projectile->width / 2;
    startCircle.y = prevY + projectile->height / 2;
    startCircle.radius = projectile->width / 2;

    pixelroot32::physics::Circle endCircle;
    endCircle.x = projectile->x + projectile->width / 2;
    endCircle.y = projectile->y + projectile->height / 2;
    endCircle.radius = projectile->width / 2;

    // Get target rectangle
    auto targetRect = target->getHitBox();

    // Perform sweep test
    float tHit;
    if (pixelroot32::physics::sweepCircleVsRect(
        startCircle, endCircle, targetRect, tHit)) {
        // Collision detected at time tHit (0.0 = at start, 1.0 = at end)
        return true;
    }

    return false;
}

Best Practices

Collision Layers

  • Plan your layers: Design the layer system before coding
  • Use bit flags: Makes combining layers easy with | operator
  • Keep it simple: Don't create too many layers
  • Document layers: Create a GameLayers.h file with all definitions

Physics

  • Use appropriate values: Test gravity, speed, and forces
  • Frame-rate independence: Always use deltaTime
  • Limit world size: Keep boundaries reasonable
  • Test on hardware: Physics may behave differently on ESP32 vs PC

Performance

  • Limit active actors: Fewer actors = faster collision checks
  • Use layers efficiently: Reduce unnecessary collision pairs
  • Simple hitboxes: AABB is fast, complex shapes are slow
  • Sweep tests sparingly: Only for fast-moving objects

Common Patterns

Collision Layer Helper

namespace CollisionLayers {
    constexpr uint16_t PLAYER = 0x0001;
    constexpr uint16_t ENEMY = 0x0002;
    constexpr uint16_t PROJECTILE = 0x0004;
    constexpr uint16_t WALL = 0x0008;
    constexpr uint16_t PICKUP = 0x0010;

    // Helper to check if actor is in specific layer
    bool isPlayer(Actor* actor) {
        return actor->isInLayer(PLAYER);
    }

    bool isEnemy(Actor* actor) {
        return actor->isInLayer(ENEMY);
    }
}

Platform Collision

void PlatformerPlayer::onCollision(Actor* other) override {
    if (other->isInLayer(Layers::PLATFORM)) {
        auto platform = other->getHitBox();

        // Check if landing on top of platform
        float playerBottom = y + height;
        float platformTop = platform.y;

        if (playerBottom <= platformTop + 5 && vy > 0) {
            // Land on platform
            y = platformTop - height;
            vy = 0;
            onGround = true;
        }
    }
}

Troubleshooting

Collisions Not Detected

  • Verify collision layers and masks overlap
  • Check that actors are added to the scene
  • Ensure Scene::update() is called
  • Verify hitboxes are correct

Physics Too Fast/Slow

  • Adjust values based on deltaTime
  • Check gravity and velocity values
  • Test on actual hardware (ESP32 may run slower)

Objects Passing Through

  • Use sweep tests for fast objects
  • Increase collision detection frequency
  • Check hitbox sizes match visual size

Next Steps

Now that you understand physics and collisions, learn about: - User Interface - Create menus and HUDs - Advanced Graphics - Advanced sprite techniques - Camera and Scrolling - Create scrolling levels


See also: - API Reference - PhysicsActor - API Reference - CollisionSystem - Manual - Physics Overview - Manual - Collision Detection