Add parent/child

This commit is contained in:
2026-05-17 21:46:08 -05:00
parent 782fd07a8d
commit 54254348b8
8 changed files with 244 additions and 103 deletions
+1 -1
View File
@@ -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"
@@ -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]);
}
}
}
@@ -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);
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);
+4 -4
View File
@@ -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;
+1 -1
View File
@@ -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.
+3
View File
@@ -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;
+2 -2
View File
@@ -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) {
@@ -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
);
}