Add area and flag on physics
This commit is contained in:
+15
-1
@@ -34,7 +34,7 @@
|
||||
"displayState": { "cull": false, "depthTest": true, "blend": false }
|
||||
},
|
||||
{ "type": "PLAYER", "moveSpeed": 4, "jumpImpulse": 6 },
|
||||
{ "type": "PHYSICS", "bodyType": "DYNAMIC" }
|
||||
{ "type": "PHYSICS", "bodyType": "DYNAMIC", "collideMask": 3 }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -55,6 +55,20 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "testArea",
|
||||
"components": [
|
||||
{ "type": "POSITION", "x": 2, "y": 1, "z": 0 },
|
||||
{
|
||||
"type": "TRIGGER",
|
||||
"collideMask": 2,
|
||||
"shape": {
|
||||
"type": "CUBE",
|
||||
"halfExtentX": 1.5, "halfExtentY": 1.5, "halfExtentZ": 1.5
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"components": [
|
||||
{
|
||||
|
||||
@@ -6,3 +6,4 @@
|
||||
# Subdirs
|
||||
add_subdirectory(display)
|
||||
add_subdirectory(physics)
|
||||
add_subdirectory(trigger)
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -8,5 +8,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
physicsworld.c
|
||||
physicstest.c
|
||||
physicsshape.c
|
||||
physicsshapemesh.c
|
||||
triggersystem.c
|
||||
)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -8,3 +8,4 @@ include(dusktest)
|
||||
# Tests
|
||||
dusktest(test_entitymanager.c)
|
||||
dusktest(test_entityposition.c)
|
||||
dusktest(test_entitytrigger.c)
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* 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/trigger/entitytrigger.h"
|
||||
#include "entity/component/trigger/entitytriggerevents.h"
|
||||
|
||||
static uint32_t CALLBACK_ENTER_COUNT;
|
||||
static uint32_t CALLBACK_ACTIVE_COUNT;
|
||||
static uint32_t CALLBACK_MOVE_COUNT;
|
||||
static uint32_t CALLBACK_LEAVE_COUNT;
|
||||
static entityid_t CALLBACK_LAST_OTHER;
|
||||
static void *CALLBACK_LAST_USER;
|
||||
|
||||
static void test_resetCallbackCounts(void) {
|
||||
CALLBACK_ENTER_COUNT = 0;
|
||||
CALLBACK_ACTIVE_COUNT = 0;
|
||||
CALLBACK_MOVE_COUNT = 0;
|
||||
CALLBACK_LEAVE_COUNT = 0;
|
||||
CALLBACK_LAST_OTHER = ENTITY_ID_INVALID;
|
||||
CALLBACK_LAST_USER = NULL;
|
||||
}
|
||||
|
||||
static void test_onEnter(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t triggerEntityId,
|
||||
const componentid_t triggerComponentId,
|
||||
const entityid_t otherEntityId,
|
||||
const componentid_t otherComponentId,
|
||||
void *user
|
||||
) {
|
||||
CALLBACK_ENTER_COUNT++;
|
||||
CALLBACK_LAST_OTHER = otherEntityId;
|
||||
CALLBACK_LAST_USER = user;
|
||||
}
|
||||
|
||||
static void test_onActive(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t triggerEntityId,
|
||||
const componentid_t triggerComponentId,
|
||||
const entityid_t otherEntityId,
|
||||
const componentid_t otherComponentId,
|
||||
void *user
|
||||
) {
|
||||
CALLBACK_ACTIVE_COUNT++;
|
||||
}
|
||||
|
||||
static void test_onMove(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t triggerEntityId,
|
||||
const componentid_t triggerComponentId,
|
||||
const entityid_t otherEntityId,
|
||||
const componentid_t otherComponentId,
|
||||
void *user
|
||||
) {
|
||||
CALLBACK_MOVE_COUNT++;
|
||||
}
|
||||
|
||||
static void test_onLeave(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t triggerEntityId,
|
||||
const componentid_t triggerComponentId,
|
||||
const entityid_t otherEntityId,
|
||||
const componentid_t otherComponentId,
|
||||
void *user
|
||||
) {
|
||||
CALLBACK_LEAVE_COUNT++;
|
||||
}
|
||||
|
||||
static void test_entityTriggerDefaultsAndAccessors(void **state) {
|
||||
entitymanager_t mgr;
|
||||
entityManagerInit(&mgr);
|
||||
|
||||
entityid_t entity = entityManagerAdd(&mgr);
|
||||
componentid_t trig = entityAddComponent(
|
||||
&mgr, entity, COMPONENT_TYPE_TRIGGER
|
||||
);
|
||||
|
||||
// Defaults: 1x1x1 cube, only bit 0 of the collide mask set, no occupants.
|
||||
physicsshape_t shape = entityTriggerGetShape(&mgr, entity, trig);
|
||||
assert_int_equal(shape.type, PHYSICS_SHAPE_CUBE);
|
||||
assert_float_equal(shape.data.cube.halfExtents[0], 0.5f, 0.0001f);
|
||||
assert_int_equal(entityTriggerGetCollideMask(&mgr, entity, trig), 0x1);
|
||||
assert_int_equal(entityTriggerGetOccupantCount(&mgr, entity, trig), 0);
|
||||
assert_int_equal(
|
||||
entityTriggerGetOccupantEntityId(&mgr, entity, trig, 0),
|
||||
ENTITY_ID_INVALID
|
||||
);
|
||||
assert_false(entityTriggerIsOccupyingEntity(&mgr, entity, trig, 5));
|
||||
|
||||
physicsshape_t sphere = {
|
||||
.type = PHYSICS_SHAPE_SPHERE,
|
||||
.data.sphere.radius = 2.0f
|
||||
};
|
||||
entityTriggerSetShape(&mgr, entity, trig, sphere);
|
||||
physicsshape_t got = entityTriggerGetShape(&mgr, entity, trig);
|
||||
assert_int_equal(got.type, PHYSICS_SHAPE_SPHERE);
|
||||
assert_float_equal(got.data.sphere.radius, 2.0f, 0.0001f);
|
||||
|
||||
entityTriggerSetCollideMask(&mgr, entity, trig, 0x2);
|
||||
assert_int_equal(entityTriggerGetCollideMask(&mgr, entity, trig), 0x2);
|
||||
|
||||
entityManagerDispose(&mgr);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_entityTriggerSerializeRoundTrip(void **state) {
|
||||
entitymanager_t mgr;
|
||||
entityManagerInit(&mgr);
|
||||
|
||||
entityid_t entity = entityManagerAdd(&mgr);
|
||||
componentid_t trig = entityAddComponent(
|
||||
&mgr, entity, COMPONENT_TYPE_TRIGGER
|
||||
);
|
||||
|
||||
physicsshape_t sphere = {
|
||||
.type = PHYSICS_SHAPE_SPHERE,
|
||||
.data.sphere.radius = 3.0f
|
||||
};
|
||||
entityTriggerSetShape(&mgr, entity, trig, sphere);
|
||||
entityTriggerSetCollideMask(&mgr, entity, trig, 0x4);
|
||||
|
||||
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
|
||||
yyjson_mut_val *root = yyjson_mut_obj(doc);
|
||||
yyjson_mut_doc_set_root(doc, root);
|
||||
assert_true(
|
||||
errorIsOk(entityTriggerSerialize(&mgr, entity, trig, doc, root))
|
||||
);
|
||||
|
||||
size_t len = 0;
|
||||
char_t *jsonStr = yyjson_mut_write(doc, 0, &len);
|
||||
assert_non_null(jsonStr);
|
||||
yyjson_mut_doc_free(doc);
|
||||
|
||||
yyjson_doc *readDoc = yyjson_read(jsonStr, len, 0);
|
||||
free(jsonStr);
|
||||
assert_non_null(readDoc);
|
||||
|
||||
entityid_t entity2 = entityManagerAdd(&mgr);
|
||||
componentid_t trig2 = entityAddComponent(
|
||||
&mgr, entity2, COMPONENT_TYPE_TRIGGER
|
||||
);
|
||||
assert_true(errorIsOk(entityTriggerDeserialize(
|
||||
&mgr, entity2, trig2, yyjson_doc_get_root(readDoc)
|
||||
)));
|
||||
yyjson_doc_free(readDoc);
|
||||
|
||||
physicsshape_t got = entityTriggerGetShape(&mgr, entity2, trig2);
|
||||
assert_int_equal(got.type, PHYSICS_SHAPE_SPHERE);
|
||||
assert_float_equal(got.data.sphere.radius, 3.0f, 0.0001f);
|
||||
assert_int_equal(entityTriggerGetCollideMask(&mgr, entity2, trig2), 0x4);
|
||||
|
||||
entityManagerDispose(&mgr);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_entityTriggerEventSubscription(void **state) {
|
||||
test_resetCallbackCounts();
|
||||
|
||||
entitymanager_t mgr;
|
||||
entityManagerInit(&mgr);
|
||||
|
||||
entityid_t entity = entityManagerAdd(&mgr);
|
||||
componentid_t trig = entityAddComponent(
|
||||
&mgr, entity, COMPONENT_TYPE_TRIGGER
|
||||
);
|
||||
|
||||
int32_t userValue = 42;
|
||||
entityTriggerOnEnterAdd(&mgr, entity, trig, test_onEnter, &userValue);
|
||||
entityTriggerOnActiveAdd(&mgr, entity, trig, test_onActive, NULL);
|
||||
entityTriggerOnMoveAdd(&mgr, entity, trig, test_onMove, NULL);
|
||||
entityTriggerOnLeaveAdd(&mgr, entity, trig, test_onLeave, NULL);
|
||||
|
||||
entityTriggerFireEnter(&mgr, entity, trig, 7, 0);
|
||||
assert_int_equal(CALLBACK_ENTER_COUNT, 1);
|
||||
assert_int_equal(CALLBACK_LAST_OTHER, 7);
|
||||
assert_ptr_equal(CALLBACK_LAST_USER, &userValue);
|
||||
|
||||
entityTriggerFireActive(&mgr, entity, trig, 7, 0);
|
||||
assert_int_equal(CALLBACK_ACTIVE_COUNT, 1);
|
||||
|
||||
entityTriggerFireMove(&mgr, entity, trig, 7, 0);
|
||||
assert_int_equal(CALLBACK_MOVE_COUNT, 1);
|
||||
|
||||
entityTriggerFireLeave(&mgr, entity, trig, 7, 0);
|
||||
assert_int_equal(CALLBACK_LEAVE_COUNT, 1);
|
||||
|
||||
// After removing, the callback no longer fires.
|
||||
entityTriggerOnEnterRemove(&mgr, entity, trig, test_onEnter);
|
||||
entityTriggerFireEnter(&mgr, entity, trig, 8, 0);
|
||||
assert_int_equal(CALLBACK_ENTER_COUNT, 1);
|
||||
|
||||
entityManagerDispose(&mgr);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_entityTriggerDefaultsAndAccessors),
|
||||
cmocka_unit_test(test_entityTriggerSerializeRoundTrip),
|
||||
cmocka_unit_test(test_entityTriggerEventSubscription),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -9,3 +9,5 @@ include(dusktest)
|
||||
dusktest(test_physicstest.c)
|
||||
dusktest(test_physicsworld.c)
|
||||
dusktest(test_physicsshapemesh.c)
|
||||
dusktest(test_physicsshape.c)
|
||||
dusktest(test_triggersystem.c)
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 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 "physics/physicsshape.h"
|
||||
|
||||
static yyjson_val *test_roundTripToImmutable(
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *root,
|
||||
yyjson_doc **outReadDoc
|
||||
) {
|
||||
size_t len = 0;
|
||||
char_t *jsonStr = yyjson_mut_write(doc, 0, &len);
|
||||
assert_non_null(jsonStr);
|
||||
yyjson_mut_doc_free(doc);
|
||||
|
||||
*outReadDoc = yyjson_read(jsonStr, len, 0);
|
||||
free(jsonStr);
|
||||
assert_non_null(*outReadDoc);
|
||||
return yyjson_doc_get_root(*outReadDoc);
|
||||
}
|
||||
|
||||
static void test_physicsShapeCubeRoundTrip(void **state) {
|
||||
physicsshape_t shape = {
|
||||
.type = PHYSICS_SHAPE_CUBE,
|
||||
.data.cube.halfExtents = { 1.0f, 2.0f, 3.0f }
|
||||
};
|
||||
|
||||
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
|
||||
yyjson_mut_val *root = yyjson_mut_obj(doc);
|
||||
yyjson_mut_doc_set_root(doc, root);
|
||||
assert_true(errorIsOk(physicsShapeSerialize(doc, root, &shape)));
|
||||
|
||||
yyjson_doc *readDoc;
|
||||
yyjson_val *root2 = test_roundTripToImmutable(doc, root, &readDoc);
|
||||
|
||||
physicsshape_t got;
|
||||
assert_true(errorIsOk(
|
||||
physicsShapeDeserialize(yyjson_obj_get(root2, "shape"), &got)
|
||||
));
|
||||
yyjson_doc_free(readDoc);
|
||||
|
||||
assert_int_equal(got.type, PHYSICS_SHAPE_CUBE);
|
||||
assert_float_equal(got.data.cube.halfExtents[0], 1.0f, 0.0001f);
|
||||
assert_float_equal(got.data.cube.halfExtents[1], 2.0f, 0.0001f);
|
||||
assert_float_equal(got.data.cube.halfExtents[2], 3.0f, 0.0001f);
|
||||
}
|
||||
|
||||
static void test_physicsShapeSphereRoundTrip(void **state) {
|
||||
physicsshape_t shape = {
|
||||
.type = PHYSICS_SHAPE_SPHERE,
|
||||
.data.sphere.radius = 4.5f
|
||||
};
|
||||
|
||||
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
|
||||
yyjson_mut_val *root = yyjson_mut_obj(doc);
|
||||
yyjson_mut_doc_set_root(doc, root);
|
||||
assert_true(errorIsOk(physicsShapeSerialize(doc, root, &shape)));
|
||||
|
||||
yyjson_doc *readDoc;
|
||||
yyjson_val *root2 = test_roundTripToImmutable(doc, root, &readDoc);
|
||||
|
||||
physicsshape_t got;
|
||||
assert_true(errorIsOk(
|
||||
physicsShapeDeserialize(yyjson_obj_get(root2, "shape"), &got)
|
||||
));
|
||||
yyjson_doc_free(readDoc);
|
||||
|
||||
assert_int_equal(got.type, PHYSICS_SHAPE_SPHERE);
|
||||
assert_float_equal(got.data.sphere.radius, 4.5f, 0.0001f);
|
||||
}
|
||||
|
||||
static void test_physicsShapeCapsuleRoundTrip(void **state) {
|
||||
physicsshape_t shape = {
|
||||
.type = PHYSICS_SHAPE_CAPSULE,
|
||||
.data.capsule = { .radius = 0.5f, .halfHeight = 1.5f }
|
||||
};
|
||||
|
||||
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
|
||||
yyjson_mut_val *root = yyjson_mut_obj(doc);
|
||||
yyjson_mut_doc_set_root(doc, root);
|
||||
assert_true(errorIsOk(physicsShapeSerialize(doc, root, &shape)));
|
||||
|
||||
yyjson_doc *readDoc;
|
||||
yyjson_val *root2 = test_roundTripToImmutable(doc, root, &readDoc);
|
||||
|
||||
physicsshape_t got;
|
||||
assert_true(errorIsOk(
|
||||
physicsShapeDeserialize(yyjson_obj_get(root2, "shape"), &got)
|
||||
));
|
||||
yyjson_doc_free(readDoc);
|
||||
|
||||
assert_int_equal(got.type, PHYSICS_SHAPE_CAPSULE);
|
||||
assert_float_equal(got.data.capsule.radius, 0.5f, 0.0001f);
|
||||
assert_float_equal(got.data.capsule.halfHeight, 1.5f, 0.0001f);
|
||||
}
|
||||
|
||||
static void test_physicsShapePlaneRoundTrip(void **state) {
|
||||
physicsshape_t shape = {
|
||||
.type = PHYSICS_SHAPE_PLANE,
|
||||
.data.plane = { .normal = { 0.0f, 1.0f, 0.0f }, .distance = 2.0f }
|
||||
};
|
||||
|
||||
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
|
||||
yyjson_mut_val *root = yyjson_mut_obj(doc);
|
||||
yyjson_mut_doc_set_root(doc, root);
|
||||
assert_true(errorIsOk(physicsShapeSerialize(doc, root, &shape)));
|
||||
|
||||
yyjson_doc *readDoc;
|
||||
yyjson_val *root2 = test_roundTripToImmutable(doc, root, &readDoc);
|
||||
|
||||
physicsshape_t got;
|
||||
assert_true(errorIsOk(
|
||||
physicsShapeDeserialize(yyjson_obj_get(root2, "shape"), &got)
|
||||
));
|
||||
yyjson_doc_free(readDoc);
|
||||
|
||||
assert_int_equal(got.type, PHYSICS_SHAPE_PLANE);
|
||||
assert_float_equal(got.data.plane.normal[1], 1.0f, 0.0001f);
|
||||
assert_float_equal(got.data.plane.distance, 2.0f, 0.0001f);
|
||||
}
|
||||
|
||||
static void test_physicsShapeDeserializeUnknownTypeFails(void **state) {
|
||||
const char_t *json = "{\"shape\":{\"type\":\"NOT_A_SHAPE\"}}";
|
||||
yyjson_doc *readDoc = yyjson_read(json, strlen(json), YYJSON_READ_NOFLAG);
|
||||
assert_non_null(readDoc);
|
||||
|
||||
physicsshape_t got;
|
||||
errorret_t ret = physicsShapeDeserialize(
|
||||
yyjson_obj_get(yyjson_doc_get_root(readDoc), "shape"), &got
|
||||
);
|
||||
assert_true(errorIsNotOk(ret));
|
||||
errorCatch(ret);
|
||||
|
||||
yyjson_doc_free(readDoc);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_physicsShapeCubeRoundTrip),
|
||||
cmocka_unit_test(test_physicsShapeSphereRoundTrip),
|
||||
cmocka_unit_test(test_physicsShapeCapsuleRoundTrip),
|
||||
cmocka_unit_test(test_physicsShapePlaneRoundTrip),
|
||||
cmocka_unit_test(test_physicsShapeDeserializeUnknownTypeFails),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -191,6 +191,49 @@ static void test_physicsWorldDynamicVsDynamicSeparates(void **state) {
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_physicsWorldCollideMaskPreventsCollision(void **state) {
|
||||
entitymanager_t mgr;
|
||||
entityManagerInit(&mgr);
|
||||
physicsworld_t world;
|
||||
physicsWorldInit(&world);
|
||||
|
||||
entityid_t entityA = entityManagerAdd(&mgr);
|
||||
componentid_t posA = entityAddComponent(
|
||||
&mgr, entityA, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
componentid_t physA = entityAddComponent(
|
||||
&mgr, entityA, COMPONENT_TYPE_PHYSICS
|
||||
);
|
||||
vec3 posAStart = { 0.0f, 0.0f, 0.3f };
|
||||
entityPositionSetLocalPosition(&mgr, entityA, posA, posAStart);
|
||||
entityPhysicsGet(&mgr, entityA, physA)->gravityScale = 0.0f;
|
||||
entityPhysicsSetCollideMask(&mgr, entityA, physA, 0x1);
|
||||
|
||||
entityid_t entityB = entityManagerAdd(&mgr);
|
||||
componentid_t posB = entityAddComponent(
|
||||
&mgr, entityB, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
componentid_t physB = entityAddComponent(
|
||||
&mgr, entityB, COMPONENT_TYPE_PHYSICS
|
||||
);
|
||||
vec3 posBStart = { 0.0f, 0.0f, -0.3f };
|
||||
entityPositionSetLocalPosition(&mgr, entityB, posB, posBStart);
|
||||
entityPhysicsGet(&mgr, entityB, physB)->gravityScale = 0.0f;
|
||||
entityPhysicsSetCollideMask(&mgr, entityB, physB, 0x2);
|
||||
|
||||
// Overlapping by 0.4 on Z, but masks share no bits -- no separation.
|
||||
physicsWorldStep(&world, &mgr, 1.0f / 60.0f);
|
||||
|
||||
vec3 finalA, finalB;
|
||||
entityPositionGetWorldPosition(&mgr, entityA, posA, finalA);
|
||||
entityPositionGetWorldPosition(&mgr, entityB, posB, finalB);
|
||||
assert_float_equal(finalA[2], posAStart[2], 0.0001f);
|
||||
assert_float_equal(finalB[2], posBStart[2], 0.0001f);
|
||||
|
||||
entityManagerDispose(&mgr);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_physicsWorldStepNoPhysicsEntitiesIsNoop(void **state) {
|
||||
entitymanager_t mgr;
|
||||
entityManagerInit(&mgr);
|
||||
@@ -210,6 +253,7 @@ int main(int argc, char **argv) {
|
||||
cmocka_unit_test(test_physicsWorldGravityIntegratesDynamicBody),
|
||||
cmocka_unit_test(test_physicsWorldRestsOnStaticFloor),
|
||||
cmocka_unit_test(test_physicsWorldDynamicVsDynamicSeparates),
|
||||
cmocka_unit_test(test_physicsWorldCollideMaskPreventsCollision),
|
||||
cmocka_unit_test(test_physicsWorldStepNoPhysicsEntitiesIsNoop),
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* 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"
|
||||
#include "entity/component/physics/entityphysics.h"
|
||||
#include "entity/component/trigger/entitytrigger.h"
|
||||
#include "entity/component/trigger/entitytriggerevents.h"
|
||||
#include "physics/triggersystem.h"
|
||||
|
||||
static uint32_t TS_ENTER_COUNT;
|
||||
static uint32_t TS_ACTIVE_COUNT;
|
||||
static uint32_t TS_MOVE_COUNT;
|
||||
static uint32_t TS_LEAVE_COUNT;
|
||||
|
||||
static void test_resetCounts(void) {
|
||||
TS_ENTER_COUNT = 0;
|
||||
TS_ACTIVE_COUNT = 0;
|
||||
TS_MOVE_COUNT = 0;
|
||||
TS_LEAVE_COUNT = 0;
|
||||
}
|
||||
|
||||
static void ts_onEnter(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t triggerEntityId,
|
||||
const componentid_t triggerComponentId,
|
||||
const entityid_t otherEntityId,
|
||||
const componentid_t otherComponentId,
|
||||
void *user
|
||||
) {
|
||||
TS_ENTER_COUNT++;
|
||||
}
|
||||
|
||||
static void ts_onActive(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t triggerEntityId,
|
||||
const componentid_t triggerComponentId,
|
||||
const entityid_t otherEntityId,
|
||||
const componentid_t otherComponentId,
|
||||
void *user
|
||||
) {
|
||||
TS_ACTIVE_COUNT++;
|
||||
}
|
||||
|
||||
static void ts_onMove(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t triggerEntityId,
|
||||
const componentid_t triggerComponentId,
|
||||
const entityid_t otherEntityId,
|
||||
const componentid_t otherComponentId,
|
||||
void *user
|
||||
) {
|
||||
TS_MOVE_COUNT++;
|
||||
}
|
||||
|
||||
static void ts_onLeave(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t triggerEntityId,
|
||||
const componentid_t triggerComponentId,
|
||||
const entityid_t otherEntityId,
|
||||
const componentid_t otherComponentId,
|
||||
void *user
|
||||
) {
|
||||
TS_LEAVE_COUNT++;
|
||||
}
|
||||
|
||||
static void test_triggerSystemLifecycle(void **state) {
|
||||
test_resetCounts();
|
||||
|
||||
entitymanager_t mgr;
|
||||
entityManagerInit(&mgr);
|
||||
|
||||
entityid_t trigEntity = entityManagerAdd(&mgr);
|
||||
entityAddComponent(&mgr, trigEntity, COMPONENT_TYPE_POSITION);
|
||||
componentid_t trig = entityAddComponent(
|
||||
&mgr, trigEntity, COMPONENT_TYPE_TRIGGER
|
||||
);
|
||||
entityTriggerOnEnterAdd(&mgr, trigEntity, trig, ts_onEnter, NULL);
|
||||
entityTriggerOnActiveAdd(&mgr, trigEntity, trig, ts_onActive, NULL);
|
||||
entityTriggerOnMoveAdd(&mgr, trigEntity, trig, ts_onMove, NULL);
|
||||
entityTriggerOnLeaveAdd(&mgr, trigEntity, trig, ts_onLeave, NULL);
|
||||
|
||||
entityid_t otherEntity = entityManagerAdd(&mgr);
|
||||
componentid_t otherPos = entityAddComponent(
|
||||
&mgr, otherEntity, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
entityAddComponent(&mgr, otherEntity, COMPONENT_TYPE_PHYSICS);
|
||||
|
||||
// Starts far away: no overlap, no events.
|
||||
vec3 farPos = { 10.0f, 0.0f, 0.0f };
|
||||
entityPositionSetLocalPosition(&mgr, otherEntity, otherPos, farPos);
|
||||
triggerSystemStep(&mgr);
|
||||
assert_int_equal(TS_ENTER_COUNT, 0);
|
||||
assert_int_equal(entityTriggerGetOccupantCount(&mgr, trigEntity, trig), 0);
|
||||
|
||||
// Moves fully inside: onEnter fires once, not onActive.
|
||||
vec3 insidePos = { 0.0f, 0.0f, 0.0f };
|
||||
entityPositionSetLocalPosition(&mgr, otherEntity, otherPos, insidePos);
|
||||
triggerSystemStep(&mgr);
|
||||
assert_int_equal(TS_ENTER_COUNT, 1);
|
||||
assert_int_equal(TS_ACTIVE_COUNT, 0);
|
||||
assert_int_equal(entityTriggerGetOccupantCount(&mgr, trigEntity, trig), 1);
|
||||
assert_true(
|
||||
entityTriggerIsOccupyingEntity(&mgr, trigEntity, trig, otherEntity)
|
||||
);
|
||||
|
||||
// Stays put: onActive fires, no onMove (position unchanged).
|
||||
triggerSystemStep(&mgr);
|
||||
assert_int_equal(TS_ENTER_COUNT, 1);
|
||||
assert_int_equal(TS_ACTIVE_COUNT, 1);
|
||||
assert_int_equal(TS_MOVE_COUNT, 0);
|
||||
|
||||
// Shifts slightly but still overlapping: onActive + onMove both fire.
|
||||
vec3 shiftedPos = { 0.1f, 0.0f, 0.0f };
|
||||
entityPositionSetLocalPosition(&mgr, otherEntity, otherPos, shiftedPos);
|
||||
triggerSystemStep(&mgr);
|
||||
assert_int_equal(TS_ACTIVE_COUNT, 2);
|
||||
assert_int_equal(TS_MOVE_COUNT, 1);
|
||||
|
||||
// Leaves: onLeave fires, occupant list empties.
|
||||
entityPositionSetLocalPosition(&mgr, otherEntity, otherPos, farPos);
|
||||
triggerSystemStep(&mgr);
|
||||
assert_int_equal(TS_LEAVE_COUNT, 1);
|
||||
assert_int_equal(entityTriggerGetOccupantCount(&mgr, trigEntity, trig), 0);
|
||||
assert_false(
|
||||
entityTriggerIsOccupyingEntity(&mgr, trigEntity, trig, otherEntity)
|
||||
);
|
||||
|
||||
entityManagerDispose(&mgr);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_triggerSystemCollideMaskFilter(void **state) {
|
||||
test_resetCounts();
|
||||
|
||||
entitymanager_t mgr;
|
||||
entityManagerInit(&mgr);
|
||||
|
||||
entityid_t trigEntity = entityManagerAdd(&mgr);
|
||||
entityAddComponent(&mgr, trigEntity, COMPONENT_TYPE_POSITION);
|
||||
componentid_t trig = entityAddComponent(
|
||||
&mgr, trigEntity, COMPONENT_TYPE_TRIGGER
|
||||
);
|
||||
entityTriggerSetCollideMask(&mgr, trigEntity, trig, 0x1);
|
||||
entityTriggerOnEnterAdd(&mgr, trigEntity, trig, ts_onEnter, NULL);
|
||||
|
||||
entityid_t otherEntity = entityManagerAdd(&mgr);
|
||||
entityAddComponent(&mgr, otherEntity, COMPONENT_TYPE_POSITION);
|
||||
componentid_t otherPhys = entityAddComponent(
|
||||
&mgr, otherEntity, COMPONENT_TYPE_PHYSICS
|
||||
);
|
||||
entityPhysicsSetCollideMask(&mgr, otherEntity, otherPhys, 0x2);
|
||||
|
||||
// Fully overlapping (both default to the origin), but masks share no
|
||||
// bits -- must not be detected.
|
||||
triggerSystemStep(&mgr);
|
||||
assert_int_equal(TS_ENTER_COUNT, 0);
|
||||
assert_int_equal(entityTriggerGetOccupantCount(&mgr, trigEntity, trig), 0);
|
||||
|
||||
// Sharing bit 0x1 now -- detected.
|
||||
entityPhysicsSetCollideMask(&mgr, otherEntity, otherPhys, 0x3);
|
||||
triggerSystemStep(&mgr);
|
||||
assert_int_equal(TS_ENTER_COUNT, 1);
|
||||
assert_int_equal(entityTriggerGetOccupantCount(&mgr, trigEntity, trig), 1);
|
||||
|
||||
entityManagerDispose(&mgr);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_triggerSystemStepNoTriggersIsNoop(void **state) {
|
||||
entitymanager_t mgr;
|
||||
entityManagerInit(&mgr);
|
||||
|
||||
entityManagerAdd(&mgr);
|
||||
triggerSystemStep(&mgr);
|
||||
|
||||
entityManagerDispose(&mgr);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_triggerSystemLifecycle),
|
||||
cmocka_unit_test(test_triggerSystemCollideMaskFilter),
|
||||
cmocka_unit_test(test_triggerSystemStepNoTriggersIsNoop),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
Reference in New Issue
Block a user