Restore ECS

This commit is contained in:
2026-07-18 19:05:51 -05:00
parent 11e5d37563
commit 1fa5cd316e
37 changed files with 3130 additions and 1033 deletions
+1
View File
@@ -58,6 +58,7 @@ add_subdirectory(assert)
add_subdirectory(asset)
add_subdirectory(console)
add_subdirectory(display)
add_subdirectory(entity)
add_subdirectory(log)
add_subdirectory(engine)
add_subdirectory(error)
+37
View File
@@ -12,6 +12,8 @@
#include "locale/localemanager.h"
#include "display/display.h"
#include "scene/scene.h"
#include "entity/entitymanager.h"
#include "entity/component/display/entityposition.h"
#include "asset/asset.h"
#include "ui/ui.h"
#include "assert/assert.h"
@@ -46,6 +48,31 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
errorChain(networkInit());
errorChain(sceneInit());
// Test: a spinning cube, viewed by a static camera.
ENGINE.testSceneId = sceneCreate();
sceneSetActive(ENGINE.testSceneId);
entitymanager_t *testEntities = sceneGetEntities(ENGINE.testSceneId);
entityid_t testCamera = entityManagerAdd(testEntities);
componentid_t testCameraPosition = entityAddComponent(
testEntities, testCamera, COMPONENT_TYPE_POSITION
);
entityAddComponent(testEntities, testCamera, COMPONENT_TYPE_CAMERA);
entityPositionLookAt(
testEntities, testCamera, testCameraPosition,
(vec3){ 3.0f, 3.0f, 3.0f },
(vec3){ 0.0f, 0.0f, 0.0f },
(vec3){ 0.0f, 1.0f, 0.0f }
);
ENGINE.testCubeEntity = entityManagerAdd(testEntities);
ENGINE.testCubePositionComponent = entityAddComponent(
testEntities, ENGINE.testCubeEntity, COMPONENT_TYPE_POSITION
);
entityAddComponent(
testEntities, ENGINE.testCubeEntity, COMPONENT_TYPE_RENDERABLE
);
networkRequestConnection(
engineNetworkOnConnected,
engineNetworkOnFailed,
@@ -83,6 +110,16 @@ errorret_t engineUpdate(void) {
if(dialogType == SYSTEM_DIALOG_TYPE_NONE) {
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());
+8
View File
@@ -10,6 +10,8 @@
// Important to be included first:
#include "display/display.h"
#include "error/error.h"
#include "scene/scenebase.h"
#include "entity/entitybase.h"
typedef struct {
bool_t running;
@@ -20,6 +22,12 @@ typedef struct {
// Test: disconnects the network 10 seconds after it connects.
bool_t networkDisconnectTestPending;
float_t networkDisconnectTestAt;
// Test: a spinning cube, viewed by a static camera.
sceneid_t testSceneId;
entityid_t testCubeEntity;
componentid_t testCubePositionComponent;
float_t testCubeRotation;
} engine_t;
extern engine_t ENGINE;
+15
View File
@@ -0,0 +1,15 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
entity.c
entitymanager.c
component.c
)
# Subdirs
add_subdirectory(component)
+152
View File
@@ -0,0 +1,152 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entitymanager.h"
#include "assert/assert.h"
#include "util/memory.h"
componentdefinition_t COMPONENT_DEFINITIONS[] = {
[COMPONENT_TYPE_NULL] = { 0 },
#define X(enm, type, field, iMethod, dMethod, rMethod) \
[COMPONENT_TYPE_##enm] = { \
.enumName = #enm, \
.name = #field, \
.init = iMethod, \
.dispose = dMethod, \
.render = rMethod \
},
#include "componentlist.h"
#undef X
[COMPONENT_TYPE_COUNT] = { 0 }
};
void componentInit(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const componenttype_t type
) {
assertNotNull(mgr, "Entity manager cannot be null");
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot initialize null component");
componentindex_t index = componentGetIndex(entityId, componentId);
component_t *cmp = &mgr->components[index];
memoryZero(cmp, sizeof(component_t));
cmp->type = type;
if(COMPONENT_DEFINITIONS[type].init) {
COMPONENT_DEFINITIONS[type].init(mgr, entityId, componentId);
}
}
void * componentGetData(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const componenttype_t type
) {
assertNotNull(mgr, "Entity manager cannot be null");
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot get data of null component");
componentindex_t index = componentGetIndex(entityId, componentId);
component_t *cmp = &mgr->components[index];
assertTrue(cmp->type == type, "Component type mismatch");
return &cmp->data;
}
componentindex_t componentGetIndex(
const entityid_t entityId,
const componentid_t componentId
) {
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
return (entityId * ENTITY_COMPONENT_COUNT_MAX) + componentId;
}
entityid_t componentGetEntitiesWithComponent(
entitymanager_t *mgr,
const componenttype_t type,
entityid_t outEntities[ENTITY_COUNT_MAX],
componentid_t outComponents[ENTITY_COUNT_MAX]
) {
assertNotNull(mgr, "Entity manager cannot be null");
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot check NULL type");
assertNotNull(outEntities, "Output entities array cannot be null");
assertNotNull(outComponents, "Output components array cannot be null");
entityid_t written = 0;
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
componentid_t used = mgr->entitiesWithComponent[
type * ENTITY_COUNT_MAX + i
];
if(used == COMPONENT_ID_INVALID) continue;
assertTrue(
mgr->components[componentGetIndex(i, used)].type == type,
"Component type mismatch in entitiesWithComponent lookup"
);
assertTrue(
(mgr->entities[i].state & ENTITY_STATE_ACTIVE) != 0,
"Inactive entity in entitiesWithComponent lookup"
);
assertTrue(
used < ENTITY_COMPONENT_COUNT_MAX,
"Component ID OOB in entitiesWithComponent lookup"
);
assertTrue(
componentGetIndex(i,used) < ENTITY_COUNT_MAX*ENTITY_COMPONENT_COUNT_MAX,
"Component index OOB in entitiesWithComponent lookup"
);
outComponents[written] = used;
outEntities[written++] = i;
}
return written;
}
errorret_t componentRenderAll(entitymanager_t *mgr) {
assertNotNull(mgr, "Entity manager cannot be null");
for(entityid_t eid = 0; eid < ENTITY_COUNT_MAX; eid++) {
if(!(mgr->entities[eid].state & ENTITY_STATE_ACTIVE)) continue;
for(componentid_t cid = 0; cid < ENTITY_COMPONENT_COUNT_MAX; cid++) {
component_t *cmp = &mgr->components[componentGetIndex(eid, cid)];
if(cmp->type == COMPONENT_TYPE_NULL) continue;
if(!COMPONENT_DEFINITIONS[cmp->type].render) continue;
errorChain(COMPONENT_DEFINITIONS[cmp->type].render(mgr, eid, cid));
}
}
errorOk();
}
void componentDispose(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
) {
assertNotNull(mgr, "Entity manager cannot be null");
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
componentindex_t index = componentGetIndex(entityId, componentId);
component_t *cmp = &mgr->components[index];
if(cmp->type == COMPONENT_TYPE_NULL) return;
if(COMPONENT_DEFINITIONS[cmp->type].dispose) {
COMPONENT_DEFINITIONS[cmp->type].dispose(mgr, entityId, componentId);
}
cmp->type = COMPONENT_TYPE_NULL;
}
+129
View File
@@ -0,0 +1,129 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entitybase.h"
#define X(enumName, type, field, init, dispose, render) \
// do nothing
#include "componentlist.h"
#undef X
typedef union {
#define X(enumName, type, field, init, dispose, render) type field;
#include "componentlist.h"
#undef X
} componentdata_t;
typedef struct {
const char_t *enumName;
const char_t *name;
void (*init)(entitymanager_t *, const entityid_t, const componentid_t);
void (*dispose)(entitymanager_t *, const entityid_t, const componentid_t);
errorret_t (*render)(entitymanager_t *, const entityid_t, const componentid_t);
} componentdefinition_t;
typedef enum {
COMPONENT_TYPE_NULL,
#define X(enumName, type, field, init, dispose, render) \
COMPONENT_TYPE_##enumName,
#include "componentlist.h"
#undef X
COMPONENT_TYPE_COUNT
} componenttype_t;
typedef struct {
componenttype_t type;
componentdata_t data;
} component_t;
extern componentdefinition_t COMPONENT_DEFINITIONS[];
/**
* Initializes a component of the given type for the entity with component ID.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param type The type of the component to initialize.
*/
void componentInit(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const componenttype_t type
);
/**
* Gets the pointer to the data of a component for the entity with component ID.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param type The type of the component to get, only used for assertion.
* @return A pointer to the component data.
*/
void * componentGetData(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const componenttype_t type
);
/**
* Gets the index of a component for the entity with component ID.
*
* @param entityId The entity ID.
* @param componentId The component ID.
* @return The index of the component in the component array.
*/
componentindex_t componentGetIndex(
const entityid_t entityId,
const componentid_t componentId
);
/**
* Gets the entity IDs of all entities with a component of the given type.
*
* @param mgr The entity manager to search.
* @param type The type of the component to get entities for.
* @param outEntities An array to write the entity IDs to, must be at least
* ENTITY_COUNT_MAX in size.
* @param outComponents An array to write the component IDs to.
* @return The number of entity IDs written to outEntities.
*/
entityid_t componentGetEntitiesWithComponent(
entitymanager_t *mgr,
const componenttype_t type,
entityid_t outEntities[ENTITY_COUNT_MAX],
componentid_t outComponents[ENTITY_COUNT_MAX]
);
/**
* Disposes of a component for the entity with component ID.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
*/
void componentDispose(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
);
/**
* Calls the render callback on every active component that defines one.
* Iterates all active entities and all their component slots. No-op for
* components whose definition has render == NULL.
*
* @param mgr The entity manager to render.
* @return Error state.
*/
errorret_t componentRenderAll(entitymanager_t *mgr);
@@ -3,10 +3,5 @@
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(dusktest)
# Tests
dusktest(test_physicsbody.c)
dusktest(test_physicsworld.c)
# Subdirs
add_subdirectory(display)
@@ -0,0 +1,12 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
entityposition.c
entitycamera.c
entityrenderable.c
)
@@ -0,0 +1,140 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entity/entitymanager.h"
#include "entity/entity.h"
#include "entity/component/display/entityposition.h"
#include "display/screen/screen.h"
void entityCameraInit(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
) {
entitycamera_t *cam = (entitycamera_t *)componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_CAMERA
);
cam->nearClip = 0.1f;
cam->farClip = 5000.0f;
cam->projType = ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE;
cam->perspective.fov = glm_rad(45.0f);
}
void entityCameraGetProjection(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
mat4 out
) {
entitycamera_t *cam = (entitycamera_t *)componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_CAMERA
);
if(
cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE ||
cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED
) {
glm_mat4_identity(out);
glm_perspective(
cam->perspective.fov,
SCREEN.aspect,
cam->nearClip,
cam->farClip,
out
);
if(cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED) {
out[1][1] *= -1.0f;
}
} else if(cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) {
glm_mat4_identity(out);
glm_ortho(
cam->orthographic.left,
cam->orthographic.right,
cam->orthographic.top,
cam->orthographic.bottom,
cam->nearClip,
cam->farClip,
out
);
}
}
entityid_t entityCameraGetCurrent(entitymanager_t *mgr) {
entityid_t camEnts[ENTITY_COUNT_MAX];
componentid_t camComps[ENTITY_COUNT_MAX];
entityid_t count = componentGetEntitiesWithComponent(
mgr, COMPONENT_TYPE_CAMERA, camEnts, camComps
);
if(count == 0) return ENTITY_ID_INVALID;
return camEnts[0];
}
void entityCameraGetForward(
entitymanager_t *mgr,
const entityid_t entityId,
vec2 out
) {
componentid_t posComp = entityGetComponent(
mgr, entityId, COMPONENT_TYPE_POSITION
);
mat4 transform;
entityPositionGetTransform(mgr, entityId, posComp, transform);
// transform is an object->world matrix; column 2 is the entity's local Z
// axis expressed in world space. Cameras look down their local -Z.
float_t fx = -transform[2][0];
float_t fz = -transform[2][2];
float_t len = sqrtf(fx * fx + fz * fz);
if(len > 1e-6f) { fx /= len; fz /= len; }
out[0] = fx;
out[1] = fz;
}
void entityCameraGetRight(
entitymanager_t *mgr,
const entityid_t entityId,
vec2 out
) {
componentid_t posComp = entityGetComponent(
mgr, entityId, COMPONENT_TYPE_POSITION
);
mat4 transform;
entityPositionGetTransform(mgr, entityId, posComp, transform);
// transform is an object->world matrix; column 0 is the entity's local X
// (right) axis expressed in world space.
float_t rx = transform[0][0];
float_t rz = transform[0][2];
float_t len = sqrtf(rx * rx + rz * rz);
if(len > 1e-6f) { rx /= len; rz /= len; }
out[0] = rx;
out[1] = rz;
}
void entityCameraLookAtPixelPerfect(
entitymanager_t *mgr,
const entityid_t ent,
const componentid_t posComp,
const componentid_t camComp,
const vec3 point,
const vec3 eyeOffset,
const float_t scale
) {
entitycamera_t *cam = (entitycamera_t *)componentGetData(
mgr, ent, camComp, COMPONENT_TYPE_CAMERA
);
float_t dist = (
(float_t)SCREEN.height / (2.0f * scale * tanf(cam->perspective.fov * 0.5f))
);
vec3 eye = {
point[0] + eyeOffset[0],
point[1] + dist + eyeOffset[1],
point[2] + eyeOffset[2]
};
vec3 up = { 0.0f, 0.0f, -1.0f };
entityPositionLookAt(mgr, ent, posComp, eye, (float_t *)point, up);
}
@@ -0,0 +1,121 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entity/entitybase.h"
typedef enum {
ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE,
ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED,
ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC
} entitycameraprojectiontype_t;
typedef struct {
union {
struct {
float_t fov;
} perspective;
struct {
float_t left;
float_t right;
float_t top;
float_t bottom;
} orthographic;
};
float_t nearClip;
float_t farClip;
entitycameraprojectiontype_t projType;
} entitycamera_t;
/**
* Initializes an entity camera component.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
*/
void entityCameraInit(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
);
/**
* Renders out the projection matrix for the given camera.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param out The output projection matrix.
*/
void entityCameraGetProjection(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
mat4 out
);
/**
* Returns the entity ID of the first active camera, or ENTITY_ID_INVALID if
* none are active.
*
* @param mgr The entity manager to search.
*/
entityid_t entityCameraGetCurrent(entitymanager_t *mgr);
/**
* Gets the camera's horizontal forward direction (XZ plane) from its
* position component. Automatically finds the position component on the
* entity.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The camera entity ID.
* @param out Output vec2: {forwardX, forwardZ} normalized.
*/
void entityCameraGetForward(
entitymanager_t *mgr,
const entityid_t entityId,
vec2 out
);
/**
* Gets the camera's horizontal right direction (XZ plane) from its position
* component. Automatically finds the position component on the entity.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The camera entity ID.
* @param out Output vec2: {rightX, rightZ} normalized.
*/
void entityCameraGetRight(
entitymanager_t *mgr,
const entityid_t entityId,
vec2 out
);
/**
* Positions the camera to look at a 3D point at a pixel-perfect distance
* derived from the camera's FOV and screen height.
*
* @param mgr The entity manager that owns the entity.
* @param ent The camera entity ID.
* @param posComp The position component ID.
* @param camComp The camera component ID.
* @param point World position to look at.
* @param eyeOffset Offset added to the eye position only (not the target).
* @param scale Pixels per world unit. 1.0 = pixel perfect, 2.0 = 2px per unit.
*/
void entityCameraLookAtPixelPerfect(
entitymanager_t *mgr,
const entityid_t ent,
const componentid_t posComp,
const componentid_t camComp,
const vec3 point,
const vec3 eyeOffset,
const float_t scale
);
@@ -0,0 +1,657 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entity/entitymanager.h"
void entityPositionInit(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
) {
entityposition_t *pos = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
);
pos->flags = 0;
pos->parentEntityId = ENTITY_ID_INVALID;
pos->parentComponentId = COMPONENT_ID_INVALID;
pos->childCount = 0;
glm_vec3_zero(pos->position);
glm_vec3_zero(pos->rotation);
glm_vec3_one(pos->scale);
glm_mat4_identity(pos->localTransform);
glm_mat4_identity(pos->worldTransform);
}
void entityPositionLookAt(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 eye,
vec3 target,
vec3 up
) {
entityposition_t *pos = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
);
// glm_lookat() produces a view matrix (world -> eye space). Every other
// setter treats localTransform as this entity's placement in world space
// (eye -> world), so invert it here to keep that meaning consistent --
// callers that need a view matrix (e.g. scene rendering) invert it back.
mat4 view;
glm_lookat(eye, target, up, view);
glm_mat4_inv(view, pos->localTransform);
// localTransform is now authoritative; PRS cache is stale.
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_PRS_DIRTY)
& ~(ENTITY_POSITION_FLAG_ROTATION_DIRTY |
ENTITY_POSITION_FLAG_POSITION_DIRTY);
entityPositionMarkDirty(mgr, pos);
}
void entityPositionGetTransform(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
mat4 dest
) {
entityposition_t *pos = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
);
entityPositionEnsureWorld(mgr, pos);
glm_mat4_copy(
pos->parentEntityId == ENTITY_ID_INVALID
? pos->localTransform : pos->worldTransform,
dest
);
}
void entityPositionGetLocalTransform(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
mat4 dest
) {
entityposition_t *pos = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
);
entityPositionEnsureLocal(pos);
glm_mat4_copy(pos->localTransform, dest);
}
void entityPositionGetLocalPosition(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 dest
) {
entityposition_t *pos = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
);
entityPositionEnsurePRS(pos);
glm_vec3_copy(pos->position, dest);
}
void entityPositionGetWorldPosition(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 dest
) {
entityposition_t *pos = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
);
if(pos->parentEntityId == ENTITY_ID_INVALID) {
entityPositionEnsurePRS(pos);
glm_vec3_copy(pos->position, dest);
return;
}
entityPositionEnsureWorld(mgr, pos);
dest[0] = pos->worldTransform[3][0];
dest[1] = pos->worldTransform[3][1];
dest[2] = pos->worldTransform[3][2];
}
void entityPositionSetWorldPosition(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 position
) {
entityposition_t *pos = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
);
if(pos->parentEntityId == ENTITY_ID_INVALID) {
glm_vec3_copy(position, pos->position);
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_POSITION_DIRTY)
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
entityPositionMarkDirty(mgr, pos);
return;
}
entityposition_t *parent = componentGetData(
mgr, pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION
);
entityPositionEnsureWorld(mgr, parent);
mat4 invParent;
glm_mat4_inv(parent->worldTransform, invParent);
vec3 localPos;
glm_mat4_mulv3(invParent, position, 1.0f, localPos);
glm_vec3_copy(localPos, pos->position);
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_POSITION_DIRTY)
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
entityPositionMarkDirty(mgr, pos);
}
void entityPositionSetLocalPosition(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 position
) {
entityposition_t *pos = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
);
glm_vec3_copy(position, pos->position);
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_POSITION_DIRTY)
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
entityPositionMarkDirty(mgr, pos);
}
void entityPositionGetLocalRotation(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 dest
) {
entityposition_t *pos = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
);
entityPositionEnsurePRS(pos);
glm_vec3_copy(pos->rotation, dest);
}
void entityPositionGetWorldRotation(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 dest
) {
entityposition_t *pos = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
);
if(pos->parentEntityId == ENTITY_ID_INVALID) {
entityPositionEnsurePRS(pos);
glm_vec3_copy(pos->rotation, dest);
return;
}
entityPositionEnsureWorld(mgr, pos);
const float_t (*wt)[4] = pos->worldTransform;
const float_t sx = sqrtf(
wt[0][0]*wt[0][0] + wt[0][1]*wt[0][1] + wt[0][2]*wt[0][2]
);
const float_t sy = sqrtf(
wt[1][0]*wt[1][0] + wt[1][1]*wt[1][1] + wt[1][2]*wt[1][2]
);
const float_t sz = sqrtf(
wt[2][0]*wt[2][0] + wt[2][1]*wt[2][1] + wt[2][2]*wt[2][2]
);
const float_t r00 = sx > 0.0f ? wt[0][0]/sx : 0.0f;
const float_t r10 = sy > 0.0f ? wt[1][0]/sy : 0.0f;
const float_t r20 = sz > 0.0f ? wt[2][0]/sz : 0.0f;
const float_t r01 = sx > 0.0f ? wt[0][1]/sx : 0.0f;
const float_t r11 = sy > 0.0f ? wt[1][1]/sy : 0.0f;
const float_t r21 = sz > 0.0f ? wt[2][1]/sz : 0.0f;
const float_t r22 = sz > 0.0f ? wt[2][2]/sz : 0.0f;
const float_t sinBeta = glm_clamp(r20, -1.0f, 1.0f);
dest[1] = asinf(sinBeta);
const float_t cosBeta = cosf(dest[1]);
if(fabsf(cosBeta) > 1e-6f) {
dest[0] = atan2f(-r21, r22);
dest[2] = atan2f(-r10, r00);
} else {
dest[2] = 0.0f;
dest[0] = (sinBeta > 0.0f) ? atan2f(r01, r11) : -atan2f(r01, r11);
}
}
void entityPositionSetLocalRotation(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 rotation
) {
entityposition_t *pos = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
);
glm_vec3_copy(rotation, pos->rotation);
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
entityPositionMarkDirty(mgr, pos);
}
void entityPositionSetWorldRotation(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 rotation
) {
entityposition_t *pos = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
);
if(pos->parentEntityId == ENTITY_ID_INVALID) {
glm_vec3_copy(rotation, pos->rotation);
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
entityPositionMarkDirty(mgr, pos);
return;
}
entityposition_t *parent = componentGetData(
mgr, pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION
);
entityPositionEnsureWorld(mgr, parent);
// Build target world rotation matrix (unit scale) from XYZ euler.
const float_t c0 = cosf(rotation[0]), s0 = sinf(rotation[0]);
const float_t c1 = cosf(rotation[1]), s1 = sinf(rotation[1]);
const float_t c2 = cosf(rotation[2]), s2 = sinf(rotation[2]);
const float_t s0s1 = s0*s1, c0s1 = c0*s1;
// Named wr[col_stored][row_stored] matching cglm column-major layout.
const float_t wr00 = c1*c2;
const float_t wr01 = c0*s2 + s0s1*c2;
const float_t wr02 = s0*s2 - c0s1*c2;
const float_t wr10 = -c1*s2;
const float_t wr11 = c0*c2 - s0s1*s2;
const float_t wr12 = s0*c2 + c0s1*s2;
const float_t wr20 = s1;
const float_t wr21 = -s0*c1;
const float_t wr22 = c0*c1;
// Normalize parent world columns to extract pure rotation.
const float_t (*pt)[4] = parent->worldTransform;
const float_t psx = sqrtf(
pt[0][0]*pt[0][0] + pt[0][1]*pt[0][1] + pt[0][2]*pt[0][2]
);
const float_t psy = sqrtf(
pt[1][0]*pt[1][0] + pt[1][1]*pt[1][1] + pt[1][2]*pt[1][2]
);
const float_t psz = sqrtf(
pt[2][0]*pt[2][0] + pt[2][1]*pt[2][1] + pt[2][2]*pt[2][2]
);
const float_t pr00 = psx > 0.f ? pt[0][0]/psx : 0.f;
const float_t pr01 = psx > 0.f ? pt[0][1]/psx : 0.f;
const float_t pr02 = psx > 0.f ? pt[0][2]/psx : 0.f;
const float_t pr10 = psy > 0.f ? pt[1][0]/psy : 0.f;
const float_t pr11 = psy > 0.f ? pt[1][1]/psy : 0.f;
const float_t pr12 = psy > 0.f ? pt[1][2]/psy : 0.f;
const float_t pr20 = psz > 0.f ? pt[2][0]/psz : 0.f;
const float_t pr21 = psz > 0.f ? pt[2][1]/psz : 0.f;
const float_t pr22 = psz > 0.f ? pt[2][2]/psz : 0.f;
// local_R = parent_R^T * world_R (R^-1 == R^T for orthogonal matrices).
// Compute only the 7 entries of the local rotation matrix needed for XYZ
// euler extraction (stored column-major: [col][row] = math [row][col]).
// sinBeta = stored[2][0] = math[0][2]
// r21/r22 = stored[2][1..2] = math[1..2][2]
// r10/r00 = stored[1][0], stored[0][0] = math[0][1], math[0][0]
// gimbal = stored[0][1], stored[1][1] = math[1][0], math[1][1]
const float_t lr00 = pr00*wr00 + pr01*wr10 + pr02*wr20; // math[0][0]
const float_t lr10 = pr00*wr01 + pr01*wr11 + pr02*wr21; // math[0][1]
const float_t lr20 = pr00*wr02 + pr01*wr12 + pr02*wr22; // [0][2] -> sinBeta
const float_t lr01 = pr10*wr00 + pr11*wr10 + pr12*wr20; // math[1][0]
const float_t lr11 = pr10*wr01 + pr11*wr11 + pr12*wr21; // math[1][1]
const float_t lr21 = pr10*wr02 + pr11*wr12 + pr12*wr22; // [1][2] -> r21
const float_t lr22 = pr20*wr02 + pr21*wr12 + pr22*wr22; // [2][2] -> r22
const float_t sinBeta = glm_clamp(lr20, -1.0f, 1.0f);
pos->rotation[1] = asinf(sinBeta);
const float_t cosBeta = cosf(pos->rotation[1]);
if(fabsf(cosBeta) > 1e-6f) {
pos->rotation[0] = atan2f(-lr21, lr22);
pos->rotation[2] = atan2f(-lr10, lr00);
} else {
pos->rotation[2] = 0.0f;
pos->rotation[0] = (sinBeta > 0.0f)
? atan2f(lr01, lr11) : -atan2f(lr01, lr11);
}
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
entityPositionMarkDirty(mgr, pos);
}
void entityPositionGetLocalScale(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 dest
) {
entityposition_t *pos = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
);
entityPositionEnsurePRS(pos);
glm_vec3_copy(pos->scale, dest);
}
void entityPositionGetWorldScale(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 dest
) {
entityposition_t *pos = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
);
if(pos->parentEntityId == ENTITY_ID_INVALID) {
entityPositionEnsurePRS(pos);
glm_vec3_copy(pos->scale, dest);
return;
}
entityPositionEnsureWorld(mgr, pos);
const float_t (*wt)[4] = pos->worldTransform;
dest[0] = sqrtf(wt[0][0]*wt[0][0] + wt[0][1]*wt[0][1] + wt[0][2]*wt[0][2]);
dest[1] = sqrtf(wt[1][0]*wt[1][0] + wt[1][1]*wt[1][1] + wt[1][2]*wt[1][2]);
dest[2] = sqrtf(wt[2][0]*wt[2][0] + wt[2][1]*wt[2][1] + wt[2][2]*wt[2][2]);
}
void entityPositionSetLocalScale(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 scale
) {
entityposition_t *pos = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
);
glm_vec3_copy(scale, pos->scale);
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
entityPositionMarkDirty(mgr, pos);
}
void entityPositionSetWorldScale(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 scale
) {
entityposition_t *pos = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
);
if(pos->parentEntityId == ENTITY_ID_INVALID) {
glm_vec3_copy(scale, pos->scale);
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
entityPositionMarkDirty(mgr, pos);
return;
}
entityposition_t *parent = componentGetData(
mgr, pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION
);
entityPositionEnsureWorld(mgr, parent);
const float_t (*pt)[4] = parent->worldTransform;
const float_t psx = sqrtf(
pt[0][0]*pt[0][0] + pt[0][1]*pt[0][1] + pt[0][2]*pt[0][2]
);
const float_t psy = sqrtf(
pt[1][0]*pt[1][0] + pt[1][1]*pt[1][1] + pt[1][2]*pt[1][2]
);
const float_t psz = sqrtf(
pt[2][0]*pt[2][0] + pt[2][1]*pt[2][1] + pt[2][2]*pt[2][2]
);
pos->scale[0] = psx > 0.0f ? scale[0] / psx : scale[0];
pos->scale[1] = psy > 0.0f ? scale[1] / psy : scale[1];
pos->scale[2] = psz > 0.0f ? scale[2] / psz : scale[2];
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
entityPositionMarkDirty(mgr, pos);
}
void entityPositionSetParent(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const entityid_t parentEntityId,
const componentid_t parentComponentId
) {
entityposition_t *pos = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
);
// Remove from old parent's child list.
if(pos->parentEntityId != ENTITY_ID_INVALID) {
entityposition_t *oldParent = componentGetData(
mgr, pos->parentEntityId, pos->parentComponentId,
COMPONENT_TYPE_POSITION
);
for(uint8_t i = 0; i < oldParent->childCount; i++) {
if(
oldParent->childEntityIds[i] == entityId &&
oldParent->childComponentIds[i] == componentId
) {
oldParent->childCount--;
for(uint8_t j = i; j < oldParent->childCount; j++) {
oldParent->childEntityIds[j] = oldParent->childEntityIds[j + 1];
oldParent->childComponentIds[j] = oldParent->childComponentIds[j + 1];
}
break;
}
}
}
pos->parentEntityId = parentEntityId;
pos->parentComponentId = parentComponentId;
// Register with new parent.
if(parentEntityId != ENTITY_ID_INVALID) {
entityposition_t *parent = componentGetData(
mgr, parentEntityId, parentComponentId, COMPONENT_TYPE_POSITION
);
if(parent->childCount < ENTITY_POSITION_CHILDREN_MAX) {
parent->childEntityIds[parent->childCount] = entityId;
parent->childComponentIds[parent->childCount] = componentId;
parent->childCount++;
}
}
entityPositionMarkDirty(mgr, pos);
}
entityposition_t *entityPositionGet(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
) {
return componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
);
}
void entityPositionRebuild(entitymanager_t *mgr, entityposition_t *pos) {
pos->flags = (
pos->flags |
ENTITY_POSITION_FLAG_ROTATION_DIRTY |
ENTITY_POSITION_FLAG_POSITION_DIRTY
) & ~ENTITY_POSITION_FLAG_PRS_DIRTY;
entityPositionMarkDirty(mgr, pos);
}
void entityPositionMarkDirty(entitymanager_t *mgr, entityposition_t *pos) {
if(pos->flags & ENTITY_POSITION_FLAG_WORLD_DIRTY) return;
pos->flags |= ENTITY_POSITION_FLAG_WORLD_DIRTY;
for(uint8_t i = 0; i < pos->childCount; i++) {
entityposition_t *child = componentGetData(
mgr, pos->childEntityIds[i], pos->childComponentIds[i],
COMPONENT_TYPE_POSITION
);
entityPositionMarkDirty(mgr, child);
}
}
void entityPositionDisposeDeep(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
) {
entityposition_t *pos = entityPositionGet(mgr, entityId, componentId);
// Detach from parent so the parent's child list stays consistent.
if(pos->parentEntityId != ENTITY_ID_INVALID) {
entityPositionSetParent(
mgr, entityId, componentId, ENTITY_ID_INVALID, COMPONENT_ID_INVALID
);
}
// Copy the child list before disposing self (entityDispose invalidates
// pos).
uint8_t childCount = pos->childCount;
entityid_t childEntityIds[ENTITY_POSITION_CHILDREN_MAX];
componentid_t childComponentIds[ENTITY_POSITION_CHILDREN_MAX];
for(uint8_t i = 0; i < childCount; i++) {
childEntityIds[i] = pos->childEntityIds[i];
childComponentIds[i] = pos->childComponentIds[i];
// Sever child's parent link so it won't try to modify our disposed
// data.
entityposition_t *child = entityPositionGet(
mgr, childEntityIds[i], childComponentIds[i]
);
child->parentEntityId = ENTITY_ID_INVALID;
child->parentComponentId = COMPONENT_ID_INVALID;
}
entityDispose(mgr, entityId);
for(uint8_t i = 0; i < childCount; i++) {
entityPositionDisposeDeep(mgr, childEntityIds[i], childComponentIds[i]);
}
}
void entityPositionDecompose(entityposition_t *pos) {
// Translation: column 3
pos->position[0] = pos->localTransform[3][0];
pos->position[1] = pos->localTransform[3][1];
pos->position[2] = pos->localTransform[3][2];
// Scale: length of each basis column (xyz only)
pos->scale[0] = sqrtf(
pos->localTransform[0][0] * pos->localTransform[0][0] +
pos->localTransform[0][1] * pos->localTransform[0][1] +
pos->localTransform[0][2] * pos->localTransform[0][2]
);
pos->scale[1] = sqrtf(
pos->localTransform[1][0] * pos->localTransform[1][0] +
pos->localTransform[1][1] * pos->localTransform[1][1] +
pos->localTransform[1][2] * pos->localTransform[1][2]
);
pos->scale[2] = sqrtf(
pos->localTransform[2][0] * pos->localTransform[2][0] +
pos->localTransform[2][1] * pos->localTransform[2][1] +
pos->localTransform[2][2] * pos->localTransform[2][2]
);
// Normalize columns to isolate the rotation matrix (no mat4 needed).
const float_t invS0 = pos->scale[0] > 0.0f ? 1.0f / pos->scale[0] : 0.0f;
const float_t invS1 = pos->scale[1] > 0.0f ? 1.0f / pos->scale[1] : 0.0f;
const float_t invS2 = pos->scale[2] > 0.0f ? 1.0f / pos->scale[2] : 0.0f;
const float_t r00 = pos->localTransform[0][0] * invS0;
const float_t r01 = pos->localTransform[0][1] * invS0;
const float_t r02 = pos->localTransform[0][2] * invS0;
const float_t r10 = pos->localTransform[1][0] * invS1;
const float_t r11 = pos->localTransform[1][1] * invS1;
const float_t r20 = pos->localTransform[2][0] * invS2;
const float_t r21 = pos->localTransform[2][1] * invS2;
const float_t r22 = pos->localTransform[2][2] * invS2;
// Extract XYZ euler angles (R = Rx * Ry * Rz, column-major)
const float_t sinBeta = glm_clamp(r20, -1.0f, 1.0f);
pos->rotation[1] = asinf(sinBeta);
const float_t cosBeta = cosf(pos->rotation[1]);
if(fabsf(cosBeta) > 1e-6f) {
pos->rotation[0] = atan2f(-r21, r22);
pos->rotation[2] = atan2f(-r10, r00);
} else {
// Gimbal lock: pin Z to 0, recover X.
pos->rotation[2] = 0.0f;
pos->rotation[0] = (sinBeta > 0.0f)
? atan2f(r01, r11)
: -atan2f(r01, r11);
}
}
void entityPositionEnsurePRS(entityposition_t *pos) {
if(!(pos->flags & ENTITY_POSITION_FLAG_PRS_DIRTY)) return;
entityPositionDecompose(pos);
pos->flags &= ~ENTITY_POSITION_FLAG_PRS_DIRTY;
}
void entityPositionEnsureLocal(entityposition_t *pos) {
const uint8_t dirty = pos->flags & (
ENTITY_POSITION_FLAG_ROTATION_DIRTY | ENTITY_POSITION_FLAG_POSITION_DIRTY
);
if(!dirty) return;
if(dirty & ENTITY_POSITION_FLAG_ROTATION_DIRTY) {
// Rotation or scale changed: rebuild cols 0-2 analytically (XYZ euler).
const float_t c0 = cosf(pos->rotation[0]), s0 = sinf(pos->rotation[0]);
const float_t c1 = cosf(pos->rotation[1]), s1 = sinf(pos->rotation[1]);
const float_t c2 = cosf(pos->rotation[2]), s2 = sinf(pos->rotation[2]);
const float_t s0s1 = s0 * s1;
const float_t c0s1 = c0 * s1;
pos->localTransform[0][0] = c1 * c2 * pos->scale[0];
pos->localTransform[0][1] = (c0 * s2 + s0s1 * c2) * pos->scale[0];
pos->localTransform[0][2] = (s0 * s2 - c0s1 * c2) * pos->scale[0];
pos->localTransform[0][3] = 0.0f;
pos->localTransform[1][0] = -c1 * s2 * pos->scale[1];
pos->localTransform[1][1] = (c0 * c2 - s0s1 * s2) * pos->scale[1];
pos->localTransform[1][2] = (s0 * c2 + c0s1 * s2) * pos->scale[1];
pos->localTransform[1][3] = 0.0f;
pos->localTransform[2][0] = s1 * pos->scale[2];
pos->localTransform[2][1] = -s0 * c1 * pos->scale[2];
pos->localTransform[2][2] = c0 * c1 * pos->scale[2];
pos->localTransform[2][3] = 0.0f;
}
if(dirty & ENTITY_POSITION_FLAG_POSITION_DIRTY) {
// Only position changed: update column 3 only (no trig needed).
pos->localTransform[3][0] = pos->position[0];
pos->localTransform[3][1] = pos->position[1];
pos->localTransform[3][2] = pos->position[2];
pos->localTransform[3][3] = 1.0f;
}
pos->flags &= ~(
ENTITY_POSITION_FLAG_ROTATION_DIRTY | ENTITY_POSITION_FLAG_POSITION_DIRTY
);
}
void entityPositionEnsureWorld(entitymanager_t *mgr, entityposition_t *pos) {
if(!(pos->flags & ENTITY_POSITION_FLAG_WORLD_DIRTY)) return;
entityPositionEnsureLocal(pos);
if(pos->parentEntityId != ENTITY_ID_INVALID) {
// Parented: world = parent.world x local. worldTransform must be
// written because children (and this node's getters) read it.
entityposition_t *parent = componentGetData(
mgr, pos->parentEntityId, pos->parentComponentId,
COMPONENT_TYPE_POSITION
);
entityPositionEnsureWorld(mgr, parent);
glm_mat4_mul(
parent->worldTransform, pos->localTransform, pos->worldTransform
);
} else if(pos->childCount > 0) {
// Parentless root with children: children need a valid worldTransform
// to multiply against, but world == local, so just copy.
glm_mat4_copy(pos->localTransform, pos->worldTransform);
}
// Parentless leaf: world == local. Getters read localTransform directly;
// no copy needed.
pos->flags &= ~ENTITY_POSITION_FLAG_WORLD_DIRTY;
}
@@ -0,0 +1,445 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entity/entitybase.h"
/** Maximum number of child position components this node can track. */
#define ENTITY_POSITION_CHILDREN_MAX 8
/**
* PRS cache is stale. localTransform was written directly (e.g. lookAt) and
* position/rotation/scale need to be decomposed before they can be read.
*/
#define ENTITY_POSITION_FLAG_PRS_DIRTY (1 << 0)
/**
* Columns 0-2 of localTransform are stale. Rotation or scale changed; the
* basis vectors need to be rebuilt analytically before the matrix can be used.
* Does not imply column 3 (translation) is stale.
*/
#define ENTITY_POSITION_FLAG_ROTATION_DIRTY (1 << 1)
/**
* Column 3 of localTransform is stale. Position changed; only the
* translation column needs to be written. Does not imply columns 0-2 are
* stale.
*/
#define ENTITY_POSITION_FLAG_POSITION_DIRTY (1 << 2)
/**
* worldTransform is stale. Either the local matrix changed or an ancestor
* moved; worldTransform must be recomputed before world data can be read.
*/
#define ENTITY_POSITION_FLAG_WORLD_DIRTY (1 << 3)
typedef struct {
/*
* Hot fields - flag checks, parent/child traversal (markDirty, ensureWorld)
* only touch these. Kept at the front so they share the first cache line.
*/
/** ENTITY_POSITION_FLAG_* bitmask; describes which caches are stale. */
uint8_t flags;
/** Entity ID of the parent node, or ENTITY_ID_INVALID if none. */
entityid_t parentEntityId;
/** Component ID of the parent position, or COMPONENT_ID_INVALID if none. */
componentid_t parentComponentId;
/** Number of currently registered children. */
uint8_t childCount;
/** Entity IDs of child nodes. */
entityid_t childEntityIds[ENTITY_POSITION_CHILDREN_MAX];
/** Component IDs of child position components. */
componentid_t childComponentIds[ENTITY_POSITION_CHILDREN_MAX];
/*
* Warm fields - read/written by PRS getters/setters.
* Accessed more often than the matrices but less often than flags.
*/
/** Cached local position (XYZ). Stale when PRS_DIRTY is set. */
vec3 position;
/** Cached local rotation (XYZ euler, radians). Stale when PRS_DIRTY. */
vec3 rotation;
/** Cached local scale (XYZ). Stale when PRS_DIRTY is set. */
vec3 scale;
/*
* Cold fields - only touched when actually rebuilding transforms.
*/
/** Local transform matrix, rebuilt lazily from position/rotation/scale. */
mat4 localTransform;
/** World transform matrix, recomputed lazily from the parent chain. */
mat4 worldTransform;
} entityposition_t;
/**
* Initializes the entity position component, setting identity transforms and
* zeroing all parent/child state.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
*/
void entityPositionInit(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
);
/**
* Positions and orients the entity at eye, facing target. Stores this as
* the entity's normal world-space placement (consistent with every other
* setter), not as a view matrix -- invert entityPositionGetTransform()'s
* result to get a view matrix for rendering.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param eye The eye/camera position.
* @param target The target point to look at.
* @param up The up vector.
*/
void entityPositionLookAt(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 eye,
vec3 target,
vec3 up
);
/**
* Gets the world-space transform matrix, recomputing it lazily if dirty.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param dest Destination matrix.
*/
void entityPositionGetTransform(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
mat4 dest
);
/**
* Gets the local transform matrix (does not include parent transforms).
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param dest Destination matrix.
*/
void entityPositionGetLocalTransform(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
mat4 dest
);
/**
* Gets the cached local position (XYZ). Decomposes localTransform into PRS
* first if ENTITY_POSITION_FLAG_PRS_DIRTY is set; never triggers a matrix
* rebuild or world-transform update.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param dest Destination vector.
*/
void entityPositionGetLocalPosition(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 dest
);
/**
* Gets the world-space position. For parentless entities this is the same as
* the local position.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param dest Destination vector.
*/
void entityPositionGetWorldPosition(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 dest
);
/**
* Sets the world-space position. For parentless entities this is equivalent
* to entityPositionSetLocalPosition. For parented entities the position is
* converted to local space via the inverted parent world transform.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param position The desired world-space position.
*/
void entityPositionSetWorldPosition(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 position
);
/**
* Sets the local position, marks localTransform and worldTransform (self +
* descendants) dirty.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param position The new local position.
*/
void entityPositionSetLocalPosition(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 position
);
/**
* Gets the cached local euler rotation (XYZ, radians). Decomposes
* localTransform first if ENTITY_POSITION_FLAG_PRS_DIRTY is set.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param dest Destination vector.
*/
void entityPositionGetLocalRotation(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 dest
);
/**
* Gets the world-space euler rotation (XYZ, radians) by decomposing the
* world transform. For parentless entities this is the same as local
* rotation.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param dest Destination vector.
*/
void entityPositionGetWorldRotation(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 dest
);
/**
* Sets the local euler rotation (XYZ, radians) and marks transforms dirty.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param rotation The new local rotation.
*/
void entityPositionSetLocalRotation(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 rotation
);
/**
* Sets the world-space euler rotation (XYZ, radians). For parentless
* entities this is equivalent to entityPositionSetLocalRotation. For
* parented entities the rotation is converted to local space by removing
* the parent world rotation.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param rotation The desired world-space euler rotation.
*/
void entityPositionSetWorldRotation(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 rotation
);
/**
* Gets the cached local scale. Decomposes localTransform first if
* ENTITY_POSITION_FLAG_PRS_DIRTY is set.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param dest Destination vector.
*/
void entityPositionGetLocalScale(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 dest
);
/**
* Gets the world-space scale by extracting column lengths from the world
* transform. For parentless entities this is the same as local scale.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param dest Destination vector.
*/
void entityPositionGetWorldScale(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 dest
);
/**
* Sets the local scale and marks transforms dirty.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param scale The new local scale.
*/
void entityPositionSetLocalScale(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 scale
);
/**
* Sets the world-space scale. For parentless entities this is equivalent to
* entityPositionSetLocalScale. For parented entities the scale is converted
* to local space by dividing by the parent world scale (assumes no shear).
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param scale The desired world-space scale.
*/
void entityPositionSetWorldScale(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
vec3 scale
);
/**
* Sets the parent of this entity's position component.
* Pass ENTITY_ID_INVALID / COMPONENT_ID_INVALID to detach from any parent.
*
* @param mgr The entity manager that owns both entities.
* @param entityId The child entity ID.
* @param componentId The child component ID.
* @param parentEntityId The parent entity ID.
* @param parentComponentId The parent component ID.
*/
void entityPositionSetParent(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const entityid_t parentEntityId,
const componentid_t parentComponentId
);
/**
* Returns a direct pointer to the entity position component data.
* After modifying localTransform directly, call entityPositionMarkDirty() to
* set ENTITY_POSITION_FLAG_WORLD_DIRTY on self and descendants. After
* modifying PRS directly, call entityPositionRebuild() instead.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @return Pointer to the component data.
*/
entityposition_t *entityPositionGet(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
);
/**
* Signals that the PRS cache was modified externally. Sets both
* ENTITY_POSITION_FLAG_ROTATION_DIRTY and ENTITY_POSITION_FLAG_POSITION_DIRTY
* so all of localTransform is rebuilt lazily on the next read, clears
* ENTITY_POSITION_FLAG_PRS_DIRTY, propagates ENTITY_POSITION_FLAG_WORLD_DIRTY
* to self and all descendants.
*
* @param mgr The entity manager that owns pos and its descendants.
* @param pos The position component whose PRS was modified.
*/
void entityPositionRebuild(entitymanager_t *mgr, entityposition_t *pos);
/**
* Sets ENTITY_POSITION_FLAG_WORLD_DIRTY on this node and all descendants,
* indicating that worldTransform must be recomputed before it is read.
* Call this after modifying localTransform directly.
*
* @param mgr The entity manager that owns pos and its descendants.
* @param pos The position component to mark dirty.
*/
void entityPositionMarkDirty(entitymanager_t *mgr, entityposition_t *pos);
/**
* Disposes this entity and all of its position-component descendants
* recursively. Detaches from any parent before destroying.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The root entity ID.
* @param componentId The root position component ID.
*/
void entityPositionDisposeDeep(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
);
/**
* Decomposes the local transform matrix back into the position, rotation
* (XYZ euler, radians), and scale cache fields.
*
* @param pos The position component to decompose.
*/
void entityPositionDecompose(entityposition_t *pos);
/**
* Internal. Decomposes localTransform into the PRS cache if
* ENTITY_POSITION_FLAG_PRS_DIRTY is set.
*
* @param pos The position component to update.
*/
void entityPositionEnsurePRS(entityposition_t *pos);
/**
* Internal. Rebuilds localTransform from the PRS cache, touching only the
* columns flagged as stale (ROTATION_DIRTY and/or POSITION_DIRTY).
*
* @param pos The position component to update.
*/
void entityPositionEnsureLocal(entityposition_t *pos);
/**
* Internal. Recomputes worldTransform from the parent chain if
* ENTITY_POSITION_FLAG_WORLD_DIRTY is set.
*
* @param mgr The entity manager that owns pos and its ancestors.
* @param pos The position component to update.
*/
void entityPositionEnsureWorld(entitymanager_t *mgr, entityposition_t *pos);
@@ -0,0 +1,150 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entityrenderable.h"
#include "entity/entitymanager.h"
#include "display/shader/shadermaterial.h"
#include "display/shader/shaderunlit.h"
#include "display/display.h"
#include "display/mesh/cube.h"
#include "util/memory.h"
#include "assert/assert.h"
void entityRenderableInit(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
) {
entityrenderable_t *r = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
);
memoryZero(r, sizeof(entityrenderable_t));
r->type = ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL;
r->data.material.shaderType = SHADER_LIST_SHADER_UNLIT;
r->data.material.material.unlit.color = COLOR_WHITE;
r->data.material.meshes[0] = &CUBE_MESH_SIMPLE;
r->data.material.meshOffsets[0] = 0;
r->data.material.meshCounts[0] = -1;
r->data.material.meshCount = 1;
r->data.material.state.flags = DISPLAY_STATE_FLAG_DEPTH_TEST;
}
void entityRenderableDispose(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
) {
}
void entityRenderableSetType(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const entityrenderabletype_t type
) {
entityrenderable_t *r = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
);
r->type = type;
}
void entityRenderableSetPriority(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const int8_t priority
) {
entityrenderable_t *r = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
);
r->priority = priority;
}
void entityRenderableSetDraw(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
errorret_t (*draw)(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
void *user
),
void *user
) {
assertNotNull(draw, "Draw callback cannot be null");
entityrenderable_t *r = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
);
r->type = ENTITY_RENDERABLE_TYPE_CUSTOM;
r->data.custom.draw = draw;
r->data.custom.drawUser = user;
}
errorret_t entityRenderableDraw(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
) {
entityrenderable_t *r = componentGetData(
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
);
switch(r->type) {
case ENTITY_RENDERABLE_TYPE_SPRITEBATCH:
return entityRenderableDrawSpritebatch(&r->data.spritebatch);
case ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL:
return entityRenderableDrawMaterial(&r->data.material);
case ENTITY_RENDERABLE_TYPE_CUSTOM:
return entityRenderableDrawCustom(
mgr, entityId, componentId, &r->data.custom
);
default:
assertUnreachable("Invalid renderable type");
}
}
errorret_t entityRenderableDrawSpritebatch(
const entityrenderablespritebatch_t *sb
) {
if(sb->spriteCount == 0) errorOk();
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_BLEND
}));
spriteBatchClear();
shadermaterial_t mat;
memoryZero(&mat, sizeof(shadermaterial_t));
mat.unlit.texture = sb->texture;
mat.unlit.color = COLOR_WHITE;
errorChain(spriteBatchBuffer(
sb->sprites, sb->spriteCount,
SHADER_LIST_DEFS[SHADER_LIST_SHADER_UNLIT].shader, mat
));
return spriteBatchFlush();
}
errorret_t entityRenderableDrawMaterial(const entityrenderablematerial_t *m) {
errorChain(displaySetState(m->state));
shader_t *shader = SHADER_LIST_DEFS[m->shaderType].shader;
assertNotNull(shader, "Shader cannot be null for material type");
errorChain(shaderBind(shader));
errorChain(shaderSetMaterial(shader, &m->material));
for(uint8_t i = 0; i < m->meshCount; i++) {
errorChain(meshDraw(m->meshes[i], m->meshOffsets[i], m->meshCounts[i]));
}
errorOk();
}
errorret_t entityRenderableDrawCustom(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const entityrenderablecustom_t *custom
) {
return custom->draw(mgr, entityId, componentId, custom->drawUser);
}
@@ -0,0 +1,196 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entity/entitybase.h"
#include "display/mesh/mesh.h"
#include "display/shader/shadermaterial.h"
#include "display/spritebatch/spritebatch.h"
#include "display/displaystate.h"
#define ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX 64
#define ENTITY_RENDERABLE_MESHES_MAX 8
typedef enum {
ENTITY_RENDERABLE_TYPE_CUSTOM = 0,
ENTITY_RENDERABLE_TYPE_SPRITEBATCH,
ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL,
} entityrenderabletype_t;
typedef struct {
spritebatchsprite_t sprites[ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX];
uint32_t spriteCount;
texture_t *texture;
} entityrenderablespritebatch_t;
typedef struct {
mesh_t *meshes[ENTITY_RENDERABLE_MESHES_MAX];
int32_t meshOffsets[ENTITY_RENDERABLE_MESHES_MAX];
int32_t meshCounts[ENTITY_RENDERABLE_MESHES_MAX];
uint8_t meshCount;
shaderlistshader_t shaderType;
shadermaterial_t material;
displaystate_t state;
} entityrenderablematerial_t;
typedef struct {
errorret_t (*draw)(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
void *user
);
void *drawUser;
} entityrenderablecustom_t;
typedef union entityrenderabledata_u {
entityrenderablespritebatch_t spritebatch;
entityrenderablematerial_t material;
entityrenderablecustom_t custom;
} entityrenderabledata_t;
typedef struct {
entityrenderabletype_t type;
entityrenderabledata_t data;
/**
* Render priority. 0 = auto (derived from type/flags). Higher values
* render later (on top of lower values). Range: [-128..127] with 0 auto.
*/
int8_t priority;
} entityrenderable_t;
/**
* Initializes the entity renderable component. Defaults to
* ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL with the unlit shader, a white
* cube, and depth-test enabled.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity to initialize the component for.
* @param componentId The renderable component of the entity.
*/
void entityRenderableInit(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
);
/**
* Disposes the entity renderable component.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity to dispose the component for.
* @param componentId The renderable component of the entity.
*/
void entityRenderableDispose(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
);
/**
* Sets the rendering type for the renderable component. Resets
* type-specific data to zero.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity to configure.
* @param componentId The renderable component.
* @param type The rendering type to use.
*/
void entityRenderableSetType(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const entityrenderabletype_t type
);
/**
* Sets the render priority. 0 = auto (derived from type/flags). Higher
* values render later (on top). Use non-zero to force ordering.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity to configure.
* @param componentId The renderable component.
* @param priority The priority value, or 0 for auto.
*/
void entityRenderableSetPriority(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const int8_t priority
);
/**
* Sets the draw callback, switching the type to
* ENTITY_RENDERABLE_TYPE_CUSTOM.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity to configure.
* @param componentId The renderable component of the entity.
* @param draw The draw callback to assign.
* @param user Userdata passed to the callback.
*/
void entityRenderableSetDraw(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
errorret_t (*draw)(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
void *user
),
void *user
);
/**
* Draws the entity using its renderable component data.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity to draw.
* @param componentId The renderable component of the entity.
* @return Any error state that happened.
*/
errorret_t entityRenderableDraw(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
);
/**
* Internal. Draws a spritebatch-type renderable.
*
* @param sb The spritebatch data to draw.
* @return Error state.
*/
errorret_t entityRenderableDrawSpritebatch(
const entityrenderablespritebatch_t *sb
);
/**
* Internal. Draws a shader-material-type renderable.
*
* @param m The material data to draw.
* @return Error state.
*/
errorret_t entityRenderableDrawMaterial(const entityrenderablematerial_t *m);
/**
* Internal. Invokes a custom-type renderable's draw callback.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity being drawn.
* @param componentId The renderable component of the entity.
* @param custom The custom draw data.
* @return Error state.
*/
errorret_t entityRenderableDrawCustom(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const entityrenderablecustom_t *custom
);
+22
View File
@@ -0,0 +1,22 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entity/component/display/entityposition.h"
#include "entity/component/display/entitycamera.h"
#include "entity/component/display/entityrenderable.h"
// Name (Uppercase)
// Structure
// Field name (lowercase)
// Init function (optional)
// Dispose function (optional)
// Render function (optional)
X(POSITION, entityposition_t, position, entityPositionInit, NULL, NULL)
X(CAMERA, entitycamera_t, camera, entityCameraInit, NULL, NULL)
X(RENDERABLE, entityrenderable_t, renderable,
entityRenderableInit, entityRenderableDispose, NULL)
+186
View File
@@ -0,0 +1,186 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entitymanager.h"
#include "component/display/entityposition.h"
#include "util/memory.h"
#include "assert/assert.h"
void entityInit(entitymanager_t *mgr, const entityid_t entityId) {
entity_t *ent = &mgr->entities[entityId];
memoryZero(ent, sizeof(entity_t));
// Mark all component types not using this entity.
for(
componenttype_t compType = 0;
compType < COMPONENT_TYPE_COUNT;
compType++
) {
mgr->entitiesWithComponent[
compType * ENTITY_COUNT_MAX + entityId
] = COMPONENT_ID_INVALID;
}
ent->state |= ENTITY_STATE_ACTIVE;
}
componentid_t entityAddComponent(
entitymanager_t *mgr,
const entityid_t entityId,
const componenttype_t type
) {
componentindex_t compInd;
for(componentid_t i = 0; i < ENTITY_COMPONENT_COUNT_MAX; i++) {
compInd = componentGetIndex(entityId, i);
if(mgr->components[compInd].type != COMPONENT_TYPE_NULL) {
assertTrue(
mgr->components[compInd].type != type,
"Entity already has component of this type"
);
continue;
}
componentInit(mgr, entityId, i, type);
mgr->entitiesWithComponent[
type * ENTITY_COUNT_MAX + entityId
] = i;
return i;
}
assertUnreachable("Entity has no more component slots available");
return COMPONENT_ID_INVALID;
}
componentid_t entityGetComponent(
entitymanager_t *mgr,
const entityid_t entityId,
const componenttype_t type
) {
componentid_t compId = mgr->entitiesWithComponent[
type * ENTITY_COUNT_MAX + entityId
];
if(compId == COMPONENT_ID_INVALID) return compId;
assertTrue(
mgr->components[componentGetIndex(entityId, compId)].type == type,
"Component type mismatch"
);
return compId;
}
void entityDisposeDeep(entitymanager_t *mgr, const entityid_t entityId) {
componentid_t posComp = entityGetComponent(
mgr, entityId, COMPONENT_TYPE_POSITION
);
if(posComp != COMPONENT_ID_INVALID) {
entityPositionDisposeDeep(mgr, entityId, posComp);
} else {
entityDispose(mgr, 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]);
}
}
void entityDispose(entitymanager_t *mgr, const entityid_t entityId) {
componentindex_t compInd;
entity_t *ent = &mgr->entities[entityId];
for(uint8_t i = 0; i < ent->disposeCount; i++) {
ent->onDispose[i](
entityId, ent->disposeComponentId[i], ent->disposeUser[i]
);
}
for(componentid_t i = 0; i < ENTITY_COMPONENT_COUNT_MAX; i++) {
compInd = componentGetIndex(entityId, i);
componenttype_t type = mgr->components[compInd].type;
if(type == COMPONENT_TYPE_NULL) continue;
mgr->entitiesWithComponent[
type * ENTITY_COUNT_MAX + entityId
] = COMPONENT_ID_INVALID;
componentDispose(mgr, entityId, i);
}
ent->state = 0;
}
void entityUpdateAdd(
entitymanager_t *mgr,
const entityid_t entityId,
const entitycallback_t callback,
const componentid_t componentId,
void *user
) {
entity_t *ent = &mgr->entities[entityId];
assertTrue(
ent->updateCount < ENTITY_UPDATE_CALLBACK_COUNT_MAX,
"Entity update callback slots full"
);
ent->onUpdate[ent->updateCount] = callback;
ent->updateComponentId[ent->updateCount] = componentId;
ent->updateUser[ent->updateCount] = user;
ent->updateCount++;
}
void entityUpdateRemove(
entitymanager_t *mgr,
const entityid_t entityId,
const entitycallback_t callback
) {
entity_t *ent = &mgr->entities[entityId];
for(uint8_t i = 0; i < ent->updateCount; i++) {
if(ent->onUpdate[i] != callback) continue;
ent->updateCount--;
for(uint8_t j = i; j < ent->updateCount; j++) {
ent->onUpdate[j] = ent->onUpdate[j + 1];
ent->updateComponentId[j] = ent->updateComponentId[j + 1];
ent->updateUser[j] = ent->updateUser[j + 1];
}
return;
}
}
void entityDisposeAdd(
entitymanager_t *mgr,
const entityid_t entityId,
const entitycallback_t callback,
const componentid_t componentId,
void *user
) {
entity_t *ent = &mgr->entities[entityId];
assertTrue(
ent->disposeCount < ENTITY_DISPOSE_CALLBACK_COUNT_MAX,
"Entity dispose callback slots full"
);
ent->onDispose[ent->disposeCount] = callback;
ent->disposeComponentId[ent->disposeCount] = componentId;
ent->disposeUser[ent->disposeCount] = user;
ent->disposeCount++;
}
void entityDisposeRemove(
entitymanager_t *mgr,
const entityid_t entityId,
const entitycallback_t callback
) {
entity_t *ent = &mgr->entities[entityId];
for(uint8_t i = 0; i < ent->disposeCount; i++) {
if(ent->onDispose[i] != callback) continue;
ent->disposeCount--;
for(uint8_t j = i; j < ent->disposeCount; j++) {
ent->onDispose[j] = ent->onDispose[j + 1];
ent->disposeComponentId[j] = ent->disposeComponentId[j + 1];
ent->disposeUser[j] = ent->disposeUser[j + 1];
}
return;
}
}
+157
View File
@@ -0,0 +1,157 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "component.h"
#define ENTITY_STATE_ACTIVE (1 << 0)
#define ENTITY_UPDATE_CALLBACK_COUNT_MAX 5
#define ENTITY_DISPOSE_CALLBACK_COUNT_MAX 5
typedef void (*entitycallback_t)(
const entityid_t entityId,
const componentid_t componentId,
void *user
);
typedef struct {
uint8_t state;
uint8_t updateCount;
uint8_t disposeCount;
entitycallback_t onUpdate[ENTITY_UPDATE_CALLBACK_COUNT_MAX];
componentid_t updateComponentId[ENTITY_UPDATE_CALLBACK_COUNT_MAX];
void *updateUser[ENTITY_UPDATE_CALLBACK_COUNT_MAX];
entitycallback_t onDispose[ENTITY_DISPOSE_CALLBACK_COUNT_MAX];
componentid_t disposeComponentId[ENTITY_DISPOSE_CALLBACK_COUNT_MAX];
void *disposeUser[ENTITY_DISPOSE_CALLBACK_COUNT_MAX];
} entity_t;
/**
* Initializes an entity with the given ID.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The ID of the entity to initialize.
*/
void entityInit(entitymanager_t *mgr, const entityid_t entityId);
/**
* Adds a component of the given type to the entity with the given ID.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The ID of the entity to add the component to.
* @param type The type of the component to add.
* @return The ID of the entity with component.
*/
componentid_t entityAddComponent(
entitymanager_t *mgr,
const entityid_t entityId,
const componenttype_t type
);
/**
* Gets the ID of the component of the given type on the entity with the
* given ID, or COMPONENT_ID_INVALID if the entity lacks the component.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The ID of the entity to get the component from.
* @param type The type of the component to get.
* @return The ID of the component.
*/
componentid_t entityGetComponent(
entitymanager_t *mgr,
const entityid_t entityId,
const componenttype_t type
);
/**
* Runs all registered update callbacks for the entity.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The ID of the entity to update.
*/
void entityUpdate(entitymanager_t *mgr, const entityid_t entityId);
/**
* Disposes of an entity with the given ID. Fires all dispose callbacks
* before cleaning up components and state.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The ID of the entity to dispose of.
*/
void entityDispose(entitymanager_t *mgr, const entityid_t entityId);
/**
* Disposes of an entity and all of its position-component descendants
* recursively. If the entity has no position component, behaves like
* entityDispose.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The root entity ID.
*/
void entityDisposeDeep(entitymanager_t *mgr, const entityid_t entityId);
/**
* Registers an update callback, invoked each time entityUpdate is called.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity to register on.
* @param callback The function to call.
* @param componentId The component ID passed back to the callback.
* @param user Userdata passed back to the callback.
*/
void entityUpdateAdd(
entitymanager_t *mgr,
const entityid_t entityId,
const entitycallback_t callback,
const componentid_t componentId,
void *user
);
/**
* Removes a previously registered update callback.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity to remove from.
* @param callback The function to remove.
*/
void entityUpdateRemove(
entitymanager_t *mgr,
const entityid_t entityId,
const entitycallback_t callback
);
/**
* Registers a dispose callback, invoked at the start of entityDispose
* before any component or state cleanup.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity to register on.
* @param callback The function to call.
* @param componentId The component ID passed back to the callback.
* @param user Userdata passed back to the callback.
*/
void entityDisposeAdd(
entitymanager_t *mgr,
const entityid_t entityId,
const entitycallback_t callback,
const componentid_t componentId,
void *user
);
/**
* Removes a previously registered dispose callback.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity to remove from.
* @param callback The function to remove.
*/
void entityDisposeRemove(
entitymanager_t *mgr,
const entityid_t entityId,
const entitycallback_t callback
);
+24
View File
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#define ENTITY_COUNT_MAX 64
#define ENTITY_COMPONENT_COUNT_MAX 16
#define ENTITY_ID_INVALID 0xFF
#define COMPONENT_ID_INVALID 0xFF
typedef uint8_t entityid_t;
typedef uint8_t componentid_t;
typedef uint16_t componentindex_t;
// Forward declared here (rather than in component.h) so every entity/
// component header can reference entitymanager_t* regardless of include
// order, without needing a full entitymanager.h include.
typedef struct entitymanager_t entitymanager_t;
+56
View File
@@ -0,0 +1,56 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entitymanager.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "console/console.h"
void entityManagerInit(entitymanager_t *mgr) {
assertNotNull(mgr, "Entity manager cannot be null");
memoryZero(mgr, sizeof(entitymanager_t));
memorySet(
mgr->entitiesWithComponent, COMPONENT_ID_INVALID,
sizeof(componentid_t) * COMPONENT_TYPE_COUNT * ENTITY_COUNT_MAX
);
consolePrint(
"Entity manager size: %zu bytes (%.2f KB)",
sizeof(entitymanager_t),
sizeof(entitymanager_t) / 1024.0f
);
}
entityid_t entityManagerAdd(entitymanager_t *mgr) {
assertNotNull(mgr, "Entity manager cannot be null");
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
if((mgr->entities[i].state & ENTITY_STATE_ACTIVE) != 0) continue;
entityInit(mgr, i);
return i;
}
assertUnreachable("No more entity IDs available");
return ENTITY_ID_INVALID;
}
void entityManagerUpdate(entitymanager_t *mgr) {
assertNotNull(mgr, "Entity manager cannot be null");
entityid_t i = 0;
while(i < ENTITY_COUNT_MAX) {
if((mgr->entities[i].state & ENTITY_STATE_ACTIVE) != 0) {
entityUpdate(mgr, i);
}
i++;
}
}
void entityManagerDispose(entitymanager_t *mgr) {
assertNotNull(mgr, "Entity manager cannot be null");
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
if((mgr->entities[i].state & ENTITY_STATE_ACTIVE) == 0) continue;
entityDispose(mgr, i);
}
}
+46
View File
@@ -0,0 +1,46 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entity.h"
typedef struct entitymanager_t {
entity_t entities[ENTITY_COUNT_MAX];
component_t components[ENTITY_COUNT_MAX * ENTITY_COMPONENT_COUNT_MAX];
componentid_t entitiesWithComponent[COMPONENT_TYPE_COUNT * ENTITY_COUNT_MAX];
} entitymanager_t;
/**
* Initializes an entity manager, marking all entities and components as
* unused.
*
* @param mgr The entity manager to initialize.
*/
void entityManagerInit(entitymanager_t *mgr);
/**
* Adds / Reserves a new entity ID in the given manager.
*
* @param mgr The entity manager to allocate the entity in.
* @return The new entity ID.
*/
entityid_t entityManagerAdd(entitymanager_t *mgr);
/**
* Updates all active entities in the given manager.
*
* @param mgr The entity manager to update.
*/
void entityManagerUpdate(entitymanager_t *mgr);
/**
* Disposes of the entity manager, in turn freeing all entities and
* components.
*
* @param mgr The entity manager to dispose.
*/
void entityManagerDispose(entitymanager_t *mgr);
+143 -4
View File
@@ -10,26 +10,165 @@
#include "display/screen/screen.h"
#include "display/shader/shaderunlit.h"
#include "display/display.h"
#include "entity/component/display/entitycamera.h"
#include "entity/component/display/entityposition.h"
#include "entity/component/display/entityrenderable.h"
#include "ui/ui.h"
#include "asset/asset.h"
#include "asset/loader/assetloader.h"
#include "console/console.h"
scene_t SCENE;
scenemanager_t SCENE_MANAGER;
errorret_t sceneInit(void) {
memoryZero(&SCENE, sizeof(scene_t));
memoryZero(&SCENE_MANAGER, sizeof(scenemanager_t));
SCENE_MANAGER.active = SCENE_ID_INVALID;
errorOk();
}
sceneid_t sceneCreate(void) {
for(sceneid_t i = 0; i < SCENE_COUNT_MAX; i++) {
if(SCENE_MANAGER.scenes[i].used) continue;
scene_t *scene = &SCENE_MANAGER.scenes[i];
entityManagerInit(&scene->entities);
scene->used = true;
return i;
}
assertUnreachable("No more scene IDs available");
return SCENE_ID_INVALID;
}
void sceneDestroy(const sceneid_t id) {
assertTrue(id < SCENE_COUNT_MAX, "Scene ID OOB");
scene_t *scene = &SCENE_MANAGER.scenes[id];
if(!scene->used) return;
entityManagerDispose(&scene->entities);
scene->used = false;
if(SCENE_MANAGER.active == id) SCENE_MANAGER.active = SCENE_ID_INVALID;
}
void sceneSetActive(const sceneid_t id) {
assertTrue(
id == SCENE_ID_INVALID || id < SCENE_COUNT_MAX, "Scene ID OOB"
);
SCENE_MANAGER.active = id;
}
sceneid_t sceneGetActive(void) {
return SCENE_MANAGER.active;
}
entitymanager_t *sceneGetEntities(const sceneid_t id) {
assertTrue(id < SCENE_COUNT_MAX, "Scene ID OOB");
assertTrue(SCENE_MANAGER.scenes[id].used, "Scene is not in use");
return &SCENE_MANAGER.scenes[id].entities;
}
errorret_t sceneUpdate(void) {
if(SCENE_MANAGER.active == SCENE_ID_INVALID) errorOk();
#if DUSK_TIME_DYNAMIC
if(!TIME.dynamicUpdate) errorOk();
#endif
entityManagerUpdate(sceneGetEntities(SCENE_MANAGER.active));
errorOk();
}
errorret_t sceneRender(void) {
if(SCENE_MANAGER.active != SCENE_ID_INVALID) {
entitymanager_t *mgr = sceneGetEntities(SCENE_MANAGER.active);
entityid_t camEntity = entityCameraGetCurrent(mgr);
if(camEntity != ENTITY_ID_INVALID) {
componentid_t camComp = entityGetComponent(
mgr, camEntity, COMPONENT_TYPE_CAMERA
);
componentid_t camPosComp = entityGetComponent(
mgr, camEntity, COMPONENT_TYPE_POSITION
);
mat4 proj;
entityCameraGetProjection(mgr, camEntity, camComp, proj);
mat4 view;
glm_mat4_identity(view);
if(camPosComp != COMPONENT_ID_INVALID) {
mat4 camTransform;
entityPositionGetTransform(mgr, camEntity, camPosComp, camTransform);
// Position components always store world-space placement (eye ->
// world), regardless of how they were set (SetPosition/Rotation or
// LookAt) -- invert to get the view matrix (world -> eye).
glm_mat4_inv(camTransform, view);
}
errorChain(shaderBind(&SHADER_UNLIT));
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, proj));
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, view));
entityid_t renderEntities[ENTITY_COUNT_MAX];
componentid_t renderComponents[ENTITY_COUNT_MAX];
entityid_t renderCount = componentGetEntitiesWithComponent(
mgr, COMPONENT_TYPE_RENDERABLE, renderEntities, renderComponents
);
for(entityid_t i = 0; i < renderCount; i++) {
mat4 model;
componentid_t posComp = entityGetComponent(
mgr, renderEntities[i], COMPONENT_TYPE_POSITION
);
if(posComp == COMPONENT_ID_INVALID) {
glm_mat4_identity(model);
} else {
entityPositionGetTransform(mgr, renderEntities[i], posComp, model);
}
errorChain(
shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, model)
);
errorChain(entityRenderableDraw(
mgr, renderEntities[i], renderComponents[i]
));
}
}
}
// Screen-space matrices for UI rendering.
mat4 screenIdentity;
mat4 screenProj;
mat4 screenView;
glm_mat4_identity(screenIdentity);
glm_ortho(
0.0f, (float_t)(SCREEN.width / SCREEN.scaleUi),
(float_t)(SCREEN.height / SCREEN.scaleUi), 0.0f,
0.1f, 100.0f,
screenProj
);
glm_lookat(
(vec3){ 0.0f, 0.0f, 1.0f },
(vec3){ 0.0f, 0.0f, 0.0f },
(vec3){ 0.0f, 1.0f, 0.0f },
screenView
);
errorChain(shaderBind(&SHADER_UNLIT));
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, screenIdentity));
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, screenProj));
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, screenView));
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_BLEND
}));
errorChain(uiRender());
errorOk();
}
errorret_t sceneDispose(void) {
for(sceneid_t i = 0; i < SCENE_COUNT_MAX; i++) {
sceneDestroy(i);
}
errorOk();
}
+61 -19
View File
@@ -6,46 +6,88 @@
*/
#pragma once
#include "error/error.h"
#include "scenebase.h"
#include "entity/entitymanager.h"
typedef struct {
void *nothing;
bool_t used;
entitymanager_t entities;
} scene_t;
extern scene_t SCENE;
typedef struct {
scene_t scenes[SCENE_COUNT_MAX];
sceneid_t active;
} scenemanager_t;
extern scenemanager_t SCENE_MANAGER;
/**
* Initialises the scene manager.
*
* Initialises the scene manager. No scenes are created or active until
* sceneCreate()/sceneSetActive() are called.
*
* @return An error if the init failed, or errorOk() if it succeeded.
*/
errorret_t sceneInit(void);
/**
* Ticks the scene manager. Processes any pending scene transition, then
* calls scriptSceneUpdate on the active scene.
*
* Creates a new, empty scene with its own isolated entity/component pool.
* The new scene is not made active automatically.
*
* @return The ID of the new scene.
*/
sceneid_t sceneCreate(void);
/**
* Destroys a scene, disposing all of its entities/components. If the scene
* was the active scene, no scene is active afterwards.
*
* @param id The ID of the scene to destroy.
*/
void sceneDestroy(const sceneid_t id);
/**
* Sets which scene is active. Only the active scene is ticked and rendered
* by sceneUpdate()/sceneRender(). Previously active scenes are left running
* in the background (paused, not disposed) until sceneDestroy() is called.
*
* @param id The ID of the scene to make active, or SCENE_ID_INVALID to make
* no scene active.
*/
void sceneSetActive(const sceneid_t id);
/**
* Gets the ID of the currently active scene.
*
* @return The active scene ID, or SCENE_ID_INVALID if none is active.
*/
sceneid_t sceneGetActive(void);
/**
* Gets the entity manager owned by a given scene, for spawning/querying
* entities scoped to that scene.
*
* @param id The ID of the scene.
* @return Pointer to the scene's entity manager.
*/
entitymanager_t *sceneGetEntities(const sceneid_t id);
/**
* Ticks the active scene's entities.
*
* @return An error if the update failed, or errorOk() if it succeeded.
*/
errorret_t sceneUpdate(void);
/**
* Renders the current scene (entities, render pipeline, UI).
*
* Renders the active scene (entities, render pipeline, UI).
*
* @return An error if the render failed, or errorOk() if it succeeded.
*/
errorret_t sceneRender(void);
/**
* Requests the next frame to change to this scene.
*
* @param type The scene type to change to.
*/
// void sceneSet(const scenetype_t type);
/**
* Disposes the active scene immediately.
*
* Destroys every created scene.
*
* @return An error if the dispose failed, or errorOk() if it succeeded.
*/
errorret_t sceneDispose(void);
+14
View File
@@ -0,0 +1,14 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#define SCENE_COUNT_MAX 4
#define SCENE_ID_INVALID 0xFF
typedef uint8_t sceneid_t;
+2 -1
View File
@@ -9,7 +9,8 @@ add_subdirectory(error)
add_subdirectory(network)
add_subdirectory(thread)
add_subdirectory(display)
add_subdirectory(rpg)
add_subdirectory(entity)
add_subdirectory(scene)
# add_subdirectory(item)
add_subdirectory(time)
add_subdirectory(util)
@@ -6,7 +6,5 @@
include(dusktest)
# Tests
dusktest(test_maparea.c)
dusktest(test_tileshape.c)
# Subdirs
dusktest(test_entitymanager.c)
dusktest(test_entityposition.c)
+97
View File
@@ -0,0 +1,97 @@
/**
* 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 "entity/entitymanager.h"
#include "entity/component/display/entityposition.h"
static void test_entityManagerAddIsolatesEntities(void **state) {
entitymanager_t mgr;
entityManagerInit(&mgr);
entityid_t a = entityManagerAdd(&mgr);
entityid_t b = entityManagerAdd(&mgr);
assert_true(a != ENTITY_ID_INVALID);
assert_true(b != ENTITY_ID_INVALID);
assert_true(a != b);
entityManagerDispose(&mgr);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityManagerFullAsserts(void **state) {
entitymanager_t mgr;
entityManagerInit(&mgr);
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
assert_true(entityManagerAdd(&mgr) != ENTITY_ID_INVALID);
}
expect_assert_failure(entityManagerAdd(&mgr));
entityManagerDispose(&mgr);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityAddGetDisposeComponent(void **state) {
entitymanager_t mgr;
entityManagerInit(&mgr);
entityid_t entity = entityManagerAdd(&mgr);
assert_int_equal(
entityGetComponent(&mgr, entity, COMPONENT_TYPE_POSITION),
COMPONENT_ID_INVALID
);
componentid_t posComp = entityAddComponent(
&mgr, entity, COMPONENT_TYPE_POSITION
);
assert_true(posComp != COMPONENT_ID_INVALID);
assert_int_equal(
entityGetComponent(&mgr, entity, COMPONENT_TYPE_POSITION), posComp
);
entityposition_t *pos = entityPositionGet(&mgr, entity, posComp);
assert_non_null(pos);
entityDispose(&mgr, entity);
assert_int_equal(
entityGetComponent(&mgr, entity, COMPONENT_TYPE_POSITION),
COMPONENT_ID_INVALID
);
assert_true((mgr.entities[entity].state & ENTITY_STATE_ACTIVE) == 0);
entityManagerDispose(&mgr);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityAddDuplicateComponentAsserts(void **state) {
entitymanager_t mgr;
entityManagerInit(&mgr);
entityid_t entity = entityManagerAdd(&mgr);
entityAddComponent(&mgr, entity, COMPONENT_TYPE_POSITION);
expect_assert_failure(
entityAddComponent(&mgr, entity, COMPONENT_TYPE_POSITION)
);
entityManagerDispose(&mgr);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(int argc, char **argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_entityManagerAddIsolatesEntities),
cmocka_unit_test(test_entityManagerFullAsserts),
cmocka_unit_test(test_entityAddGetDisposeComponent),
cmocka_unit_test(test_entityAddDuplicateComponentAsserts),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+130
View File
@@ -0,0 +1,130 @@
/**
* 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 "entity/entitymanager.h"
#include "entity/component/display/entityposition.h"
static void test_entityPositionLocalGetSet(void **state) {
entitymanager_t mgr;
entityManagerInit(&mgr);
entityid_t entity = entityManagerAdd(&mgr);
componentid_t comp = entityAddComponent(
&mgr, entity, COMPONENT_TYPE_POSITION
);
vec3 setPos = { 1.0f, 2.0f, 3.0f };
entityPositionSetLocalPosition(&mgr, entity, comp, setPos);
vec3 getPos;
entityPositionGetLocalPosition(&mgr, entity, comp, getPos);
assert_float_equal(getPos[0], 1.0f, 0.0001f);
assert_float_equal(getPos[1], 2.0f, 0.0001f);
assert_float_equal(getPos[2], 3.0f, 0.0001f);
// World position of a parentless entity matches local position.
vec3 worldPos;
entityPositionGetWorldPosition(&mgr, entity, comp, worldPos);
assert_float_equal(worldPos[0], 1.0f, 0.0001f);
assert_float_equal(worldPos[1], 2.0f, 0.0001f);
assert_float_equal(worldPos[2], 3.0f, 0.0001f);
entityManagerDispose(&mgr);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityPositionParentChildWorldTransform(void **state) {
entitymanager_t mgr;
entityManagerInit(&mgr);
entityid_t parent = entityManagerAdd(&mgr);
componentid_t parentComp = entityAddComponent(
&mgr, parent, COMPONENT_TYPE_POSITION
);
entityid_t child = entityManagerAdd(&mgr);
componentid_t childComp = entityAddComponent(
&mgr, child, COMPONENT_TYPE_POSITION
);
vec3 parentPos = { 10.0f, 0.0f, 0.0f };
entityPositionSetLocalPosition(&mgr, parent, parentComp, parentPos);
vec3 childLocalPos = { 1.0f, 0.0f, 0.0f };
entityPositionSetLocalPosition(&mgr, child, childComp, childLocalPos);
entityPositionSetParent(&mgr, child, childComp, parent, parentComp);
vec3 childWorldPos;
entityPositionGetWorldPosition(&mgr, child, childComp, childWorldPos);
assert_float_equal(childWorldPos[0], 11.0f, 0.0001f);
assert_float_equal(childWorldPos[1], 0.0f, 0.0001f);
assert_float_equal(childWorldPos[2], 0.0f, 0.0001f);
// Moving the parent should lazily update the child's world position too.
vec3 parentPosMoved = { 20.0f, 5.0f, 0.0f };
entityPositionSetLocalPosition(&mgr, parent, parentComp, parentPosMoved);
entityPositionGetWorldPosition(&mgr, child, childComp, childWorldPos);
assert_float_equal(childWorldPos[0], 21.0f, 0.0001f);
assert_float_equal(childWorldPos[1], 5.0f, 0.0001f);
assert_float_equal(childWorldPos[2], 0.0f, 0.0001f);
// Detaching does not preserve world position -- the child's local
// position (never touched since entityPositionSetLocalPosition above) is
// now its world position too, since it no longer has a parent.
entityPositionSetParent(
&mgr, child, childComp, ENTITY_ID_INVALID, COMPONENT_ID_INVALID
);
vec3 parentPosMovedAgain = { 100.0f, 100.0f, 100.0f };
entityPositionSetLocalPosition(
&mgr, parent, parentComp, parentPosMovedAgain
);
entityPositionGetWorldPosition(&mgr, child, childComp, childWorldPos);
assert_float_equal(childWorldPos[0], childLocalPos[0], 0.0001f);
assert_float_equal(childWorldPos[1], childLocalPos[1], 0.0001f);
assert_float_equal(childWorldPos[2], childLocalPos[2], 0.0001f);
entityManagerDispose(&mgr);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityPositionDisposeDeep(void **state) {
entitymanager_t mgr;
entityManagerInit(&mgr);
entityid_t parent = entityManagerAdd(&mgr);
componentid_t parentComp = entityAddComponent(
&mgr, parent, COMPONENT_TYPE_POSITION
);
entityid_t child = entityManagerAdd(&mgr);
componentid_t childComp = entityAddComponent(
&mgr, child, COMPONENT_TYPE_POSITION
);
entityPositionSetParent(&mgr, child, childComp, parent, parentComp);
entityPositionDisposeDeep(&mgr, parent, parentComp);
assert_true((mgr.entities[parent].state & ENTITY_STATE_ACTIVE) == 0);
assert_true((mgr.entities[child].state & ENTITY_STATE_ACTIVE) == 0);
entityManagerDispose(&mgr);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(int argc, char **argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_entityPositionLocalGetSet),
cmocka_unit_test(test_entityPositionParentChildWorldTransform),
cmocka_unit_test(test_entityPositionDisposeDeep),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-14
View File
@@ -1,14 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(dusktest)
# Tests
dusktest(test_rpg.c)
# Subdirs
add_subdirectory(overworld)
add_subdirectory(physics)
add_subdirectory(entity)
-105
View File
@@ -1,105 +0,0 @@
/**
* 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 "rpg/entity/entity.h"
static void testEntitiesReset(void) {
memoryZero(ENTITIES, sizeof(ENTITIES));
}
static void testEntityPlace(
const uint8_t index,
const entitytype_t type,
const vec3 position,
const entitydir_t direction
) {
entityInit(&ENTITIES[index], type);
ENTITIES[index].direction = direction;
const vec3 extents = ENTITY_PHYSICS_EXTENTS_DEFAULT;
physicsBodyInit(&ENTITIES[index].body, position, extents);
}
static void test_entityGetFacingFindsEntityAhead(void **state) {
testEntitiesReset();
testEntityPlace(0, ENTITY_TYPE_PLAYER, (vec3){ 0.0f, 0.0f, 0.0f },
ENTITY_DIR_EAST);
testEntityPlace(1, ENTITY_TYPE_NPC, (vec3){ 1.0f, 0.0f, 0.0f },
ENTITY_DIR_NORTH);
entity_t *result = entityGetFacing(&ENTITIES[0], ENTITY_INTERACT_RANGE);
assert_ptr_equal(result, &ENTITIES[1]);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityGetFacingIgnoresEntityBehind(void **state) {
testEntitiesReset();
testEntityPlace(0, ENTITY_TYPE_PLAYER, (vec3){ 0.0f, 0.0f, 0.0f },
ENTITY_DIR_EAST);
testEntityPlace(1, ENTITY_TYPE_NPC, (vec3){ -2.0f, 0.0f, 0.0f },
ENTITY_DIR_NORTH);
entity_t *result = entityGetFacing(&ENTITIES[0], ENTITY_INTERACT_RANGE);
assert_null(result);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityGetFacingIgnoresEntityToSide(void **state) {
testEntitiesReset();
testEntityPlace(0, ENTITY_TYPE_PLAYER, (vec3){ 0.0f, 0.0f, 0.0f },
ENTITY_DIR_EAST);
testEntityPlace(1, ENTITY_TYPE_NPC, (vec3){ 0.0f, 3.0f, 0.0f },
ENTITY_DIR_NORTH);
entity_t *result = entityGetFacing(&ENTITIES[0], ENTITY_INTERACT_RANGE);
assert_null(result);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityGetFacingReturnsNullWhenNothingInRange(void **state) {
testEntitiesReset();
testEntityPlace(0, ENTITY_TYPE_PLAYER, (vec3){ 0.0f, 0.0f, 0.0f },
ENTITY_DIR_EAST);
entity_t *result = entityGetFacing(&ENTITIES[0], ENTITY_INTERACT_RANGE);
assert_null(result);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityGetFacingPicksNearest(void **state) {
testEntitiesReset();
testEntityPlace(0, ENTITY_TYPE_PLAYER, (vec3){ 0.0f, 0.0f, 0.0f },
ENTITY_DIR_EAST);
// Farther candidate placed at the lower array index, to prove the
// result is chosen by distance, not array/insertion order.
testEntityPlace(1, ENTITY_TYPE_NPC, (vec3){ 1.5f, 0.0f, 0.0f },
ENTITY_DIR_NORTH);
testEntityPlace(2, ENTITY_TYPE_NPC, (vec3){ 1.0f, 0.0f, 0.0f },
ENTITY_DIR_NORTH);
entity_t *result = entityGetFacing(&ENTITIES[0], ENTITY_INTERACT_RANGE);
assert_ptr_equal(result, &ENTITIES[2]);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_entityGetFacingFindsEntityAhead),
cmocka_unit_test(test_entityGetFacingIgnoresEntityBehind),
cmocka_unit_test(test_entityGetFacingIgnoresEntityToSide),
cmocka_unit_test(test_entityGetFacingReturnsNullWhenNothingInRange),
cmocka_unit_test(test_entityGetFacingPicksNearest),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-84
View File
@@ -1,84 +0,0 @@
/**
* 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 "rpg/entity/entitydir.h"
static void test_entityDirToVec2(void **state) {
vec2 out;
entityDirToVec2(ENTITY_DIR_NORTH, out);
assert_float_equal(out[0], 0.0f, 0.0001f);
assert_float_equal(out[1], 1.0f, 0.0001f);
entityDirToVec2(ENTITY_DIR_EAST, out);
assert_float_equal(out[0], 1.0f, 0.0001f);
assert_float_equal(out[1], 0.0f, 0.0001f);
entityDirToVec2(ENTITY_DIR_SOUTH, out);
assert_float_equal(out[0], 0.0f, 0.0001f);
assert_float_equal(out[1], -1.0f, 0.0001f);
entityDirToVec2(ENTITY_DIR_WEST, out);
assert_float_equal(out[0], -1.0f, 0.0001f);
assert_float_equal(out[1], 0.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityDirFromVec2Cardinal(void **state) {
assert_int_equal(
entityDirFromVec2((vec2){ 0.0f, 1.0f }), ENTITY_DIR_NORTH
);
assert_int_equal(
entityDirFromVec2((vec2){ 1.0f, 0.0f }), ENTITY_DIR_EAST
);
assert_int_equal(
entityDirFromVec2((vec2){ 0.0f, -1.0f }), ENTITY_DIR_SOUTH
);
assert_int_equal(
entityDirFromVec2((vec2){ -1.0f, 0.0f }), ENTITY_DIR_WEST
);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityDirFromVec2DominantAxis(void **state) {
// |x| > |y| picks east/west regardless of y's sign.
assert_int_equal(
entityDirFromVec2((vec2){ 2.0f, 1.0f }), ENTITY_DIR_EAST
);
assert_int_equal(
entityDirFromVec2((vec2){ -2.0f, -1.0f }), ENTITY_DIR_WEST
);
// |y| >= |x| picks north/south.
assert_int_equal(
entityDirFromVec2((vec2){ 1.0f, 2.0f }), ENTITY_DIR_NORTH
);
assert_int_equal(
entityDirFromVec2((vec2){ -1.0f, -3.0f }), ENTITY_DIR_SOUTH
);
// Exact diagonal ties break toward north/south.
assert_int_equal(
entityDirFromVec2((vec2){ 0.7f, 0.7f }), ENTITY_DIR_NORTH
);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_entityDirToVec2),
cmocka_unit_test(test_entityDirFromVec2Cardinal),
cmocka_unit_test(test_entityDirFromVec2DominantAxis),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-126
View File
@@ -1,126 +0,0 @@
/**
* 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 "rpg/overworld/maparea.h"
static uint32_t TEST_ENTER_COUNT;
static uint32_t TEST_STEP_COUNT;
static uint32_t TEST_EXIT_COUNT;
static void testAreaCallback(entity_t *entity, const uint8_t trigger) {
if(trigger == MAP_TRIGGER_ENTER) TEST_ENTER_COUNT++;
else if(trigger == MAP_TRIGGER_STEP) TEST_STEP_COUNT++;
else if(trigger == MAP_TRIGGER_EXIT) TEST_EXIT_COUNT++;
}
static void testMapAreaReset(void) {
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
TEST_ENTER_COUNT = 0;
TEST_STEP_COUNT = 0;
TEST_EXIT_COUNT = 0;
}
static entity_t testEntityAt(const entitytype_t type, const worldpos_t pos) {
entity_t entity;
memoryZero(&entity, sizeof(entity));
entity.id = 0;
entity.type = type;
entity.position = pos;
return entity;
}
static void test_mapAreaCheckEntityFiresEnterAndExit(void **state) {
testMapAreaReset();
mapAreaAdd(
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
testAreaCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
);
entity_t entity = testEntityAt(ENTITY_TYPE_PLAYER, (worldpos_t){ 10, 10, 0 });
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_ENTER_COUNT, 0);
entity.position = (worldpos_t){ 2, 2, 0 };
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_ENTER_COUNT, 1);
assert_int_equal(TEST_STEP_COUNT, 0);
entity.position = (worldpos_t){ 10, 10, 0 };
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_EXIT_COUNT, 1);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_mapAreaCheckEntityStepFiresOncePerNewTile(void **state) {
testMapAreaReset();
mapAreaAdd(
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
testAreaCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
);
entity_t entity = testEntityAt(ENTITY_TYPE_PLAYER, (worldpos_t){ 1, 1, 0 });
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_ENTER_COUNT, 1);
assert_int_equal(TEST_STEP_COUNT, 0);
// Same tile, checked repeatedly (simulating multiple frames without the
// entity moving) - STEP must not fire.
mapAreaCheckEntity(&entity);
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_STEP_COUNT, 0);
// Moves to a new tile, still inside - STEP fires once.
entity.position = (worldpos_t){ 2, 1, 0 };
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_STEP_COUNT, 1);
// Same new tile again over multiple calls - no additional STEP.
mapAreaCheckEntity(&entity);
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_STEP_COUNT, 1);
// Another new tile - STEP fires again.
entity.position = (worldpos_t){ 3, 1, 0 };
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_STEP_COUNT, 2);
assert_int_equal(TEST_ENTER_COUNT, 1);
assert_int_equal(TEST_EXIT_COUNT, 0);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_mapAreaCheckEntityNotifyFlagsRespected(void **state) {
testMapAreaReset();
mapAreaAdd(
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
testAreaCallback, MAP_AREA_NOTIFY_PLAYER, MAP_TRIGGER_ALL
);
entity_t npc = testEntityAt(ENTITY_TYPE_NPC, (worldpos_t){ 1, 1, 0 });
mapAreaCheckEntity(&npc);
assert_int_equal(TEST_ENTER_COUNT, 0);
entity_t player = testEntityAt(ENTITY_TYPE_PLAYER, (worldpos_t){ 1, 1, 0 });
mapAreaCheckEntity(&player);
assert_int_equal(TEST_ENTER_COUNT, 1);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_mapAreaCheckEntityFiresEnterAndExit),
cmocka_unit_test(test_mapAreaCheckEntityStepFiresOncePerNewTile),
cmocka_unit_test(test_mapAreaCheckEntityNotifyFlagsRespected),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-140
View File
@@ -1,140 +0,0 @@
/**
* 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 "rpg/overworld/tileshape.h"
static void test_tileShapeGetRampHeightGroundIsAlwaysFlat(void **state) {
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_GROUND, 0.0f, 0.0f), 0.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_GROUND, 1.0f, 1.0f), 0.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_GROUND, 0.5f, 0.5f), 0.0f, 0.0001f
);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_tileShapeGetRampHeightCardinalRamps(void **state) {
// RAMP_NORTH: rises going north (+y) - height equals localY.
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTH, 0.5f, 0.0f), 0.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTH, 0.5f, 1.0f), 1.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTH, 0.0f, 0.5f), 0.5f, 0.0001f
);
// RAMP_SOUTH: rises going south (-y) - height equals 1 - localY.
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_SOUTH, 0.5f, 0.0f), 1.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_SOUTH, 0.5f, 1.0f), 0.0f, 0.0001f
);
// RAMP_EAST: rises going east (+x) - height equals localX.
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_EAST, 0.0f, 0.5f), 0.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_EAST, 1.0f, 0.5f), 1.0f, 0.0001f
);
// RAMP_WEST: rises going west (-x) - height equals 1 - localX.
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_WEST, 0.0f, 0.5f), 1.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_WEST, 1.0f, 0.5f), 0.0f, 0.0001f
);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_tileShapeGetRampHeightOuterCornerRamps(void **state) {
// RAMP_NORTHEAST: only the NE corner raised - a hip shape, height
// equal to min(localX, localY) since the mesh is split along the
// SW-NE diagonal into two flat triangles.
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST, 0.0f, 0.0f),
0.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST, 1.0f, 1.0f),
1.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST, 0.3f, 0.7f),
0.3f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST, 0.7f, 0.3f),
0.3f, 0.0001f
);
// RAMP_SOUTHWEST: only the SW corner raised - height equal to
// min(1 - localX, 1 - localY).
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_SOUTHWEST, 0.0f, 0.0f),
1.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_SOUTHWEST, 1.0f, 1.0f),
0.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_SOUTHWEST, 1.0f, 0.0f),
0.0f, 0.0001f
);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_tileShapeGetRampHeightInnerCornerRamp(void **state) {
// RAMP_NORTHEAST_INNER: only the opposite (SW) corner lowered, rest of
// the tile raised - height equal to max(localX, localY).
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST_INNER, 0.0f, 0.0f),
0.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST_INNER, 1.0f, 1.0f),
1.0f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST_INNER, 0.3f, 0.7f),
0.7f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST_INNER, 0.7f, 0.3f),
0.7f, 0.0001f
);
assert_float_equal(
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST_INNER, 1.0f, 0.0f),
1.0f, 0.0001f
);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_tileShapeGetRampHeightGroundIsAlwaysFlat),
cmocka_unit_test(test_tileShapeGetRampHeightCardinalRamps),
cmocka_unit_test(test_tileShapeGetRampHeightOuterCornerRamps),
cmocka_unit_test(test_tileShapeGetRampHeightInnerCornerRamp),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-78
View File
@@ -1,78 +0,0 @@
/**
* 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 "rpg/physics/physicsbody.h"
static void test_physicsBodyInit(void **state) {
physicsbody_t body;
const vec3 position = { 1.0f, 2.0f, 3.0f };
const vec3 extents = { 1.0f, 2.0f, 3.0f };
physicsBodyInit(&body, position, extents);
assert_float_equal(body.position[0], 1.0f, 0.0001f);
assert_float_equal(body.position[1], 2.0f, 0.0001f);
assert_float_equal(body.position[2], 3.0f, 0.0001f);
assert_float_equal(body.extents[0], 1.0f, 0.0001f);
assert_float_equal(body.extents[1], 2.0f, 0.0001f);
assert_float_equal(body.extents[2], 3.0f, 0.0001f);
assert_float_equal(body.velocity[0], 0.0f, 0.0001f);
assert_float_equal(body.velocity[1], 0.0f, 0.0001f);
assert_float_equal(body.velocity[2], 0.0f, 0.0001f);
assert_false(body.grounded);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsBodyGetBounds(void **state) {
physicsbody_t body;
const vec3 position = { 2.0f, 3.0f, 4.0f };
const vec3 extents = { 1.0f, 2.0f, 0.5f };
physicsBodyInit(&body, position, extents);
vec3 min, max;
physicsBodyGetBounds(&body, min, max);
assert_float_equal(min[0], 2.0f, 0.0001f);
assert_float_equal(min[1], 3.0f, 0.0001f);
assert_float_equal(min[2], 4.0f, 0.0001f);
assert_float_equal(max[0], 3.0f, 0.0001f);
assert_float_equal(max[1], 5.0f, 0.0001f);
assert_float_equal(max[2], 4.5f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsBodyGetBoundsNegativeCoordinates(void **state) {
physicsbody_t body;
const vec3 position = { -5.0f, -1.5f, -2.0f };
const vec3 extents = { 2.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
vec3 min, max;
physicsBodyGetBounds(&body, min, max);
assert_float_equal(min[0], -5.0f, 0.0001f);
assert_float_equal(min[1], -1.5f, 0.0001f);
assert_float_equal(min[2], -2.0f, 0.0001f);
assert_float_equal(max[0], -3.0f, 0.0001f);
assert_float_equal(max[1], -0.5f, 0.0001f);
assert_float_equal(max[2], -1.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_physicsBodyInit),
cmocka_unit_test(test_physicsBodyGetBounds),
cmocka_unit_test(test_physicsBodyGetBoundsNegativeCoordinates),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-432
View File
@@ -1,432 +0,0 @@
/**
* 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 "time/time.h"
#include "rpg/physics/physicsbody.h"
#include "rpg/physics/physicsworld.h"
#include "rpg/overworld/map.h"
#include "rpg/overworld/tile.h"
#include "rpg/overworld/tileshape.h"
#include "rpg/overworld/worldpos.h"
static void testMapReset(void) {
memoryZero(&MAP, sizeof(map_t));
MAP.loaded = true;
MAP.chunkPosition = (chunkpos_t){ 0, 0, 0 };
// Push every chunk but the first out of the loaded window, so only
// MAP.chunks[0] (positioned at the origin below) resolves through
// mapRebuildChunkOrder - otherwise every chunk would default to
// position (0,0,0) too and collide on the same chunk order slot.
for(chunkindex_t i = 1; i < MAP_CHUNK_COUNT; i++) {
MAP.chunks[i].position = (chunkpos_t){ 100, 100, 100 };
}
MAP.chunks[0].position = (chunkpos_t){ 0, 0, 0 };
mapRebuildChunkOrder();
}
static void testMapSetTile(
const worldunit_t x,
const worldunit_t y,
const worldunit_t z,
const tileshape_t shape
) {
const worldpos_t pos = { x, y, z };
const chunktileindex_t index = worldPosToChunkTileIndex(&pos);
const uint8_t localZ = worldPosToChunkLocalZ(&pos);
MAP.chunks[0].tiles[index] = (tile_t){ .shape = shape, .z = localZ };
}
static void test_physicsWorldStepStraightLineNoObstacles(void **state) {
testMapReset();
for(worldunit_t x = 0; x < 10; x++) {
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
}
physicsworld_t world;
physicsWorldInit(&world, 0.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 0.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
body.velocity[0] = 1.0f;
const uint32_t steps = 10;
for(uint32_t i = 0; i < steps; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
}
assert_float_equal(body.position[0], 1.0f * steps * DUSK_TIME_STEP, 0.001f);
assert_float_equal(body.position[1], 0.0f, 0.0001f);
assert_float_equal(body.position[2], 0.0f, 0.0001f);
assert_float_equal(body.velocity[0], 1.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepBlockedHorizontally(void **state) {
testMapReset();
testMapSetTile(0, 0, 0, TILE_SHAPE_GROUND);
testMapSetTile(1, 0, 0, TILE_SHAPE_GROUND);
testMapSetTile(2, 0, 0, TILE_SHAPE_GROUND);
// x = 3 left as TILE_SHAPE_NULL, acting as a wall.
physicsworld_t world;
physicsWorldInit(&world, 0.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 0.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
body.velocity[0] = 5.0f;
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
}
assert_float_equal(body.position[0], 2.0f, 0.0001f);
assert_float_equal(body.velocity[0], 0.0f, 0.0001f);
// Further steps must not push it past the wall.
for(uint32_t i = 0; i < 5; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
assert_float_equal(body.position[0], 2.0f, 0.0001f);
}
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepGravitySettlesOnFloor(void **state) {
testMapReset();
testMapSetTile(0, 0, 0, TILE_SHAPE_GROUND);
physicsworld_t world;
physicsWorldInit(&world, 20.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 3.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
for(uint32_t i = 0; i < 200; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
assert_true(body.position[2] >= -0.0001f);
}
assert_float_equal(body.position[2], 0.0f, 0.0001f);
assert_float_equal(body.velocity[2], 0.0f, 0.0001f);
assert_true(body.grounded);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepFallsThroughHole(void **state) {
testMapReset();
// No tiles set anywhere - every column is a hole.
physicsworld_t world;
physicsWorldInit(&world, 20.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 5.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
for(uint32_t i = 0; i < 50; i++) {
const float_t previousZ = body.position[2];
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
assert_true(body.position[2] < previousZ);
assert_false(body.grounded);
}
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepTerminalVelocityClamp(void **state) {
testMapReset();
// No tiles set anywhere - every column is a hole.
physicsworld_t world;
physicsWorldInit(&world, 20.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 5.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
for(uint32_t i = 0; i < 300; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
assert_true(fabsf(body.velocity[2]) <= world.terminalVelocity + 0.0001f);
}
assert_float_equal(fabsf(body.velocity[2]), world.terminalVelocity, 0.01f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepBlockedByCeiling(void **state) {
testMapReset();
testMapSetTile(0, 0, 2, TILE_SHAPE_GROUND);
physicsworld_t world;
physicsWorldInit(&world, 0.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 0.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
body.velocity[2] = 1.0f;
for(uint32_t i = 0; i < 200; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
}
assert_float_equal(body.position[2], 1.0f, 0.0001f);
assert_float_equal(body.velocity[2], 0.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepMultiColumnFootprint(void **state) {
testMapReset();
for(worldunit_t x = 0; x < 6; x++) {
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
}
testMapSetTile(0, 1, 0, TILE_SHAPE_GROUND);
testMapSetTile(1, 1, 0, TILE_SHAPE_GROUND);
// x = 2, y = 1 left as TILE_SHAPE_NULL, blocking only the second row
// spanned by the body's 2-unit-deep footprint.
physicsworld_t world;
physicsWorldInit(&world, 0.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 0.0f };
const vec3 extents = { 1.0f, 2.0f, 1.0f };
physicsBodyInit(&body, position, extents);
body.velocity[0] = 5.0f;
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
}
assert_float_equal(body.position[0], 1.0f, 0.0001f);
assert_float_equal(body.velocity[0], 0.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepClimbsRampGoingUp(void **state) {
testMapReset();
testMapSetTile(0, 0, 0, TILE_SHAPE_GROUND);
testMapSetTile(1, 0, 0, TILE_SHAPE_RAMP_EAST);
for(worldunit_t x = 2; x < 6; x++) {
testMapSetTile(x, 0, 1, TILE_SHAPE_GROUND);
}
physicsworld_t world;
physicsWorldInit(&world, 20.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 0.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
body.velocity[0] = 1.0f;
for(uint32_t i = 0; i < 400; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
// Ground height is driven by the body's center (not its corner
// position), so a 1-wide body barely touching a taller neighbouring
// column isn't yanked onto that column's full height - see
// physicsWorldResolveAxisZ. While the center is over the ramp tile,
// height should track how far across it smoothly, not snap flat.
const float_t centerX = body.position[0] + 0.5f;
if(centerX >= 1.0f && centerX < 2.0f) {
const float_t expected = centerX - 1.0f;
assert_float_equal(body.position[2], expected, 0.005f);
}
}
// Fully across, resting on the elevated ground one layer up.
assert_float_equal(body.position[0], 5.0f, 0.001f);
assert_float_equal(body.position[2], 1.0f, 0.005f);
assert_true(body.grounded);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepDescendsRampGoingDown(void **state) {
testMapReset();
testMapSetTile(0, 0, 1, TILE_SHAPE_GROUND);
testMapSetTile(1, 0, 0, TILE_SHAPE_RAMP_WEST);
for(worldunit_t x = 2; x < 6; x++) {
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
}
physicsworld_t world;
physicsWorldInit(&world, 20.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 1.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
body.velocity[0] = 1.0f;
for(uint32_t i = 0; i < 400; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
// Never falls through as a hole while crossing the ramp - descent
// may lag slightly behind the ideal slope (gravity closes the gap
// each step, see physicsWorldStep's documented limitation) but must
// stay close to it, never dropping toward the void below.
assert_true(body.position[2] >= -0.2f);
}
// Fully across, resting on the lower ground.
assert_float_equal(body.position[0], 5.0f, 0.001f);
assert_float_equal(body.position[2], 0.0f, 0.005f);
assert_true(body.grounded);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepFlatGroundUnaffectedByRampLogic(
void **state
) {
testMapReset();
for(worldunit_t x = 0; x < 6; x++) {
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
}
physicsworld_t world;
physicsWorldInit(&world, 20.0f, 40.0f);
physicsbody_t body;
const vec3 position = { 0.0f, 0.0f, 0.0f };
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsBodyInit(&body, position, extents);
body.velocity[0] = 5.0f;
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
assert_float_equal(body.position[2], 0.0f, 0.0001f);
}
assert_true(body.position[0] > 1.5f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepBlockedByOtherBody(void **state) {
testMapReset();
for(worldunit_t x = 0; x < 10; x++) {
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
}
physicsworld_t world;
physicsWorldInit(&world, 0.0f, 40.0f);
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsbody_t other;
physicsBodyInit(&other, (vec3){ 5.0f, 0.0f, 0.0f }, extents);
physicsbody_t body;
physicsBodyInit(&body, (vec3){ 0.0f, 0.0f, 0.0f }, extents);
body.velocity[0] = 5.0f;
physicsbody_t *others[] = { &other };
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, others, 1);
}
// Other occupies [5,6) - body (1 unit wide) should stop exactly
// touching it, at x=4.
assert_float_equal(body.position[0], 4.0f, 0.0001f);
assert_float_equal(body.velocity[0], 0.0f, 0.0001f);
assert_float_equal(other.position[0], 5.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepBlockedByNearestOfMultipleBodies(
void **state
) {
testMapReset();
for(worldunit_t x = 0; x < 20; x++) {
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
}
physicsworld_t world;
physicsWorldInit(&world, 0.0f, 40.0f);
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsbody_t nearOther;
physicsBodyInit(&nearOther, (vec3){ 5.0f, 0.0f, 0.0f }, extents);
physicsbody_t farOther;
physicsBodyInit(&farOther, (vec3){ 15.0f, 0.0f, 0.0f }, extents);
physicsbody_t body;
physicsBodyInit(&body, (vec3){ 0.0f, 0.0f, 0.0f }, extents);
body.velocity[0] = 5.0f;
physicsbody_t *others[] = { &farOther, &nearOther };
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, others, 2);
}
assert_float_equal(body.position[0], 4.0f, 0.0001f);
assert_float_equal(body.velocity[0], 0.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldResolveBodyOverlapIgnoresSelfAndNull(
void **state
) {
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsbody_t body;
physicsBodyInit(&body, (vec3){ 0.0f, 0.0f, 0.0f }, extents);
body.velocity[0] = 1.0f;
physicsbody_t *others[] = { &body, NULL };
physicsWorldResolveBodyOverlap(&body, 0, others, 2);
assert_float_equal(body.position[0], 0.0f, 0.0001f);
assert_float_equal(body.velocity[0], 1.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_physicsWorldStepStraightLineNoObstacles),
cmocka_unit_test(test_physicsWorldStepBlockedHorizontally),
cmocka_unit_test(test_physicsWorldStepGravitySettlesOnFloor),
cmocka_unit_test(test_physicsWorldStepFallsThroughHole),
cmocka_unit_test(test_physicsWorldStepTerminalVelocityClamp),
cmocka_unit_test(test_physicsWorldStepBlockedByCeiling),
cmocka_unit_test(test_physicsWorldStepMultiColumnFootprint),
cmocka_unit_test(test_physicsWorldStepClimbsRampGoingUp),
cmocka_unit_test(test_physicsWorldStepDescendsRampGoingDown),
cmocka_unit_test(test_physicsWorldStepFlatGroundUnaffectedByRampLogic),
cmocka_unit_test(test_physicsWorldStepBlockedByOtherBody),
cmocka_unit_test(test_physicsWorldStepBlockedByNearestOfMultipleBodies),
cmocka_unit_test(test_physicsWorldResolveBodyOverlapIgnoresSelfAndNull),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-16
View File
@@ -1,16 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
int main(int argc, char** argv) {
const struct CMUnitTest tests[] = {
// Add RPG tests here in the future
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
@@ -6,7 +6,4 @@
include(dusktest)
# Tests
dusktest(test_entitydir.c)
dusktest(test_entity.c)
# Subdirs
dusktest(test_scene.c)
+125
View File
@@ -0,0 +1,125 @@
/**
* 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 "time/time.h"
#include "scene/scene.h"
#include "entity/entitymanager.h"
static void countingUpdateCallback(
const entityid_t entityId,
const componentid_t componentId,
void *user
) {
uint32_t *counter = (uint32_t *)user;
(*counter)++;
}
static void test_sceneCreateDestroy(void **state) {
sceneInit();
sceneid_t id = sceneCreate();
assert_true(id != SCENE_ID_INVALID);
assert_non_null(sceneGetEntities(id));
sceneDestroy(id);
assert_int_equal(sceneGetActive(), SCENE_ID_INVALID);
sceneDispose();
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_sceneEntitiesAreIsolatedPerScene(void **state) {
sceneInit();
sceneid_t sceneA = sceneCreate();
sceneid_t sceneB = sceneCreate();
assert_true(sceneA != sceneB);
entitymanager_t *entitiesA = sceneGetEntities(sceneA);
entitymanager_t *entitiesB = sceneGetEntities(sceneB);
assert_true(entitiesA != entitiesB);
// Fill scene A completely; scene B must be entirely unaffected.
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
assert_true(entityManagerAdd(entitiesA) != ENTITY_ID_INVALID);
}
expect_assert_failure(entityManagerAdd(entitiesA));
entityid_t entityB = entityManagerAdd(entitiesB);
assert_true(entityB != ENTITY_ID_INVALID);
sceneDispose();
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_sceneUpdateOnlyTicksActiveScene(void **state) {
sceneInit();
timeInit();
TIME.dynamicUpdate = true;
sceneid_t sceneA = sceneCreate();
sceneid_t sceneB = sceneCreate();
uint32_t counterA = 0;
uint32_t counterB = 0;
entitymanager_t *entitiesA = sceneGetEntities(sceneA);
entityid_t entityA = entityManagerAdd(entitiesA);
entityUpdateAdd(
entitiesA, entityA, countingUpdateCallback, COMPONENT_ID_INVALID,
&counterA
);
entitymanager_t *entitiesB = sceneGetEntities(sceneB);
entityid_t entityB = entityManagerAdd(entitiesB);
entityUpdateAdd(
entitiesB, entityB, countingUpdateCallback, COMPONENT_ID_INVALID,
&counterB
);
sceneSetActive(sceneA);
sceneUpdate();
assert_int_equal(counterA, 1);
assert_int_equal(counterB, 0);
sceneSetActive(sceneB);
sceneUpdate();
assert_int_equal(counterA, 1);
assert_int_equal(counterB, 1);
sceneDispose();
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_sceneDestroyClearsActive(void **state) {
sceneInit();
sceneid_t id = sceneCreate();
sceneSetActive(id);
assert_int_equal(sceneGetActive(), id);
sceneDestroy(id);
assert_int_equal(sceneGetActive(), SCENE_ID_INVALID);
sceneDispose();
assert_int_equal(memoryGetAllocatedCount(), 0);
}
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_sceneDestroyClearsActive),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}