Add area and flag on physics

This commit is contained in:
2026-07-20 12:31:14 -05:00
parent 373f1c1010
commit b9f06fef7a
27 changed files with 1956 additions and 114 deletions
+1
View File
@@ -6,3 +6,4 @@
# Subdirs
add_subdirectory(display)
add_subdirectory(physics)
add_subdirectory(trigger)
+28 -104
View File
@@ -28,6 +28,7 @@ void entityPhysicsInit(
phys->shape.data.cube.halfExtents[2] = 0.5f;
phys->gravityScale = 1.0f;
phys->onGround = false;
phys->collideMask = 0x1;
}
entityphysics_t *entityPhysicsGet(
@@ -116,6 +117,25 @@ physicsbodytype_t entityPhysicsGetBodyType(
return phys->type;
}
void entityPhysicsSetCollideMask(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const uint32_t mask
) {
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
phys->collideMask = mask;
}
uint32_t entityPhysicsGetCollideMask(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
) {
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
return phys->collideMask;
}
errorret_t entityPhysicsSerialize(
entitymanager_t *mgr,
const entityid_t entityId,
@@ -141,66 +161,14 @@ errorret_t entityPhysicsSerialize(
}
yyjson_mut_obj_add_str(doc, json, "bodyType", bodyTypeName);
yyjson_mut_val *shape = yyjson_mut_obj(doc);
yyjson_mut_obj_add_val(doc, json, "shape", shape);
switch(phys->shape.type) {
case PHYSICS_SHAPE_CUBE:
yyjson_mut_obj_add_str(doc, shape, "type", "CUBE");
yyjson_mut_obj_add_real(
doc, shape, "halfExtentX", phys->shape.data.cube.halfExtents[0]
);
yyjson_mut_obj_add_real(
doc, shape, "halfExtentY", phys->shape.data.cube.halfExtents[1]
);
yyjson_mut_obj_add_real(
doc, shape, "halfExtentZ", phys->shape.data.cube.halfExtents[2]
);
break;
case PHYSICS_SHAPE_SPHERE:
yyjson_mut_obj_add_str(doc, shape, "type", "SPHERE");
yyjson_mut_obj_add_real(
doc, shape, "radius", phys->shape.data.sphere.radius
);
break;
case PHYSICS_SHAPE_CAPSULE:
yyjson_mut_obj_add_str(doc, shape, "type", "CAPSULE");
yyjson_mut_obj_add_real(
doc, shape, "radius", phys->shape.data.capsule.radius
);
yyjson_mut_obj_add_real(
doc, shape, "halfHeight", phys->shape.data.capsule.halfHeight
);
break;
case PHYSICS_SHAPE_PLANE:
yyjson_mut_obj_add_str(doc, shape, "type", "PLANE");
yyjson_mut_obj_add_real(
doc, shape, "normalX", phys->shape.data.plane.normal[0]
);
yyjson_mut_obj_add_real(
doc, shape, "normalY", phys->shape.data.plane.normal[1]
);
yyjson_mut_obj_add_real(
doc, shape, "normalZ", phys->shape.data.plane.normal[2]
);
yyjson_mut_obj_add_real(
doc, shape, "distance", phys->shape.data.plane.distance
);
break;
case PHYSICS_SHAPE_CUSTOM:
errorThrow(
"Cannot serialize a PHYSICS_SHAPE_CUSTOM shape (callback/userData "
"are not representable in JSON)"
);
default:
assertUnreachable("Unknown physics shape type");
}
errorChain(physicsShapeSerialize(doc, json, &phys->shape));
yyjson_mut_obj_add_real(doc, json, "velocityX", phys->velocity[0]);
yyjson_mut_obj_add_real(doc, json, "velocityY", phys->velocity[1]);
yyjson_mut_obj_add_real(doc, json, "velocityZ", phys->velocity[2]);
yyjson_mut_obj_add_real(doc, json, "gravityScale", phys->gravityScale);
yyjson_mut_obj_add_bool(doc, json, "onGround", phys->onGround);
yyjson_mut_obj_add_uint(doc, json, "collideMask", phys->collideMask);
errorOk();
}
@@ -229,56 +197,9 @@ errorret_t entityPhysicsDeserialize(
yyjson_val *shape = yyjson_obj_get(json, "shape");
if(shape) {
yyjson_val *shapeTypeVal = yyjson_obj_get(shape, "type");
assertNotNull(shapeTypeVal, "Physics shape JSON missing 'type' field");
const char_t *shapeTypeName = yyjson_get_str(shapeTypeVal);
yyjson_val *v;
if(stringEquals(shapeTypeName, "CUBE")) {
physicsshape_t s = { .type = PHYSICS_SHAPE_CUBE };
if((v = yyjson_obj_get(shape, "halfExtentX"))) {
s.data.cube.halfExtents[0] = (float_t)yyjson_get_num(v);
}
if((v = yyjson_obj_get(shape, "halfExtentY"))) {
s.data.cube.halfExtents[1] = (float_t)yyjson_get_num(v);
}
if((v = yyjson_obj_get(shape, "halfExtentZ"))) {
s.data.cube.halfExtents[2] = (float_t)yyjson_get_num(v);
}
entityPhysicsSetShape(mgr, entityId, componentId, s);
} else if(stringEquals(shapeTypeName, "SPHERE")) {
physicsshape_t s = { .type = PHYSICS_SHAPE_SPHERE };
if((v = yyjson_obj_get(shape, "radius"))) {
s.data.sphere.radius = (float_t)yyjson_get_num(v);
}
entityPhysicsSetShape(mgr, entityId, componentId, s);
} else if(stringEquals(shapeTypeName, "CAPSULE")) {
physicsshape_t s = { .type = PHYSICS_SHAPE_CAPSULE };
if((v = yyjson_obj_get(shape, "radius"))) {
s.data.capsule.radius = (float_t)yyjson_get_num(v);
}
if((v = yyjson_obj_get(shape, "halfHeight"))) {
s.data.capsule.halfHeight = (float_t)yyjson_get_num(v);
}
entityPhysicsSetShape(mgr, entityId, componentId, s);
} else if(stringEquals(shapeTypeName, "PLANE")) {
physicsshape_t s = { .type = PHYSICS_SHAPE_PLANE };
if((v = yyjson_obj_get(shape, "normalX"))) {
s.data.plane.normal[0] = (float_t)yyjson_get_num(v);
}
if((v = yyjson_obj_get(shape, "normalY"))) {
s.data.plane.normal[1] = (float_t)yyjson_get_num(v);
}
if((v = yyjson_obj_get(shape, "normalZ"))) {
s.data.plane.normal[2] = (float_t)yyjson_get_num(v);
}
if((v = yyjson_obj_get(shape, "distance"))) {
s.data.plane.distance = (float_t)yyjson_get_num(v);
}
entityPhysicsSetShape(mgr, entityId, componentId, s);
} else {
errorThrow("Unknown physics shape type '%s'", shapeTypeName);
}
physicsshape_t s;
errorChain(physicsShapeDeserialize(shape, &s));
entityPhysicsSetShape(mgr, entityId, componentId, s);
}
yyjson_val *v;
@@ -307,6 +228,9 @@ errorret_t entityPhysicsDeserialize(
if((v = yyjson_obj_get(json, "onGround"))) {
phys->onGround = yyjson_get_bool(v);
}
if((v = yyjson_obj_get(json, "collideMask"))) {
phys->collideMask = (uint32_t)yyjson_get_uint(v);
}
errorOk();
}
@@ -18,6 +18,14 @@ typedef struct {
vec3 velocity;
float_t gravityScale;
bool_t onGround;
/**
* Bitmask of collision layers/categories this body belongs to. Two
* bodies only collide with each other if (a.collideMask & b.collideMask)
* is non-zero -- i.e. they share at least one set bit. Defaults to 0x1
* (only bit 0 set).
*/
uint32_t collideMask;
} entityphysics_t;
/**
@@ -173,14 +181,44 @@ physicsbodytype_t entityPhysicsGetBodyType(
const componentid_t componentId
);
/**
* Sets the collision mask of the entity's physics body. Two bodies only
* collide if their masks share at least one set bit.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param mask The new collision mask to set on the physics body.
*/
void entityPhysicsSetCollideMask(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const uint32_t mask
);
/**
* Gets the collision mask of the entity's physics body.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @return The collision mask of the physics body.
*/
uint32_t entityPhysicsGetCollideMask(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
);
/**
* Serializes the physics body into the given JSON object: "bodyType"
* ("STATIC"/"DYNAMIC"/"KINEMATIC"), "shape" (an object with its own
* "type" of "CUBE"/"SPHERE"/"CAPSULE"/"PLANE" plus that shape's fields),
* "velocity" ({"x","y","z"}), "gravityScale", and "onGround". Fails if
* the body's shape is PHYSICS_SHAPE_CUSTOM -- custom shapes carry a
* callback and opaque userData pointer that cannot be represented in
* JSON.
* "velocity" ({"x","y","z"}), "gravityScale", "onGround", and
* "collideMask". Fails if the body's shape is PHYSICS_SHAPE_CUSTOM --
* custom shapes carry a callback and opaque userData pointer that cannot
* be represented in JSON.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
@@ -0,0 +1,11 @@
# 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
entitytrigger.c
entitytriggerevents.c
)
@@ -0,0 +1,142 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entitytrigger.h"
#include "entity/entitymanager.h"
#include "util/memory.h"
#include "assert/assert.h"
void entityTriggerInit(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
) {
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
memoryZero(trig, sizeof(entitytrigger_t));
trig->shape.type = PHYSICS_SHAPE_CUBE;
trig->shape.data.cube.halfExtents[0] = 0.5f;
trig->shape.data.cube.halfExtents[1] = 0.5f;
trig->shape.data.cube.halfExtents[2] = 0.5f;
trig->collideMask = 0x1;
}
entitytrigger_t *entityTriggerGet(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
) {
return componentGetData(mgr, entityId, componentId, COMPONENT_TYPE_TRIGGER);
}
void entityTriggerSetShape(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const physicsshape_t shape
) {
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
trig->shape = shape;
}
physicsshape_t entityTriggerGetShape(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
) {
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
return trig->shape;
}
void entityTriggerSetCollideMask(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const uint32_t mask
) {
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
trig->collideMask = mask;
}
uint32_t entityTriggerGetCollideMask(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
) {
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
return trig->collideMask;
}
uint8_t entityTriggerGetOccupantCount(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
) {
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
return trig->occupantCount;
}
entityid_t entityTriggerGetOccupantEntityId(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const uint8_t index
) {
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
if(index >= trig->occupantCount) return ENTITY_ID_INVALID;
return trig->occupants[index].entityId;
}
bool_t entityTriggerIsOccupyingEntity(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const entityid_t otherEntityId
) {
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
for(uint8_t i = 0; i < trig->occupantCount; i++) {
if(trig->occupants[i].entityId == otherEntityId) return true;
}
return false;
}
errorret_t entityTriggerSerialize(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
yyjson_mut_doc *doc,
yyjson_mut_val *json
) {
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
errorChain(physicsShapeSerialize(doc, json, &trig->shape));
yyjson_mut_obj_add_uint(doc, json, "collideMask", trig->collideMask);
errorOk();
}
errorret_t entityTriggerDeserialize(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
yyjson_val *json
) {
yyjson_val *shape = yyjson_obj_get(json, "shape");
if(shape) {
physicsshape_t s;
errorChain(physicsShapeDeserialize(shape, &s));
entityTriggerSetShape(mgr, entityId, componentId, s);
}
yyjson_val *v = yyjson_obj_get(json, "collideMask");
if(v) {
entityTriggerSetCollideMask(
mgr, entityId, componentId, (uint32_t)yyjson_get_uint(v)
);
}
errorOk();
}
@@ -0,0 +1,265 @@
/**
* 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 "physics/physicsshape.h"
#include "error/error.h"
#include "yyjson.h"
/** Maximum number of entities a single trigger can track at once. */
#define ENTITY_TRIGGER_OCCUPANTS_MAX 8
/** Maximum number of subscribers per trigger event (onEnter/onActive/...). */
#define ENTITY_TRIGGER_CALLBACK_COUNT_MAX 4
/**
* Callback invoked for a trigger's onEnter/onActive/onMove/onLeave events.
* See entityTriggerOnEnterAdd() and its onActive/onMove/onLeave siblings.
*
* @param mgr The entity manager that owns both entities.
* @param triggerEntityId The entity ID of the trigger volume.
* @param triggerComponentId The TRIGGER component ID.
* @param otherEntityId The entity ID that entered/is inside/left the
* trigger volume.
* @param otherComponentId The other entity's POSITION component ID.
* @param user The user pointer passed to the matching *Add() call.
*/
typedef void (*triggercallback_t)(
entitymanager_t *mgr,
const entityid_t triggerEntityId,
const componentid_t triggerComponentId,
const entityid_t otherEntityId,
const componentid_t otherComponentId,
void *user
);
/** Tracks one entity currently overlapping a trigger volume. */
typedef struct {
/** The occupying entity's ID. */
entityid_t entityId;
/** The occupying entity's POSITION component ID. */
componentid_t componentId;
/** The occupying entity's local position as of the last step, used to
* detect movement (see entityTriggerOnMoveAdd()). */
vec3 lastPosition;
} entitytriggeroccupant_t;
typedef struct {
/** The trigger volume's shape, in the owning entity's local space. */
physicsshape_t shape;
/**
* Bitmask of collision layers/categories this trigger detects. An
* entity's PHYSICS body is only tested against this trigger if
* (trigger.collideMask & body.collideMask) is non-zero -- i.e. they
* share at least one set bit. Defaults to 0x1 (only bit 0 set). See
* entityPhysicsSetCollideMask().
*/
uint32_t collideMask;
/** Number of entities currently tracked in occupants. */
uint8_t occupantCount;
/** Entities currently overlapping this trigger. */
entitytriggeroccupant_t occupants[ENTITY_TRIGGER_OCCUPANTS_MAX];
/** Fired the step an entity begins overlapping this trigger. */
uint8_t onEnterCount;
triggercallback_t onEnter[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
void *onEnterUser[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
/** Fired every step an entity continues overlapping this trigger,
* starting the step after onEnter fired. */
uint8_t onActiveCount;
triggercallback_t onActive[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
void *onActiveUser[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
/** Fired alongside onActive on any step where the entity's position
* changed since the previous step. */
uint8_t onMoveCount;
triggercallback_t onMove[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
void *onMoveUser[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
/** Fired the step an entity stops overlapping this trigger. */
uint8_t onLeaveCount;
triggercallback_t onLeave[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
void *onLeaveUser[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
} entitytrigger_t;
/**
* Initializes the trigger component: defaults to a 1x1x1 cube volume, no
* tracked occupants, and no event subscribers.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
*/
void entityTriggerInit(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
);
/**
* Gets the underlying trigger structure (temporarily) for the given
* entity. Prefer the dedicated getters/setters where possible.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @return The trigger component data for the given entity and component ID.
*/
entitytrigger_t *entityTriggerGet(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
);
/**
* Sets the shape of the trigger volume.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param shape The new shape to set on the trigger volume.
*/
void entityTriggerSetShape(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const physicsshape_t shape
);
/**
* Gets the shape of the trigger volume.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @return The shape of the trigger volume.
*/
physicsshape_t entityTriggerGetShape(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
);
/**
* Sets the collision mask of the trigger volume. An entity's PHYSICS body
* is only detected if their masks share at least one set bit.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param mask The new collision mask to set on the trigger volume.
*/
void entityTriggerSetCollideMask(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const uint32_t mask
);
/**
* Gets the collision mask of the trigger volume.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @return The collision mask of the trigger volume.
*/
uint32_t entityTriggerGetCollideMask(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
);
/**
* Gets the number of entities currently overlapping this trigger.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @return The number of currently tracked occupants.
*/
uint8_t entityTriggerGetOccupantCount(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId
);
/**
* Gets the entity ID of the occupant at the given index, in [0,
* entityTriggerGetOccupantCount()). Order is not stable across steps.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param index The occupant index.
* @return The occupying entity's ID, or ENTITY_ID_INVALID if index is out
* of range.
*/
entityid_t entityTriggerGetOccupantEntityId(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const uint8_t index
);
/**
* Checks whether a specific entity is currently overlapping this trigger.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param otherEntityId The entity ID to check for.
* @return True if otherEntityId is a currently tracked occupant.
*/
bool_t entityTriggerIsOccupyingEntity(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const entityid_t otherEntityId
);
/**
* Serializes the trigger's shape (see physicsShapeSerialize()) and
* "collideMask" into the given JSON object. Occupants and event
* subscribers are runtime state and are not serialized.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param doc The mutable JSON document to allocate values from.
* @param json The JSON object to write the component's fields into.
* @return Error state.
*/
errorret_t entityTriggerSerialize(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
yyjson_mut_doc *doc,
yyjson_mut_val *json
);
/**
* Reads "shape" and "collideMask" (see entityTriggerSerialize()) from the
* given JSON object and applies whichever are present, leaving the rest
* at their current values.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param json The JSON object to read the component's fields from.
* @return Error state.
*/
errorret_t entityTriggerDeserialize(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
yyjson_val *json
);
@@ -0,0 +1,221 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entitytriggerevents.h"
#include "assert/assert.h"
void entityTriggerOnEnterAdd(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const triggercallback_t callback,
void *user
) {
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
assertTrue(
trig->onEnterCount < ENTITY_TRIGGER_CALLBACK_COUNT_MAX,
"Trigger onEnter callback slots full"
);
trig->onEnter[trig->onEnterCount] = callback;
trig->onEnterUser[trig->onEnterCount] = user;
trig->onEnterCount++;
}
void entityTriggerOnEnterRemove(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const triggercallback_t callback
) {
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
for(uint8_t i = 0; i < trig->onEnterCount; i++) {
if(trig->onEnter[i] != callback) continue;
trig->onEnterCount--;
for(uint8_t j = i; j < trig->onEnterCount; j++) {
trig->onEnter[j] = trig->onEnter[j + 1];
trig->onEnterUser[j] = trig->onEnterUser[j + 1];
}
return;
}
}
void entityTriggerOnActiveAdd(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const triggercallback_t callback,
void *user
) {
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
assertTrue(
trig->onActiveCount < ENTITY_TRIGGER_CALLBACK_COUNT_MAX,
"Trigger onActive callback slots full"
);
trig->onActive[trig->onActiveCount] = callback;
trig->onActiveUser[trig->onActiveCount] = user;
trig->onActiveCount++;
}
void entityTriggerOnActiveRemove(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const triggercallback_t callback
) {
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
for(uint8_t i = 0; i < trig->onActiveCount; i++) {
if(trig->onActive[i] != callback) continue;
trig->onActiveCount--;
for(uint8_t j = i; j < trig->onActiveCount; j++) {
trig->onActive[j] = trig->onActive[j + 1];
trig->onActiveUser[j] = trig->onActiveUser[j + 1];
}
return;
}
}
void entityTriggerOnMoveAdd(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const triggercallback_t callback,
void *user
) {
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
assertTrue(
trig->onMoveCount < ENTITY_TRIGGER_CALLBACK_COUNT_MAX,
"Trigger onMove callback slots full"
);
trig->onMove[trig->onMoveCount] = callback;
trig->onMoveUser[trig->onMoveCount] = user;
trig->onMoveCount++;
}
void entityTriggerOnMoveRemove(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const triggercallback_t callback
) {
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
for(uint8_t i = 0; i < trig->onMoveCount; i++) {
if(trig->onMove[i] != callback) continue;
trig->onMoveCount--;
for(uint8_t j = i; j < trig->onMoveCount; j++) {
trig->onMove[j] = trig->onMove[j + 1];
trig->onMoveUser[j] = trig->onMoveUser[j + 1];
}
return;
}
}
void entityTriggerOnLeaveAdd(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const triggercallback_t callback,
void *user
) {
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
assertTrue(
trig->onLeaveCount < ENTITY_TRIGGER_CALLBACK_COUNT_MAX,
"Trigger onLeave callback slots full"
);
trig->onLeave[trig->onLeaveCount] = callback;
trig->onLeaveUser[trig->onLeaveCount] = user;
trig->onLeaveCount++;
}
void entityTriggerOnLeaveRemove(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const triggercallback_t callback
) {
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
for(uint8_t i = 0; i < trig->onLeaveCount; i++) {
if(trig->onLeave[i] != callback) continue;
trig->onLeaveCount--;
for(uint8_t j = i; j < trig->onLeaveCount; j++) {
trig->onLeave[j] = trig->onLeave[j + 1];
trig->onLeaveUser[j] = trig->onLeaveUser[j + 1];
}
return;
}
}
void entityTriggerFireEnter(
entitymanager_t *mgr,
const entityid_t triggerEntityId,
const componentid_t triggerComponentId,
const entityid_t otherEntityId,
const componentid_t otherComponentId
) {
entitytrigger_t *trig = entityTriggerGet(
mgr, triggerEntityId, triggerComponentId
);
for(uint8_t i = 0; i < trig->onEnterCount; i++) {
trig->onEnter[i](
mgr, triggerEntityId, triggerComponentId,
otherEntityId, otherComponentId, trig->onEnterUser[i]
);
}
}
void entityTriggerFireActive(
entitymanager_t *mgr,
const entityid_t triggerEntityId,
const componentid_t triggerComponentId,
const entityid_t otherEntityId,
const componentid_t otherComponentId
) {
entitytrigger_t *trig = entityTriggerGet(
mgr, triggerEntityId, triggerComponentId
);
for(uint8_t i = 0; i < trig->onActiveCount; i++) {
trig->onActive[i](
mgr, triggerEntityId, triggerComponentId,
otherEntityId, otherComponentId, trig->onActiveUser[i]
);
}
}
void entityTriggerFireMove(
entitymanager_t *mgr,
const entityid_t triggerEntityId,
const componentid_t triggerComponentId,
const entityid_t otherEntityId,
const componentid_t otherComponentId
) {
entitytrigger_t *trig = entityTriggerGet(
mgr, triggerEntityId, triggerComponentId
);
for(uint8_t i = 0; i < trig->onMoveCount; i++) {
trig->onMove[i](
mgr, triggerEntityId, triggerComponentId,
otherEntityId, otherComponentId, trig->onMoveUser[i]
);
}
}
void entityTriggerFireLeave(
entitymanager_t *mgr,
const entityid_t triggerEntityId,
const componentid_t triggerComponentId,
const entityid_t otherEntityId,
const componentid_t otherComponentId
) {
entitytrigger_t *trig = entityTriggerGet(
mgr, triggerEntityId, triggerComponentId
);
for(uint8_t i = 0; i < trig->onLeaveCount; i++) {
trig->onLeave[i](
mgr, triggerEntityId, triggerComponentId,
otherEntityId, otherComponentId, trig->onLeaveUser[i]
);
}
}
@@ -0,0 +1,220 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entitytrigger.h"
/**
* Subscribes a callback to a trigger's onEnter event, fired the step an
* entity begins overlapping the trigger volume.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param callback The callback to invoke.
* @param user Arbitrary pointer forwarded to the callback unchanged.
*/
void entityTriggerOnEnterAdd(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const triggercallback_t callback,
void *user
);
/**
* Unsubscribes a previously added onEnter callback. No-op if not
* currently subscribed.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param callback The callback that was passed to entityTriggerOnEnterAdd().
*/
void entityTriggerOnEnterRemove(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const triggercallback_t callback
);
/**
* Subscribes a callback to a trigger's onActive event, fired every step an
* entity continues overlapping the trigger volume (starting the step
* after onEnter fired).
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param callback The callback to invoke.
* @param user Arbitrary pointer forwarded to the callback unchanged.
*/
void entityTriggerOnActiveAdd(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const triggercallback_t callback,
void *user
);
/**
* Unsubscribes a previously added onActive callback. No-op if not
* currently subscribed.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param callback The callback that was passed to entityTriggerOnActiveAdd().
*/
void entityTriggerOnActiveRemove(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const triggercallback_t callback
);
/**
* Subscribes a callback to a trigger's onMove event, fired alongside
* onActive on any step where the overlapping entity's position changed
* since the previous step.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param callback The callback to invoke.
* @param user Arbitrary pointer forwarded to the callback unchanged.
*/
void entityTriggerOnMoveAdd(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const triggercallback_t callback,
void *user
);
/**
* Unsubscribes a previously added onMove callback. No-op if not currently
* subscribed.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param callback The callback that was passed to entityTriggerOnMoveAdd().
*/
void entityTriggerOnMoveRemove(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const triggercallback_t callback
);
/**
* Subscribes a callback to a trigger's onLeave event, fired the step an
* entity stops overlapping the trigger volume.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param callback The callback to invoke.
* @param user Arbitrary pointer forwarded to the callback unchanged.
*/
void entityTriggerOnLeaveAdd(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const triggercallback_t callback,
void *user
);
/**
* Unsubscribes a previously added onLeave callback. No-op if not
* currently subscribed.
*
* @param mgr The entity manager that owns the entity.
* @param entityId The entity ID.
* @param componentId The component ID.
* @param callback The callback that was passed to entityTriggerOnLeaveAdd().
*/
void entityTriggerOnLeaveRemove(
entitymanager_t *mgr,
const entityid_t entityId,
const componentid_t componentId,
const triggercallback_t callback
);
/**
* Internal. Invokes every subscribed onEnter callback for a trigger.
* Called by triggerSystemStep() when an entity begins overlapping.
*
* @param mgr The entity manager that owns both entities.
* @param triggerEntityId The entity ID of the trigger volume.
* @param triggerComponentId The TRIGGER component ID.
* @param otherEntityId The entity ID that entered the trigger volume.
* @param otherComponentId The other entity's POSITION component ID.
*/
void entityTriggerFireEnter(
entitymanager_t *mgr,
const entityid_t triggerEntityId,
const componentid_t triggerComponentId,
const entityid_t otherEntityId,
const componentid_t otherComponentId
);
/**
* Internal. Invokes every subscribed onActive callback for a trigger.
* Called by triggerSystemStep() for every entity still overlapping.
*
* @param mgr The entity manager that owns both entities.
* @param triggerEntityId The entity ID of the trigger volume.
* @param triggerComponentId The TRIGGER component ID.
* @param otherEntityId The entity ID still inside the trigger volume.
* @param otherComponentId The other entity's POSITION component ID.
*/
void entityTriggerFireActive(
entitymanager_t *mgr,
const entityid_t triggerEntityId,
const componentid_t triggerComponentId,
const entityid_t otherEntityId,
const componentid_t otherComponentId
);
/**
* Internal. Invokes every subscribed onMove callback for a trigger.
* Called by triggerSystemStep() when an overlapping entity's position
* changed since the previous step.
*
* @param mgr The entity manager that owns both entities.
* @param triggerEntityId The entity ID of the trigger volume.
* @param triggerComponentId The TRIGGER component ID.
* @param otherEntityId The entity ID that moved within the trigger volume.
* @param otherComponentId The other entity's POSITION component ID.
*/
void entityTriggerFireMove(
entitymanager_t *mgr,
const entityid_t triggerEntityId,
const componentid_t triggerComponentId,
const entityid_t otherEntityId,
const componentid_t otherComponentId
);
/**
* Internal. Invokes every subscribed onLeave callback for a trigger.
* Called by triggerSystemStep() when an entity stops overlapping.
*
* @param mgr The entity manager that owns both entities.
* @param triggerEntityId The entity ID of the trigger volume.
* @param triggerComponentId The TRIGGER component ID.
* @param otherEntityId The entity ID that left the trigger volume.
* @param otherComponentId The other entity's POSITION component ID.
*/
void entityTriggerFireLeave(
entitymanager_t *mgr,
const entityid_t triggerEntityId,
const componentid_t triggerComponentId,
const entityid_t otherEntityId,
const componentid_t otherComponentId
);
+3
View File
@@ -9,6 +9,7 @@
#include "entity/component/display/entitycamera.h"
#include "entity/component/display/entityrenderable.h"
#include "entity/component/physics/entityphysics.h"
#include "entity/component/trigger/entitytrigger.h"
// Name (Uppercase)
// Structure
@@ -28,6 +29,8 @@ X(RENDERABLE, entityrenderable_t, renderable,
entityRenderableSerialize, entityRenderableDeserialize)
X(PHYSICS, entityphysics_t, physics, entityPhysicsInit, NULL, NULL,
entityPhysicsSerialize, entityPhysicsDeserialize)
X(TRIGGER, entitytrigger_t, trigger, entityTriggerInit, NULL, NULL,
entityTriggerSerialize, entityTriggerDeserialize)
// Game-specific components
#include "entity/gamecomponentlist.h"
+22
View File
@@ -7,6 +7,7 @@
#pragma once
#include "error/error.h"
#include "entity/entitybase.h"
/**
* Initializes the game. Called once, after the engine and all of its
@@ -30,3 +31,24 @@ errorret_t gameUpdate(void);
* @return An error code indicating success or failure.
*/
errorret_t gameDispose(void);
/**
* Test-only TRIGGER onEnter callback, subscribed by gameInit() to the
* "testArea" entity in the test scene (see assets/scenes/test.json).
* Prints a debug message identifying whichever entity just entered.
*
* @param mgr The entity manager that owns both entities.
* @param triggerEntityId The entity ID of the "testArea" trigger volume.
* @param triggerComponentId The TRIGGER component ID.
* @param otherEntityId The entity ID that entered the trigger volume.
* @param otherComponentId The other entity's POSITION component ID.
* @param user Unused.
*/
void gameTestAreaOnEnter(
entitymanager_t *mgr,
const entityid_t triggerEntityId,
const componentid_t triggerComponentId,
const entityid_t otherEntityId,
const componentid_t otherComponentId,
void *user
);
+2
View File
@@ -8,5 +8,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
physicsworld.c
physicstest.c
physicsshape.c
physicsshapemesh.c
triggersystem.c
)
+131
View File
@@ -0,0 +1,131 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "physicsshape.h"
#include "assert/assert.h"
#include "util/string.h"
errorret_t physicsShapeSerialize(
yyjson_mut_doc *doc,
yyjson_mut_val *parentJson,
const physicsshape_t *shape
) {
yyjson_mut_val *shapeJson = yyjson_mut_obj(doc);
yyjson_mut_obj_add_val(doc, parentJson, "shape", shapeJson);
switch(shape->type) {
case PHYSICS_SHAPE_CUBE:
yyjson_mut_obj_add_str(doc, shapeJson, "type", "CUBE");
yyjson_mut_obj_add_real(
doc, shapeJson, "halfExtentX", shape->data.cube.halfExtents[0]
);
yyjson_mut_obj_add_real(
doc, shapeJson, "halfExtentY", shape->data.cube.halfExtents[1]
);
yyjson_mut_obj_add_real(
doc, shapeJson, "halfExtentZ", shape->data.cube.halfExtents[2]
);
break;
case PHYSICS_SHAPE_SPHERE:
yyjson_mut_obj_add_str(doc, shapeJson, "type", "SPHERE");
yyjson_mut_obj_add_real(
doc, shapeJson, "radius", shape->data.sphere.radius
);
break;
case PHYSICS_SHAPE_CAPSULE:
yyjson_mut_obj_add_str(doc, shapeJson, "type", "CAPSULE");
yyjson_mut_obj_add_real(
doc, shapeJson, "radius", shape->data.capsule.radius
);
yyjson_mut_obj_add_real(
doc, shapeJson, "halfHeight", shape->data.capsule.halfHeight
);
break;
case PHYSICS_SHAPE_PLANE:
yyjson_mut_obj_add_str(doc, shapeJson, "type", "PLANE");
yyjson_mut_obj_add_real(
doc, shapeJson, "normalX", shape->data.plane.normal[0]
);
yyjson_mut_obj_add_real(
doc, shapeJson, "normalY", shape->data.plane.normal[1]
);
yyjson_mut_obj_add_real(
doc, shapeJson, "normalZ", shape->data.plane.normal[2]
);
yyjson_mut_obj_add_real(
doc, shapeJson, "distance", shape->data.plane.distance
);
break;
case PHYSICS_SHAPE_CUSTOM:
errorThrow(
"Cannot serialize a PHYSICS_SHAPE_CUSTOM shape (callback/userData "
"are not representable in JSON)"
);
default:
assertUnreachable("Unknown physics shape type");
}
errorOk();
}
errorret_t physicsShapeDeserialize(
yyjson_val *shapeJson,
physicsshape_t *outShape
) {
yyjson_val *typeVal = yyjson_obj_get(shapeJson, "type");
assertNotNull(typeVal, "Physics shape JSON missing 'type' field");
const char_t *typeName = yyjson_get_str(typeVal);
yyjson_val *v;
if(stringEquals(typeName, "CUBE")) {
physicsshape_t s = { .type = PHYSICS_SHAPE_CUBE };
if((v = yyjson_obj_get(shapeJson, "halfExtentX"))) {
s.data.cube.halfExtents[0] = (float_t)yyjson_get_num(v);
}
if((v = yyjson_obj_get(shapeJson, "halfExtentY"))) {
s.data.cube.halfExtents[1] = (float_t)yyjson_get_num(v);
}
if((v = yyjson_obj_get(shapeJson, "halfExtentZ"))) {
s.data.cube.halfExtents[2] = (float_t)yyjson_get_num(v);
}
*outShape = s;
} else if(stringEquals(typeName, "SPHERE")) {
physicsshape_t s = { .type = PHYSICS_SHAPE_SPHERE };
if((v = yyjson_obj_get(shapeJson, "radius"))) {
s.data.sphere.radius = (float_t)yyjson_get_num(v);
}
*outShape = s;
} else if(stringEquals(typeName, "CAPSULE")) {
physicsshape_t s = { .type = PHYSICS_SHAPE_CAPSULE };
if((v = yyjson_obj_get(shapeJson, "radius"))) {
s.data.capsule.radius = (float_t)yyjson_get_num(v);
}
if((v = yyjson_obj_get(shapeJson, "halfHeight"))) {
s.data.capsule.halfHeight = (float_t)yyjson_get_num(v);
}
*outShape = s;
} else if(stringEquals(typeName, "PLANE")) {
physicsshape_t s = { .type = PHYSICS_SHAPE_PLANE };
if((v = yyjson_obj_get(shapeJson, "normalX"))) {
s.data.plane.normal[0] = (float_t)yyjson_get_num(v);
}
if((v = yyjson_obj_get(shapeJson, "normalY"))) {
s.data.plane.normal[1] = (float_t)yyjson_get_num(v);
}
if((v = yyjson_obj_get(shapeJson, "normalZ"))) {
s.data.plane.normal[2] = (float_t)yyjson_get_num(v);
}
if((v = yyjson_obj_get(shapeJson, "distance"))) {
s.data.plane.distance = (float_t)yyjson_get_num(v);
}
*outShape = s;
} else {
errorThrow("Unknown physics shape type '%s'", typeName);
}
errorOk();
}
+34
View File
@@ -7,6 +7,8 @@
#pragma once
#include "dusk.h"
#include "error/error.h"
#include "yyjson.h"
typedef enum {
PHYSICS_SHAPE_CUBE,
@@ -107,3 +109,35 @@ typedef struct physicsshape_t {
physicshapetype_t type;
physicsshapedata_t data;
} physicsshape_t;
/**
* Serializes a shape descriptor into a nested "shape" object on the given
* parent JSON object: {"type": "CUBE"/"SPHERE"/"CAPSULE"/"PLANE", ...that
* shape's own fields}. Fails if shape->type is PHYSICS_SHAPE_CUSTOM --
* custom shapes carry a callback and opaque userData pointer that cannot
* be represented in JSON.
*
* @param doc The mutable JSON document to allocate values from.
* @param parentJson The JSON object to write the "shape" key into.
* @param shape The shape descriptor to serialize.
* @return Error state.
*/
errorret_t physicsShapeSerialize(
yyjson_mut_doc *doc,
yyjson_mut_val *parentJson,
const physicsshape_t *shape
);
/**
* Deserializes a shape descriptor from a "shape" JSON object (see
* physicsShapeSerialize()). Requires a "type" field; any other field left
* absent defaults to zero on the returned shape.
*
* @param shapeJson The "shape" JSON object to read from.
* @param outShape Destination shape descriptor, fully overwritten.
* @return Error state.
*/
errorret_t physicsShapeDeserialize(
yyjson_val *shapeJson,
physicsshape_t *outShape
);
+2
View File
@@ -101,6 +101,7 @@ void physicsWorldStep(
entityid_t j = otherIndices[oj];
if(!positions[j]) continue;
entityphysics_t *otherPhys = physBodies[j];
if(!(phys->collideMask & otherPhys->collideMask)) continue;
vec3 normal; float_t depth;
if(!physicsTestShapeVsShape(
@@ -136,6 +137,7 @@ void physicsWorldStep(
if(!positions[j]) continue;
entityphysics_t *physB = physBodies[j];
float_t *posB = positions[j]->position;
if(!(physA->collideMask & physB->collideMask)) continue;
vec3 normal; float_t depth;
if(!physicsTestShapeVsShape(
+4 -1
View File
@@ -34,7 +34,10 @@ void physicsWorldInit(physicsworld_t *world);
* Steps every PHYSICS-component entity in mgr forward by dt: integrates
* gravity into dynamic bodies, resolves dynamic-vs-static/kinematic and
* dynamic-vs-dynamic overlap, then rebuilds each moved entity's transform.
* A no-op if mgr has no entities with a PHYSICS component.
* A pair is skipped entirely (no overlap test, no resolution) unless
* their collideMask bitmasks share at least one set bit (see
* entityPhysicsSetCollideMask()). A no-op if mgr has no entities with a
* PHYSICS component.
*
* @param world The physics world configuration (gravity) to step with.
* @param mgr The entity manager whose PHYSICS-component entities to step.
+153
View File
@@ -0,0 +1,153 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "triggersystem.h"
#include "assert/assert.h"
#include "entity/entitymanager.h"
#include "entity/component/display/entityposition.h"
#include "entity/component/physics/entityphysics.h"
#include "entity/component/trigger/entitytrigger.h"
#include "entity/component/trigger/entitytriggerevents.h"
#include "physicstest.h"
/**
* Position deltas smaller than this (squared distance) are not considered
* movement, avoiding onMove firing from floating point noise.
*/
#define TRIGGER_SYSTEM_MOVE_EPSILON_SQ 0.000001f
void triggerSystemStep(entitymanager_t *mgr) {
assertNotNull(mgr, "Entity manager cannot be null");
entityid_t trigEnts[ENTITY_COUNT_MAX];
componentid_t trigComps[ENTITY_COUNT_MAX];
entityid_t trigCount = componentGetEntitiesWithComponent(
mgr, COMPONENT_TYPE_TRIGGER, trigEnts, trigComps
);
if(trigCount == 0) return;
entityid_t physEnts[ENTITY_COUNT_MAX];
componentid_t physComps[ENTITY_COUNT_MAX];
entityid_t physCount = componentGetEntitiesWithComponent(
mgr, COMPONENT_TYPE_PHYSICS, physEnts, physComps
);
for(entityid_t ti = 0; ti < trigCount; ti++) {
entityid_t trigEntityId = trigEnts[ti];
componentid_t trigComponentId = trigComps[ti];
componentid_t trigPosComp = entityGetComponent(
mgr, trigEntityId, COMPONENT_TYPE_POSITION
);
if(trigPosComp == COMPONENT_ID_INVALID) continue;
entitytrigger_t *trig = entityTriggerGet(
mgr, trigEntityId, trigComponentId
);
entityposition_t *trigPos = entityPositionGet(
mgr, trigEntityId, trigPosComp
);
entityPositionEnsurePRS(trigPos);
entityid_t currentEntityIds[ENTITY_TRIGGER_OCCUPANTS_MAX];
componentid_t currentComponentIds[ENTITY_TRIGGER_OCCUPANTS_MAX];
vec3 currentPositions[ENTITY_TRIGGER_OCCUPANTS_MAX];
uint8_t currentCount = 0;
for(entityid_t oi = 0; oi < physCount; oi++) {
entityid_t otherEntityId = physEnts[oi];
if(otherEntityId == trigEntityId) continue;
entityphysics_t *otherPhys = entityPhysicsGet(
mgr, otherEntityId, physComps[oi]
);
if(!(trig->collideMask & otherPhys->collideMask)) continue;
componentid_t otherPosComp = entityGetComponent(
mgr, otherEntityId, COMPONENT_TYPE_POSITION
);
if(otherPosComp == COMPONENT_ID_INVALID) continue;
entityposition_t *otherPos = entityPositionGet(
mgr, otherEntityId, otherPosComp
);
entityPositionEnsurePRS(otherPos);
vec3 normal; float_t depth;
if(!physicsTestShapeVsShape(
trigPos->position, &trig->shape,
otherPos->position, &otherPhys->shape,
normal, &depth
)) continue;
assertTrue(
currentCount < ENTITY_TRIGGER_OCCUPANTS_MAX,
"Trigger occupant capacity exceeded"
);
currentEntityIds[currentCount] = otherEntityId;
currentComponentIds[currentCount] = otherPosComp;
glm_vec3_copy(otherPos->position, currentPositions[currentCount]);
currentCount++;
}
for(uint8_t ci = 0; ci < currentCount; ci++) {
bool_t found = false;
uint8_t foundIndex = 0;
for(uint8_t pi = 0; pi < trig->occupantCount; pi++) {
if(trig->occupants[pi].entityId != currentEntityIds[ci]) continue;
found = true;
foundIndex = pi;
break;
}
if(!found) {
entityTriggerFireEnter(
mgr, trigEntityId, trigComponentId,
currentEntityIds[ci], currentComponentIds[ci]
);
continue;
}
entityTriggerFireActive(
mgr, trigEntityId, trigComponentId,
currentEntityIds[ci], currentComponentIds[ci]
);
if(
glm_vec3_distance2(
trig->occupants[foundIndex].lastPosition, currentPositions[ci]
) > TRIGGER_SYSTEM_MOVE_EPSILON_SQ
) {
entityTriggerFireMove(
mgr, trigEntityId, trigComponentId,
currentEntityIds[ci], currentComponentIds[ci]
);
}
}
for(uint8_t pi = 0; pi < trig->occupantCount; pi++) {
bool_t stillIn = false;
for(uint8_t ci = 0; ci < currentCount; ci++) {
if(currentEntityIds[ci] != trig->occupants[pi].entityId) continue;
stillIn = true;
break;
}
if(stillIn) continue;
entityTriggerFireLeave(
mgr, trigEntityId, trigComponentId,
trig->occupants[pi].entityId, trig->occupants[pi].componentId
);
}
trig->occupantCount = currentCount;
for(uint8_t ci = 0; ci < currentCount; ci++) {
trig->occupants[ci].entityId = currentEntityIds[ci];
trig->occupants[ci].componentId = currentComponentIds[ci];
glm_vec3_copy(currentPositions[ci], trig->occupants[ci].lastPosition);
}
}
}
+24
View File
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entity/entitybase.h"
/**
* Steps every TRIGGER-component entity in mgr: tests each PHYSICS-component
* entity's shape against the trigger's shape (via physicsTestShapeVsShape,
* no push-out or velocity resolution is applied), and fires
* onEnter/onActive/onMove/onLeave based on the change in overlap state
* since the previous call. A PHYSICS entity is only tested against a
* trigger if their collideMask bitmasks share at least one set bit (see
* entityTriggerSetCollideMask() and entityPhysicsSetCollideMask()).
* Entities missing a POSITION component (trigger or candidate) are
* skipped. A no-op if mgr has no TRIGGER-component entities.
*
* @param mgr The entity manager whose TRIGGER-component entities to step.
*/
void triggerSystemStep(entitymanager_t *mgr);
+2
View File
@@ -14,6 +14,7 @@
#include "entity/component/display/entitycamera.h"
#include "entity/component/display/entityposition.h"
#include "entity/component/display/entityrenderable.h"
#include "physics/triggersystem.h"
#include "ui/ui.h"
#include "console/console.h"
@@ -87,6 +88,7 @@ errorret_t sceneFixedUpdate(void) {
scene_t *scene = &SCENE_MANAGER.scenes[SCENE_MANAGER.active];
entityManagerUpdate(&scene->entities);
physicsWorldStep(&scene->physics, &scene->entities, TIME.delta);
triggerSystemStep(&scene->entities);
errorOk();
}
+4 -4
View File
@@ -99,10 +99,10 @@ errorret_t sceneUpdate(void);
/**
* Ticks the active scene's entities (update callbacks, then a physics
* step) by one fixed timestep (TIME.delta). Called by sceneUpdate() at
* the fixed-timestep cadence described there -- never once per rendered
* frame on a dynamic-time platform, so game logic and physics stay
* frame-rate independent and deterministic.
* step, then a trigger step) by one fixed timestep (TIME.delta). Called
* by sceneUpdate() at the fixed-timestep cadence described there -- never
* once per rendered frame on a dynamic-time platform, so game logic and
* physics stay frame-rate independent and deterministic.
*
* @return An error if the update failed, or errorOk() if it succeeded.
*/
+24
View File
@@ -34,6 +34,19 @@ errorret_t gameInit(void) {
assetUnlockEntry(sceneEntry);
errorChain(ret);
// Test-only: print a debug message whenever an entity enters "testArea"
// (see assets/scenes/test.json), exercising the TRIGGER component.
entitymanager_t *mgr = sceneGetEntities(testSceneId);
entityid_t testAreaEntity = entityFindByName(mgr, "testArea");
if(testAreaEntity != ENTITY_ID_INVALID) {
componentid_t testAreaTrig = entityGetComponent(
mgr, testAreaEntity, COMPONENT_TYPE_TRIGGER
);
entityTriggerOnEnterAdd(
mgr, testAreaEntity, testAreaTrig, gameTestAreaOnEnter, NULL
);
}
sceneSetActive(testSceneId);
errorOk();
}
@@ -45,3 +58,14 @@ errorret_t gameUpdate(void) {
errorret_t gameDispose(void) {
errorOk();
}
void gameTestAreaOnEnter(
entitymanager_t *mgr,
const entityid_t triggerEntityId,
const componentid_t triggerComponentId,
const entityid_t otherEntityId,
const componentid_t otherComponentId,
void *user
) {
logDebug("testArea: entity %d entered", otherEntityId);
}