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