diff --git a/src/dusk/entity/component.c b/src/dusk/entity/component.c index 6eea637a..272fdb72 100644 --- a/src/dusk/entity/component.c +++ b/src/dusk/entity/component.c @@ -87,7 +87,7 @@ entityid_t componentGetEntitiesWithComponent( componentid_t used = ENTITY_MANAGER.entitiesWithComponent[ type * ENTITY_COUNT_MAX + i ]; - if(used == 0xFF) continue; + if(used == COMPONENT_ID_INVALID) continue; assertTrue( ENTITY_MANAGER.components[componentGetIndex(i, used)].type == type, "Component type mismatch in entitiesWithComponent lookup" diff --git a/src/dusk/entity/component/display/entityposition.c b/src/dusk/entity/component/display/entityposition.c index 43d1aa2e..8856ead5 100644 --- a/src/dusk/entity/component/display/entityposition.c +++ b/src/dusk/entity/component/display/entityposition.c @@ -1,12 +1,39 @@ /** * Copyright (c) 2026 Dominic Masters - * + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #include "entity/entitymanager.h" +// Lazily recompute worldTransform from the parent chain. +static void entityPositionUpdateWorld(entityposition_t *pos) { + if(!pos->dirty) return; + + if(pos->parentEntityId == ENTITY_ID_INVALID) { + glm_mat4_copy(pos->localTransform, pos->worldTransform); + } else { + entityposition_t *parent = componentGetData( + pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION + ); + entityPositionUpdateWorld(parent); + glm_mat4_mul(parent->worldTransform, pos->localTransform, pos->worldTransform); + } + + pos->dirty = false; +} + +void entityPositionMarkDirty(entityposition_t *pos) { + pos->dirty = true; + for(uint8_t i = 0; i < pos->childCount; i++) { + entityposition_t *child = componentGetData( + pos->childEntityIds[i], pos->childComponentIds[i], COMPONENT_TYPE_POSITION + ); + entityPositionMarkDirty(child); + } +} + void entityPositionInit( const entityid_t entityId, const componentid_t componentId @@ -18,7 +45,12 @@ void entityPositionInit( glm_vec3_zero(pos->position); glm_vec3_zero(pos->rotation); glm_vec3_one(pos->scale); - glm_mat4_identity(pos->transform); + glm_mat4_identity(pos->localTransform); + glm_mat4_identity(pos->worldTransform); + pos->dirty = false; + pos->parentEntityId = ENTITY_ID_INVALID; + pos->parentComponentId = COMPONENT_ID_INVALID; + pos->childCount = 0; } void entityPositionLookAt( @@ -31,8 +63,9 @@ void entityPositionLookAt( entityposition_t *pos = componentGetData( entityId, componentId, COMPONENT_TYPE_POSITION ); - glm_lookat(eye, target, up, pos->transform); + glm_lookat(eye, target, up, pos->localTransform); entityPositionDecompose(pos); + entityPositionMarkDirty(pos); } void entityPositionGetTransform( @@ -43,7 +76,19 @@ void entityPositionGetTransform( entityposition_t *pos = componentGetData( entityId, componentId, COMPONENT_TYPE_POSITION ); - glm_mat4_copy(pos->transform, dest); + entityPositionUpdateWorld(pos); + glm_mat4_copy(pos->worldTransform, dest); +} + +void entityPositionGetLocalTransform( + const entityid_t entityId, + const componentid_t componentId, + mat4 dest +) { + entityposition_t *pos = componentGetData( + entityId, componentId, COMPONENT_TYPE_POSITION + ); + glm_mat4_copy(pos->localTransform, dest); } void entityPositionGetPosition( @@ -115,6 +160,54 @@ void entityPositionSetScale( entityPositionRebuild(pos); } +void entityPositionSetParent( + const entityid_t entityId, + const componentid_t componentId, + const entityid_t parentEntityId, + const componentid_t parentComponentId +) { + entityposition_t *pos = componentGetData( + entityId, componentId, COMPONENT_TYPE_POSITION + ); + + // Remove from old parent's child list. + if(pos->parentEntityId != ENTITY_ID_INVALID) { + entityposition_t *oldParent = componentGetData( + 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( + 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(pos); +} + entityposition_t *entityPositionGet( const entityid_t entityId, const componentid_t componentId @@ -125,57 +218,56 @@ entityposition_t *entityPositionGet( } void entityPositionRebuild(entityposition_t *pos) { - glm_mat4_identity(pos->transform); - glm_translate(pos->transform, pos->position); - glm_rotate_x(pos->transform, pos->rotation[0], pos->transform); - glm_rotate_y(pos->transform, pos->rotation[1], pos->transform); - glm_rotate_z(pos->transform, pos->rotation[2], pos->transform); - glm_scale(pos->transform, pos->scale); + glm_mat4_identity(pos->localTransform); + glm_translate(pos->localTransform, pos->position); + glm_rotate_x(pos->localTransform, pos->rotation[0], pos->localTransform); + glm_rotate_y(pos->localTransform, pos->rotation[1], pos->localTransform); + glm_rotate_z(pos->localTransform, pos->rotation[2], pos->localTransform); + glm_scale(pos->localTransform, pos->scale); + entityPositionMarkDirty(pos); } void entityPositionDecompose(entityposition_t *pos) { // Translation: column 3 - pos->position[0] = pos->transform[3][0]; - pos->position[1] = pos->transform[3][1]; - pos->position[2] = pos->transform[3][2]; + 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->transform[0][0] * pos->transform[0][0] + - pos->transform[0][1] * pos->transform[0][1] + - pos->transform[0][2] * pos->transform[0][2] + 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->transform[1][0] * pos->transform[1][0] + - pos->transform[1][1] * pos->transform[1][1] + - pos->transform[1][2] * pos->transform[1][2] + 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->transform[2][0] * pos->transform[2][0] + - pos->transform[2][1] * pos->transform[2][1] + - pos->transform[2][2] * pos->transform[2][2] + 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 + // Normalize columns to isolate the rotation matrix. float invS0 = pos->scale[0] > 0.0f ? 1.0f / pos->scale[0] : 0.0f; float invS1 = pos->scale[1] > 0.0f ? 1.0f / pos->scale[1] : 0.0f; float invS2 = pos->scale[2] > 0.0f ? 1.0f / pos->scale[2] : 0.0f; mat4 r; glm_mat4_identity(r); - r[0][0] = pos->transform[0][0] * invS0; - r[0][1] = pos->transform[0][1] * invS0; - r[0][2] = pos->transform[0][2] * invS0; - r[1][0] = pos->transform[1][0] * invS1; - r[1][1] = pos->transform[1][1] * invS1; - r[1][2] = pos->transform[1][2] * invS1; - r[2][0] = pos->transform[2][0] * invS2; - r[2][1] = pos->transform[2][1] * invS2; - r[2][2] = pos->transform[2][2] * invS2; + r[0][0] = pos->localTransform[0][0] * invS0; + r[0][1] = pos->localTransform[0][1] * invS0; + r[0][2] = pos->localTransform[0][2] * invS0; + r[1][0] = pos->localTransform[1][0] * invS1; + r[1][1] = pos->localTransform[1][1] * invS1; + r[1][2] = pos->localTransform[1][2] * invS1; + r[2][0] = pos->localTransform[2][0] * invS2; + r[2][1] = pos->localTransform[2][1] * invS2; + r[2][2] = pos->localTransform[2][2] * invS2; // Extract XYZ euler angles (R = Rx * Ry * Rz, column-major) - // r[2][0] = sin(Y), r[2][1] = -sin(X)*cos(Y), r[2][2] = cos(X)*cos(Y) - // r[0][0] = cos(Y)*cos(Z), r[1][0] = -cos(Y)*sin(Z) float sinBeta = glm_clamp(r[2][0], -1.0f, 1.0f); pos->rotation[1] = asinf(sinBeta); float cosBeta = cosf(pos->rotation[1]); @@ -184,10 +276,10 @@ void entityPositionDecompose(entityposition_t *pos) { pos->rotation[0] = atan2f(-r[2][1], r[2][2]); pos->rotation[2] = atan2f(-r[1][0], r[0][0]); } else { - // Gimbal lock: pin Z to 0, recover X from the remaining degree of freedom + // Gimbal lock: pin Z to 0, recover X. pos->rotation[2] = 0.0f; pos->rotation[0] = (sinBeta > 0.0f) ? atan2f(r[0][1], r[1][1]) : -atan2f(r[0][1], r[1][1]); } -} \ No newline at end of file +} diff --git a/src/dusk/entity/component/display/entityposition.h b/src/dusk/entity/component/display/entityposition.h index 8a564368..d9843960 100644 --- a/src/dusk/entity/component/display/entityposition.h +++ b/src/dusk/entity/component/display/entityposition.h @@ -1,6 +1,6 @@ /** * Copyright (c) 2026 Dominic Masters - * + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ @@ -8,18 +8,24 @@ #pragma once #include "entity/entitybase.h" +#define ENTITY_POSITION_CHILDREN_MAX 8 + typedef struct { - mat4 transform; + mat4 localTransform; + mat4 worldTransform; vec3 position; vec3 rotation; vec3 scale; + bool dirty; + entityid_t parentEntityId; + componentid_t parentComponentId; + uint8_t childCount; + entityid_t childEntityIds[ENTITY_POSITION_CHILDREN_MAX]; + componentid_t childComponentIds[ENTITY_POSITION_CHILDREN_MAX]; } entityposition_t; /** * Initialize the entity position component. - * - * @param entityId The entity ID. - * @param componentId The component ID. */ void entityPositionInit( const entityid_t entityId, @@ -27,13 +33,13 @@ void entityPositionInit( ); /** - * Transforms the entity's position to look at a target point. - * - * @param entityId The entity ID. + * Transforms the entity's local transform to look at a target point. + * + * @param entityId The entity ID. * @param componentId The component ID. - * @param target The target point to look at. - * @param up The up vector for the look at transformation. - * @param eye The position of the camera/eye for the look at transformation. + * @param target The target point to look at. + * @param up The up vector. + * @param eye The eye/camera position. */ void entityPositionLookAt( const entityid_t entityId, @@ -44,11 +50,11 @@ void entityPositionLookAt( ); /** - * Gets the transform matrix of the entity position component. + * Gets the world-space transform matrix, recomputing it lazily if dirty. * - * @param entityId The entity ID. + * @param entityId The entity ID. * @param componentId The component ID. - * @param dest The destination matrix to write the transform to. + * @param dest Destination matrix. */ void entityPositionGetTransform( const entityid_t entityId, @@ -57,11 +63,20 @@ void entityPositionGetTransform( ); /** - * Gets the cached position of the entity. + * Gets the local transform matrix (does not include parent transforms). * - * @param entityId The entity ID. + * @param entityId The entity ID. * @param componentId The component ID. - * @param dest The destination vec3 to write the position to. + * @param dest Destination matrix. + */ +void entityPositionGetLocalTransform( + const entityid_t entityId, + const componentid_t componentId, + mat4 dest +); + +/** + * Gets the cached local position. */ void entityPositionGetPosition( const entityid_t entityId, @@ -70,11 +85,7 @@ void entityPositionGetPosition( ); /** - * Sets the position of the entity and rebuilds the transform matrix. - * - * @param entityId The entity ID. - * @param componentId The component ID. - * @param position The new position. + * Sets the local position and marks the world transform dirty. */ void entityPositionSetPosition( const entityid_t entityId, @@ -83,11 +94,7 @@ void entityPositionSetPosition( ); /** - * Gets the cached euler rotation (XYZ, radians) of the entity. - * - * @param entityId The entity ID. - * @param componentId The component ID. - * @param dest The destination vec3 to write the rotation to. + * Gets the cached local euler rotation (XYZ, radians). */ void entityPositionGetRotation( const entityid_t entityId, @@ -96,12 +103,7 @@ void entityPositionGetRotation( ); /** - * Sets the euler rotation (XYZ, radians) of the entity and rebuilds the - * transform matrix. - * - * @param entityId The entity ID. - * @param componentId The component ID. - * @param rotation The new euler rotation in radians. + * Sets the local euler rotation (XYZ, radians) and marks the world transform dirty. */ void entityPositionSetRotation( const entityid_t entityId, @@ -110,11 +112,7 @@ void entityPositionSetRotation( ); /** - * Gets the cached scale of the entity. - * - * @param entityId The entity ID. - * @param componentId The component ID. - * @param dest The destination vec3 to write the scale to. + * Gets the cached local scale. */ void entityPositionGetScale( const entityid_t entityId, @@ -123,11 +121,7 @@ void entityPositionGetScale( ); /** - * Sets the scale of the entity and rebuilds the transform matrix. - * - * @param entityId The entity ID. - * @param componentId The component ID. - * @param scale The new scale. + * Sets the local scale and marks the world transform dirty. */ void entityPositionSetScale( const entityid_t entityId, @@ -136,13 +130,24 @@ void entityPositionSetScale( ); /** - * Returns a direct pointer to the entity position component data. - * After modifying position, rotation, or scale directly, call - * entityPositionRebuild() to update the transform matrix. + * Sets the parent of this entity's position component. + * Pass ENTITY_ID_INVALID / COMPONENT_ID_INVALID to detach from any parent. * - * @param entityId The entity ID. - * @param componentId The component ID. - * @return Pointer to the entity position component data. + * @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( + 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(). */ entityposition_t *entityPositionGet( const entityid_t entityId, @@ -150,15 +155,18 @@ entityposition_t *entityPositionGet( ); /** - * Internal function to rebuild the transform matrix of the entity position - * component based on the current position, rotation, and scale. + * Rebuilds the local transform matrix from the cached position/rotation/scale, + * then marks this node and all descendants dirty. */ void entityPositionRebuild(entityposition_t *pos); /** - * Decomposes the transform matrix back into the position, rotation (XYZ euler, - * radians), and scale cache fields. Call after any direct matrix modification. - * - * @param pos Pointer to the entity position component data. + * Marks this node and all descendants as having a stale world transform. */ -void entityPositionDecompose(entityposition_t *pos); \ No newline at end of file +void entityPositionMarkDirty(entityposition_t *pos); + +/** + * Decomposes the local transform matrix back into the position, rotation + * (XYZ euler, radians), and scale cache fields. + */ +void entityPositionDecompose(entityposition_t *pos); diff --git a/src/dusk/entity/entity.c b/src/dusk/entity/entity.c index 25fbc0b8..8b36824e 100644 --- a/src/dusk/entity/entity.c +++ b/src/dusk/entity/entity.c @@ -22,7 +22,7 @@ void entityInit(const entityid_t entityId) { ) { ENTITY_MANAGER.entitiesWithComponent[ compType * ENTITY_COUNT_MAX + entityId - ] = 0xFF; + ] = COMPONENT_ID_INVALID; } ent->state |= ENTITY_STATE_ACTIVE; @@ -52,7 +52,7 @@ componentid_t entityAddComponent( } assertUnreachable("Entity has no more component slots available"); - return 0xFF; + return COMPONENT_ID_INVALID; } componentid_t entityGetComponent( @@ -62,7 +62,7 @@ componentid_t entityGetComponent( componentid_t compId = ENTITY_MANAGER.entitiesWithComponent[ type * ENTITY_COUNT_MAX + entityId ]; - if(compId == 0xFF) return compId; + if(compId == COMPONENT_ID_INVALID) return compId; assertTrue( ENTITY_MANAGER.components[componentGetIndex(entityId, compId)].type == type, "Component type mismatch" @@ -80,7 +80,7 @@ void entityDispose(const entityid_t entityId) { componentDispose(entityId, i); ENTITY_MANAGER.entitiesWithComponent[ ENTITY_MANAGER.components[compInd].type * ENTITY_COUNT_MAX + entityId - ] = 0xFF; + ] = COMPONENT_ID_INVALID; } ent->state = 0; diff --git a/src/dusk/entity/entity.h b/src/dusk/entity/entity.h index 9c839a65..651ce03d 100644 --- a/src/dusk/entity/entity.h +++ b/src/dusk/entity/entity.h @@ -35,7 +35,7 @@ componentid_t entityAddComponent( /** * Gets the ID of the component of the given type on the entity with the given - * ID, or 0xFF if the entity lacks the component. + * ID, or COMPONENT_ID_INVALID if the entity lacks the component. * * @param entityId The ID of the entity to get the component from. * @param type The type of the component to get. diff --git a/src/dusk/entity/entitybase.h b/src/dusk/entity/entitybase.h index 66a67ed5..a61f1170 100644 --- a/src/dusk/entity/entitybase.h +++ b/src/dusk/entity/entitybase.h @@ -11,6 +11,9 @@ #define ENTITY_COUNT_MAX 20 #define ENTITY_COMPONENT_COUNT_MAX 8 +#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; \ No newline at end of file diff --git a/src/dusk/entity/entitymanager.c b/src/dusk/entity/entitymanager.c index 28f62f82..a74a424b 100644 --- a/src/dusk/entity/entitymanager.c +++ b/src/dusk/entity/entitymanager.c @@ -15,7 +15,7 @@ entitymanager_t ENTITY_MANAGER; void entityManagerInit(void) { memoryZero(&ENTITY_MANAGER, sizeof(entitymanager_t)); memorySet( - ENTITY_MANAGER.entitiesWithComponent, 0xFF, + ENTITY_MANAGER.entitiesWithComponent, COMPONENT_ID_INVALID, sizeof(entityid_t) * COMPONENT_TYPE_COUNT * ENTITY_COUNT_MAX ); @@ -33,7 +33,7 @@ entityid_t entityManagerAdd() { return i; } assertUnreachable("No more entity IDs available"); - return 0xFF; + return ENTITY_ID_INVALID; } void entityManagerDispose(void) { diff --git a/src/dusk/script/module/entity/component/moduleentityposition.h b/src/dusk/script/module/entity/component/moduleentityposition.h index e1df4e6f..cc1132eb 100644 --- a/src/dusk/script/module/entity/component/moduleentityposition.h +++ b/src/dusk/script/module/entity/component/moduleentityposition.h @@ -97,7 +97,11 @@ moduleBaseFunction(moduleEntityPositionSetScale) { moduleBaseFunction(moduleEntityPositionLookAt) { if(argc < 1) return moduleBaseThrow("Expected at least 1 argument"); - entityposition_t *pos = moduleEntityPositionGet(callInfo); + componenthandle_t *h = scriptProtoGetValue( + &MODULE_ENTITY_POSITION_PROTO, callInfo->this_value + ); + if(!h) return jerry_undefined(); + entityposition_t *pos = entityPositionGet(h->eid, h->cid); if(!pos) return jerry_undefined(); vec3 target; if(!moduleVec3AnyCheck(args[0], target)) { @@ -107,8 +111,37 @@ moduleBaseFunction(moduleEntityPositionLookAt) { if(argc >= 2 && !moduleVec3AnyCheck(args[1], up)) { return moduleBaseThrow("expected Vec3 up"); } - glm_lookat(pos->position, target, up, pos->transform); - entityPositionDecompose(pos); + entityPositionLookAt(h->eid, h->cid, target, up, pos->position); + return jerry_undefined(); +} + +moduleBaseFunction(moduleEntityPositionGetParent) { + componenthandle_t *h = scriptProtoGetValue( + &MODULE_ENTITY_POSITION_PROTO, callInfo->this_value + ); + if(!h) return jerry_undefined(); + entityposition_t *pos = entityPositionGet(h->eid, h->cid); + if(!pos || pos->parentEntityId == ENTITY_ID_INVALID) return jerry_null(); + componenthandle_t ph = { .eid = pos->parentEntityId, .cid = pos->parentComponentId }; + return scriptProtoCreateValue(&MODULE_ENTITY_POSITION_PROTO, &ph); +} + +moduleBaseFunction(moduleEntityPositionSetParentProp) { + componenthandle_t *h = scriptProtoGetValue( + &MODULE_ENTITY_POSITION_PROTO, callInfo->this_value + ); + if(!h) return jerry_undefined(); + + if(argc < 1 || jerry_value_is_null(args[0]) || jerry_value_is_undefined(args[0])) { + entityPositionSetParent(h->eid, h->cid, ENTITY_ID_INVALID, COMPONENT_ID_INVALID); + return jerry_undefined(); + } + + componenthandle_t *ph = scriptProtoGetValue( + &MODULE_ENTITY_POSITION_PROTO, args[0] + ); + if(!ph) return moduleBaseThrow("expected EntityPosition"); + entityPositionSetParent(h->eid, h->cid, ph->eid, ph->cid); return jerry_undefined(); } @@ -147,4 +180,9 @@ static void moduleEntityPOSITION(void) { &MODULE_ENTITY_POSITION_PROTO, "lookAt", moduleEntityPositionLookAt ); + + scriptProtoDefineProp( + &MODULE_ENTITY_POSITION_PROTO, "parent", + moduleEntityPositionGetParent, moduleEntityPositionSetParentProp + ); } \ No newline at end of file