Roadmap and physics
This commit is contained in:
@@ -14,4 +14,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
add_subdirectory(cutscene)
|
||||
add_subdirectory(entity)
|
||||
add_subdirectory(overworld)
|
||||
add_subdirectory(item)
|
||||
add_subdirectory(item)
|
||||
add_subdirectory(physics)
|
||||
@@ -0,0 +1,10 @@
|
||||
# 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
|
||||
physicsbody.c
|
||||
physicsworld.c
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "physicsbody.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
void physicsBodyInit(
|
||||
physicsbody_t *body, const vec3 position, const vec3 extents
|
||||
) {
|
||||
assertNotNull(body, "body must not be null");
|
||||
assertNotNull(position, "position must not be null");
|
||||
assertNotNull(extents, "extents must not be null");
|
||||
assertTrue(extents[0] > 0.0f, "extents.x must be greater than 0");
|
||||
assertTrue(extents[1] > 0.0f, "extents.y must be greater than 0");
|
||||
assertTrue(extents[2] > 0.0f, "extents.z must be greater than 0");
|
||||
|
||||
glm_vec3_copy((float_t *)position, body->position);
|
||||
glm_vec3_copy((float_t *)extents, body->extents);
|
||||
glm_vec3_zero(body->velocity);
|
||||
body->grounded = false;
|
||||
}
|
||||
|
||||
void physicsBodyGetBounds(
|
||||
const physicsbody_t *body, vec3 outMin, vec3 outMax
|
||||
) {
|
||||
assertNotNull(body, "body must not be null");
|
||||
assertNotNull(outMin, "outMin must not be null");
|
||||
assertNotNull(outMax, "outMax must not be null");
|
||||
|
||||
glm_vec3_copy((float_t *)body->position, outMin);
|
||||
glm_vec3_add((float_t *)body->position, (float_t *)body->extents, outMax);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "util/math.h"
|
||||
|
||||
typedef struct physicsbody_s {
|
||||
// Base/foot position, in raw grid units (same convention as worldpos_t -
|
||||
// 1.0 equals one tile in X/Y, one Z-layer in Z). Not pre-scaled by
|
||||
// WORLD_LAYER_HEIGHT, which is a cosmetic render-space value only.
|
||||
vec3 position;
|
||||
|
||||
// Velocity, in grid units per second, per axis.
|
||||
vec3 velocity;
|
||||
|
||||
// Full box size, in grid units. The box is anchored at position on all
|
||||
// three axes and extends in the positive direction - i.e. it occupies
|
||||
// [position, position + extents) - matching the tile grid's own
|
||||
// convention that tile n occupies [n, n + 1).
|
||||
vec3 extents;
|
||||
|
||||
// True if the last physicsWorldStep clamped a downward Z velocity
|
||||
// against a walkable tile beneath the body.
|
||||
bool_t grounded;
|
||||
} physicsbody_t;
|
||||
|
||||
/**
|
||||
* Initializes a physics body at the given position with the given extents.
|
||||
* Velocity is zeroed and grounded is set to false.
|
||||
*
|
||||
* @param body Pointer to the physics body to initialize.
|
||||
* @param position The initial position of the body.
|
||||
* @param extents The size of the body's collision box.
|
||||
*/
|
||||
void physicsBodyInit(
|
||||
physicsbody_t *body, const vec3 position, const vec3 extents
|
||||
);
|
||||
|
||||
/**
|
||||
* Computes the world-space min/max bounds of a physics body's collision
|
||||
* box, from its position and extents. The box is anchored at position
|
||||
* and extends in the positive direction on every axis, so outMin equals
|
||||
* position and outMax equals position + extents.
|
||||
*
|
||||
* @param body Pointer to the physics body.
|
||||
* @param outMin Output, set to the box's minimum corner.
|
||||
* @param outMax Output, set to the box's maximum corner.
|
||||
*/
|
||||
void physicsBodyGetBounds(
|
||||
const physicsbody_t *body, vec3 outMin, vec3 outMax
|
||||
);
|
||||
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "physicsworld.h"
|
||||
#include "assert/assert.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
#include "rpg/overworld/tile.h"
|
||||
#include "rpg/overworld/tileshape.h"
|
||||
#include "rpg/overworld/worldpos.h"
|
||||
|
||||
// Tolerance used when converting a float boundary coordinate into a tile
|
||||
// column/layer index, so that a coordinate sitting exactly on a tile
|
||||
// boundary is treated as belonging to the tile it is entering/leaving,
|
||||
// not the neighbour on the far side of the boundary.
|
||||
#define PHYSICS_EPSILON 0.0001f
|
||||
|
||||
void physicsWorldInit(
|
||||
physicsworld_t *world,
|
||||
const float_t gravity,
|
||||
const float_t terminalVelocity
|
||||
) {
|
||||
assertNotNull(world, "world must not be null");
|
||||
assertTrue(terminalVelocity > 0.0f, "terminalVelocity must be positive");
|
||||
|
||||
world->gravity = gravity;
|
||||
world->terminalVelocity = terminalVelocity;
|
||||
}
|
||||
|
||||
void physicsWorldStep(
|
||||
const physicsworld_t *world, physicsbody_t *body, const float_t dt
|
||||
) {
|
||||
assertNotNull(world, "world must not be null");
|
||||
assertNotNull(body, "body must not be null");
|
||||
|
||||
body->velocity[2] -= world->gravity * dt;
|
||||
body->velocity[2] = mathClamp(
|
||||
body->velocity[2], -world->terminalVelocity, world->terminalVelocity
|
||||
);
|
||||
|
||||
physicsWorldResolveAxisX(world, body, dt);
|
||||
physicsWorldResolveAxisY(world, body, dt);
|
||||
physicsWorldResolveAxisZ(world, body, dt);
|
||||
}
|
||||
|
||||
void physicsWorldResolveAxisX(
|
||||
const physicsworld_t *world, physicsbody_t *body, const float_t dt
|
||||
) {
|
||||
assertNotNull(world, "world must not be null");
|
||||
assertNotNull(body, "body must not be null");
|
||||
|
||||
const float_t vx = body->velocity[0];
|
||||
if(vx == 0.0f) return;
|
||||
|
||||
vec3 min, max;
|
||||
physicsBodyGetBounds(body, min, max);
|
||||
|
||||
const worldunit_t yStart = (worldunit_t)floorf(min[1] + PHYSICS_EPSILON);
|
||||
const worldunit_t yEnd = (worldunit_t)floorf(max[1] - PHYSICS_EPSILON);
|
||||
const worldunit_t layerZ =
|
||||
(worldunit_t)floorf(body->position[2] + PHYSICS_EPSILON);
|
||||
|
||||
if(vx > 0.0f) {
|
||||
const worldunit_t oldColMax =
|
||||
(worldunit_t)floorf(max[0] - PHYSICS_EPSILON);
|
||||
const float_t newMax = max[0] + vx * dt;
|
||||
const worldunit_t newColMax =
|
||||
(worldunit_t)floorf(newMax - PHYSICS_EPSILON);
|
||||
|
||||
for(worldunit_t col = oldColMax + 1; col <= newColMax; col++) {
|
||||
bool_t blocked = false;
|
||||
for(worldunit_t y = yStart; y <= yEnd; y++) {
|
||||
const worldpos_t pos = { col, y, layerZ };
|
||||
if(!tileShapeIsWalkable(mapGetTile(pos).shape)) {
|
||||
blocked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(blocked) {
|
||||
body->position[0] = (float_t)col - body->extents[0];
|
||||
body->velocity[0] = 0.0f;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
body->position[0] += vx * dt;
|
||||
} else {
|
||||
const worldunit_t oldColMin =
|
||||
(worldunit_t)floorf(min[0] + PHYSICS_EPSILON);
|
||||
const float_t newMin = min[0] + vx * dt;
|
||||
const worldunit_t newColMin =
|
||||
(worldunit_t)floorf(newMin + PHYSICS_EPSILON);
|
||||
|
||||
for(worldunit_t col = oldColMin - 1; col >= newColMin; col--) {
|
||||
bool_t blocked = false;
|
||||
for(worldunit_t y = yStart; y <= yEnd; y++) {
|
||||
const worldpos_t pos = { col, y, layerZ };
|
||||
if(!tileShapeIsWalkable(mapGetTile(pos).shape)) {
|
||||
blocked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(blocked) {
|
||||
body->position[0] = (float_t)(col + 1);
|
||||
body->velocity[0] = 0.0f;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
body->position[0] += vx * dt;
|
||||
}
|
||||
}
|
||||
|
||||
void physicsWorldResolveAxisY(
|
||||
const physicsworld_t *world, physicsbody_t *body, const float_t dt
|
||||
) {
|
||||
assertNotNull(world, "world must not be null");
|
||||
assertNotNull(body, "body must not be null");
|
||||
|
||||
const float_t vy = body->velocity[1];
|
||||
if(vy == 0.0f) return;
|
||||
|
||||
vec3 min, max;
|
||||
physicsBodyGetBounds(body, min, max);
|
||||
|
||||
const worldunit_t xStart = (worldunit_t)floorf(min[0] + PHYSICS_EPSILON);
|
||||
const worldunit_t xEnd = (worldunit_t)floorf(max[0] - PHYSICS_EPSILON);
|
||||
const worldunit_t layerZ =
|
||||
(worldunit_t)floorf(body->position[2] + PHYSICS_EPSILON);
|
||||
|
||||
if(vy > 0.0f) {
|
||||
const worldunit_t oldRowMax =
|
||||
(worldunit_t)floorf(max[1] - PHYSICS_EPSILON);
|
||||
const float_t newMax = max[1] + vy * dt;
|
||||
const worldunit_t newRowMax =
|
||||
(worldunit_t)floorf(newMax - PHYSICS_EPSILON);
|
||||
|
||||
for(worldunit_t row = oldRowMax + 1; row <= newRowMax; row++) {
|
||||
bool_t blocked = false;
|
||||
for(worldunit_t x = xStart; x <= xEnd; x++) {
|
||||
const worldpos_t pos = { x, row, layerZ };
|
||||
if(!tileShapeIsWalkable(mapGetTile(pos).shape)) {
|
||||
blocked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(blocked) {
|
||||
body->position[1] = (float_t)row - body->extents[1];
|
||||
body->velocity[1] = 0.0f;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
body->position[1] += vy * dt;
|
||||
} else {
|
||||
const worldunit_t oldRowMin =
|
||||
(worldunit_t)floorf(min[1] + PHYSICS_EPSILON);
|
||||
const float_t newMin = min[1] + vy * dt;
|
||||
const worldunit_t newRowMin =
|
||||
(worldunit_t)floorf(newMin + PHYSICS_EPSILON);
|
||||
|
||||
for(worldunit_t row = oldRowMin - 1; row >= newRowMin; row--) {
|
||||
bool_t blocked = false;
|
||||
for(worldunit_t x = xStart; x <= xEnd; x++) {
|
||||
const worldpos_t pos = { x, row, layerZ };
|
||||
if(!tileShapeIsWalkable(mapGetTile(pos).shape)) {
|
||||
blocked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(blocked) {
|
||||
body->position[1] = (float_t)(row + 1);
|
||||
body->velocity[1] = 0.0f;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
body->position[1] += vy * dt;
|
||||
}
|
||||
}
|
||||
|
||||
void physicsWorldResolveAxisZ(
|
||||
const physicsworld_t *world, physicsbody_t *body, const float_t dt
|
||||
) {
|
||||
assertNotNull(world, "world must not be null");
|
||||
assertNotNull(body, "body must not be null");
|
||||
|
||||
const float_t vz = body->velocity[2];
|
||||
if(vz == 0.0f) return;
|
||||
|
||||
vec3 min, max;
|
||||
physicsBodyGetBounds(body, min, max);
|
||||
|
||||
const worldunit_t xStart = (worldunit_t)floorf(min[0] + PHYSICS_EPSILON);
|
||||
const worldunit_t xEnd = (worldunit_t)floorf(max[0] - PHYSICS_EPSILON);
|
||||
const worldunit_t yStart = (worldunit_t)floorf(min[1] + PHYSICS_EPSILON);
|
||||
const worldunit_t yEnd = (worldunit_t)floorf(max[1] - PHYSICS_EPSILON);
|
||||
|
||||
if(vz < 0.0f) {
|
||||
const worldunit_t layer =
|
||||
(worldunit_t)floorf(body->position[2] + PHYSICS_EPSILON);
|
||||
|
||||
bool_t solid = true;
|
||||
for(worldunit_t x = xStart; x <= xEnd && solid; x++) {
|
||||
for(worldunit_t y = yStart; y <= yEnd && solid; y++) {
|
||||
const worldpos_t pos = { x, y, layer };
|
||||
if(!tileShapeIsWalkable(mapGetTile(pos).shape)) solid = false;
|
||||
}
|
||||
}
|
||||
|
||||
const float_t newZ = body->position[2] + vz * dt;
|
||||
if(solid && newZ < (float_t)layer) {
|
||||
body->position[2] = (float_t)layer;
|
||||
body->velocity[2] = 0.0f;
|
||||
body->grounded = true;
|
||||
} else {
|
||||
body->position[2] = newZ;
|
||||
body->grounded = false;
|
||||
}
|
||||
} else {
|
||||
const float_t head = max[2];
|
||||
const worldunit_t layer = (worldunit_t)ceilf(head - PHYSICS_EPSILON);
|
||||
|
||||
bool_t solid = true;
|
||||
for(worldunit_t x = xStart; x <= xEnd && solid; x++) {
|
||||
for(worldunit_t y = yStart; y <= yEnd && solid; y++) {
|
||||
const worldpos_t pos = { x, y, layer };
|
||||
if(!tileShapeIsWalkable(mapGetTile(pos).shape)) solid = false;
|
||||
}
|
||||
}
|
||||
|
||||
const float_t newHead = head + vz * dt;
|
||||
if(solid && newHead > (float_t)layer) {
|
||||
body->position[2] = (float_t)layer - body->extents[2];
|
||||
body->velocity[2] = 0.0f;
|
||||
} else {
|
||||
body->position[2] += vz * dt;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "physicsbody.h"
|
||||
|
||||
#define PHYSICS_WORLD_GRAVITY_DEFAULT 20.0f
|
||||
#define PHYSICS_WORLD_TERMINAL_VELOCITY_DEFAULT 40.0f
|
||||
|
||||
typedef struct physicsworld_s {
|
||||
// Downward acceleration applied to velocity.z every step, in grid units
|
||||
// per second squared.
|
||||
float_t gravity;
|
||||
|
||||
// Maximum magnitude velocity.z may reach while falling, in grid units
|
||||
// per second. A safety clamp bounding how far a single step can move,
|
||||
// to limit (not eliminate) the tunneling limitation documented on
|
||||
// physicsWorldStep.
|
||||
float_t terminalVelocity;
|
||||
} physicsworld_t;
|
||||
|
||||
/**
|
||||
* Initializes a physics world with the given gravity and terminal
|
||||
* velocity.
|
||||
*
|
||||
* @param world Pointer to the physics world to initialize.
|
||||
* @param gravity Downward acceleration applied every step.
|
||||
* @param terminalVelocity Maximum falling speed.
|
||||
*/
|
||||
void physicsWorldInit(
|
||||
physicsworld_t *world,
|
||||
const float_t gravity,
|
||||
const float_t terminalVelocity
|
||||
);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Known limitations, deliberately out of scope for this very basic pass:
|
||||
* - No ramp/slope support - ramp tiles are treated as flat walkable
|
||||
* ground at whatever Z layer they are queried at.
|
||||
* - No wall/hole distinction - horizontal movement treats any
|
||||
* non-walkable (or unloaded) column at the body's current Z layer as
|
||||
* a solid wall, rather than an edge to fall from.
|
||||
* - 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.
|
||||
*
|
||||
* @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).
|
||||
*/
|
||||
void physicsWorldStep(
|
||||
const physicsworld_t *world, physicsbody_t *body, const float_t dt
|
||||
);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param world Physics world configuration.
|
||||
* @param body The body to resolve.
|
||||
* @param dt Timestep, in seconds.
|
||||
*/
|
||||
void physicsWorldResolveAxisX(
|
||||
const physicsworld_t *world, physicsbody_t *body, const float_t dt
|
||||
);
|
||||
|
||||
/**
|
||||
* Resolves the body's movement along the Y axis for this step. See
|
||||
* physicsWorldResolveAxisX.
|
||||
*
|
||||
* @param world Physics world configuration.
|
||||
* @param body The body to resolve.
|
||||
* @param dt Timestep, in seconds.
|
||||
*/
|
||||
void physicsWorldResolveAxisY(
|
||||
const physicsworld_t *world, physicsbody_t *body, const float_t dt
|
||||
);
|
||||
|
||||
/**
|
||||
* Resolves the body's movement along the Z axis for this step, clamping
|
||||
* against walkable tile planes above and below the body and updating
|
||||
* grounded. See physicsWorldResolveAxisX.
|
||||
*
|
||||
* @param world Physics world configuration.
|
||||
* @param body The body to resolve.
|
||||
* @param dt Timestep, in seconds.
|
||||
*/
|
||||
void physicsWorldResolveAxisZ(
|
||||
const physicsworld_t *world, physicsbody_t *body, const float_t dt
|
||||
);
|
||||
Reference in New Issue
Block a user