Add area and flag on physics
This commit is contained in:
+15
-1
@@ -34,7 +34,7 @@
|
|||||||
"displayState": { "cull": false, "depthTest": true, "blend": false }
|
"displayState": { "cull": false, "depthTest": true, "blend": false }
|
||||||
},
|
},
|
||||||
{ "type": "PLAYER", "moveSpeed": 4, "jumpImpulse": 6 },
|
{ "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": [
|
"components": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,3 +6,4 @@
|
|||||||
# Subdirs
|
# Subdirs
|
||||||
add_subdirectory(display)
|
add_subdirectory(display)
|
||||||
add_subdirectory(physics)
|
add_subdirectory(physics)
|
||||||
|
add_subdirectory(trigger)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ void entityPhysicsInit(
|
|||||||
phys->shape.data.cube.halfExtents[2] = 0.5f;
|
phys->shape.data.cube.halfExtents[2] = 0.5f;
|
||||||
phys->gravityScale = 1.0f;
|
phys->gravityScale = 1.0f;
|
||||||
phys->onGround = false;
|
phys->onGround = false;
|
||||||
|
phys->collideMask = 0x1;
|
||||||
}
|
}
|
||||||
|
|
||||||
entityphysics_t *entityPhysicsGet(
|
entityphysics_t *entityPhysicsGet(
|
||||||
@@ -116,6 +117,25 @@ physicsbodytype_t entityPhysicsGetBodyType(
|
|||||||
return phys->type;
|
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(
|
errorret_t entityPhysicsSerialize(
|
||||||
entitymanager_t *mgr,
|
entitymanager_t *mgr,
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
@@ -141,66 +161,14 @@ errorret_t entityPhysicsSerialize(
|
|||||||
}
|
}
|
||||||
yyjson_mut_obj_add_str(doc, json, "bodyType", bodyTypeName);
|
yyjson_mut_obj_add_str(doc, json, "bodyType", bodyTypeName);
|
||||||
|
|
||||||
yyjson_mut_val *shape = yyjson_mut_obj(doc);
|
errorChain(physicsShapeSerialize(doc, json, &phys->shape));
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
yyjson_mut_obj_add_real(doc, json, "velocityX", phys->velocity[0]);
|
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, "velocityY", phys->velocity[1]);
|
||||||
yyjson_mut_obj_add_real(doc, json, "velocityZ", phys->velocity[2]);
|
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_real(doc, json, "gravityScale", phys->gravityScale);
|
||||||
yyjson_mut_obj_add_bool(doc, json, "onGround", phys->onGround);
|
yyjson_mut_obj_add_bool(doc, json, "onGround", phys->onGround);
|
||||||
|
yyjson_mut_obj_add_uint(doc, json, "collideMask", phys->collideMask);
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -229,56 +197,9 @@ errorret_t entityPhysicsDeserialize(
|
|||||||
|
|
||||||
yyjson_val *shape = yyjson_obj_get(json, "shape");
|
yyjson_val *shape = yyjson_obj_get(json, "shape");
|
||||||
if(shape) {
|
if(shape) {
|
||||||
yyjson_val *shapeTypeVal = yyjson_obj_get(shape, "type");
|
physicsshape_t s;
|
||||||
assertNotNull(shapeTypeVal, "Physics shape JSON missing 'type' field");
|
errorChain(physicsShapeDeserialize(shape, &s));
|
||||||
const char_t *shapeTypeName = yyjson_get_str(shapeTypeVal);
|
entityPhysicsSetShape(mgr, entityId, componentId, s);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
yyjson_val *v;
|
yyjson_val *v;
|
||||||
@@ -307,6 +228,9 @@ errorret_t entityPhysicsDeserialize(
|
|||||||
if((v = yyjson_obj_get(json, "onGround"))) {
|
if((v = yyjson_obj_get(json, "onGround"))) {
|
||||||
phys->onGround = yyjson_get_bool(v);
|
phys->onGround = yyjson_get_bool(v);
|
||||||
}
|
}
|
||||||
|
if((v = yyjson_obj_get(json, "collideMask"))) {
|
||||||
|
phys->collideMask = (uint32_t)yyjson_get_uint(v);
|
||||||
|
}
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ typedef struct {
|
|||||||
vec3 velocity;
|
vec3 velocity;
|
||||||
float_t gravityScale;
|
float_t gravityScale;
|
||||||
bool_t onGround;
|
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;
|
} entityphysics_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -173,14 +181,44 @@ physicsbodytype_t entityPhysicsGetBodyType(
|
|||||||
const componentid_t componentId
|
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"
|
* Serializes the physics body into the given JSON object: "bodyType"
|
||||||
* ("STATIC"/"DYNAMIC"/"KINEMATIC"), "shape" (an object with its own
|
* ("STATIC"/"DYNAMIC"/"KINEMATIC"), "shape" (an object with its own
|
||||||
* "type" of "CUBE"/"SPHERE"/"CAPSULE"/"PLANE" plus that shape's fields),
|
* "type" of "CUBE"/"SPHERE"/"CAPSULE"/"PLANE" plus that shape's fields),
|
||||||
* "velocity" ({"x","y","z"}), "gravityScale", and "onGround". Fails if
|
* "velocity" ({"x","y","z"}), "gravityScale", "onGround", and
|
||||||
* the body's shape is PHYSICS_SHAPE_CUSTOM -- custom shapes carry a
|
* "collideMask". Fails if the body's shape is PHYSICS_SHAPE_CUSTOM --
|
||||||
* callback and opaque userData pointer that cannot be represented in
|
* custom shapes carry a callback and opaque userData pointer that cannot
|
||||||
* JSON.
|
* be represented in JSON.
|
||||||
*
|
*
|
||||||
* @param mgr The entity manager that owns the entity.
|
* @param mgr The entity manager that owns the entity.
|
||||||
* @param entityId The entity ID.
|
* @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/entitycamera.h"
|
||||||
#include "entity/component/display/entityrenderable.h"
|
#include "entity/component/display/entityrenderable.h"
|
||||||
#include "entity/component/physics/entityphysics.h"
|
#include "entity/component/physics/entityphysics.h"
|
||||||
|
#include "entity/component/trigger/entitytrigger.h"
|
||||||
|
|
||||||
// Name (Uppercase)
|
// Name (Uppercase)
|
||||||
// Structure
|
// Structure
|
||||||
@@ -28,6 +29,8 @@ X(RENDERABLE, entityrenderable_t, renderable,
|
|||||||
entityRenderableSerialize, entityRenderableDeserialize)
|
entityRenderableSerialize, entityRenderableDeserialize)
|
||||||
X(PHYSICS, entityphysics_t, physics, entityPhysicsInit, NULL, NULL,
|
X(PHYSICS, entityphysics_t, physics, entityPhysicsInit, NULL, NULL,
|
||||||
entityPhysicsSerialize, entityPhysicsDeserialize)
|
entityPhysicsSerialize, entityPhysicsDeserialize)
|
||||||
|
X(TRIGGER, entitytrigger_t, trigger, entityTriggerInit, NULL, NULL,
|
||||||
|
entityTriggerSerialize, entityTriggerDeserialize)
|
||||||
|
|
||||||
// Game-specific components
|
// Game-specific components
|
||||||
#include "entity/gamecomponentlist.h"
|
#include "entity/gamecomponentlist.h"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
|
#include "entity/entitybase.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the game. Called once, after the engine and all of its
|
* 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.
|
* @return An error code indicating success or failure.
|
||||||
*/
|
*/
|
||||||
errorret_t gameDispose(void);
|
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
|
PUBLIC
|
||||||
physicsworld.c
|
physicsworld.c
|
||||||
physicstest.c
|
physicstest.c
|
||||||
|
physicsshape.c
|
||||||
physicsshapemesh.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
|
#pragma once
|
||||||
#include "dusk.h"
|
#include "dusk.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
#include "yyjson.h"
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
PHYSICS_SHAPE_CUBE,
|
PHYSICS_SHAPE_CUBE,
|
||||||
@@ -107,3 +109,35 @@ typedef struct physicsshape_t {
|
|||||||
physicshapetype_t type;
|
physicshapetype_t type;
|
||||||
physicsshapedata_t data;
|
physicsshapedata_t data;
|
||||||
} physicsshape_t;
|
} 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];
|
entityid_t j = otherIndices[oj];
|
||||||
if(!positions[j]) continue;
|
if(!positions[j]) continue;
|
||||||
entityphysics_t *otherPhys = physBodies[j];
|
entityphysics_t *otherPhys = physBodies[j];
|
||||||
|
if(!(phys->collideMask & otherPhys->collideMask)) continue;
|
||||||
|
|
||||||
vec3 normal; float_t depth;
|
vec3 normal; float_t depth;
|
||||||
if(!physicsTestShapeVsShape(
|
if(!physicsTestShapeVsShape(
|
||||||
@@ -136,6 +137,7 @@ void physicsWorldStep(
|
|||||||
if(!positions[j]) continue;
|
if(!positions[j]) continue;
|
||||||
entityphysics_t *physB = physBodies[j];
|
entityphysics_t *physB = physBodies[j];
|
||||||
float_t *posB = positions[j]->position;
|
float_t *posB = positions[j]->position;
|
||||||
|
if(!(physA->collideMask & physB->collideMask)) continue;
|
||||||
|
|
||||||
vec3 normal; float_t depth;
|
vec3 normal; float_t depth;
|
||||||
if(!physicsTestShapeVsShape(
|
if(!physicsTestShapeVsShape(
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ void physicsWorldInit(physicsworld_t *world);
|
|||||||
* Steps every PHYSICS-component entity in mgr forward by dt: integrates
|
* Steps every PHYSICS-component entity in mgr forward by dt: integrates
|
||||||
* gravity into dynamic bodies, resolves dynamic-vs-static/kinematic and
|
* gravity into dynamic bodies, resolves dynamic-vs-static/kinematic and
|
||||||
* dynamic-vs-dynamic overlap, then rebuilds each moved entity's transform.
|
* 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 world The physics world configuration (gravity) to step with.
|
||||||
* @param mgr The entity manager whose PHYSICS-component entities to step.
|
* @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/entitycamera.h"
|
||||||
#include "entity/component/display/entityposition.h"
|
#include "entity/component/display/entityposition.h"
|
||||||
#include "entity/component/display/entityrenderable.h"
|
#include "entity/component/display/entityrenderable.h"
|
||||||
|
#include "physics/triggersystem.h"
|
||||||
#include "ui/ui.h"
|
#include "ui/ui.h"
|
||||||
#include "console/console.h"
|
#include "console/console.h"
|
||||||
|
|
||||||
@@ -87,6 +88,7 @@ errorret_t sceneFixedUpdate(void) {
|
|||||||
scene_t *scene = &SCENE_MANAGER.scenes[SCENE_MANAGER.active];
|
scene_t *scene = &SCENE_MANAGER.scenes[SCENE_MANAGER.active];
|
||||||
entityManagerUpdate(&scene->entities);
|
entityManagerUpdate(&scene->entities);
|
||||||
physicsWorldStep(&scene->physics, &scene->entities, TIME.delta);
|
physicsWorldStep(&scene->physics, &scene->entities, TIME.delta);
|
||||||
|
triggerSystemStep(&scene->entities);
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,10 +99,10 @@ errorret_t sceneUpdate(void);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Ticks the active scene's entities (update callbacks, then a physics
|
* Ticks the active scene's entities (update callbacks, then a physics
|
||||||
* step) by one fixed timestep (TIME.delta). Called by sceneUpdate() at
|
* step, then a trigger step) by one fixed timestep (TIME.delta). Called
|
||||||
* the fixed-timestep cadence described there -- never once per rendered
|
* by sceneUpdate() at the fixed-timestep cadence described there -- never
|
||||||
* frame on a dynamic-time platform, so game logic and physics stay
|
* once per rendered frame on a dynamic-time platform, so game logic and
|
||||||
* frame-rate independent and deterministic.
|
* physics stay frame-rate independent and deterministic.
|
||||||
*
|
*
|
||||||
* @return An error if the update failed, or errorOk() if it succeeded.
|
* @return An error if the update failed, or errorOk() if it succeeded.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -34,6 +34,19 @@ errorret_t gameInit(void) {
|
|||||||
assetUnlockEntry(sceneEntry);
|
assetUnlockEntry(sceneEntry);
|
||||||
errorChain(ret);
|
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);
|
sceneSetActive(testSceneId);
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -45,3 +58,14 @@ errorret_t gameUpdate(void) {
|
|||||||
errorret_t gameDispose(void) {
|
errorret_t gameDispose(void) {
|
||||||
errorOk();
|
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
|
# Tests
|
||||||
dusktest(test_entitymanager.c)
|
dusktest(test_entitymanager.c)
|
||||||
dusktest(test_entityposition.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_physicstest.c)
|
||||||
dusktest(test_physicsworld.c)
|
dusktest(test_physicsworld.c)
|
||||||
dusktest(test_physicsshapemesh.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);
|
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) {
|
static void test_physicsWorldStepNoPhysicsEntitiesIsNoop(void **state) {
|
||||||
entitymanager_t mgr;
|
entitymanager_t mgr;
|
||||||
entityManagerInit(&mgr);
|
entityManagerInit(&mgr);
|
||||||
@@ -210,6 +253,7 @@ int main(int argc, char **argv) {
|
|||||||
cmocka_unit_test(test_physicsWorldGravityIntegratesDynamicBody),
|
cmocka_unit_test(test_physicsWorldGravityIntegratesDynamicBody),
|
||||||
cmocka_unit_test(test_physicsWorldRestsOnStaticFloor),
|
cmocka_unit_test(test_physicsWorldRestsOnStaticFloor),
|
||||||
cmocka_unit_test(test_physicsWorldDynamicVsDynamicSeparates),
|
cmocka_unit_test(test_physicsWorldDynamicVsDynamicSeparates),
|
||||||
|
cmocka_unit_test(test_physicsWorldCollideMaskPreventsCollision),
|
||||||
cmocka_unit_test(test_physicsWorldStepNoPhysicsEntitiesIsNoop),
|
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