From 8cde9818f49c999c3fa79037c6595f856d5b5974 Mon Sep 17 00:00:00 2001 From: Pabloader Date: Sun, 31 May 2026 11:37:33 +0000 Subject: [PATCH] Fix engine bugs --- src/common/physics/engine.c | 140 ++++++++++++++++++++++++++++------- src/common/physics/index.ts | 4 + src/games/dropballs/index.ts | 6 +- 3 files changed, 118 insertions(+), 32 deletions(-) diff --git a/src/common/physics/engine.c b/src/common/physics/engine.c index 84c2bdf..7287981 100644 --- a/src/common/physics/engine.c +++ b/src/common/physics/engine.c @@ -36,6 +36,8 @@ typedef struct { typedef size_t rigid_body_index; typedef void (*rigid_body_collision_callback_t)(rigid_body_index a, rigid_body_index b); +constexpr float COLLISION_EPSILON = 1e-6f; + ////////// Prototypes void rigid_body_resolve_collision(rigid_body_index rb); @@ -49,18 +51,72 @@ static rigid_body* rigid_bodies = NULL; static size_t rigid_bodies_cap = 0; static rigid_body_collision_callback_t rigid_body_collision_callback = NULL; +static vec2 gravity = {0, 0}; + +// Queued during the solve and dispatched only after it finishes, so callbacks may +// free or create bodies (reallocating `rigid_bodies`) without invalidating the body +// pointers held mid-solve. +typedef struct { + rigid_body_index a; + rigid_body_index b; +} collision_event; + +static collision_event* collision_events = NULL; +static size_t collision_events_count = 0; +static size_t collision_events_cap = 0; + ////////// Functions inline static rigid_body* rb_get(rigid_body_index idx) { return (rigid_bodies + (idx)); } +static void collision_event_push(rigid_body_index a, rigid_body_index b) { + if (collision_events_count == collision_events_cap) { + size_t new_cap = collision_events_cap ? collision_events_cap * 2 : 16; + collision_event* grown = realloc(collision_events, new_cap * sizeof(collision_event)); + if (!grown) { + return; + } + collision_events = grown; + collision_events_cap = new_cap; + } + collision_events[collision_events_count].a = a; + collision_events[collision_events_count].b = b; + collision_events_count++; +} + +static void rigid_body_dispatch_collisions() { + if (rigid_body_collision_callback) { + for (size_t i = 0; i < collision_events_count; i++) { + rigid_body_collision_callback(collision_events[i].a, collision_events[i].b); + } + } + collision_events_count = 0; +} + +static void rigid_body_integrate(rigid_body* rb, float dt) { + if (!isinf(rb->mass)) { + rb->vel.x += (rb->force.x / rb->mass + gravity.x) * dt; + rb->vel.y += (rb->force.y / rb->mass + gravity.y) * dt; + + rb->pos.x += rb->vel.x * dt; + rb->pos.y += rb->vel.y * dt; + } + + rb->force.x = 0; + rb->force.y = 0; +} + JS_EXPORT rigid_body* rigid_body_get(rigid_body_index idx) { return rb_get(idx); } size_t rigid_body_new(float x, float y, float vx, float vy, float mass) { rigid_body_index idx = rigid_body_find_empty(); + if (idx == SIZE_MAX) { + return SIZE_MAX; // allocation failed + } rigid_body* rb = rb_get(idx); rb->type = TYPE_EMPTY; @@ -80,6 +136,9 @@ size_t rigid_body_new(float x, float y, float vx, float vy, float mass) { JS_EXPORT rigid_body_index rigid_body_new_circle(float x, float y, float vx, float vy, float mass, float radius) { rigid_body_index idx = rigid_body_new(x, y, vx, vy, mass); + if (idx == SIZE_MAX) { + return idx; + } rigid_body* rb = rb_get(idx); rb->type = TYPE_CIRCLE; @@ -90,6 +149,9 @@ JS_EXPORT rigid_body_index rigid_body_new_circle(float x, float y, float vx, flo JS_EXPORT rigid_body_index rigid_body_new_plane(float x, float y, float nx, float ny) { rigid_body_index idx = rigid_body_new(x, y, 0, 0, INFINITY); + if (idx == SIZE_MAX) { + return idx; + } rigid_body* rb = rb_get(idx); rb->type = TYPE_PLANE; @@ -103,19 +165,9 @@ JS_EXPORT void rigid_body_free(rigid_body_index idx) { } JS_EXPORT void rigid_body_update(rigid_body_index idx, float dt) { + rigid_body_integrate(rb_get(idx), dt); rigid_body_resolve_collision(idx); - rigid_body* rb = rb_get(idx); - - if (!isinf(rb->mass)) { - rb->vel.x += rb->force.x * dt / rb->mass; - rb->vel.y += rb->force.y * dt / rb->mass; - - rb->pos.x += rb->vel.x * dt; - rb->pos.y += rb->vel.y * dt; - } - - rb->force.x = 0; - rb->force.y = 0; + rigid_body_dispatch_collisions(); } JS_EXPORT void rigid_body_update_all(float dt) { @@ -124,8 +176,18 @@ JS_EXPORT void rigid_body_update_all(float dt) { if (current->type == TYPE_EMPTY) { continue; } - rigid_body_update(idx, dt); + rigid_body_integrate(current, dt); } + + for (rigid_body_index idx = 0; idx < rigid_bodies_cap; idx++) { + rigid_body* current = rb_get(idx); + if (current->type == TYPE_EMPTY) { + continue; + } + rigid_body_resolve_collision(idx); + } + + rigid_body_dispatch_collisions(); } JS_EXPORT void rigid_body_add_force(rigid_body_index idx, float fx, float fy) { @@ -148,6 +210,11 @@ JS_EXPORT void rigid_body_set_collision_callback(rigid_body_collision_callback_t rigid_body_collision_callback = callback; } +JS_EXPORT void rigid_body_set_gravity(float gx, float gy) { + gravity.x = gx; + gravity.y = gy; +} + void rigid_body_resolve_collision(rigid_body_index idx) { rigid_body* rb = rb_get(idx); int is_static = isinf(rb->mass); @@ -171,11 +238,11 @@ void rigid_body_handle_collision(rigid_body_index idx1, rigid_body_index idx2) { return; } - if (rigid_body_collision_callback) { - rigid_body_collision_callback(idx1, idx2); - } + collision_event_push(idx1, idx2); + + // Coincident centres have no separation axis; pick an arbitrary one. + vec2 n = (distance > COLLISION_EPSILON) ? vec2_div(d, distance) : (vec2){1.0f, 0.0f}; - vec2 n = vec2_normalize(d); float factor = (isinf(rb1->mass) || isinf(rb2->mass)) ? 1 : 0.5; vec2 n_overlap = vec2_mul(n, overlap * factor); @@ -189,12 +256,18 @@ void rigid_body_handle_collision(rigid_body_index idx1, rigid_body_index idx2) { float v1n = vec2_dot(rb1->vel, n); float v2n = vec2_dot(rb2->vel, n); + // Skip the impulse if the bodies are already separating along the normal. + if (v1n - v2n <= 0) { + return; + } + float v1n_new = v1n; float v2n_new = v2n; if (!isinf(rb1->mass) && !isinf(rb2->mass)) { - v1n_new = v1n - 2.0f * (v1n - v2n) * rb1->mass / (rb1->mass + rb2->mass); - v2n_new = v2n + 2.0f * (v1n - v2n) * rb2->mass / (rb1->mass + rb2->mass); + // Elastic collision: each body weighted by the *other* body's mass. + v1n_new = v1n - 2.0f * (v1n - v2n) * rb2->mass / (rb1->mass + rb2->mass); + v2n_new = v2n + 2.0f * (v1n - v2n) * rb1->mass / (rb1->mass + rb2->mass); } else if (isinf(rb1->mass)) { v2n_new = -v2n; } else if (isinf(rb2->mass)) { @@ -220,15 +293,17 @@ void rigid_body_handle_collision(rigid_body_index idx1, rigid_body_index idx2) { return; } - if (rigid_body_collision_callback) { - rigid_body_collision_callback(idx1, idx2); - } + collision_event_push(idx1, idx2); vec2 n = rb1->plane.normal; vec2 n_overlap = vec2_mul(n, overlap); rb2->pos = vec2_add(rb2->pos, n_overlap); - rb2->vel = vec2_reflect(rb2->vel, n); + + // Reflect only if the circle is moving into the plane. + if (vec2_dot(rb2->vel, n) < 0) { + rb2->vel = vec2_reflect(rb2->vel, n); + } } else if (rb1->type == TYPE_CIRCLE && rb2->type == TYPE_PLANE) { rigid_body_handle_collision(idx2, idx1); } @@ -248,14 +323,23 @@ rigid_body_index rigid_body_find_empty() { return idx; } } - size_t new_cap = rigid_bodies_cap * 2; - rigid_bodies = realloc(rigid_bodies, new_cap * sizeof(rigid_body)); - memset(rigid_bodies + rigid_bodies_cap, 0, rigid_bodies_cap * sizeof(rigid_body)); + size_t old_cap = rigid_bodies_cap; + size_t new_cap = old_cap * 2; + rigid_body* grown = realloc(rigid_bodies, new_cap * sizeof(rigid_body)); + if (!grown) { + return SIZE_MAX; // keep the old buffer intact; caller handles failure + } + rigid_bodies = grown; + memset(rigid_bodies + old_cap, 0, (new_cap - old_cap) * sizeof(rigid_body)); rigid_bodies_cap = new_cap; - return rigid_body_find_empty(); + return old_cap; // first slot of the freshly grown region } else { + rigid_body* allocated = malloc(2 * sizeof(rigid_body)); + if (!allocated) { + return SIZE_MAX; + } + rigid_bodies = allocated; rigid_bodies_cap = 2; - rigid_bodies = malloc(rigid_bodies_cap * sizeof(rigid_body)); memset(rigid_bodies, 0, rigid_bodies_cap * sizeof(rigid_body)); return 0; } diff --git a/src/common/physics/index.ts b/src/common/physics/index.ts index 4f28cd3..7531eb5 100644 --- a/src/common/physics/index.ts +++ b/src/common/physics/index.ts @@ -26,6 +26,10 @@ namespace Physics { E.rigid_body_add_global_force(fx, fy); } + export function setGravity(gx: number, gy: number) { + E.rigid_body_set_gravity(gx, gy); + } + export function update(dt: number) { E.rigid_body_update_all(dt); } diff --git a/src/games/dropballs/index.ts b/src/games/dropballs/index.ts index 2a907a3..04b5f75 100644 --- a/src/games/dropballs/index.ts +++ b/src/games/dropballs/index.ts @@ -130,6 +130,7 @@ const setup = (): State => { Physics.newPlane(0, 0, 0, 1); Physics.newPlane(canvas.width, 0, -1, 0); + Physics.setGravity(0, 100); Physics.setCollisionCallback((a, b) => onCollision(state, a, b)); update(); @@ -137,10 +138,7 @@ const setup = (): State => { return state; } -const frame = (dt: number, state: State) => { - Physics.addGlobalForce(0, 100); - Physics.update(dt); - +const frame = (_dt: number, state: State) => { const { ctx, canvas } = state; ctx.fillStyle = `#111`; ctx.fillRect(0, 0, canvas.width, canvas.height);