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:
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.hfile 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