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
-1
View File
@@ -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
)
+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;
+1
View File
@@ -8,3 +8,4 @@ include(dusktest)
# Tests
dusktest(test_physicstest.c)
dusktest(test_physicsworld.c)
dusktest(test_physicsshapemesh.c)
+185
View File
@@ -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
View File
@@ -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),
};