Fase: 4 — O Cérebro
Namespace:Caffeine::Physics2D
Arquivo:src/physics/PhysicsSystem2D.hpp
Status: ✅ Implementado
RF: RF4.10
Sistema de física 2D simples com suporte a rigid body dynamics, AABB e circle collision, layers e resposta simples de colisão (restitution + friction).
Escopo intencional: Este sistema cobre 90% dos casos de jogos 2D. Para features avançadas (joints, CCD, convex hulls), integrar biblioteca dedicada (Box2D/Jolt) como escalation trigger.
namespace Caffeine::Physics2D {
struct PhysicsMaterial {
f32 friction = 0.5f;
f32 restitution = 0.3f;
static PhysicsMaterial ice();
static PhysicsMaterial rubber();
static PhysicsMaterial metal();
static PhysicsMaterial wood();
static PhysicsMaterial stone();
};
struct RigidBody2D {
f32 mass = 1.0f;
f32 restitution = 0.3f;
f32 friction = 0.5f;
f32 linearDamping = 0.0f;
bool isKinematic = false;
bool lockRotation = true;
};
struct Collider2D {
Vec2 size = { 32.0f, 32.0f };
Vec2 offset = { 0.0f, 0.0f };
f32 radius = 16.0f;
u32 layer = 0;
u32 layerMask = 0xFFFFFFFF;
ColliderShape shape = ColliderShape::AABB;
bool isStatic = false;
bool isTrigger = false;
bool isOneWay = false;
};
struct Rect2D { Vec2 position; Vec2 size; };
struct RaycastHit { ECS::Entity entity; Vec2 point; Vec2 normal; f32 distance; bool hit; };
struct CollisionManifold { Vec2 contactPoint; Vec2 normal; f32 penetration; };
class PhysicsSystem2D : public ECS::ISystem {
public:
explicit PhysicsSystem2D(Events::EventBus* eventBus = nullptr);
void onUpdate(ECS::World& world, f32 dt) override;
void setGravity(Vec2 gravity);
Vec2 gravity() const;
RaycastHit raycast(ECS::World& world, Vec2 origin, Vec2 dir, f32 maxDist, u32 layerMask = 0xFFFFFFFF);
std::vector<ECS::Entity> overlapCircle(ECS::World& world, Vec2 center, f32 radius, u32 layerMask = 0xFFFFFFFF);
std::vector<ECS::Entity> overlapAABB(ECS::World& world, Rect2D rect, u32 layerMask = 0xFFFFFFFF);
void applyForce(ECS::Entity e, Vec2 force);
void applyImpulse(ECS::Entity e, Vec2 impulse);
void setKinematic(ECS::Entity e, bool kinematic);
void teleport(ECS::Entity e, Vec2 position);
const std::vector<CollisionManifold>& lastManifolds() const;
};
}namespace Caffeine::Physics2D {
// ============================================================================
// @brief Resultado de um raycast.
// ============================================================================
struct RaycastHit {
ECS::Entity entity;
Vec2 point;
Vec2 normal;
f32 distance;
bool hit = false;
};
// ============================================================================
// @brief Manifold de colisão — dados de contato entre dois corpos.
// ============================================================================
struct CollisionManifold {
Vec2 contactPoint;
Vec2 normal; // de B para A
f32 penetration;
};
// ============================================================================
// @brief Sistema de física 2D.
//
// Integração com ECS:
// - Lê: RigidBody2D, Collider2D, Position2D, Velocity2D
// - Escreve: Position2D, Velocity2D
// - Publica: Events::OnCollision2D via EventBus
//
// NÃO suporta (para integrar biblioteca dedicada quando necessário):
// - Rotação de rigid bodies
// - Joints / constraints
// - Complex shapes (convex hulls)
// - CCD (Continuous Collision Detection)
// ============================================================================
class PhysicsSystem2D : public ECS::ISystem {
public:
explicit PhysicsSystem2D(Events::EventBus* eventBus = nullptr);
void update(ECS::World& world, f64 dt) override;
i32 priority() const override { return 100; }
const char* name() const override { return "Physics2D"; }
// ── Configuração ───────────────────────────────────────────
void setGravity(Vec2 gravity);
Vec2 gravity() const { return m_gravity; }
// ── Queries espaciais ──────────────────────────────────────
RaycastHit raycast(Vec2 origin, Vec2 direction, f32 maxDistance,
u32 layerMask = 0xFFFFFFFF) const;
Array<ECS::Entity, 32> overlapCircle(Vec2 center, f32 radius,
u32 layerMask = 0xFFFFFFFF) const;
Array<ECS::Entity, 32> overlapAABB(Rect2D rect,
u32 layerMask = 0xFFFFFFFF) const;
// ── Controle de bodies ─────────────────────────────────────
void applyForce(ECS::Entity e, Vec2 force); // acumulado, limpo no update
void applyImpulse(ECS::Entity e, Vec2 impulse); // instantâneo
void setKinematic(ECS::Entity e, bool kinematic);
void teleport(ECS::Entity e, Vec2 position); // sem sweep de colisão
private:
void integrate(ECS::Entity e, RigidBody2D& rb, Position2D& pos,
Velocity2D& vel, f32 dt);
bool detectAABBvsAABB(const Collider2D& a, const Position2D& pa,
const Collider2D& b, const Position2D& pb,
CollisionManifold* out);
bool detectCirclevsCircle(f32 rA, Vec2 cA, f32 rB, Vec2 cB,
CollisionManifold* out);
void resolveCollision(ECS::Entity a, ECS::Entity b,
const CollisionManifold& m);
void publishCollisionEvent(ECS::Entity a, ECS::Entity b,
const CollisionManifold& m);
Vec2 m_gravity = {0, -9.81f * 60.0f}; // pixels/s²
HashMap<ECS::Entity, Vec2> m_pendingForces;
Events::EventBus* m_eventBus = nullptr;
};
} // namespace Caffeine::Physics2D1. Broad phase
└── AABB bounding box check para todos os pares
(evita narrow phase para objetos distantes)
2. Narrow phase
├── AABB vs AABB (Separating Axis Theorem)
└── Circle vs Circle
3. Resolver colisões
└── Apply impulse baseado em restitution/friction/mass
4. Integração
└── pos += vel * dt (Euler semi-implícito)
vel += (gravity + forces) * dt
5. Publicar eventos
└── EventBus::publishDeferred(OnCollision2D{...})
// ── Setup ─────────────────────────────────────────────────────
auto* physics = world.registerSystem<PhysicsSystem2D>(&eventBus);
physics->setGravity({0, -500.0f}); // pixels/s²
// ── Entidade com física ───────────────────────────────────────
world.add<RigidBody2D>(player, {
.mass = 5.0f,
.restitution = 0.3f,
.friction = 0.8f
});
world.add<Collider2D>(player, {
.size = {30, 60},
.offset = {0, 0},
.isStatic = false,
.layer = 1 // player layer
});
// ── Plataforma estática ───────────────────────────────────────
world.add<Collider2D>(platform, {
.size = {200, 20},
.isStatic = true,
.layer = 0 // world layer
});
// ── Aplicar força de pulo ─────────────────────────────────────
if (input.justPressed(Action::Jump)) {
physics->applyImpulse(player, {0, 800.0f});
}
// ── Raycast para "chão existe abaixo?" ────────────────────────
auto hit = physics->raycast(playerPos, {0, -1}, 32.0f);
bool onGround = hit.hit;
// ── Overlap para área de ataque ───────────────────────────────
Rect2D attackBox = { playerPos + Vec2{20, 0}, {40, 40} };
auto hits = physics->overlapAABB(attackBox);
for (auto& enemy : hits) {
world.add<TakeDamage>(enemy, { .amount = 25 });
}Layer 0 = World (plataformas, paredes)
Layer 1 = Player
Layer 2 = Enemy
Layer 3 = Projectile
Layer 4 = Trigger
Máscara: layerMask = (1<<0)|(1<<2) = colide com World E Enemy
Se o jogo precisar de:
- Rotação de rigid bodies → integrar Box2D ou Jolt Physics
- Joints/constraints → integrar biblioteca dedicada
- CCD (evitar tunelamento em velocidade alta) → biblioteca dedicada
| Decisão | Justificativa |
|---|---|
| AABB simples | Cobre 90% dos casos 2D sem complexidade |
| Semi-implicit Euler | Estável e simples para plataformers |
| Eventos de colisão via EventBus | Physics não acopla a Audio/FX |
priority = 100 |
Física antes de tudo — base para movement/animation |
pendingForces mapa |
Permite aplicar forças de múltiplos sistemas |
- 1K rigid bodies a 60fps
- Physics step < 3ms para 1K bodies
-
tsanclean —applyForcethread-safe - Raycast retorna hit correto para todos ângulos
- Colisões publicam
OnCollision2Dcorretamente
- Upstream: ECS Core, Event Bus, Fase 1 — Math
- Downstream: Audio (reage a colisões), Fase 5 — Spatial Partitioning
| Tópico | Descrição |
|---|---|
| Sistemas de Jogo | Physics como sistema ECS |
| Memória & Dados | AABB trees e dados espaciais |
docs/architecture_specs.md— §13 Physics 2Ddocs/core/events.md—OnCollision2Devent- Box2D/LiquidFun — referência para casos avançados
- Índice de Tópicos Transversais