physics stuff

This commit is contained in:
2026-07-17 19:07:32 -05:00
parent 0bc80d5df3
commit 5f08337726
38 changed files with 945 additions and 634 deletions
-1
View File
@@ -12,7 +12,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
player.c
)
add_subdirectory(anim)
add_subdirectory(interact)
add_subdirectory(npc)
add_subdirectory(item)
-13
View File
@@ -1,13 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
entityanim.c
entityanimidle.c
entityanimturn.c
entityanimwalk.c
entityanimrun.c
)
-44
View File
@@ -1,44 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/entity/entity.h"
#include "rpg/overworld/map.h"
#include "rpg/overworld/tile.h"
#include "time/time.h"
#include "entityanimwalk.h"
const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT] = {
[ENTITY_ANIM_IDLE] = { entityAnimIdleUpdate },
[ENTITY_ANIM_TURN] = { entityAnimTurnUpdate },
[ENTITY_ANIM_WALK] = { entityAnimWalkUpdate },
[ENTITY_ANIM_RUN] = { entityAnimRunUpdate },
};
float_t entityAnimTileZOffset(const worldpos_t pos) {
return tileShapeIsRamp(mapGetTile(pos).shape) ? 0.5f : 0.0f;
}
void entityAnimUpdate(entity_t *entity) {
if(entity->animation != ENTITY_ANIM_IDLE) {
entity->animTime -= TIME.delta;
if(entity->animTime <= 0) {
if(
entity->animation == ENTITY_ANIM_WALK ||
entity->animation == ENTITY_ANIM_RUN
) {
entity->walkEndCooldown = ENTITY_ANIM_WALK_TURN_COOLDOWN;
}
entity->animation = ENTITY_ANIM_IDLE;
entity->animTime = 0;
}
}
if(entity->walkEndCooldown > 0) {
entity->walkEndCooldown -= TIME.delta;
if(entity->walkEndCooldown < 0) entity->walkEndCooldown = 0;
}
ENTITY_ANIM_CALLBACKS[entity->animation].update(entity);
}
-46
View File
@@ -1,46 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#include "entityanimidle.h"
#include "entityanimturn.h"
#include "entityanimwalk.h"
#include "entityanimrun.h"
typedef struct entity_s entity_t;
typedef enum {
ENTITY_ANIM_IDLE,
ENTITY_ANIM_TURN,
ENTITY_ANIM_WALK,
ENTITY_ANIM_RUN,
ENTITY_ANIM_COUNT
} entityanim_t;
typedef struct {
/** Updates the render position for this animation state. */
void (*update)(entity_t *entity);
} entityanimcallback_t;
extern const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT];
/**
* Updates the entity animation timer and render position.
*
* @param entity Pointer to the entity to update.
*/
void entityAnimUpdate(entity_t *entity);
/**
* Returns 0.5 if the tile at pos is a ramp, 0.0 otherwise.
* Used to lift entity render position to mid-ramp height.
*
* @param pos World position to sample.
* @returns float_t The Z offset to apply.
*/
float_t entityAnimTileZOffset(const worldpos_t pos);
-17
View File
@@ -1,17 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/entity/entity.h"
#include "entityanim.h"
void entityAnimIdleUpdate(entity_t *entity) {
entity->renderPosition[0] = (float_t)entity->position.x;
entity->renderPosition[1] = (float_t)entity->position.y;
entity->renderPosition[2] = (
(float_t)entity->position.z + entityAnimTileZOffset(entity->position)
) * WORLD_LAYER_HEIGHT;
}
-18
View File
@@ -1,18 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct entity_s entity_t;
/**
* Updates render position for the idle animation state.
*
* @param entity Pointer to the entity to update.
*/
void entityAnimIdleUpdate(entity_t *entity);
-24
View File
@@ -1,24 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/entity/entity.h"
#include "entityanim.h"
void entityAnimRunUpdate(entity_t *entity) {
float_t t = 1.0f - (entity->animTime / ENTITY_ANIM_RUN_DURATION);
float_t zFrom = (float_t)entity->lastPosition.z
+ entityAnimTileZOffset(entity->lastPosition);
float_t zTo = (float_t)entity->position.z
+ entityAnimTileZOffset(entity->position);
entity->renderPosition[0] = (float_t)entity->lastPosition.x + t * (
(float_t)entity->position.x - (float_t)entity->lastPosition.x
);
entity->renderPosition[1] = (float_t)entity->lastPosition.y + t * (
(float_t)entity->position.y - (float_t)entity->lastPosition.y
);
entity->renderPosition[2] = (zFrom + t * (zTo - zFrom)) * WORLD_LAYER_HEIGHT;
}
-21
View File
@@ -1,21 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#include "time/time.h"
typedef struct entity_s entity_t;
#define ENTITY_ANIM_RUN_DURATION TIME_TICKS_TO_TIME(6)
/**
* Updates render position for the run animation state.
*
* @param entity Pointer to the entity to update.
*/
void entityAnimRunUpdate(entity_t *entity);
-17
View File
@@ -1,17 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/entity/entity.h"
#include "entityanim.h"
void entityAnimTurnUpdate(entity_t *entity) {
entity->renderPosition[0] = (float_t)entity->position.x;
entity->renderPosition[1] = (float_t)entity->position.y;
entity->renderPosition[2] = (
(float_t)entity->position.z + entityAnimTileZOffset(entity->position)
) * WORLD_LAYER_HEIGHT;
}
-21
View File
@@ -1,21 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#include "time/time.h"
typedef struct entity_s entity_t;
#define ENTITY_ANIM_TURN_DURATION TIME_TICKS_TO_TIME(4)
/**
* Updates render position for the turn animation state.
*
* @param entity Pointer to the entity to update.
*/
void entityAnimTurnUpdate(entity_t *entity);
-24
View File
@@ -1,24 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/entity/entity.h"
#include "entityanim.h"
void entityAnimWalkUpdate(entity_t *entity) {
float_t t = 1.0f - (entity->animTime / ENTITY_ANIM_WALK_DURATION);
float_t zFrom = (float_t)entity->lastPosition.z
+ entityAnimTileZOffset(entity->lastPosition);
float_t zTo = (float_t)entity->position.z
+ entityAnimTileZOffset(entity->position);
entity->renderPosition[0] = (float_t)entity->lastPosition.x + t * (
(float_t)entity->position.x - (float_t)entity->lastPosition.x
);
entity->renderPosition[1] = (float_t)entity->lastPosition.y + t * (
(float_t)entity->position.y - (float_t)entity->lastPosition.y
);
entity->renderPosition[2] = (zFrom + t * (zTo - zFrom)) * WORLD_LAYER_HEIGHT;
}
-22
View File
@@ -1,22 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#include "time/time.h"
typedef struct entity_s entity_t;
#define ENTITY_ANIM_WALK_DURATION TIME_TICKS_TO_TIME(12)
#define ENTITY_ANIM_WALK_TURN_COOLDOWN TIME_TICKS_TO_TIME(4)
/**
* Updates render position for the walk animation state.
*
* @param entity Pointer to the entity to update.
*/
void entityAnimWalkUpdate(entity_t *entity);
+118 -190
View File
@@ -1,6 +1,6 @@
/**
* Copyright (c) 2025 Dominic Masters
*
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
@@ -8,14 +8,14 @@
#include "entity.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "time/time.h"
#include "util/math.h"
#include "time/time.h"
#include "rpg/overworld/map.h"
#include "rpg/overworld/maparea.h"
#include "rpg/overworld/chunk.h"
#include "rpg/overworld/tile.h"
entity_t ENTITIES[ENTITY_COUNT];
physicsworld_t ENTITY_PHYSICS_WORLD;
void entityInit(entity_t *entity, const entitytype_t type) {
assertNotNull(entity, "Entity pointer cannot be NULL");
@@ -32,6 +32,9 @@ void entityInit(entity_t *entity, const entitytype_t type) {
entity->type = type;
entity->chunkIndex = 0xFF;
const vec3 extents = ENTITY_PHYSICS_EXTENTS_DEFAULT;
physicsBodyInit(&entity->body, (vec3){ 0.0f, 0.0f, 0.0f }, extents);
if(ENTITY_CALLBACKS[type].init != NULL) ENTITY_CALLBACKS[type].init(entity);
}
@@ -40,212 +43,115 @@ void entityUpdate(entity_t *entity) {
assertTrue(entity->type < ENTITY_TYPE_COUNT, "Invalid entity type");
assertTrue(entity->type != ENTITY_TYPE_NULL, "Cannot have NULL entity type");
// What state is the entity in?
entityAnimUpdate(entity);
// Movement code.
if(ENTITY_CALLBACKS[entity->type].movement != NULL) {
ENTITY_CALLBACKS[entity->type].movement(entity);
}
}
bool_t entityCanTurn(entity_t *entity) {
return entity->animation == ENTITY_ANIM_IDLE &&
entity->walkEndCooldown <= 0;
}
physicsbody_t *others[ENTITY_COUNT];
uint32_t othersCount = 0;
for(uint8_t i = 0; i < ENTITY_COUNT; i++) {
if(ENTITIES[i].type == ENTITY_TYPE_NULL) continue;
if(&ENTITIES[i] == entity) continue;
others[othersCount++] = &ENTITIES[i].body;
}
bool_t entityCanWalk(entity_t *entity) {
return entity->animation == ENTITY_ANIM_IDLE;
}
bool_t entityCanRun(entity_t *entity) {
return entity->animation == ENTITY_ANIM_IDLE;
physicsWorldStep(
&ENTITY_PHYSICS_WORLD, &entity->body, TIME.delta, others, othersCount
);
entitySyncFromPhysics(entity);
}
bool_t entityCanUnload(entity_t *entity) {
return entity->globalId < ENTITY_GLOBAL_ID_START;
}
void entityMove(
entity_t *entity, const vec2 direction, const bool_t running
) {
assertNotNull(entity, "Entity pointer cannot be NULL");
const float_t magSq =
direction[0] * direction[0] + direction[1] * direction[1];
if(magSq <= ENTITY_MOVE_DEADZONE * ENTITY_MOVE_DEADZONE) {
entity->body.velocity[0] = 0.0f;
entity->body.velocity[1] = 0.0f;
entity->animation = ENTITY_ANIM_IDLE;
return;
}
const float_t mag = sqrtf(magSq);
const float_t clampedMag = mathMin(mag, 1.0f);
const float_t speed = running ? ENTITY_MOVE_RUN_SPEED : ENTITY_MOVE_WALK_SPEED;
entity->body.velocity[0] = (direction[0] / mag) * clampedMag * speed;
entity->body.velocity[1] = (direction[1] / mag) * clampedMag * speed;
entity->direction = entityDirFromVec2(direction);
entity->animation = running ? ENTITY_ANIM_RUN : ENTITY_ANIM_WALK;
}
void entityStop(entity_t *entity) {
const vec2 zero = { 0.0f, 0.0f };
entityMove(entity, zero, false);
}
void entityTurn(entity_t *entity, const entitydir_t direction) {
if(!entityCanTurn(entity)) return;
assertNotNull(entity, "Entity pointer cannot be NULL");
entity->direction = direction;
entity->animation = ENTITY_ANIM_TURN;
entity->animTime = ENTITY_ANIM_TURN_DURATION;
}
void entityWalk(entity_t *entity, const entitydir_t direction) {
if(!entityCanWalk(entity)) return;
// TODO: Animation, delay, etc.
entity->direction = direction;
// Where are we moving?
worldpos_t newPos = entity->position;
worldunits_t relX, relY;
{
entityDirGetRelative(direction, &relX, &relY);
newPos.x += relX;
newPos.y += relY;
}
// Get tile under foot
tile_t tileCurrent = mapGetTile(entity->position);
tile_t tileNew = mapGetTile(newPos);
bool_t fall = false;
bool_t raise = false;
// Are we walking up a ramp?
if(
tileShapeIsRamp(tileCurrent.shape) &&
(
// Can only walk UP the direction the ramp faces.
(direction+TILE_SHAPE_RAMP_NORTH) == tileCurrent.shape ||
// If diagonal ramp, can go up one of two ways only. Inner ramps
// share the same allowed directions as their outer counterparts.
(
(
(
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST ||
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER
) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
) ||
(
(
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST ||
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER
) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
) ||
(
(
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST ||
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER
) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
) ||
(
(
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST ||
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER
) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
)
)
// Must be able to walk up.
)
) {
tile_t tileNewSaved = tileNew;
tileNew = TILE_NULL;
worldpos_t abovePos = newPos;
abovePos.z += 1;
tile_t tileAbove = mapGetTile(abovePos);
if(
tileAbove.shape != TILE_SHAPE_NULL &&
tileShapeIsWalkable(tileAbove.shape)
) {
raise = true;
} else {
tileNew = tileNewSaved;
}
} else if(tileNew.shape == TILE_SHAPE_NULL && newPos.z > 0) {
// Falling down?
worldpos_t belowPos = newPos;
belowPos.z -= 1;
tile_t tileBelow = mapGetTile(belowPos);
if(
tileBelow.shape != TILE_SHAPE_NULL &&
tileShapeIsRamp(tileBelow.shape) &&
(
// This handles regular cardinal ramps
(
entityDirGetOpposite(direction)+TILE_SHAPE_RAMP_NORTH
) == tileBelow.shape ||
// This handles diagonal ramps. Inner ramps share the same
// allowed directions as their outer counterparts.
(
(
(
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST ||
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER
) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
) ||
(
(
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST ||
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER
) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
) ||
(
(
tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST ||
tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER
) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
) ||
(
(
tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST ||
tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER
) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
)
)
)
) {
// We will fall to this tile.
fall = true;
}
}
// Can we walk here?
if(!raise && !fall && !tileShapeIsWalkable(tileNew.shape)) return;// Blocked
// Raise/fall must be applied before checking for blocking entities,
// otherwise the check compares against the wrong z-level.
if(raise) {
newPos.z += 1;
} else if(fall) {
newPos.z -= 1;
}
// Entity in way?
entity_t *other = ENTITIES;
do {
if(other == entity) continue;
if(other->type == ENTITY_TYPE_NULL) continue;
if(!worldPosIsEqual(other->position, newPos)) continue;
return;// Blocked
} while(++other, other < &ENTITIES[ENTITY_COUNT]);
entity->lastPosition = entity->position;
entity->position = newPos;
entity->animation = ENTITY_ANIM_WALK;
entity->animTime = ENTITY_ANIM_WALK_DURATION;// TODO: Running vs walking
entityUpdateChunk(entity);
mapAreaCheckEntity(entity);
vec2 dirVec;
entityDirToVec2(direction, dirVec);
entityMove(entity, dirVec, false);
}
void entityRun(entity_t *entity, const entitydir_t direction) {
if(!entityCanRun(entity)) return;
entityWalk(entity, direction);
if(entity->animation == ENTITY_ANIM_WALK) {
entity->animation = ENTITY_ANIM_RUN;
entity->animTime = ENTITY_ANIM_RUN_DURATION;
}
vec2 dirVec;
entityDirToVec2(direction, dirVec);
entityMove(entity, dirVec, true);
}
entity_t * entityGetAt(const worldpos_t position) {
entity_t * entityGetFacing(entity_t *entity, const float_t range) {
assertNotNull(entity, "Entity pointer cannot be NULL");
vec2 dir;
entityDirToVec2(entity->direction, dir);
vec3 min, max;
physicsBodyGetBounds(&entity->body, min, max);
const vec3 probeMin = {
min[0] + dir[0] * range, min[1] + dir[1] * range, min[2]
};
const vec3 probeMax = {
max[0] + dir[0] * range, max[1] + dir[1] * range, max[2]
};
entity_t *best = NULL;
float_t bestDistSq = 0.0f;
entity_t *ent = ENTITIES;
do {
if(ent->type == ENTITY_TYPE_NULL) continue;
if(!worldPosIsEqual(ent->position, position)) continue;
return ent;
if(ent == entity) continue;
vec3 oMin, oMax;
physicsBodyGetBounds(&ent->body, oMin, oMax);
if(probeMin[0] >= oMax[0] || probeMax[0] <= oMin[0]) continue;
if(probeMin[1] >= oMax[1] || probeMax[1] <= oMin[1]) continue;
if(probeMin[2] >= oMax[2] || probeMax[2] <= oMin[2]) continue;
const float_t dx = ent->body.position[0] - entity->body.position[0];
const float_t dy = ent->body.position[1] - entity->body.position[1];
const float_t distSq = dx * dx + dy * dy;
if(best != NULL && distSq >= bestDistSq) continue;
best = ent;
bestDistSq = distSq;
} while(++ent, ent < &ENTITIES[ENTITY_COUNT]);
return NULL;
return best;
}
entity_t * entityGetByGlobalId(const entityglobalid_t globalId) {
@@ -270,12 +176,14 @@ uint8_t entityGetAvailable() {
void entityPositionSet(entity_t *entity, const worldpos_t pos) {
assertNotNull(entity, "Entity pointer cannot be NULL");
entity->lastPosition = pos;
entity->position = pos;
const vec3 floatPos = {
(float_t)pos.x, (float_t)pos.y, (float_t)pos.z
};
const vec3 extents = ENTITY_PHYSICS_EXTENTS_DEFAULT;
physicsBodyInit(&entity->body, floatPos, extents);
entity->animation = ENTITY_ANIM_IDLE;
entity->animTime = 0;
entity->walkEndCooldown = 0;
entityUpdateChunk(entity);
entitySyncFromPhysics(entity);
}
void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
@@ -297,11 +205,14 @@ void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
if(chunkIndex != 0xFF) {
chunk_t *next = mapGetChunk(chunkIndex);
if(next != NULL) {
bool_t inserted = false;
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
if(next->entities[i] != 0xFF) continue;
next->entities[i] = entity->id;
inserted = true;
break;
}
assertTrue(inserted, "Chunk entity slot overflow");
}
}
}
@@ -312,5 +223,22 @@ void entityUpdateChunk(entity_t *entity) {
chunkpos_t cp;
worldPosToChunkPos(&entity->position, &cp);
chunkindex_t ci = mapGetChunkIndexAt(cp);
if(ci != -1) entitySetChunk(entity, (uint8_t)ci);
}
if(ci == -1 || ci == entity->chunkIndex) return;
entitySetChunk(entity, (uint8_t)ci);
}
void entitySyncFromPhysics(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL");
entity->position = (worldpos_t){
(worldunit_t)floorf(entity->body.position[0]),
(worldunit_t)floorf(entity->body.position[1]),
(worldunit_t)floorf(entity->body.position[2])
};
glm_vec3_copy(entity->body.position, entity->renderPosition);
entity->renderPosition[2] *= WORLD_LAYER_HEIGHT;
entityUpdateChunk(entity);
mapAreaCheckEntity(entity);
}
+83 -37
View File
@@ -7,10 +7,11 @@
#pragma once
#include "entitydir.h"
#include "anim/entityanim.h"
#include "interact/entityinteract.h"
#include "entitytype.h"
#include "npc/npc.h"
#include "rpg/physics/physicsbody.h"
#include "rpg/physics/physicsworld.h"
typedef struct map_s map_t;
@@ -20,6 +21,29 @@ typedef uint16_t entityglobalid_t;
#define ENTITY_GLOBAL_ID_START 1
#define ENTITY_GLOBAL_ID_PLAYER 1
// Default collision box for every entity's physics body - matches the old
// system's exact one-tile footprint.
#define ENTITY_PHYSICS_EXTENTS_DEFAULT { 1.0f, 1.0f, 1.0f }
// Movement speeds, in grid units per second - chosen to match the feel of
// the old fixed-duration one-tile-per-step system (12 ticks/tile walking,
// 6 ticks/tile running, at DUSK_TIME_STEP = 16ms).
#define ENTITY_MOVE_WALK_SPEED 5.2083f
#define ENTITY_MOVE_RUN_SPEED 10.4167f
// Movement vectors below this magnitude are treated as no movement.
#define ENTITY_MOVE_DEADZONE 0.1f
// How far in front of an entity entityGetFacing probes for a target.
#define ENTITY_INTERACT_RANGE 1.0f
typedef enum {
ENTITY_ANIM_IDLE,
ENTITY_ANIM_WALK,
ENTITY_ANIM_RUN,
ENTITY_ANIM_COUNT
} entityanim_t;
typedef struct entity_s {
uint8_t id;
entityglobalid_t globalId;
@@ -28,13 +52,18 @@ typedef struct entity_s {
// Movement
entitydir_t direction;
physicsbody_t body;
// Derived each frame from body.position (floored) - kept for systems
// that still assume an integer grid position (chunk membership, map
// area triggers, entity-at-position queries).
worldpos_t position;
worldpos_t lastPosition;
// Derived each frame from body.position - mirrors the physics position
// into render/world-float space (z scaled by WORLD_LAYER_HEIGHT).
vec3 renderPosition;
entityanim_t animation;
float_t animTime;
float_t walkEndCooldown;
entityinteract_t interact;
@@ -43,6 +72,9 @@ typedef struct entity_s {
extern entity_t ENTITIES[ENTITY_COUNT];
// Shared physics world every entity's body steps against.
extern physicsworld_t ENTITY_PHYSICS_WORLD;
/**
* Initializes an entity structure.
*
@@ -58,30 +90,6 @@ void entityInit(entity_t *entity, const entitytype_t type);
*/
void entityUpdate(entity_t *entity);
/**
* Returns true if the entity is in a state where it can turn.
*
* @param entity Pointer to the entity to check.
* @returns True if the entity can turn.
*/
bool_t entityCanTurn(entity_t *entity);
/**
* Returns true if the entity is in a state where it can walk.
*
* @param entity Pointer to the entity to check.
* @returns True if the entity can walk.
*/
bool_t entityCanWalk(entity_t *entity);
/**
* Returns true if the entity is in a state where it can run.
*
* @param entity Pointer to the entity to check.
* @returns True if the entity can run.
*/
bool_t entityCanRun(entity_t *entity);
/**
* Returns true if the entity is allowed to be unloaded. By default this is
* true for entities whose global ID falls within the randomly assigned
@@ -93,7 +101,29 @@ bool_t entityCanRun(entity_t *entity);
bool_t entityCanUnload(entity_t *entity);
/**
* Turn an entity to face a new direction.
* Moves an entity continuously in a direction, at walking or running
* speed. Sets the entity's facing to the nearest cardinal direction that
* matches the movement vector. Must be called every tick the entity
* should keep moving - unlike the old tile-stepping system this does not
* complete a move on its own; call entityStop to stop.
*
* @param entity Pointer to the entity to move.
* @param direction Movement vector, magnitude 0-1 (values longer than 1
* are clamped to 1, so diagonals aren't faster than cardinals).
* @param running Whether to move at running speed instead of walking.
*/
void entityMove(entity_t *entity, const vec2 direction, const bool_t running);
/**
* Stops an entity's horizontal movement (equivalent to
* entityMove(entity, {0, 0}, false)).
*
* @param entity Pointer to the entity to stop.
*/
void entityStop(entity_t *entity);
/**
* Turn an entity to face a new direction, instantly, without moving it.
*
* @param entity Pointer to the entity to turn.
* @param direction The direction to face.
@@ -101,7 +131,10 @@ bool_t entityCanUnload(entity_t *entity);
void entityTurn(entity_t *entity, const entitydir_t direction);
/**
* Make an entity walk in a direction.
* Makes an entity walk continuously in a cardinal direction, at walking
* speed. Convenience wrapper over entityMove for callers that only think
* in terms of the 4 cardinal directions. Must be called every tick the
* entity should keep moving.
*
* @param entity Pointer to the entity to make walk.
* @param direction The direction to walk in.
@@ -109,7 +142,8 @@ void entityTurn(entity_t *entity, const entitydir_t direction);
void entityWalk(entity_t *entity, const entitydir_t direction);
/**
* Make an entity run in a direction.
* Makes an entity walk continuously in a cardinal direction, at running
* speed. See entityWalk.
*
* @param entity Pointer to the entity to make run.
* @param direction The direction to run in.
@@ -117,13 +151,16 @@ void entityWalk(entity_t *entity, const entitydir_t direction);
void entityRun(entity_t *entity, const entitydir_t direction);
/**
* Gets the entity at a specific world position.
* Finds the closest other entity whose bounds overlap a probe box
* projected out from the given entity's own bounds, along its current
* facing direction. Used for interaction targeting - continuous-position
* aware, unlike an exact tile match.
*
* @param map Pointer to the map to check.
* @param pos The world position to check.
* @return Pointer to the entity at the position, or NULL if none.
* @param entity Pointer to the entity to probe from.
* @param range Distance, in grid units, to project the probe box.
* @return Pointer to the closest overlapping entity, or NULL if none.
*/
entity_t *entityGetAt(const worldpos_t pos);
entity_t *entityGetFacing(entity_t *entity, const float_t range);
/**
* Gets the entity with the given global ID, if one is currently loaded.
@@ -164,4 +201,13 @@ void entityUpdateChunk(entity_t *entity);
* @param entity Pointer to the entity to move.
* @param pos The world position to place the entity at.
*/
void entityPositionSet(entity_t *entity, const worldpos_t pos);
void entityPositionSet(entity_t *entity, const worldpos_t pos);
/**
* Derives position and renderPosition from the entity's physics body, and
* refreshes chunk membership and map area triggers to match. Called once
* per entity per frame, after the physics step.
*
* @param entity Pointer to the entity to sync.
*/
void entitySyncFromPhysics(entity_t *entity);
+23
View File
@@ -48,4 +48,27 @@ void entityDirGetRelative(
*outY = 0;
break;
}
}
void entityDirToVec2(const entitydir_t dir, vec2 out) {
assertValidEntityDir(dir, "Invalid direction provided");
assertNotNull(out, "Output vector cannot be NULL");
worldunits_t relX, relY;
entityDirGetRelative(dir, &relX, &relY);
out[0] = (float_t)relX;
out[1] = (float_t)relY;
}
entitydir_t entityDirFromVec2(const vec2 direction) {
assertNotNull(direction, "Direction vector cannot be NULL");
assertTrue(
direction[0] != 0.0f || direction[1] != 0.0f,
"Direction vector cannot be zero"
);
if(fabsf(direction[0]) > fabsf(direction[1])) {
return direction[0] > 0.0f ? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
}
return direction[1] > 0.0f ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
}
+21 -2
View File
@@ -44,11 +44,30 @@ entitydir_t entityDirGetOpposite(const entitydir_t dir);
/**
* Gets the relative x and y offsets for a given direction.
*
*
* @param dir The direction to get offsets for.
* @param relX Pointer to store the relative x offset.
* @param relY Pointer to store the relative y offset.
*/
void entityDirGetRelative(
const entitydir_t dir, worldunits_t *relX, worldunits_t *relY
);
);
/**
* Converts a cardinal direction to a unit 2D vector, using the same axis
* convention as entityDirGetRelative (north = +y, east = +x).
*
* @param dir The direction to convert.
* @param out Output unit vector for the direction.
*/
void entityDirToVec2(const entitydir_t dir, vec2 out);
/**
* Quantizes a 2D direction vector to the nearest cardinal direction, by
* comparing the magnitude of its x and y components.
*
* @param direction The direction vector to quantize. Must not be a zero
* vector.
* @return The nearest cardinal direction.
*/
entitydir_t entityDirFromVec2(const vec2 direction);
+35 -23
View File
@@ -9,6 +9,10 @@
#include "entitydir.h"
#include "assert/assert.h"
// Distance, in grid units, within which the entity is considered to have
// arrived at its path target.
#define ENTITY_PATH_ARRIVE_EPSILON 0.05f
bool_t entityPathStep(
entity_t *entity,
const worldpos_t target,
@@ -24,36 +28,44 @@ bool_t entityPathStep(
"Entity pointer is out of bounds"
);
if(worldPosIsEqual(entity->position, target) && entityCanWalk(entity)) {
const float_t dx = (float_t)target.x - entity->body.position[0];
const float_t dy = (float_t)target.y - entity->body.position[1];
if(
fabsf(dx) <= ENTITY_PATH_ARRIVE_EPSILON &&
fabsf(dy) <= ENTITY_PATH_ARRIVE_EPSILON
) {
entityStop(entity);
return true;
}
if(!entityCanWalk(entity)) return false;
entitydir_t dir;
if(entity->position.x != target.x) {
dir = entity->position.x < target.x ? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
} else if(entity->position.y != target.y) {
dir = entity->position.y < target.y ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
bool_t horizontal;
if(fabsf(dx) > fabsf(dy)) {
dir = dx > 0.0f ? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
horizontal = true;
} else {
dir = entity->position.z < target.z ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
dir = dy > 0.0f ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
horizontal = false;
}
// Was the entity trying to walk on this axis last tick, but ended up not
// moving (velocity clamped by a collision)? If so, treat it as blocked.
if(walkAround) {
const uint8_t axis = horizontal ? 0 : 1;
const bool_t blockedLastTick =
entity->animation != ENTITY_ANIM_IDLE &&
fabsf(entity->body.velocity[axis]) <= ENTITY_MOVE_DEADZONE;
if(blockedLastTick) {
const entitydir_t alt = horizontal
? (dy >= 0.0f ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH)
: (dx >= 0.0f ? ENTITY_DIR_EAST : ENTITY_DIR_WEST);
entityWalk(entity, alt);
return false;
}
}
entityWalk(entity, dir);
if(walkAround && entity->animation == ENTITY_ANIM_IDLE) {
// Primary direction blocked - try perpendicular axes.
entitydir_t alt, altOpp;
if(dir == ENTITY_DIR_EAST || dir == ENTITY_DIR_WEST) {
alt = entity->position.y <= target.y
? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
} else {
alt = entity->position.x <= target.x
? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
}
altOpp = entityDirGetOpposite(alt);
entityWalk(entity, alt);
if(entity->animation == ENTITY_ANIM_IDLE) entityWalk(entity, altOpp);
}
return false;
}
+9 -7
View File
@@ -9,15 +9,17 @@
#include "entity.h"
/**
* Attempts to walk the entity one tile toward target. Prefers resolving X
* first, then Y, then Z. Does nothing if the entity cannot currently walk.
* When walkAround is true and the preferred direction is blocked by an entity,
* tries perpendicular directions to navigate around it.
* Continuously walks the entity toward target's X/Y position (Z is left to
* gravity/ground collision, not deliberate path-stepping). Prefers closing
* whichever of X/Y currently differs most. Must be called every tick until
* it returns true. When walkAround is true and the last tick's movement on
* the current axis was blocked (its velocity on that axis reads as zero
* despite currently walking), tries a perpendicular direction instead.
*
* @param entity Pointer to the entity to move.
* @param target The world position to move toward.
* @param walkAround Whether to try perpendicular directions when blocked.
* @returns true if the entity is already at target, false otherwise.
* @param target The world position to move toward (Z is ignored).
* @param walkAround Whether to try a perpendicular direction when blocked.
* @returns true once the entity is within arrival distance of target.
*/
bool_t entityPathStep(
entity_t *entity,
+2 -8
View File
@@ -29,15 +29,9 @@ void npcPathAddNode(npc_t *npc, const worldpos_t pos) {
void npcPathMovement(entity_t *entity) {
npcpath_t *path = &entity->data.npc.moveData.path;
if(path->count == 0) return;
if(!entityCanWalk(entity)) return;
// Advance past any waypoints already reached (including the current one).
worldpos_t *target = &path->positions[path->index];
if(entityPathStep(entity, *target, false)) {
const worldpos_t target = path->positions[path->index];
if(entityPathStep(entity, target, false)) {
path->index = (path->index + 1) % path->count;
target = &path->positions[path->index];
// New target is the same tile - nothing to do this tick
if(worldPosIsEqual(entity->position, *target)) return;
entityPathStep(entity, *target, false);
}
}
+1 -1
View File
@@ -23,5 +23,5 @@ void npcRandomTurnMovement(entity_t *entity) {
turn->timer -= TIME.delta;
if(turn->timer > 0.0f) return;
turn->timer = randomFloat(turn->frequencyMin, turn->frequencyMax);
if(entityCanTurn(entity)) entityTurn(entity, (entitydir_t)(rand() % 4));
entityTurn(entity, (entitydir_t)(rand() % 4));
}
+22 -3
View File
@@ -16,14 +16,24 @@ void npcRandomWalkInit(npc_t *npc) {
walk->frequencyMin = NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT;
walk->frequencyMax = NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT;
walk->timer = randomFloat(walk->frequencyMin, walk->frequencyMax);
walk->moveDuration = 0.0f;
}
void npcRandomWalkMovement(entity_t *entity) {
npcrandomwalk_t *walk = &entity->data.npc.moveData.randomWalk;
if(walk->moveDuration > 0.0f) {
walk->moveDuration -= TIME.delta;
entityWalk(entity, walk->direction);
if(walk->moveDuration <= 0.0f) entityStop(entity);
return;
}
walk->timer -= TIME.delta;
if(walk->timer > 0.0f) return;
walk->timer = randomFloat(walk->frequencyMin, walk->frequencyMax);
if(entityCanWalk(entity)) entityWalk(entity, (entitydir_t)(rand() % 4));
walk->direction = (entitydir_t)(rand() % 4);
walk->moveDuration = NPC_RANDOM_WALK_MOVE_DURATION_DEFAULT;
}
void npcRandomTurnAndWalkInit(npc_t *npc) {
@@ -32,21 +42,30 @@ void npcRandomTurnAndWalkInit(npc_t *npc) {
tw->walk.frequencyMin = NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT;
tw->walk.frequencyMax = NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT;
tw->walk.timer = randomFloat(tw->walk.frequencyMin, tw->walk.frequencyMax);
tw->walk.moveDuration = 0.0f;
}
void npcRandomTurnAndWalkMovement(entity_t *entity) {
npcrandomturnandwalk_t *tw = &entity->data.npc.moveData.randomTurnAndWalk;
if(tw->walk.moveDuration > 0.0f) {
tw->walk.moveDuration -= TIME.delta;
entityWalk(entity, tw->walk.direction);
if(tw->walk.moveDuration <= 0.0f) entityStop(entity);
return;
}
tw->turn.timer -= TIME.delta;
if(tw->turn.timer <= 0.0f) {
tw->turn.timer = randomFloat(tw->turn.frequencyMin, tw->turn.frequencyMax);
if(entityCanTurn(entity)) entityTurn(entity, (entitydir_t)(rand() % 4));
entityTurn(entity, (entitydir_t)(rand() % 4));
}
tw->walk.timer -= TIME.delta;
if(tw->walk.timer <= 0.0f) {
tw->walk.timer = randomFloat(tw->walk.frequencyMin, tw->walk.frequencyMax);
if(entityCanWalk(entity)) entityWalk(entity, (entitydir_t)(rand() % 4));
tw->walk.direction = (entitydir_t)(rand() % 4);
tw->walk.moveDuration = NPC_RANDOM_WALK_MOVE_DURATION_DEFAULT;
}
}
+10
View File
@@ -8,15 +8,25 @@
#pragma once
#include "dusk.h"
#include "npcturn.h"
#include "rpg/entity/entitydir.h"
/** Default min/max seconds between NPC random-walk ticks. */
#define NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT 2.0f
#define NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT 5.0f
/** How long a random-walk move lasts once triggered, roughly one tile's
* worth of travel at walking speed. */
#define NPC_RANDOM_WALK_MOVE_DURATION_DEFAULT 0.2f
typedef struct {
float_t frequencyMin;
float_t frequencyMax;
float_t timer;
// In-progress move state - while moveDuration > 0, movement keeps
// walking in direction every tick.
float_t moveDuration;
entitydir_t direction;
} npcrandomwalk_t;
typedef struct {
+20 -53
View File
@@ -25,8 +25,11 @@ bool_t playerCanInteract(entity_t *entity) {
void playerInput(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL");
if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_PLAYER) return;
if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_PLAYER) {
entityStop(entity);
return;
}
// Toggle game menu on pause
if(uiGameMenuIsOpen() && inputPressed(INPUT_ACTION_PAUSE)) {
uiGameMenuClose();
@@ -38,63 +41,27 @@ void playerInput(entity_t *entity) {
}
// Can player act?
if(UI_FOCUS.count > 0) return;
// Determine direction player may be trying to face/go.
const playerinputdirmap_t *dirMap;
// First pass, we want to prefer a button relative to what the player is
// already facing, e.g. if they hold DOWN and start moving down, then tap
// right we want to prefer down over right.
// Determine current dirmap based on facing direction.
dirMap = PLAYER_INPUT_DIR_MAP;
do {
if(dirMap->direction != entity->direction) continue;
break;
} while((++dirMap)->action != 0xFF);
// Are we holding that button?
if(!inputIsDown(dirMap->action)) {
// No, so we need to check all directions.
dirMap = PLAYER_INPUT_DIR_MAP;
do {
if(!inputIsDown(dirMap->action)) continue;
break;
} while((++dirMap)->action != 0xFF);
if(UI_FOCUS.count > 0) {
entityStop(entity);
return;
}
// Trying to direct?
if(dirMap->action != 0xFF) {
// Is the player attempting to turn? That is, is the player idle and has the
// turn lockout passed?
if(entityCanTurn(entity)) {
if(entity->direction != dirMap->direction) {
entityTurn(entity, dirMap->direction);
return;
}
}
// Analog/free-angle movement vector from the 4 directional actions -
// already normalized so diagonals aren't faster than cardinals, and
// preserves real analog magnitude on platforms that bind an actual
// gamepad stick to these actions.
vec2 moveDir;
inputAngle2D(
INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT,
INPUT_ACTION_DOWN, INPUT_ACTION_UP,
moveDir
);
bool_t running = inputIsDown(INPUT_ACTION_CANCEL);
if(running && entityCanRun(entity)) {
entityRun(entity, dirMap->direction);
} else if(entityCanWalk(entity)) {
entityWalk(entity, dirMap->direction);
}
}
entityMove(entity, moveDir, inputIsDown(INPUT_ACTION_CANCEL));
// Interaction
if(inputPressed(INPUT_ACTION_ACCEPT) && playerCanInteract(entity)) {
worldunit_t x, y, z;
{
worldunits_t relX, relY;
entityDirGetRelative(entity->direction, &relX, &relY);
x = entity->position.x + relX;
y = entity->position.y + relY;
z = entity->position.z;
}
entity_t *target = entityGetAt((worldpos_t){ x, y, z });
entity_t *target = entityGetFacing(entity, ENTITY_INTERACT_RANGE);
if(target == NULL) return;
entityInteractWith(entity, target);
}
-14
View File
@@ -15,20 +15,6 @@ typedef struct {
void *nothing;
} player_t;
typedef struct {
inputaction_t action;
entitydir_t direction;
} playerinputdirmap_t;
static const playerinputdirmap_t PLAYER_INPUT_DIR_MAP[] = {
{ INPUT_ACTION_UP, ENTITY_DIR_NORTH },
{ INPUT_ACTION_DOWN, ENTITY_DIR_SOUTH },
{ INPUT_ACTION_LEFT, ENTITY_DIR_WEST },
{ INPUT_ACTION_RIGHT, ENTITY_DIR_EAST },
{ 0xFF, 0xFF }
};
/**
* Initializes a player entity.
*
+8
View File
@@ -168,6 +168,14 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
errorOk();
}
// Placeholder-fill with flat ground while the real data streams in
// asynchronously - entities standing on this chunk (subject to gravity)
// would otherwise fall through the zeroed/empty tiles until
// mapChunkLoaded() replaces them with the real data.
for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) {
chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND };
}
assertTrue(
MAP.loadQueueCount < MAP_CHUNK_COUNT,
"Chunk load queue overflow"
+6 -1
View File
@@ -38,6 +38,7 @@ void mapAreaInit(
area->triggerCount = 0;
memorySet(area->entities, 0, sizeof(area->entities));
memorySet(area->lastStepPosition, 0, sizeof(area->lastStepPosition));
}
bool_t mapAreaIsInside(const maparea_t *area, const worldpos_t position) {
@@ -132,9 +133,13 @@ void mapAreaCheckEntity(entity_t *entity) {
if(isInside && !wasInside) {
area->entities[entity->id] = 1;
area->lastStepPosition[entity->id] = entity->position;
trigger = MAP_TRIGGER_ENTER;
} else if(isInside && wasInside) {
trigger = MAP_TRIGGER_STEP;
if(!worldPosIsEqual(entity->position, area->lastStepPosition[entity->id])) {
area->lastStepPosition[entity->id] = entity->position;
trigger = MAP_TRIGGER_STEP;
}
} else if(!isInside && wasInside) {
area->entities[entity->id] = 0;
trigger = MAP_TRIGGER_EXIT;
+11 -2
View File
@@ -18,6 +18,8 @@ typedef struct maparea_s maparea_t;
#define MAP_AREA_NOTIFY_NPC (1 << 1)
#define MAP_AREA_NOTIFY_ALL (MAP_AREA_NOTIFY_PLAYER | MAP_AREA_NOTIFY_NPC)
// Fires once per newly-entered tile while an entity remains inside the
// area (not once per frame) - see mapAreaCheckEntity.
#define MAP_TRIGGER_STEP (1 << 0)
#define MAP_TRIGGER_ENTER (1 << 1)
#define MAP_TRIGGER_EXIT (1 << 2)
@@ -39,6 +41,11 @@ typedef struct maparea_s {
uint8_t trigger;
uint8_t entities[ENTITY_COUNT];
// The floored tile position ENTER or STEP last fired at, per entity.
// Used to fire STEP only once per newly-entered tile rather than
// every frame the entity remains inside the area.
worldpos_t lastStepPosition[ENTITY_COUNT];
// Incremented every time this area's callback is invoked. Lets other
// systems (e.g. cutscenes) detect "has this area fired since I last
// checked" without needing to be the callback themselves.
@@ -143,8 +150,10 @@ void mapAreaRemove(const uint8_t id);
* Checks every active map area against an entity's current position,
* for areas whose notify flags include the entity's type. Invokes the
* area's callback with MAP_TRIGGER_ENTER the frame the entity first
* becomes inside, MAP_TRIGGER_STEP on every subsequent frame it remains
* inside, and MAP_TRIGGER_EXIT the frame it leaves.
* becomes inside, MAP_TRIGGER_STEP the frame it moves into a different
* tile while remaining inside, and MAP_TRIGGER_EXIT the frame it leaves.
* Safe (and expected) to call every frame - trigger conditions are
* edge-detected against the entity's tile position, not the frame rate.
*
* @param entity Pointer to the entity to check.
*/
+67 -7
View File
@@ -31,7 +31,11 @@ void physicsWorldInit(
}
void physicsWorldStep(
const physicsworld_t *world, physicsbody_t *body, const float_t dt
const physicsworld_t *world,
physicsbody_t *body,
const float_t dt,
physicsbody_t * const *others,
const uint32_t othersCount
) {
assertNotNull(world, "world must not be null");
assertNotNull(body, "body must not be null");
@@ -41,13 +45,17 @@ void physicsWorldStep(
body->velocity[2], -world->terminalVelocity, world->terminalVelocity
);
physicsWorldResolveAxisX(world, body, dt);
physicsWorldResolveAxisY(world, body, dt);
physicsWorldResolveAxisZ(world, body, dt);
physicsWorldResolveAxisX(world, body, dt, others, othersCount);
physicsWorldResolveAxisY(world, body, dt, others, othersCount);
physicsWorldResolveAxisZ(world, body, dt, others, othersCount);
}
void physicsWorldResolveAxisX(
const physicsworld_t *world, physicsbody_t *body, const float_t dt
const physicsworld_t *world,
physicsbody_t *body,
const float_t dt,
physicsbody_t * const *others,
const uint32_t othersCount
) {
assertNotNull(world, "world must not be null");
assertNotNull(body, "body must not be null");
@@ -83,6 +91,7 @@ void physicsWorldResolveAxisX(
if(blocked) {
body->position[0] = (float_t)col - body->extents[0];
body->velocity[0] = 0.0f;
physicsWorldResolveBodyOverlap(body, 0, others, othersCount);
return;
}
}
@@ -108,16 +117,23 @@ void physicsWorldResolveAxisX(
if(blocked) {
body->position[0] = (float_t)(col + 1);
body->velocity[0] = 0.0f;
physicsWorldResolveBodyOverlap(body, 0, others, othersCount);
return;
}
}
body->position[0] += vx * dt;
}
physicsWorldResolveBodyOverlap(body, 0, others, othersCount);
}
void physicsWorldResolveAxisY(
const physicsworld_t *world, physicsbody_t *body, const float_t dt
const physicsworld_t *world,
physicsbody_t *body,
const float_t dt,
physicsbody_t * const *others,
const uint32_t othersCount
) {
assertNotNull(world, "world must not be null");
assertNotNull(body, "body must not be null");
@@ -153,6 +169,7 @@ void physicsWorldResolveAxisY(
if(blocked) {
body->position[1] = (float_t)row - body->extents[1];
body->velocity[1] = 0.0f;
physicsWorldResolveBodyOverlap(body, 1, others, othersCount);
return;
}
}
@@ -178,16 +195,23 @@ void physicsWorldResolveAxisY(
if(blocked) {
body->position[1] = (float_t)(row + 1);
body->velocity[1] = 0.0f;
physicsWorldResolveBodyOverlap(body, 1, others, othersCount);
return;
}
}
body->position[1] += vy * dt;
}
physicsWorldResolveBodyOverlap(body, 1, others, othersCount);
}
void physicsWorldResolveAxisZ(
const physicsworld_t *world, physicsbody_t *body, const float_t dt
const physicsworld_t *world,
physicsbody_t *body,
const float_t dt,
physicsbody_t * const *others,
const uint32_t othersCount
) {
assertNotNull(world, "world must not be null");
assertNotNull(body, "body must not be null");
@@ -244,4 +268,40 @@ void physicsWorldResolveAxisZ(
body->position[2] += vz * dt;
}
}
physicsWorldResolveBodyOverlap(body, 2, others, othersCount);
}
void physicsWorldResolveBodyOverlap(
physicsbody_t *body,
const uint8_t axis,
physicsbody_t * const *others,
const uint32_t othersCount
) {
assertNotNull(body, "body must not be null");
assertTrue(axis < 3, "axis must be 0, 1 or 2");
const float_t v = body->velocity[axis];
if(v == 0.0f) return;
for(uint32_t i = 0; i < othersCount; i++) {
physicsbody_t *other = others[i];
if(other == NULL || other == body) continue;
vec3 min, max;
physicsBodyGetBounds(body, min, max);
vec3 oMin, oMax;
physicsBodyGetBounds(other, oMin, oMax);
if(min[0] >= oMax[0] || max[0] <= oMin[0]) continue;
if(min[1] >= oMax[1] || max[1] <= oMin[1]) continue;
if(min[2] >= oMax[2] || max[2] <= oMin[2]) continue;
if(v > 0.0f) {
body->position[axis] -= max[axis] - oMin[axis];
} else {
body->position[axis] += oMax[axis] - min[axis];
}
body->velocity[axis] = 0.0f;
}
}
+60 -8
View File
@@ -41,7 +41,10 @@ void physicsWorldInit(
* Advances a body by one timestep: applies gravity, integrates velocity
* into position (semi-implicit Euler), and resolves collisions against
* the tile map one axis at a time (X, then Y, then Z), clamping position
* and zeroing velocity on any axis that hits a tile boundary.
* and zeroing velocity on any axis that hits a tile boundary. After each
* axis's tile resolution, also resolves overlap against any given other
* bodies (see physicsWorldResolveBodyOverlap) - so another body is
* treated as solid too, not just the tile map.
*
* Known limitations, deliberately out of scope for this very basic pass:
* - No ramp/slope support - ramp tiles are treated as flat walkable
@@ -52,28 +55,44 @@ void physicsWorldInit(
* - A single step can tunnel through an intervening solid Z layer if
* velocity.z * dt exceeds one grid unit; terminalVelocity bounds this
* but does not eliminate it for very small/thin floors.
* - Body-vs-body resolution only reacts to the axis currently being
* resolved - two bodies already overlapping on every axis, with no
* velocity on any of them, are never proactively separated.
*
* @param world Physics world configuration.
* @param body The body to step. Its position/velocity/grounded fields are
* updated in place.
* @param dt Timestep, in seconds (use DUSK_TIME_STEP for the fixed step).
* @param others Array of pointers to other bodies to treat as solid.
* Pass NULL (with othersCount 0) to resolve against the tile map only.
* @param othersCount Number of entries in others.
*/
void physicsWorldStep(
const physicsworld_t *world, physicsbody_t *body, const float_t dt
const physicsworld_t *world,
physicsbody_t *body,
const float_t dt,
physicsbody_t * const *others,
const uint32_t othersCount
);
/**
* Resolves the body's movement along the X axis for this step, clamping
* position and zeroing velocity.x if a non-walkable column blocks the
* move. Declared publicly as an internal step helper, not a stable public
* API on its own.
* position and zeroing velocity.x if a non-walkable column, or another
* body, blocks the move. Declared publicly as an internal step helper,
* not a stable public API on its own.
*
* @param world Physics world configuration.
* @param body The body to resolve.
* @param dt Timestep, in seconds.
* @param others Array of pointers to other bodies to treat as solid.
* @param othersCount Number of entries in others.
*/
void physicsWorldResolveAxisX(
const physicsworld_t *world, physicsbody_t *body, const float_t dt
const physicsworld_t *world,
physicsbody_t *body,
const float_t dt,
physicsbody_t * const *others,
const uint32_t othersCount
);
/**
@@ -83,9 +102,15 @@ void physicsWorldResolveAxisX(
* @param world Physics world configuration.
* @param body The body to resolve.
* @param dt Timestep, in seconds.
* @param others Array of pointers to other bodies to treat as solid.
* @param othersCount Number of entries in others.
*/
void physicsWorldResolveAxisY(
const physicsworld_t *world, physicsbody_t *body, const float_t dt
const physicsworld_t *world,
physicsbody_t *body,
const float_t dt,
physicsbody_t * const *others,
const uint32_t othersCount
);
/**
@@ -96,7 +121,34 @@ void physicsWorldResolveAxisY(
* @param world Physics world configuration.
* @param body The body to resolve.
* @param dt Timestep, in seconds.
* @param others Array of pointers to other bodies to treat as solid.
* @param othersCount Number of entries in others.
*/
void physicsWorldResolveAxisZ(
const physicsworld_t *world, physicsbody_t *body, const float_t dt
const physicsworld_t *world,
physicsbody_t *body,
const float_t dt,
physicsbody_t * const *others,
const uint32_t othersCount
);
/**
* Resolves the body's overlap against a set of other bodies along a
* single axis: if the body's bounds fully overlap (on all 3 axes)
* another body's bounds, pushes the body back along the given axis by
* the overlap depth on that axis, in the opposite direction of its
* current velocity on that axis, and zeroes that velocity component.
* Does nothing if the body has no velocity on that axis, since there is
* then no direction to know which way to push it out.
*
* @param body The body to resolve.
* @param axis The axis to resolve overlap on (0 = x, 1 = y, 2 = z).
* @param others Array of pointers to other bodies to check against.
* @param othersCount Number of entries in others.
*/
void physicsWorldResolveBodyOverlap(
physicsbody_t *body,
const uint8_t axis,
physicsbody_t * const *others,
const uint32_t othersCount
);
+6
View File
@@ -32,6 +32,12 @@ errorret_t rpgInit(void) {
memoryZero(ENTITIES, sizeof(ENTITIES));
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
physicsWorldInit(
&ENTITY_PHYSICS_WORLD,
PHYSICS_WORLD_GRAVITY_DEFAULT,
PHYSICS_WORLD_TERMINAL_VELOCITY_DEFAULT
);
backpackInit();
cutsceneSystemInit();
+16
View File
@@ -44,5 +44,21 @@ errorret_t uiPlayerPosDraw() {
errorChain(textDraw(
(float_t)SCREEN.scanX, y, text, COLOR_GREEN, &FONT_DEFAULT
));
char_t velocityText[64];
snprintf(
velocityText,
sizeof(velocityText),
"vel %.2f,%.2f,%.2f",
player->body.velocity[0],
player->body.velocity[1],
player->body.velocity[2]
);
float_t velocityY = y + (float_t)FONT_DEFAULT.tileset->tileHeight;
errorChain(textDraw(
(float_t)SCREEN.scanX, velocityY, velocityText, COLOR_GREEN, &FONT_DEFAULT
));
return spriteBatchFlush();
}
+2 -1
View File
@@ -10,4 +10,5 @@ dusktest(test_rpg.c)
# Subdirs
add_subdirectory(overworld)
add_subdirectory(physics)
add_subdirectory(physics)
add_subdirectory(entity)
+12
View File
@@ -0,0 +1,12 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(dusktest)
# Tests
dusktest(test_entitydir.c)
dusktest(test_entity.c)
# Subdirs
+105
View File
@@ -0,0 +1,105 @@
/**
* 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 "rpg/entity/entity.h"
static void testEntitiesReset(void) {
memoryZero(ENTITIES, sizeof(ENTITIES));
}
static void testEntityPlace(
const uint8_t index,
const entitytype_t type,
const vec3 position,
const entitydir_t direction
) {
entityInit(&ENTITIES[index], type);
ENTITIES[index].direction = direction;
const vec3 extents = ENTITY_PHYSICS_EXTENTS_DEFAULT;
physicsBodyInit(&ENTITIES[index].body, position, extents);
}
static void test_entityGetFacingFindsEntityAhead(void **state) {
testEntitiesReset();
testEntityPlace(0, ENTITY_TYPE_PLAYER, (vec3){ 0.0f, 0.0f, 0.0f },
ENTITY_DIR_EAST);
testEntityPlace(1, ENTITY_TYPE_NPC, (vec3){ 1.0f, 0.0f, 0.0f },
ENTITY_DIR_NORTH);
entity_t *result = entityGetFacing(&ENTITIES[0], ENTITY_INTERACT_RANGE);
assert_ptr_equal(result, &ENTITIES[1]);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityGetFacingIgnoresEntityBehind(void **state) {
testEntitiesReset();
testEntityPlace(0, ENTITY_TYPE_PLAYER, (vec3){ 0.0f, 0.0f, 0.0f },
ENTITY_DIR_EAST);
testEntityPlace(1, ENTITY_TYPE_NPC, (vec3){ -2.0f, 0.0f, 0.0f },
ENTITY_DIR_NORTH);
entity_t *result = entityGetFacing(&ENTITIES[0], ENTITY_INTERACT_RANGE);
assert_null(result);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityGetFacingIgnoresEntityToSide(void **state) {
testEntitiesReset();
testEntityPlace(0, ENTITY_TYPE_PLAYER, (vec3){ 0.0f, 0.0f, 0.0f },
ENTITY_DIR_EAST);
testEntityPlace(1, ENTITY_TYPE_NPC, (vec3){ 0.0f, 3.0f, 0.0f },
ENTITY_DIR_NORTH);
entity_t *result = entityGetFacing(&ENTITIES[0], ENTITY_INTERACT_RANGE);
assert_null(result);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityGetFacingReturnsNullWhenNothingInRange(void **state) {
testEntitiesReset();
testEntityPlace(0, ENTITY_TYPE_PLAYER, (vec3){ 0.0f, 0.0f, 0.0f },
ENTITY_DIR_EAST);
entity_t *result = entityGetFacing(&ENTITIES[0], ENTITY_INTERACT_RANGE);
assert_null(result);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityGetFacingPicksNearest(void **state) {
testEntitiesReset();
testEntityPlace(0, ENTITY_TYPE_PLAYER, (vec3){ 0.0f, 0.0f, 0.0f },
ENTITY_DIR_EAST);
// Farther candidate placed at the lower array index, to prove the
// result is chosen by distance, not array/insertion order.
testEntityPlace(1, ENTITY_TYPE_NPC, (vec3){ 1.5f, 0.0f, 0.0f },
ENTITY_DIR_NORTH);
testEntityPlace(2, ENTITY_TYPE_NPC, (vec3){ 1.0f, 0.0f, 0.0f },
ENTITY_DIR_NORTH);
entity_t *result = entityGetFacing(&ENTITIES[0], ENTITY_INTERACT_RANGE);
assert_ptr_equal(result, &ENTITIES[2]);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_entityGetFacingFindsEntityAhead),
cmocka_unit_test(test_entityGetFacingIgnoresEntityBehind),
cmocka_unit_test(test_entityGetFacingIgnoresEntityToSide),
cmocka_unit_test(test_entityGetFacingReturnsNullWhenNothingInRange),
cmocka_unit_test(test_entityGetFacingPicksNearest),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+84
View File
@@ -0,0 +1,84 @@
/**
* 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 "rpg/entity/entitydir.h"
static void test_entityDirToVec2(void **state) {
vec2 out;
entityDirToVec2(ENTITY_DIR_NORTH, out);
assert_float_equal(out[0], 0.0f, 0.0001f);
assert_float_equal(out[1], 1.0f, 0.0001f);
entityDirToVec2(ENTITY_DIR_EAST, out);
assert_float_equal(out[0], 1.0f, 0.0001f);
assert_float_equal(out[1], 0.0f, 0.0001f);
entityDirToVec2(ENTITY_DIR_SOUTH, out);
assert_float_equal(out[0], 0.0f, 0.0001f);
assert_float_equal(out[1], -1.0f, 0.0001f);
entityDirToVec2(ENTITY_DIR_WEST, out);
assert_float_equal(out[0], -1.0f, 0.0001f);
assert_float_equal(out[1], 0.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityDirFromVec2Cardinal(void **state) {
assert_int_equal(
entityDirFromVec2((vec2){ 0.0f, 1.0f }), ENTITY_DIR_NORTH
);
assert_int_equal(
entityDirFromVec2((vec2){ 1.0f, 0.0f }), ENTITY_DIR_EAST
);
assert_int_equal(
entityDirFromVec2((vec2){ 0.0f, -1.0f }), ENTITY_DIR_SOUTH
);
assert_int_equal(
entityDirFromVec2((vec2){ -1.0f, 0.0f }), ENTITY_DIR_WEST
);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_entityDirFromVec2DominantAxis(void **state) {
// |x| > |y| picks east/west regardless of y's sign.
assert_int_equal(
entityDirFromVec2((vec2){ 2.0f, 1.0f }), ENTITY_DIR_EAST
);
assert_int_equal(
entityDirFromVec2((vec2){ -2.0f, -1.0f }), ENTITY_DIR_WEST
);
// |y| >= |x| picks north/south.
assert_int_equal(
entityDirFromVec2((vec2){ 1.0f, 2.0f }), ENTITY_DIR_NORTH
);
assert_int_equal(
entityDirFromVec2((vec2){ -1.0f, -3.0f }), ENTITY_DIR_SOUTH
);
// Exact diagonal ties break toward north/south.
assert_int_equal(
entityDirFromVec2((vec2){ 0.7f, 0.7f }), ENTITY_DIR_NORTH
);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_entityDirToVec2),
cmocka_unit_test(test_entityDirFromVec2Cardinal),
cmocka_unit_test(test_entityDirFromVec2DominantAxis),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+1
View File
@@ -6,5 +6,6 @@
include(dusktest)
# Tests
dusktest(test_maparea.c)
# Subdirs
+126
View File
@@ -0,0 +1,126 @@
/**
* 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 "rpg/overworld/maparea.h"
static uint32_t TEST_ENTER_COUNT;
static uint32_t TEST_STEP_COUNT;
static uint32_t TEST_EXIT_COUNT;
static void testAreaCallback(entity_t *entity, const uint8_t trigger) {
if(trigger == MAP_TRIGGER_ENTER) TEST_ENTER_COUNT++;
else if(trigger == MAP_TRIGGER_STEP) TEST_STEP_COUNT++;
else if(trigger == MAP_TRIGGER_EXIT) TEST_EXIT_COUNT++;
}
static void testMapAreaReset(void) {
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
TEST_ENTER_COUNT = 0;
TEST_STEP_COUNT = 0;
TEST_EXIT_COUNT = 0;
}
static entity_t testEntityAt(const entitytype_t type, const worldpos_t pos) {
entity_t entity;
memoryZero(&entity, sizeof(entity));
entity.id = 0;
entity.type = type;
entity.position = pos;
return entity;
}
static void test_mapAreaCheckEntityFiresEnterAndExit(void **state) {
testMapAreaReset();
mapAreaAdd(
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
testAreaCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
);
entity_t entity = testEntityAt(ENTITY_TYPE_PLAYER, (worldpos_t){ 10, 10, 0 });
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_ENTER_COUNT, 0);
entity.position = (worldpos_t){ 2, 2, 0 };
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_ENTER_COUNT, 1);
assert_int_equal(TEST_STEP_COUNT, 0);
entity.position = (worldpos_t){ 10, 10, 0 };
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_EXIT_COUNT, 1);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_mapAreaCheckEntityStepFiresOncePerNewTile(void **state) {
testMapAreaReset();
mapAreaAdd(
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
testAreaCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL
);
entity_t entity = testEntityAt(ENTITY_TYPE_PLAYER, (worldpos_t){ 1, 1, 0 });
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_ENTER_COUNT, 1);
assert_int_equal(TEST_STEP_COUNT, 0);
// Same tile, checked repeatedly (simulating multiple frames without the
// entity moving) - STEP must not fire.
mapAreaCheckEntity(&entity);
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_STEP_COUNT, 0);
// Moves to a new tile, still inside - STEP fires once.
entity.position = (worldpos_t){ 2, 1, 0 };
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_STEP_COUNT, 1);
// Same new tile again over multiple calls - no additional STEP.
mapAreaCheckEntity(&entity);
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_STEP_COUNT, 1);
// Another new tile - STEP fires again.
entity.position = (worldpos_t){ 3, 1, 0 };
mapAreaCheckEntity(&entity);
assert_int_equal(TEST_STEP_COUNT, 2);
assert_int_equal(TEST_ENTER_COUNT, 1);
assert_int_equal(TEST_EXIT_COUNT, 0);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_mapAreaCheckEntityNotifyFlagsRespected(void **state) {
testMapAreaReset();
mapAreaAdd(
(worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 },
testAreaCallback, MAP_AREA_NOTIFY_PLAYER, MAP_TRIGGER_ALL
);
entity_t npc = testEntityAt(ENTITY_TYPE_NPC, (worldpos_t){ 1, 1, 0 });
mapAreaCheckEntity(&npc);
assert_int_equal(TEST_ENTER_COUNT, 0);
entity_t player = testEntityAt(ENTITY_TYPE_PLAYER, (worldpos_t){ 1, 1, 0 });
mapAreaCheckEntity(&player);
assert_int_equal(TEST_ENTER_COUNT, 1);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_mapAreaCheckEntityFiresEnterAndExit),
cmocka_unit_test(test_mapAreaCheckEntityStepFiresOncePerNewTile),
cmocka_unit_test(test_mapAreaCheckEntityNotifyFlagsRespected),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+97 -9
View File
@@ -61,7 +61,7 @@ static void test_physicsWorldStepStraightLineNoObstacles(void **state) {
const uint32_t steps = 10;
for(uint32_t i = 0; i < steps; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
}
assert_float_equal(body.position[0], 1.0f * steps * DUSK_TIME_STEP, 0.001f);
@@ -89,7 +89,7 @@ static void test_physicsWorldStepBlockedHorizontally(void **state) {
body.velocity[0] = 5.0f;
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
}
assert_float_equal(body.position[0], 2.0f, 0.0001f);
@@ -97,7 +97,7 @@ static void test_physicsWorldStepBlockedHorizontally(void **state) {
// Further steps must not push it past the wall.
for(uint32_t i = 0; i < 5; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
assert_float_equal(body.position[0], 2.0f, 0.0001f);
}
@@ -117,7 +117,7 @@ static void test_physicsWorldStepGravitySettlesOnFloor(void **state) {
physicsBodyInit(&body, position, extents);
for(uint32_t i = 0; i < 200; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
assert_true(body.position[2] >= -0.0001f);
}
@@ -142,7 +142,7 @@ static void test_physicsWorldStepFallsThroughHole(void **state) {
for(uint32_t i = 0; i < 50; i++) {
const float_t previousZ = body.position[2];
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
assert_true(body.position[2] < previousZ);
assert_false(body.grounded);
}
@@ -163,7 +163,7 @@ static void test_physicsWorldStepTerminalVelocityClamp(void **state) {
physicsBodyInit(&body, position, extents);
for(uint32_t i = 0; i < 300; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
assert_true(fabsf(body.velocity[2]) <= world.terminalVelocity + 0.0001f);
}
@@ -186,7 +186,7 @@ static void test_physicsWorldStepBlockedByCeiling(void **state) {
body.velocity[2] = 1.0f;
for(uint32_t i = 0; i < 200; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
}
assert_float_equal(body.position[2], 1.0f, 0.0001f);
@@ -215,7 +215,7 @@ static void test_physicsWorldStepMultiColumnFootprint(void **state) {
body.velocity[0] = 5.0f;
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
}
assert_float_equal(body.position[0], 1.0f, 0.0001f);
@@ -242,7 +242,7 @@ static void test_physicsWorldStepRampTreatedAsFlatGround(void **state) {
body.velocity[0] = 5.0f;
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
assert_float_equal(body.position[2], 0.0f, 0.0001f);
}
@@ -252,6 +252,91 @@ static void test_physicsWorldStepRampTreatedAsFlatGround(void **state) {
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepBlockedByOtherBody(void **state) {
testMapReset();
for(worldunit_t x = 0; x < 10; x++) {
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
}
physicsworld_t world;
physicsWorldInit(&world, 0.0f, 40.0f);
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsbody_t other;
physicsBodyInit(&other, (vec3){ 5.0f, 0.0f, 0.0f }, extents);
physicsbody_t body;
physicsBodyInit(&body, (vec3){ 0.0f, 0.0f, 0.0f }, extents);
body.velocity[0] = 5.0f;
physicsbody_t *others[] = { &other };
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, others, 1);
}
// Other occupies [5,6) - body (1 unit wide) should stop exactly
// touching it, at x=4.
assert_float_equal(body.position[0], 4.0f, 0.0001f);
assert_float_equal(body.velocity[0], 0.0f, 0.0001f);
assert_float_equal(other.position[0], 5.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldStepBlockedByNearestOfMultipleBodies(
void **state
) {
testMapReset();
for(worldunit_t x = 0; x < 20; x++) {
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
}
physicsworld_t world;
physicsWorldInit(&world, 0.0f, 40.0f);
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsbody_t nearOther;
physicsBodyInit(&nearOther, (vec3){ 5.0f, 0.0f, 0.0f }, extents);
physicsbody_t farOther;
physicsBodyInit(&farOther, (vec3){ 15.0f, 0.0f, 0.0f }, extents);
physicsbody_t body;
physicsBodyInit(&body, (vec3){ 0.0f, 0.0f, 0.0f }, extents);
body.velocity[0] = 5.0f;
physicsbody_t *others[] = { &farOther, &nearOther };
for(uint32_t i = 0; i < 60; i++) {
physicsWorldStep(&world, &body, DUSK_TIME_STEP, others, 2);
}
assert_float_equal(body.position[0], 4.0f, 0.0001f);
assert_float_equal(body.velocity[0], 0.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_physicsWorldResolveBodyOverlapIgnoresSelfAndNull(
void **state
) {
const vec3 extents = { 1.0f, 1.0f, 1.0f };
physicsbody_t body;
physicsBodyInit(&body, (vec3){ 0.0f, 0.0f, 0.0f }, extents);
body.velocity[0] = 1.0f;
physicsbody_t *others[] = { &body, NULL };
physicsWorldResolveBodyOverlap(&body, 0, others, 2);
assert_float_equal(body.position[0], 0.0f, 0.0001f);
assert_float_equal(body.velocity[0], 1.0f, 0.0001f);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_physicsWorldStepStraightLineNoObstacles),
@@ -262,6 +347,9 @@ int main(void) {
cmocka_unit_test(test_physicsWorldStepBlockedByCeiling),
cmocka_unit_test(test_physicsWorldStepMultiColumnFootprint),
cmocka_unit_test(test_physicsWorldStepRampTreatedAsFlatGround),
cmocka_unit_test(test_physicsWorldStepBlockedByOtherBody),
cmocka_unit_test(test_physicsWorldStepBlockedByNearestOfMultipleBodies),
cmocka_unit_test(test_physicsWorldResolveBodyOverlapIgnoresSelfAndNull),
};
return cmocka_run_group_tests(tests, NULL, NULL);