Skip to content

Flat Solver Physics Guide

Overview

The Flat Solver is PixelRoot32's optimized 2D physics engine designed specifically for resource-constrained ESP32 microcontrollers. It provides robust collision detection and response for games without the overhead of full physics simulations like Box2D.

Key Features

  • Deterministic: Fixed 1/60s timestep for reproducible physics
  • Lightweight: Minimal memory footprint (~2KB for typical scenes)
  • Efficient: Spatial partitioning reduces collision checks
  • Flexible: Three body types (Static, Kinematic, Rigid)
  • Stable: Baumgarte stabilization prevents jitter

Architecture

The physics pipeline follows a "Flat" approach:

Detect → Solve Velocity → Integrate Position → Solve Penetration → Callbacks

Physics Body Types

Type Moved By Collisions Use Case
Static Nothing Blocks others Walls, floors, obstacles
Kinematic Script/Code Stops at obstacles Player, platforms, elevators
Rigid Physics forces Fully simulated Projectiles, debris, physics objects

Collision Shapes

PixelRoot32 supports two collision primitives:

1. AABB (Axis-Aligned Bounding Box)

actor->setShape(CollisionShape::AABB);
// Uses actor's width/height automatically

Pros: Fast, simple, perfect for tile-based games
Cons: No rotation support, approximate for circular objects

2. Circle

actor->setShape(CollisionShape::CIRCLE);
actor->setRadius(16);  // pixels

Pros: Accurate for round objects, smooth sliding
Cons: Slightly more expensive than AABB

Creating Physics Actors

Static Body (Walls, Platforms)

#include <physics/PhysicsActor.h>

class Wall : public pixelroot32::core::PhysicsActor {
public:
    Wall(float x, float y, int w, int h) 
        : PhysicsActor(x, y, w, h) {
        setBodyType(PhysicsBodyType::STATIC);
        setShape(CollisionShape::AABB);
    }
};

// Usage
auto wall = std::make_unique<Wall>(100, 200, 64, 16);
scene.addEntity(wall.get());

Kinematic Body (Player Character)

#include <physics/KinematicActor.h>

class Player : public pixelroot32::physics::KinematicActor {
public:
    Player(float x, float y) 
        : KinematicActor(x, y, 32, 32) {
        // Already set to KINEMATIC by default
        setShape(CollisionShape::AABB);
        setMass(1.0f);
    }

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

        if (input.isButtonJustPressed(4) && onFloor) {  // Jump
            setVelocity(getVelocity().x, -300);
        }

        // Horizontal movement
        float moveX = 0;
        if (input.isButtonPressed(2)) moveX = -150;  // Left
        if (input.isButtonPressed(3)) moveX = 150;   // Right

        // Apply movement with collision detection
        Vector2 motion(moveX * deltaTime / 1000.0f, 0);
        moveAndSlide(motion, Vector2(0, -1));  // Slide against walls

        // Gravity
        Vector2 gravity(0, 500 * deltaTime / 1000.0f);
        moveAndSlide(gravity, Vector2(0, -1));
    }
};

Rigid Body (Projectile)

#include <physics/PhysicsActor.h>

class Bullet : public pixelroot32::core::PhysicsActor {
public:
    Bullet(float x, float y, float vx, float vy) 
        : PhysicsActor(x, y, 8, 8) {
        setBodyType(PhysicsBodyType::RIGID);
        setShape(CollisionShape::CIRCLE);
        setRadius(4);
        setMass(0.1f);
        setVelocity(vx, vy);
        setRestitution(0.8f);  // Bouncy
    }

    void onCollision(Actor* other) override {
        if (other->type == EntityType::ACTOR) {
            // Hit something
            markForRemoval();
        }
    }
};

Movement Patterns

1. moveAndSlide() - For Characters

Slides along surfaces while maintaining contact:

// Move with wall sliding
Vector2 motion(velocity.x * dt, velocity.y * dt);
moveAndSlide(motion, Vector2(0, -1));  // Up is "floor normal"

// Check if on ground after movement
if (onFloor) {
    // Can jump
}

// Check wall contact
if (onWall) {
    // Play wall slide animation
}

Parameters: - motion: Desired movement vector (pixels) - upDirection: Vector pointing "up" (typically Vector2(0, -1))

State Variables (set after call): - onFloor: True if standing on surface - onWall: True if touching wall - onCeiling: True if touching ceiling

2. moveAndCollide() - For Precise Control

Stops at first collision, returns collision info:

KinematicCollision collision;
Vector2 motion(100, 0);  // Move right 100 pixels

if (moveAndCollide(motion, &collision)) {
    // Hit something
    Actor* hit = collision.collider;
    Vector2 normal = collision.normal;

    // Bounce off
    if (hit->isPhysicsBody()) {
        Vector2 reflect = motion.reflect(normal);
        setVelocity(reflect.x * 10, reflect.y * 10);
    }
}

When to use: Projectiles, precise platforming, pinball mechanics

Collision Detection Pipeline

Broadphase (Spatial Grid)

The engine uses a uniform grid to reduce collision checks:

  • Default Cell Size: 32 pixels
  • Entities per Cell: Max 24
  • Optimization: Only checks neighboring cells

Impact: 100 objects in a grid = ~16 checks per object vs 99 in brute force

Narrowphase (Shape Tests)

Actual collision tests between pairs:

Shape A Shape B Algorithm
AABB AABB Intersection test
Circle Circle Distance < sum of radii
Circle AABB Closest point on AABB to circle center

Continuous Collision Detection (CCD)

For fast-moving objects (bullets):

// CCD automatically enabled for circles moving > radius per frame
// Sweeps circle along velocity to prevent tunneling

Physics Configuration

Build Flags (platformio.ini)

build_flags = 
    -D VELOCITY_ITERATIONS=2      ; Solver iterations (1-4)
    -D PHYSICS_MAX_PAIRS=128      ; Max collision pairs per frame
    -D SPATIAL_GRID_CELL_SIZE=32  ; Grid cell size in pixels
    -D SPATIAL_GRID_MAX_ENTITIES_PER_CELL=24

Runtime Configuration

// Actor-specific physics properties
actor->setMass(1.0f);              // kg (rigid bodies only)
actor->setRestitution(0.5f);       // Bounciness (0-1+)
actor->setFriction(0.3f);          // Surface friction (0-1)
actor->setGravityScale(1.0f);      // Multiplier for world gravity

Performance Tips

1. Use Appropriate Body Types

// ✅ GOOD: Static for walls
wall->setBodyType(PhysicsBodyType::STATIC);

// ✅ GOOD: Kinematic for player
player->setBodyType(PhysicsBodyType::KINEMATIC);

// ✅ GOOD: Rigid for physics objects
box->setBodyType(PhysicsBodyType::RIGID);

// ❌ BAD: Don't use RIGID for everything (expensive)

2. Limit Simultaneous Rigid Bodies

// Keep rigid body count low on ESP32
// Recommended: < 16 rigid bodies
// Kinematic/Static: Up to 32 total

3. Prefer AABB Over Circle

// AABB is ~20% faster than Circle
// Use AABB for tile-based games
// Use Circle when accurate round collisions needed

4. Use Layers and Masks

// Only check collisions between relevant layers
player->layer = 1;      // Player layer
player->mask = 0b1110;  // Collides with layers 1,2,3

enemy->layer = 2;       // Enemy layer
enemy->mask = 0b1001;   // Collides with player and world

// Player and enemy won't collide (no mask overlap)

Common Patterns

Platformer Character

class PlatformerPlayer : public KinematicActor {
    float speed = 150.0f;
    float jumpSpeed = 350.0f;

public:
    void update(unsigned long deltaTime) override {
        auto& input = engine.getInputManager();
        float dt = deltaTime / 1000.0f;

        // Horizontal movement
        float moveX = 0;
        if (input.isButtonPressed(2)) moveX = -speed;
        if (input.isButtonPressed(3)) moveX = speed;

        // Jump
        if (input.isButtonJustPressed(4) && onFloor) {
            velocity.y = -jumpSpeed;
        }

        // Apply gravity
        velocity.y += 980.0f * dt;  // 9.8 m/s^2 scaled

        // Move with sliding
        Vector2 motion(velocity.x * dt, velocity.y * dt);
        moveAndSlide(motion, Vector2(0, -1));
    }
};

One-Way Platforms

class OneWayPlatform : public PhysicsActor {
public:
    void onCollision(Actor* other) override {
        // Only collide if falling downward
        if (auto* phys = dynamic_cast<PhysicsActor*>(other)) {
            if (phys->getVelocity().y < 0) {
                // Moving up - disable collision
                // (Requires custom collision handling)
            }
        }
    }
};

Moving Platforms (Kinematic)

class MovingPlatform : public KinematicActor {
    Vector2 startPos, endPos;
    float speed = 50.0f;
    float t = 0;

public:
    void update(unsigned long deltaTime) override {
        // Patrol between points
        t += speed * deltaTime / 1000.0f / (endPos - startPos).length();
        if (t > 1.0f) {
            t = 0;
            std::swap(startPos, endPos);
        }

        // Move to new position
        Vector2 target = startPos + (endPos - startPos) * t;
        Vector2 motion = target - position;
        moveAndCollide(motion, nullptr, false);  // Don't slide
    }
};

Troubleshooting

Objects Tunneling Through Walls

Problem: Fast objects pass through thin walls
Solution: Use CCD or thicker collision shapes

// Enable CCD (automatic for circles)
setShape(CollisionShape::CIRCLE);

// Or increase wall thickness
wall->width = max(wall->width, 8);  // Minimum 8 pixels

Jitter/Shake When Stacked

Problem: Objects on top of each other jitter
Solution: Use Baumgarte stabilization (enabled by default)

// Adjust if needed (in platformio.ini)
-D POSITION_RELAXATION_ITERATIONS=3

Player Gets Stuck

Problem: Character stuck in walls
Solution: Use moveAndSlide not moveAndCollide

// ✅ GOOD: Slides along walls
moveAndSlide(motion, upDirection);

// ❌ BAD: Can get stuck in corners
moveAndCollide(motion, &collision);

Performance Issues

Problem: Low FPS with many objects
Checklist: - [ ] Reduce PHYSICS_MAX_PAIRS if not needed - [ ] Use more Static bodies (cheaper) - [ ] Reduce spatial grid cell size for dense scenes - [ ] Use AABB instead of Circle where possible

API Reference

Migration from v0.8.x

If upgrading from older versions:

  1. Replace move() with moveAndSlide()
  2. Update collision callbacks to use onCollision()
  3. Configure new build flags (VELOCITY_ITERATIONS, etc.)

See Migration Guide for complete details.


See also: