Restore ECS
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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)
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user