Time fixes
This commit is contained in:
@@ -55,7 +55,6 @@ target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
||||
DUSK_DISPLAY_WIDTH=480
|
||||
DUSK_DISPLAY_HEIGHT=272
|
||||
DUSK_THREAD_PTHREAD
|
||||
DUSK_TIME_DYNAMIC
|
||||
DUSK_DISPLAY_OVERSCAN=6
|
||||
)
|
||||
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,4 +8,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
physicsworld.c
|
||||
physicstest.c
|
||||
physicsshapemesh.c
|
||||
)
|
||||
|
||||
@@ -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 }
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
@@ -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).
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -8,3 +8,4 @@ include(dusktest)
|
||||
# Tests
|
||||
dusktest(test_physicstest.c)
|
||||
dusktest(test_physicsworld.c)
|
||||
dusktest(test_physicsshapemesh.c)
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "util/memory.h"
|
||||
#include "physics/physicstest.h"
|
||||
#include "physics/physicsshapemesh.h"
|
||||
|
||||
// A flat 10x10 ground quad on the XZ plane at y=0, centered at the origin
|
||||
// (two triangles, tightly packed vec3 vertices -- no indices).
|
||||
static vec3 FLAT_GROUND_VERTICES[6] = {
|
||||
{ -5.0f, 0.0f, -5.0f }, { 5.0f, 0.0f, -5.0f }, { 5.0f, 0.0f, 5.0f },
|
||||
{ -5.0f, 0.0f, -5.0f }, { 5.0f, 0.0f, 5.0f }, { -5.0f, 0.0f, 5.0f },
|
||||
};
|
||||
|
||||
static physicsshapemesh_t flatGroundMesh(void) {
|
||||
return (physicsshapemesh_t){
|
||||
.vertices = (const uint8_t *)FLAT_GROUND_VERTICES,
|
||||
.stride = sizeof(vec3),
|
||||
.triangleCount = 2
|
||||
};
|
||||
}
|
||||
|
||||
static void test_getVertexReadsRawPositions(void **state) {
|
||||
physicsshapemesh_t mesh = flatGroundMesh();
|
||||
|
||||
vec3 v0, v3;
|
||||
physicsShapeMeshGetVertex(&mesh, 0, v0);
|
||||
physicsShapeMeshGetVertex(&mesh, 3, v3);
|
||||
|
||||
assert_float_equal(v0[0], -5.0f, 0.0001f);
|
||||
assert_float_equal(v0[2], -5.0f, 0.0001f);
|
||||
assert_float_equal(v3[0], -5.0f, 0.0001f);
|
||||
assert_float_equal(v3[2], -5.0f, 0.0001f);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
// Vertex layout matching a typical interleaved render vertex (uv then
|
||||
// pos), to prove the stride-based reader can point straight at a render
|
||||
// buffer instead of a dedicated physics copy.
|
||||
typedef struct {
|
||||
float_t uv[2];
|
||||
float_t pos[3];
|
||||
} testInterleavedVertex_t;
|
||||
|
||||
static void test_supportsInterleavedRenderVertexLayout(void **state) {
|
||||
testInterleavedVertex_t vertices[3] = {
|
||||
{ .uv = { 0, 0 }, .pos = { -5.0f, 0.0f, -5.0f } },
|
||||
{ .uv = { 1, 0 }, .pos = { 5.0f, 0.0f, -5.0f } },
|
||||
{ .uv = { 0, 1 }, .pos = { -5.0f, 0.0f, 5.0f } },
|
||||
};
|
||||
physicsshapemesh_t mesh = {
|
||||
.vertices = (const uint8_t *)vertices[0].pos,
|
||||
.stride = sizeof(testInterleavedVertex_t),
|
||||
.triangleCount = 1
|
||||
};
|
||||
|
||||
vec3 v1;
|
||||
physicsShapeMeshGetVertex(&mesh, 1, v1);
|
||||
assert_float_equal(v1[0], 5.0f, 0.0001f);
|
||||
assert_float_equal(v1[2], -5.0f, 0.0001f);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_sphereRestingOnMesh(void **state) {
|
||||
physicsshapemesh_t mesh = flatGroundMesh();
|
||||
vec3 meshPos = { 0.0f, 0.0f, 0.0f };
|
||||
|
||||
// Sphere overlapping the ground by 0.2.
|
||||
vec3 sphereCenter = { 0.0f, 0.3f, 0.0f };
|
||||
vec3 normal; float_t depth;
|
||||
assert_true(physicsShapeMeshTestSphere(
|
||||
&mesh, meshPos, sphereCenter, 0.5f, normal, &depth
|
||||
));
|
||||
assert_float_equal(depth, 0.2f, 0.0001f);
|
||||
assert_float_equal(normal[1], 1.0f, 0.0001f); // pushes sphere up
|
||||
|
||||
// Far above: no overlap.
|
||||
vec3 sphereFar = { 0.0f, 10.0f, 0.0f };
|
||||
assert_false(physicsShapeMeshTestSphere(
|
||||
&mesh, meshPos, sphereFar, 0.5f, normal, &depth
|
||||
));
|
||||
|
||||
// Offsetting the mesh's own position shifts the ground with it: raising
|
||||
// both the ground and the sphere together reproduces the same overlap.
|
||||
vec3 meshPosRaised = { 0.0f, 1.0f, 0.0f };
|
||||
vec3 sphereCenterRaised = { 0.0f, 1.3f, 0.0f };
|
||||
assert_true(physicsShapeMeshTestSphere(
|
||||
&mesh, meshPosRaised, sphereCenterRaised, 0.5f, normal, &depth
|
||||
));
|
||||
assert_float_equal(depth, 0.2f, 0.0001f);
|
||||
|
||||
// But the sphere no longer overlaps the ground at its old (un-raised)
|
||||
// height, since the ground moved out from under it.
|
||||
assert_false(physicsShapeMeshTestSphere(
|
||||
&mesh, meshPosRaised, sphereCenter, 0.5f, normal, &depth
|
||||
));
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_capsuleRestingOnMesh(void **state) {
|
||||
physicsshapemesh_t mesh = flatGroundMesh();
|
||||
vec3 meshPos = { 0.0f, 0.0f, 0.0f };
|
||||
|
||||
// Standing capsule (half-height 1) whose bottom cap dips 0.1 into the
|
||||
// ground: center at y = radius + halfHeight - 0.1.
|
||||
const float_t radius = 0.5f;
|
||||
const float_t halfHeight = 1.0f;
|
||||
vec3 capsuleCenter = { 0.0f, radius + halfHeight - 0.1f, 0.0f };
|
||||
|
||||
vec3 normal; float_t depth;
|
||||
assert_true(physicsShapeMeshTestCapsule(
|
||||
&mesh, meshPos, capsuleCenter, radius, halfHeight, normal, &depth
|
||||
));
|
||||
assert_float_equal(depth, 0.1f, 0.001f);
|
||||
assert_float_equal(normal[1], 1.0f, 0.0001f);
|
||||
|
||||
// Lifted well clear of the ground: no overlap.
|
||||
vec3 capsuleHigh = { 0.0f, 10.0f, 0.0f };
|
||||
assert_false(physicsShapeMeshTestCapsule(
|
||||
&mesh, meshPos, capsuleHigh, radius, halfHeight, normal, &depth
|
||||
));
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_cubeIsUnsupported(void **state) {
|
||||
physicsshapemesh_t mesh = flatGroundMesh();
|
||||
vec3 meshPos = { 0.0f, 0.0f, 0.0f };
|
||||
|
||||
physicsshape_t cube = {
|
||||
.type = PHYSICS_SHAPE_CUBE,
|
||||
.data.cube.halfExtents = { 0.5f, 0.5f, 0.5f }
|
||||
};
|
||||
vec3 cubePos = { 0.0f, 0.3f, 0.0f };
|
||||
vec3 normal; float_t depth;
|
||||
assert_false(physicsShapeMeshTest(
|
||||
meshPos, &mesh, cubePos, &cube, normal, &depth
|
||||
));
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_physicsShapeMeshCreateIntegratesWithDispatch(void **state) {
|
||||
physicsshapemesh_t mesh = flatGroundMesh();
|
||||
physicsshape_t landscape = physicsShapeMeshCreate(&mesh);
|
||||
assert_int_equal(landscape.type, PHYSICS_SHAPE_CUSTOM);
|
||||
|
||||
physicsshape_t sphere = {
|
||||
.type = PHYSICS_SHAPE_SPHERE,
|
||||
.data.sphere.radius = 0.5f
|
||||
};
|
||||
vec3 meshPos = { 0.0f, 0.0f, 0.0f };
|
||||
vec3 sphereCenter = { 0.0f, 0.3f, 0.0f };
|
||||
|
||||
// Landscape as B, matching physicsWorldStep's dynamic(A)-vs-static(B).
|
||||
vec3 normal; float_t depth;
|
||||
assert_true(physicsTestShapeVsShape(
|
||||
sphereCenter, sphere, meshPos, landscape, normal, &depth
|
||||
));
|
||||
assert_float_equal(depth, 0.2f, 0.0001f);
|
||||
assert_float_equal(normal[1], 1.0f, 0.0001f);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_getVertexReadsRawPositions),
|
||||
cmocka_unit_test(test_supportsInterleavedRenderVertexLayout),
|
||||
cmocka_unit_test(test_sphereRestingOnMesh),
|
||||
cmocka_unit_test(test_capsuleRestingOnMesh),
|
||||
cmocka_unit_test(test_cubeIsUnsupported),
|
||||
cmocka_unit_test(test_physicsShapeMeshCreateIntegratesWithDispatch),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
+44
-5
@@ -12,6 +12,7 @@
|
||||
#include "entity/entitymanager.h"
|
||||
|
||||
static void countingUpdateCallback(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
void *user
|
||||
@@ -58,10 +59,9 @@ static void test_sceneEntitiesAreIsolatedPerScene(void **state) {
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_sceneUpdateOnlyTicksActiveScene(void **state) {
|
||||
static void test_sceneFixedUpdateOnlyTicksActiveScene(void **state) {
|
||||
sceneInit();
|
||||
timeInit();
|
||||
TIME.dynamicUpdate = true;
|
||||
|
||||
sceneid_t sceneA = sceneCreate();
|
||||
sceneid_t sceneB = sceneCreate();
|
||||
@@ -84,13 +84,13 @@ static void test_sceneUpdateOnlyTicksActiveScene(void **state) {
|
||||
);
|
||||
|
||||
sceneSetActive(sceneA);
|
||||
sceneUpdate();
|
||||
sceneFixedUpdate();
|
||||
|
||||
assert_int_equal(counterA, 1);
|
||||
assert_int_equal(counterB, 0);
|
||||
|
||||
sceneSetActive(sceneB);
|
||||
sceneUpdate();
|
||||
sceneFixedUpdate();
|
||||
|
||||
assert_int_equal(counterA, 1);
|
||||
assert_int_equal(counterB, 1);
|
||||
@@ -99,6 +99,44 @@ static void test_sceneUpdateOnlyTicksActiveScene(void **state) {
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_sceneUpdateOnlyRunsFixedUpdateOnNonDynamicFrames(void **state) {
|
||||
sceneInit();
|
||||
timeInit();
|
||||
|
||||
sceneid_t scene = sceneCreate();
|
||||
sceneSetActive(scene);
|
||||
|
||||
uint32_t counter = 0;
|
||||
entitymanager_t *entities = sceneGetEntities(scene);
|
||||
entityid_t entity = entityManagerAdd(entities);
|
||||
entityUpdateAdd(
|
||||
entities, entity, countingUpdateCallback, COMPONENT_ID_INVALID, &counter
|
||||
);
|
||||
|
||||
#if DUSK_TIME_DYNAMIC
|
||||
// Still accumulating toward the next fixed timestep: sceneUpdate must
|
||||
// not run sceneFixedUpdate yet.
|
||||
TIME.dynamicUpdate = true;
|
||||
sceneUpdate();
|
||||
assert_int_equal(counter, 0);
|
||||
|
||||
// A fixed timestep boundary was just crossed: sceneUpdate must run
|
||||
// sceneFixedUpdate exactly once.
|
||||
TIME.dynamicUpdate = false;
|
||||
sceneUpdate();
|
||||
assert_int_equal(counter, 1);
|
||||
#else
|
||||
// Non-dynamic-time platforms: every frame is a fixed timestep.
|
||||
sceneUpdate();
|
||||
assert_int_equal(counter, 1);
|
||||
sceneUpdate();
|
||||
assert_int_equal(counter, 2);
|
||||
#endif
|
||||
|
||||
sceneDispose();
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_sceneDestroyClearsActive(void **state) {
|
||||
sceneInit();
|
||||
|
||||
@@ -117,7 +155,8 @@ int main(int argc, char **argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_sceneCreateDestroy),
|
||||
cmocka_unit_test(test_sceneEntitiesAreIsolatedPerScene),
|
||||
cmocka_unit_test(test_sceneUpdateOnlyTicksActiveScene),
|
||||
cmocka_unit_test(test_sceneFixedUpdateOnlyTicksActiveScene),
|
||||
cmocka_unit_test(test_sceneUpdateOnlyRunsFixedUpdateOnNonDynamicFrames),
|
||||
cmocka_unit_test(test_sceneDestroyClearsActive),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user