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:
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)¶
Pros: Fast, simple, perfect for tile-based games
Cons: No rotation support, approximate for circular objects
2. Circle¶
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)
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¶
- PhysicsActor - Base physics body
- KinematicActor - Script-controlled body
- CollisionSystem - Global physics manager
- Collision Types - Shapes and contacts
Migration from v0.8.x¶
If upgrading from older versions:
- Replace
move()withmoveAndSlide() - Update collision callbacks to use
onCollision() - Configure new build flags (VELOCITY_ITERATIONS, etc.)
See Migration Guide for complete details.
See also: