Time fixes

This commit is contained in:
2026-07-18 20:31:42 -05:00
parent a618ff46fe
commit 1174e471f6
16 changed files with 676 additions and 22 deletions
+17 -9
View File
@@ -72,6 +72,10 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
entityAddComponent(
testEntities, ENGINE.testCubeEntity, COMPONENT_TYPE_RENDERABLE
);
entityUpdateAdd(
testEntities, ENGINE.testCubeEntity, engineTestCubeRotate,
ENGINE.testCubePositionComponent, NULL
);
networkRequestConnection(
engineNetworkOnConnected,
@@ -111,15 +115,6 @@ errorret_t engineUpdate(void) {
inputUpdate();
consoleUpdate();
// Test: spin the cube.
ENGINE.testCubeRotation += TIME.delta;
entityPositionSetLocalRotation(
sceneGetEntities(ENGINE.testSceneId),
ENGINE.testCubeEntity,
ENGINE.testCubePositionComponent,
(vec3){ 0.0f, ENGINE.testCubeRotation, 0.0f }
);
errorChain(sceneUpdate());
errorChain(assetUpdate());
errorChain(uiUpdate());
@@ -212,3 +207,16 @@ void engineNetworkOnDisconnect(errorret_t error, void *user) {
void engineNetworkDisconnectTestOnComplete(void *user) {
consolePrint("Network disconnect test complete");
}
void engineTestCubeRotate(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
void *user
) {
ENGINE.testCubeRotation += TIME.delta;
entityPositionSetLocalRotation(
mgr, entityId, componentId,
(vec3){ 0.0f, ENGINE.testCubeRotation, 0.0f }
);
}
+18
View File
@@ -98,3 +98,21 @@ void engineNetworkOnDisconnect(errorret_t error, void *user);
* @param user Unused.
*/
void engineNetworkDisconnectTestOnComplete(void *user);
/**
* Entity update callback that spins the test cube around Y. Registered
* once in engineInit() via entityUpdateAdd(), so it only runs from
* sceneFixedUpdate() -- once per fixed timestep, never once per rendered
* frame.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The test cube's entity ID.
* @param componentId The test cube's position component ID.
* @param user Unused.
*/
void engineTestCubeRotate(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
void *user
);
+4 -2
View File
@@ -86,7 +86,9 @@ void entityDisposeDeep(entitymanager_t *mgr, const entityid_t entityId) {
void entityUpdate(entitymanager_t *mgr, const entityid_t entityId) {
entity_t *ent = &mgr->entities[entityId];
for(uint8_t i = 0; i < ent->updateCount; i++) {
ent->onUpdate[i](entityId, ent->updateComponentId[i], ent->updateUser[i]);
ent->onUpdate[i](
mgr, entityId, ent->updateComponentId[i], ent->updateUser[i]
);
}
}
@@ -96,7 +98,7 @@ void entityDispose(entitymanager_t *mgr, const entityid_t entityId) {
for(uint8_t i = 0; i < ent->disposeCount; i++) {
ent->onDispose[i](
entityId, ent->disposeComponentId[i], ent->disposeUser[i]
mgr, entityId, ent->disposeComponentId[i], ent->disposeUser[i]
);
}
+1
View File
@@ -14,6 +14,7 @@
#define ENTITY_DISPOSE_CALLBACK_COUNT_MAX 5
typedef void (*entitycallback_t)(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
void *user
+1
View File
@@ -8,4 +8,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
physicsworld.c
physicstest.c
physicsshapemesh.c
)
+151
View File
@@ -0,0 +1,151 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "physicsshapemesh.h"
#include "physicstest.h"
#include "assert/assert.h"
void physicsShapeMeshGetVertex(
const physicsshapemesh_t *mesh, const uint32_t index, vec3 out
) {
assertNotNull(mesh, "Mesh cannot be null");
assertTrue(index < mesh->triangleCount * 3, "Vertex index OOB");
const float_t *p = (const float_t *)(mesh->vertices + (size_t)index * mesh->stride);
out[0] = p[0];
out[1] = p[1];
out[2] = p[2];
}
bool_t physicsShapeMeshTestSphere(
const physicsshapemesh_t *mesh, const vec3 meshPos,
const vec3 sphereCenter, const float_t sphereRadius,
vec3 outNormal, float_t *outDepth
) {
assertNotNull(mesh, "Mesh cannot be null");
bool_t found = false;
float_t bestDist2 = 0.0f;
vec3 bestPoint;
for(uint32_t i = 0; i < mesh->triangleCount; i++) {
vec3 a, b, c;
physicsShapeMeshGetVertex(mesh, i * 3 + 0, a);
physicsShapeMeshGetVertex(mesh, i * 3 + 1, b);
physicsShapeMeshGetVertex(mesh, i * 3 + 2, c);
glm_vec3_add(a, (float_t *)meshPos, a);
glm_vec3_add(b, (float_t *)meshPos, b);
glm_vec3_add(c, (float_t *)meshPos, c);
vec3 closest;
physicsTestClosestPointOnTriangle(sphereCenter, a, b, c, closest);
vec3 diff;
glm_vec3_sub((float_t *)sphereCenter, closest, diff);
float_t dist2 = glm_vec3_norm2(diff);
if(found && dist2 >= bestDist2) continue;
found = true;
bestDist2 = dist2;
glm_vec3_copy(closest, bestPoint);
}
if(!found || bestDist2 >= sphereRadius * sphereRadius) return false;
float_t dist = sqrtf(bestDist2);
*outDepth = sphereRadius - dist;
vec3 diff;
glm_vec3_sub((float_t *)sphereCenter, bestPoint, diff);
if(dist > 1e-6f) {
glm_vec3_scale(diff, 1.0f / dist, outNormal);
} else {
outNormal[0] = 0.0f;
outNormal[1] = 1.0f;
outNormal[2] = 0.0f;
}
return true;
}
bool_t physicsShapeMeshTestCapsule(
const physicsshapemesh_t *mesh, const vec3 meshPos,
const vec3 capsuleCenter, const float_t capsuleRadius,
const float_t capsuleHalfHeight,
vec3 outNormal, float_t *outDepth
) {
vec3 capA = {
capsuleCenter[0], capsuleCenter[1] - capsuleHalfHeight, capsuleCenter[2]
};
vec3 capB = {
capsuleCenter[0], capsuleCenter[1] + capsuleHalfHeight, capsuleCenter[2]
};
int32_t sampleCount = capsuleRadius > 1e-6f
? (int32_t)ceilf((2.0f * capsuleHalfHeight) / capsuleRadius) + 1
: 2;
if(sampleCount < 2) sampleCount = 2;
if(sampleCount > PHYSICS_SHAPE_MESH_CAPSULE_SAMPLES_MAX) {
sampleCount = PHYSICS_SHAPE_MESH_CAPSULE_SAMPLES_MAX;
}
bool_t found = false;
vec3 bestNormal = { 0.0f, 1.0f, 0.0f };
float_t bestDepth = 0.0f;
for(int32_t i = 0; i < sampleCount; i++) {
float_t t = (float_t)i / (float_t)(sampleCount - 1);
vec3 samplePoint;
glm_vec3_lerp(capA, capB, t, samplePoint);
vec3 normal; float_t depth;
if(!physicsShapeMeshTestSphere(
mesh, meshPos, samplePoint, capsuleRadius, normal, &depth
)) continue;
if(found && depth <= bestDepth) continue;
found = true;
bestDepth = depth;
glm_vec3_copy(normal, bestNormal);
}
if(!found) return false;
*outDepth = bestDepth;
glm_vec3_copy(bestNormal, outNormal);
return true;
}
bool_t physicsShapeMeshTest(
const vec3 selfPos, const void *selfData,
const vec3 otherPos, const physicsshape_t *otherShape,
vec3 outNormal, float_t *outDepth
) {
const physicsshapemesh_t *mesh = (const physicsshapemesh_t *)selfData;
switch(otherShape->type) {
case PHYSICS_SHAPE_SPHERE:
return physicsShapeMeshTestSphere(
mesh, selfPos, otherPos, otherShape->data.sphere.radius,
outNormal, outDepth
);
case PHYSICS_SHAPE_CAPSULE:
return physicsShapeMeshTestCapsule(
mesh, selfPos, otherPos,
otherShape->data.capsule.radius, otherShape->data.capsule.halfHeight,
outNormal, outDepth
);
default:
return false;
}
}
physicsshape_t physicsShapeMeshCreate(const physicsshapemesh_t *mesh) {
assertNotNull(mesh, "Mesh cannot be null");
return (physicsshape_t){
.type = PHYSICS_SHAPE_CUSTOM,
.data.custom = { .userData = (void *)mesh, .test = physicsShapeMeshTest }
};
}
+128
View File
@@ -0,0 +1,128 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "physicsshape.h"
/**
* Capsule-vs-mesh is approximated by sampling spheres along the capsule's
* axis (see physicsShapeMeshTestCapsule). This bounds how many samples a
* single query can take, regardless of how tall/thin the capsule is.
*/
#define PHYSICS_SHAPE_MESH_CAPSULE_SAMPLES_MAX 8
/**
* Describes an arbitrary triangle mesh (e.g. landscape/terrain geometry)
* for use as a PHYSICS_SHAPE_CUSTOM shape via physicsShapeMeshCreate().
*
* Vertex positions are read directly out of `vertices` at `stride`-byte
* intervals, so this can point straight at an existing render vertex
* buffer (e.g. `&myMeshVertices[0].pos` with `stride = sizeof(meshvertex_t)`)
* with no need to duplicate the geometry for physics -- as long as each
* vertex's position is 3 consecutive float_t.
*
* Not copied or owned: this struct (and the buffer it points into) must
* outlive any physicsshape_t built from it, so it's typically a field
* alongside whatever owns the mesh (e.g. a terrain chunk).
*
* Every query scans all triangleCount triangles (no broad-phase/BVH), so
* for good performance keep each mesh shape scoped to one reasonably-sized
* chunk rather than an entire world, and only add nearby chunks' physics
* bodies to the scene.
*/
typedef struct {
/** Pointer to the first triangle vertex's position (3 consecutive float_t). */
const uint8_t *vertices;
/** Byte stride between consecutive vertex positions. */
size_t stride;
/** Number of triangles. `vertices` holds triangleCount * 3 positions. */
uint32_t triangleCount;
} physicsshapemesh_t;
/**
* Reads the world-space position of a single mesh vertex (vertex index,
* not triangle index -- pass triangleIndex * 3 + 0/1/2 for a triangle's
* corners).
*
* @param mesh The mesh to read from.
* @param index Vertex index, in [0, triangleCount * 3).
* @param out Receives the vertex position.
*/
void physicsShapeMeshGetVertex(
const physicsshapemesh_t *mesh, const uint32_t index, vec3 out
);
/**
* Tests a sphere against every triangle in the mesh, keeping only the
* single closest triangle. outNormal points from the mesh surface toward
* the sphere (self toward other, per physicsshapecustomtest_t).
*
* @param mesh The mesh to test against.
* @param meshPos World-space position of the mesh shape's owning entity;
* added to every vertex read from mesh.
* @param sphereCenter World-space center of the sphere.
* @param sphereRadius Radius of the sphere.
* @param outNormal Push-out normal (mesh surface toward sphere).
* @param outDepth Penetration depth.
* @return true if the sphere overlaps the mesh.
*/
bool_t physicsShapeMeshTestSphere(
const physicsshapemesh_t *mesh, const vec3 meshPos,
const vec3 sphereCenter, const float_t sphereRadius,
vec3 outNormal, float_t *outDepth
);
/**
* Tests a Y-axis-aligned capsule against the mesh by approximating the
* capsule as a series of spheres sampled along its axis (see
* PHYSICS_SHAPE_MESH_CAPSULE_SAMPLES_MAX) and keeping the deepest overlap
* found. An approximation, not an exact capsule-vs-triangle test.
*
* @param mesh The mesh to test against.
* @param meshPos World-space position of the mesh shape's owning entity.
* @param capsuleCenter World-space center of the capsule.
* @param capsuleRadius Radius of the capsule.
* @param capsuleHalfHeight Half-height of the capsule's cylindrical segment.
* @param outNormal Push-out normal (mesh surface toward capsule).
* @param outDepth Penetration depth.
* @return true if the capsule overlaps the mesh.
*/
bool_t physicsShapeMeshTestCapsule(
const physicsshapemesh_t *mesh, const vec3 meshPos,
const vec3 capsuleCenter, const float_t capsuleRadius,
const float_t capsuleHalfHeight,
vec3 outNormal, float_t *outDepth
);
/**
* physicsshapecustomtest_t implementation backing physicsShapeMeshCreate().
* Routes to physicsShapeMeshTestSphere/TestCapsule depending on
* otherShape's type. Cube (AABB) bodies are not supported yet and never
* collide against a mesh shape.
*
* @param selfPos The mesh shape's position.
* @param selfData The physicsshapemesh_t this shape was created from.
* @param otherPos The other shape's position.
* @param otherShape The other shape's descriptor.
* @param outNormal Push-out normal (mesh surface toward other).
* @param outDepth Penetration depth.
* @return true if the shapes overlap.
*/
bool_t physicsShapeMeshTest(
const vec3 selfPos, const void *selfData,
const vec3 otherPos, const physicsshape_t *otherShape,
vec3 outNormal, float_t *outDepth
);
/**
* Builds a PHYSICS_SHAPE_CUSTOM shape backed by the given triangle mesh.
*
* @param mesh Mesh descriptor. Must outlive the returned shape (see
* physicsshapemesh_t).
* @return A physicsshape_t wired to test against this mesh.
*/
physicsshape_t physicsShapeMeshCreate(const physicsshapemesh_t *mesh);
+72
View File
@@ -148,6 +148,78 @@ void physicsTestClosestPointOnSegment(
glm_vec3_lerp((float_t *)a, (float_t *)b, t, out);
}
void physicsTestClosestPointOnTriangle(
const vec3 p, const vec3 a, const vec3 b, const vec3 c, vec3 out
) {
vec3 ab, ac, ap;
glm_vec3_sub((float_t *)b, (float_t *)a, ab);
glm_vec3_sub((float_t *)c, (float_t *)a, ac);
glm_vec3_sub((float_t *)p, (float_t *)a, ap);
float_t d1 = glm_vec3_dot(ab, ap);
float_t d2 = glm_vec3_dot(ac, ap);
if(d1 <= 0.0f && d2 <= 0.0f) {
glm_vec3_copy((float_t *)a, out);
return;
}
vec3 bp;
glm_vec3_sub((float_t *)p, (float_t *)b, bp);
float_t d3 = glm_vec3_dot(ab, bp);
float_t d4 = glm_vec3_dot(ac, bp);
if(d3 >= 0.0f && d4 <= d3) {
glm_vec3_copy((float_t *)b, out);
return;
}
float_t vc = d1 * d4 - d3 * d2;
if(vc <= 0.0f && d1 >= 0.0f && d3 <= 0.0f) {
float_t v = d1 / (d1 - d3);
vec3 scaled;
glm_vec3_scale(ab, v, scaled);
glm_vec3_add((float_t *)a, scaled, out);
return;
}
vec3 cp;
glm_vec3_sub((float_t *)p, (float_t *)c, cp);
float_t d5 = glm_vec3_dot(ab, cp);
float_t d6 = glm_vec3_dot(ac, cp);
if(d6 >= 0.0f && d5 <= d6) {
glm_vec3_copy((float_t *)c, out);
return;
}
float_t vb = d5 * d2 - d1 * d6;
if(vb <= 0.0f && d2 >= 0.0f && d6 <= 0.0f) {
float_t w = d2 / (d2 - d6);
vec3 scaled;
glm_vec3_scale(ac, w, scaled);
glm_vec3_add((float_t *)a, scaled, out);
return;
}
float_t va = d3 * d6 - d5 * d4;
if(va <= 0.0f && (d4 - d3) >= 0.0f && (d5 - d6) >= 0.0f) {
float_t w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
vec3 bc, scaled;
glm_vec3_sub((float_t *)c, (float_t *)b, bc);
glm_vec3_scale(bc, w, scaled);
glm_vec3_add((float_t *)b, scaled, out);
return;
}
// Interior: barycentric combination of ab and ac from a.
float_t denom = 1.0f / (va + vb + vc);
float_t v = vb * denom;
float_t w = vc * denom;
vec3 abScaled, acScaled, sum;
glm_vec3_scale(ab, v, abScaled);
glm_vec3_scale(ac, w, acScaled);
glm_vec3_add(abScaled, acScaled, sum);
glm_vec3_add((float_t *)a, sum, out);
}
void physicsTestClosestPointsBetweenSegments(
const vec3 a1, const vec3 b1,
const vec3 a2, const vec3 b2,
+15
View File
@@ -110,6 +110,21 @@ void physicsTestClosestPointOnSegment(
const vec3 a, const vec3 b, const vec3 p, vec3 out
);
/**
* Finds the closest point on triangle (a, b, c) to query point p. Handles
* all six Voronoi regions (the three vertices, the three edges, and the
* face interior).
*
* @param p Query point.
* @param a First triangle vertex.
* @param b Second triangle vertex.
* @param c Third triangle vertex.
* @param out Receives the closest point on the triangle to p.
*/
void physicsTestClosestPointOnTriangle(
const vec3 p, const vec3 a, const vec3 b, const vec3 c, vec3 out
);
/**
* Finds the closest points between two line segments.
*
+8 -3
View File
@@ -72,12 +72,17 @@ physicsworld_t *sceneGetPhysics(const sceneid_t id) {
}
errorret_t sceneUpdate(void) {
if(SCENE_MANAGER.active == SCENE_ID_INVALID) errorOk();
#if DUSK_TIME_DYNAMIC
if(!TIME.dynamicUpdate) errorOk();
if(TIME.dynamicUpdate) errorOk();
#endif
errorChain(sceneFixedUpdate());
errorOk();
}
errorret_t sceneFixedUpdate(void) {
if(SCENE_MANAGER.active == SCENE_ID_INVALID) errorOk();
scene_t *scene = &SCENE_MANAGER.scenes[SCENE_MANAGER.active];
entityManagerUpdate(&scene->entities);
physicsWorldStep(&scene->physics, &scene->entities, TIME.delta);
+18 -2
View File
@@ -83,13 +83,29 @@ entitymanager_t *sceneGetEntities(const sceneid_t id);
physicsworld_t *sceneGetPhysics(const sceneid_t id);
/**
* Ticks the active scene's entities (update callbacks, then a physics
* step) on fixed timesteps only, per DUSK_TIME_DYNAMIC.
* Called every frame (every call to engineUpdate). On a non-dynamic-time
* platform (DUSK_TIME_DYNAMIC undefined) every frame is itself a fixed
* timestep, so this calls sceneFixedUpdate() every time. On a
* dynamic-time platform, most calls are sub-steps still accumulating
* toward the next fixed timestep (TIME.dynamicUpdate is true) and do
* nothing here; this calls sceneFixedUpdate() only on the one call where
* TIME.dynamicUpdate is false, i.e. exactly once per fixed timestep.
*
* @return An error if the update failed, or errorOk() if it succeeded.
*/
errorret_t sceneUpdate(void);
/**
* Ticks the active scene's entities (update callbacks, then a physics
* step) by one fixed timestep (TIME.delta). Called by sceneUpdate() at
* the fixed-timestep cadence described there -- never once per rendered
* frame on a dynamic-time platform, so game logic and physics stay
* frame-rate independent and deterministic.
*
* @return An error if the update failed, or errorOk() if it succeeded.
*/
errorret_t sceneFixedUpdate(void);
/**
* Renders the active scene (entities, render pipeline, UI).
*
+13
View File
@@ -39,13 +39,26 @@
#endif
typedef struct {
/** Fixed simulation timestep, always DUSK_TIME_STEP. */
float_t delta;
/** Total elapsed fixed-simulation time, in DUSK_TIME_STEP increments. */
float_t time;
#ifdef DUSK_TIME_DYNAMIC
/** dynamicTime at the last fixed-timestep boundary. */
float_t lastNonDynamic;
/**
* False on the one timeUpdate() call per fixed timestep where enough
* real time has accumulated to cross a DUSK_TIME_STEP boundary (delta
* and time were just advanced) -- true on every other call, still
* accumulating toward the next one. Fixed-rate systems (scene
* fixed update, physics) should run only when this is false; systems
* that want every real frame (e.g. rendering) run unconditionally.
*/
bool_t dynamicUpdate;
/** Real elapsed time since the previous timeUpdate() call. */
float_t dynamicDelta;
/** Total elapsed real time, accumulated every timeUpdate() call. */
float_t dynamicTime;
#endif
} dusktime_t;