fad4006f60
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
234 lines
6.7 KiB
Markdown
234 lines
6.7 KiB
Markdown
---
|
|
id: "0072j"
|
|
title: "gamedev — physics 2D (Box2D integration + funciones registry)"
|
|
status: pendiente
|
|
type: feature
|
|
domain:
|
|
- gamedev
|
|
scope: multi-app
|
|
priority: media
|
|
depends:
|
|
- "0072b"
|
|
blocks: []
|
|
related: []
|
|
created: 2026-05-10
|
|
updated: 2026-05-17
|
|
tags:
|
|
- gamedev
|
|
- cpp
|
|
- physics
|
|
---
|
|
|
|
## Objetivo
|
|
|
|
Integrar Box2D (v3, MIT, ~200KB) como motor de fisica 2D, expuesto via funciones del registry. Cubre colisiones, gravedad, joints, raycasts, sensores. Suficiente para plataformeros, top-down shooters, puzzle games con fisica, juegos tipo Angry Birds.
|
|
|
|
## Por qué Box2D v3
|
|
|
|
- MIT, sin restricciones.
|
|
- ~200 KB strippable a menos.
|
|
- C-API en v3 (mas facil de wrappear que la v2 C++ API).
|
|
- Determinista (importante para replays / leaderboards firmados de 0072f).
|
|
- Probado en miles de juegos.
|
|
|
|
Alternativas descartadas:
|
|
- **Chipmunk2D** — bueno pero menos activo.
|
|
- **Rapier** — Rust, complica integracion.
|
|
- **PhysX** — overkill para 2D, licencia.
|
|
- **Custom** — no, demasiado trabajo.
|
|
|
|
## Vendoring
|
|
|
|
`cpp/vendor/box2d/` con headers + `.c` source. Compilado como subdir del cmake de cada app que lo use, NO como funcion del registry (es vendor lib, no nuestro codigo).
|
|
|
|
## Funciones a crear
|
|
|
|
`cpp/functions/gamedev/physics_*` (impure):
|
|
|
|
### physics_world
|
|
|
|
```cpp
|
|
struct PhysicsWorld {
|
|
b2WorldId world;
|
|
Vec2 gravity;
|
|
float time_step; // 1/60 default
|
|
int substeps; // 4 default
|
|
};
|
|
|
|
PhysicsWorld physics_world_create(Vec2 gravity);
|
|
void physics_world_step(PhysicsWorld& w, float dt);
|
|
void physics_world_destroy(PhysicsWorld& w);
|
|
```
|
|
|
|
### physics_body
|
|
|
|
```cpp
|
|
enum class BodyType { Static, Dynamic, Kinematic };
|
|
|
|
struct BodyDef {
|
|
BodyType type;
|
|
Vec2 position;
|
|
float rotation;
|
|
float linear_damping;
|
|
float angular_damping;
|
|
bool fixed_rotation;
|
|
void* user_data;
|
|
};
|
|
|
|
b2BodyId physics_body_create(PhysicsWorld& w, const BodyDef& def);
|
|
void physics_body_destroy(b2BodyId id);
|
|
void physics_body_set_velocity(b2BodyId id, Vec2 v);
|
|
Vec2 physics_body_get_position(b2BodyId id);
|
|
float physics_body_get_rotation(b2BodyId id);
|
|
void physics_body_apply_impulse(b2BodyId id, Vec2 impulse);
|
|
```
|
|
|
|
### physics_shape
|
|
|
|
```cpp
|
|
struct ShapeDef {
|
|
float density;
|
|
float friction;
|
|
float restitution; // bounciness 0..1
|
|
bool is_sensor;
|
|
uint16_t category_bits;
|
|
uint16_t mask_bits;
|
|
};
|
|
|
|
void physics_shape_box(b2BodyId body, Vec2 size, Vec2 center, const ShapeDef& def);
|
|
void physics_shape_circle(b2BodyId body, float radius, Vec2 center, const ShapeDef& def);
|
|
void physics_shape_polygon(b2BodyId body, const std::vector<Vec2>& verts, const ShapeDef& def);
|
|
void physics_shape_chain(b2BodyId body, const std::vector<Vec2>& verts, bool loop);
|
|
```
|
|
|
|
### physics_query
|
|
|
|
```cpp
|
|
struct RaycastHit {
|
|
b2BodyId body;
|
|
Vec2 point;
|
|
Vec2 normal;
|
|
float fraction;
|
|
bool hit;
|
|
};
|
|
|
|
RaycastHit physics_raycast(PhysicsWorld& w, Vec2 from, Vec2 to,
|
|
uint16_t mask = 0xFFFF);
|
|
|
|
std::vector<b2BodyId> physics_query_aabb(PhysicsWorld& w, Vec2 min, Vec2 max);
|
|
|
|
bool physics_overlap_circle(PhysicsWorld& w, Vec2 center, float radius,
|
|
std::vector<b2BodyId>& out_bodies);
|
|
```
|
|
|
|
### physics_contacts
|
|
|
|
```cpp
|
|
struct ContactEvent {
|
|
b2BodyId a, b;
|
|
Vec2 point;
|
|
Vec2 normal;
|
|
float impulse;
|
|
};
|
|
|
|
// Llamar despues de world_step
|
|
std::vector<ContactEvent> physics_get_begin_contacts(PhysicsWorld& w);
|
|
std::vector<ContactEvent> physics_get_end_contacts(PhysicsWorld& w);
|
|
std::vector<ContactEvent> physics_get_sensor_events(PhysicsWorld& w);
|
|
```
|
|
|
|
### physics_joints
|
|
|
|
```cpp
|
|
b2JointId physics_joint_revolute(PhysicsWorld& w, b2BodyId a, b2BodyId b, Vec2 anchor);
|
|
b2JointId physics_joint_distance(PhysicsWorld& w, b2BodyId a, b2BodyId b,
|
|
Vec2 anchor_a, Vec2 anchor_b, float length);
|
|
b2JointId physics_joint_prismatic(PhysicsWorld& w, b2BodyId a, b2BodyId b,
|
|
Vec2 anchor, Vec2 axis);
|
|
void physics_joint_destroy(b2JointId id);
|
|
```
|
|
|
|
### physics_debug_draw
|
|
|
|
```cpp
|
|
// Pinta shapes/aabb/contacts usando sprite_batch o lineas con sokol_gfx
|
|
void physics_debug_draw(PhysicsWorld& w, SpriteBatch& batch, const Camera2D& cam,
|
|
bool draw_shapes = true,
|
|
bool draw_aabbs = false,
|
|
bool draw_contacts = false);
|
|
```
|
|
|
|
Util para debugging. No usar en release.
|
|
|
|
## Tipos del registry
|
|
|
|
`cpp/types/gamedev/`:
|
|
- `BodyType` (sum: Static | Dynamic | Kinematic)
|
|
- `BodyDef` (product)
|
|
- `ShapeDef` (product)
|
|
- `RaycastHit` (product)
|
|
- `ContactEvent` (product)
|
|
|
|
`b2BodyId`, `b2JointId`, `b2WorldId` son types opacos del vendor; documentarlos como tales en el `.md` correspondiente.
|
|
|
|
## Integracion con runtime
|
|
|
|
`game_loop_cpp_gamedev` (de 0072b) ya tiene `on_fixed_update(dt)`. Ahi se llama `physics_world_step`. El render interpola entre dos snapshots de body positions (ya soportado por el game loop con `interp` factor).
|
|
|
|
## Patrones documentados en GAMEDEV.md
|
|
|
|
| Patron | Cuando |
|
|
|---|---|
|
|
| Static body + chain shape | Ground/walls de tilemap |
|
|
| Dynamic body + box shape | Player, enemies |
|
|
| Sensor (no-collide trigger) | Coins, checkpoints, damage zones |
|
|
| Kinematic body | Plataformas moviles |
|
|
| Raycast | Line of sight, bullets |
|
|
| AABB query | Spatial culling, area-of-effect |
|
|
| Categorias y masks | Player vs enemy vs wall vs trigger filtering |
|
|
|
|
## Determinismo
|
|
|
|
Box2D v3 es determinista si:
|
|
1. Fixed timestep (no variable dt).
|
|
2. Mismas operaciones en mismo orden.
|
|
3. Misma version del compilador (con cuidado de `-ffast-math` que rompe determinism).
|
|
|
|
Para replays / leaderboards firmados (0072f): documentar que el game loop usa `fixed_dt` y `physics_world_step` siempre con ese dt. Inputs grabados → replay determinista.
|
|
|
|
## Tamaño
|
|
|
|
| Componente | KB |
|
|
|---|---|
|
|
| Box2D v3 stripped | ~200 |
|
|
| Wrappers (registry funcs) | ~30 |
|
|
| Total | ~230 |
|
|
|
|
Cabe en el budget global.
|
|
|
|
## Tests
|
|
|
|
App `cpp/apps/physics_test/` con `--self-test`:
|
|
1. Crea world, body, shape.
|
|
2. Step 100 frames.
|
|
3. Verifica que el body cae (gravity).
|
|
4. Verifica raycast contra un static body.
|
|
5. Verifica contact event tras colision.
|
|
6. Verifica determinismo: dos worlds idénticos producen mismas posiciones tras N steps.
|
|
|
|
## Criterio de exito
|
|
|
|
- [x] Funciones registradas en registry con `.md` + tests.
|
|
- [x] App `physics_test --self-test` pasa.
|
|
- [x] Demo: caja cae sobre suelo, rebota, se detiene (en `engine_demo` de 0072k).
|
|
- [x] Debug draw funcional (toggle en menu del editor).
|
|
- [x] Tamaño contribuye ≤ 250 KB al wasm gzip.
|
|
- [x] Documentacion `cpp/GAMEDEV.md` seccion Physics.
|
|
|
|
## No-objetivos
|
|
|
|
- Physics 3D (no, este stack es 2D).
|
|
- GPU physics (overkill).
|
|
- Soft body / cloth / fluids (Box2D no lo hace, OK).
|
|
- Networking deterministic rollback (sub-issue futuro si hace falta multiplayer).
|