Roadmap and physics
This commit is contained in:
+1
-1
@@ -9,7 +9,7 @@ add_subdirectory(error)
|
||||
add_subdirectory(network)
|
||||
add_subdirectory(thread)
|
||||
add_subdirectory(display)
|
||||
# add_subdirectory(rpg)
|
||||
add_subdirectory(rpg)
|
||||
# add_subdirectory(item)
|
||||
add_subdirectory(time)
|
||||
add_subdirectory(util)
|
||||
@@ -9,4 +9,5 @@ include(dusktest)
|
||||
dusktest(test_rpg.c)
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(overworld)
|
||||
add_subdirectory(overworld)
|
||||
add_subdirectory(physics)
|
||||
@@ -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_physicsbody.c)
|
||||
dusktest(test_physicsworld.c)
|
||||
|
||||
# Subdirs
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 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/physics/physicsbody.h"
|
||||
|
||||
static void test_physicsBodyInit(void **state) {
|
||||
physicsbody_t body;
|
||||
const vec3 position = { 1.0f, 2.0f, 3.0f };
|
||||
const vec3 extents = { 1.0f, 2.0f, 3.0f };
|
||||
physicsBodyInit(&body, position, extents);
|
||||
|
||||
assert_float_equal(body.position[0], 1.0f, 0.0001f);
|
||||
assert_float_equal(body.position[1], 2.0f, 0.0001f);
|
||||
assert_float_equal(body.position[2], 3.0f, 0.0001f);
|
||||
assert_float_equal(body.extents[0], 1.0f, 0.0001f);
|
||||
assert_float_equal(body.extents[1], 2.0f, 0.0001f);
|
||||
assert_float_equal(body.extents[2], 3.0f, 0.0001f);
|
||||
assert_float_equal(body.velocity[0], 0.0f, 0.0001f);
|
||||
assert_float_equal(body.velocity[1], 0.0f, 0.0001f);
|
||||
assert_float_equal(body.velocity[2], 0.0f, 0.0001f);
|
||||
assert_false(body.grounded);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_physicsBodyGetBounds(void **state) {
|
||||
physicsbody_t body;
|
||||
const vec3 position = { 2.0f, 3.0f, 4.0f };
|
||||
const vec3 extents = { 1.0f, 2.0f, 0.5f };
|
||||
physicsBodyInit(&body, position, extents);
|
||||
|
||||
vec3 min, max;
|
||||
physicsBodyGetBounds(&body, min, max);
|
||||
|
||||
assert_float_equal(min[0], 2.0f, 0.0001f);
|
||||
assert_float_equal(min[1], 3.0f, 0.0001f);
|
||||
assert_float_equal(min[2], 4.0f, 0.0001f);
|
||||
assert_float_equal(max[0], 3.0f, 0.0001f);
|
||||
assert_float_equal(max[1], 5.0f, 0.0001f);
|
||||
assert_float_equal(max[2], 4.5f, 0.0001f);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_physicsBodyGetBoundsNegativeCoordinates(void **state) {
|
||||
physicsbody_t body;
|
||||
const vec3 position = { -5.0f, -1.5f, -2.0f };
|
||||
const vec3 extents = { 2.0f, 1.0f, 1.0f };
|
||||
physicsBodyInit(&body, position, extents);
|
||||
|
||||
vec3 min, max;
|
||||
physicsBodyGetBounds(&body, min, max);
|
||||
|
||||
assert_float_equal(min[0], -5.0f, 0.0001f);
|
||||
assert_float_equal(min[1], -1.5f, 0.0001f);
|
||||
assert_float_equal(min[2], -2.0f, 0.0001f);
|
||||
assert_float_equal(max[0], -3.0f, 0.0001f);
|
||||
assert_float_equal(max[1], -0.5f, 0.0001f);
|
||||
assert_float_equal(max[2], -1.0f, 0.0001f);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_physicsBodyInit),
|
||||
cmocka_unit_test(test_physicsBodyGetBounds),
|
||||
cmocka_unit_test(test_physicsBodyGetBoundsNegativeCoordinates),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* 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 "time/time.h"
|
||||
#include "rpg/physics/physicsbody.h"
|
||||
#include "rpg/physics/physicsworld.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
#include "rpg/overworld/tile.h"
|
||||
#include "rpg/overworld/tileshape.h"
|
||||
#include "rpg/overworld/worldpos.h"
|
||||
|
||||
static void testMapReset(void) {
|
||||
memoryZero(&MAP, sizeof(map_t));
|
||||
MAP.loaded = true;
|
||||
MAP.chunkPosition = (chunkpos_t){ 0, 0, 0 };
|
||||
|
||||
// Push every chunk but the first out of the loaded window, so only
|
||||
// MAP.chunks[0] (positioned at the origin below) resolves through
|
||||
// mapRebuildChunkOrder - otherwise every chunk would default to
|
||||
// position (0,0,0) too and collide on the same chunk order slot.
|
||||
for(chunkindex_t i = 1; i < MAP_CHUNK_COUNT; i++) {
|
||||
MAP.chunks[i].position = (chunkpos_t){ 100, 100, 100 };
|
||||
}
|
||||
MAP.chunks[0].position = (chunkpos_t){ 0, 0, 0 };
|
||||
|
||||
mapRebuildChunkOrder();
|
||||
}
|
||||
|
||||
static void testMapSetTile(
|
||||
const worldunit_t x,
|
||||
const worldunit_t y,
|
||||
const worldunit_t z,
|
||||
const tileshape_t shape
|
||||
) {
|
||||
const worldpos_t pos = { x, y, z };
|
||||
const chunktileindex_t index = worldPosToChunkTileIndex(&pos);
|
||||
const uint8_t localZ = worldPosToChunkLocalZ(&pos);
|
||||
MAP.chunks[0].tiles[index] = (tile_t){ .shape = shape, .z = localZ };
|
||||
}
|
||||
|
||||
static void test_physicsWorldStepStraightLineNoObstacles(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);
|
||||
|
||||
physicsbody_t body;
|
||||
const vec3 position = { 0.0f, 0.0f, 0.0f };
|
||||
const vec3 extents = { 1.0f, 1.0f, 1.0f };
|
||||
physicsBodyInit(&body, position, extents);
|
||||
body.velocity[0] = 1.0f;
|
||||
|
||||
const uint32_t steps = 10;
|
||||
for(uint32_t i = 0; i < steps; i++) {
|
||||
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
|
||||
}
|
||||
|
||||
assert_float_equal(body.position[0], 1.0f * steps * DUSK_TIME_STEP, 0.001f);
|
||||
assert_float_equal(body.position[1], 0.0f, 0.0001f);
|
||||
assert_float_equal(body.position[2], 0.0f, 0.0001f);
|
||||
assert_float_equal(body.velocity[0], 1.0f, 0.0001f);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_physicsWorldStepBlockedHorizontally(void **state) {
|
||||
testMapReset();
|
||||
testMapSetTile(0, 0, 0, TILE_SHAPE_GROUND);
|
||||
testMapSetTile(1, 0, 0, TILE_SHAPE_GROUND);
|
||||
testMapSetTile(2, 0, 0, TILE_SHAPE_GROUND);
|
||||
// x = 3 left as TILE_SHAPE_NULL, acting as a wall.
|
||||
|
||||
physicsworld_t world;
|
||||
physicsWorldInit(&world, 0.0f, 40.0f);
|
||||
|
||||
physicsbody_t body;
|
||||
const vec3 position = { 0.0f, 0.0f, 0.0f };
|
||||
const vec3 extents = { 1.0f, 1.0f, 1.0f };
|
||||
physicsBodyInit(&body, position, extents);
|
||||
body.velocity[0] = 5.0f;
|
||||
|
||||
for(uint32_t i = 0; i < 60; i++) {
|
||||
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
|
||||
}
|
||||
|
||||
assert_float_equal(body.position[0], 2.0f, 0.0001f);
|
||||
assert_float_equal(body.velocity[0], 0.0f, 0.0001f);
|
||||
|
||||
// Further steps must not push it past the wall.
|
||||
for(uint32_t i = 0; i < 5; i++) {
|
||||
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
|
||||
assert_float_equal(body.position[0], 2.0f, 0.0001f);
|
||||
}
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_physicsWorldStepGravitySettlesOnFloor(void **state) {
|
||||
testMapReset();
|
||||
testMapSetTile(0, 0, 0, TILE_SHAPE_GROUND);
|
||||
|
||||
physicsworld_t world;
|
||||
physicsWorldInit(&world, 20.0f, 40.0f);
|
||||
|
||||
physicsbody_t body;
|
||||
const vec3 position = { 0.0f, 0.0f, 3.0f };
|
||||
const vec3 extents = { 1.0f, 1.0f, 1.0f };
|
||||
physicsBodyInit(&body, position, extents);
|
||||
|
||||
for(uint32_t i = 0; i < 200; i++) {
|
||||
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
|
||||
assert_true(body.position[2] >= -0.0001f);
|
||||
}
|
||||
|
||||
assert_float_equal(body.position[2], 0.0f, 0.0001f);
|
||||
assert_float_equal(body.velocity[2], 0.0f, 0.0001f);
|
||||
assert_true(body.grounded);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_physicsWorldStepFallsThroughHole(void **state) {
|
||||
testMapReset();
|
||||
// No tiles set anywhere - every column is a hole.
|
||||
|
||||
physicsworld_t world;
|
||||
physicsWorldInit(&world, 20.0f, 40.0f);
|
||||
|
||||
physicsbody_t body;
|
||||
const vec3 position = { 0.0f, 0.0f, 5.0f };
|
||||
const vec3 extents = { 1.0f, 1.0f, 1.0f };
|
||||
physicsBodyInit(&body, position, extents);
|
||||
|
||||
for(uint32_t i = 0; i < 50; i++) {
|
||||
const float_t previousZ = body.position[2];
|
||||
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
|
||||
assert_true(body.position[2] < previousZ);
|
||||
assert_false(body.grounded);
|
||||
}
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_physicsWorldStepTerminalVelocityClamp(void **state) {
|
||||
testMapReset();
|
||||
// No tiles set anywhere - every column is a hole.
|
||||
|
||||
physicsworld_t world;
|
||||
physicsWorldInit(&world, 20.0f, 40.0f);
|
||||
|
||||
physicsbody_t body;
|
||||
const vec3 position = { 0.0f, 0.0f, 5.0f };
|
||||
const vec3 extents = { 1.0f, 1.0f, 1.0f };
|
||||
physicsBodyInit(&body, position, extents);
|
||||
|
||||
for(uint32_t i = 0; i < 300; i++) {
|
||||
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
|
||||
assert_true(fabsf(body.velocity[2]) <= world.terminalVelocity + 0.0001f);
|
||||
}
|
||||
|
||||
assert_float_equal(fabsf(body.velocity[2]), world.terminalVelocity, 0.01f);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_physicsWorldStepBlockedByCeiling(void **state) {
|
||||
testMapReset();
|
||||
testMapSetTile(0, 0, 2, TILE_SHAPE_GROUND);
|
||||
|
||||
physicsworld_t world;
|
||||
physicsWorldInit(&world, 0.0f, 40.0f);
|
||||
|
||||
physicsbody_t body;
|
||||
const vec3 position = { 0.0f, 0.0f, 0.0f };
|
||||
const vec3 extents = { 1.0f, 1.0f, 1.0f };
|
||||
physicsBodyInit(&body, position, extents);
|
||||
body.velocity[2] = 1.0f;
|
||||
|
||||
for(uint32_t i = 0; i < 200; i++) {
|
||||
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
|
||||
}
|
||||
|
||||
assert_float_equal(body.position[2], 1.0f, 0.0001f);
|
||||
assert_float_equal(body.velocity[2], 0.0f, 0.0001f);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_physicsWorldStepMultiColumnFootprint(void **state) {
|
||||
testMapReset();
|
||||
for(worldunit_t x = 0; x < 6; x++) {
|
||||
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
|
||||
}
|
||||
testMapSetTile(0, 1, 0, TILE_SHAPE_GROUND);
|
||||
testMapSetTile(1, 1, 0, TILE_SHAPE_GROUND);
|
||||
// x = 2, y = 1 left as TILE_SHAPE_NULL, blocking only the second row
|
||||
// spanned by the body's 2-unit-deep footprint.
|
||||
|
||||
physicsworld_t world;
|
||||
physicsWorldInit(&world, 0.0f, 40.0f);
|
||||
|
||||
physicsbody_t body;
|
||||
const vec3 position = { 0.0f, 0.0f, 0.0f };
|
||||
const vec3 extents = { 1.0f, 2.0f, 1.0f };
|
||||
physicsBodyInit(&body, position, extents);
|
||||
body.velocity[0] = 5.0f;
|
||||
|
||||
for(uint32_t i = 0; i < 60; i++) {
|
||||
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
|
||||
}
|
||||
|
||||
assert_float_equal(body.position[0], 1.0f, 0.0001f);
|
||||
assert_float_equal(body.velocity[0], 0.0f, 0.0001f);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_physicsWorldStepRampTreatedAsFlatGround(void **state) {
|
||||
testMapReset();
|
||||
testMapSetTile(0, 0, 0, TILE_SHAPE_GROUND);
|
||||
testMapSetTile(1, 0, 0, TILE_SHAPE_RAMP_EAST);
|
||||
for(worldunit_t x = 2; x < 6; x++) {
|
||||
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
|
||||
}
|
||||
|
||||
physicsworld_t world;
|
||||
physicsWorldInit(&world, 0.0f, 40.0f);
|
||||
|
||||
physicsbody_t body;
|
||||
const vec3 position = { 0.0f, 0.0f, 0.0f };
|
||||
const vec3 extents = { 1.0f, 1.0f, 1.0f };
|
||||
physicsBodyInit(&body, position, extents);
|
||||
body.velocity[0] = 5.0f;
|
||||
|
||||
for(uint32_t i = 0; i < 60; i++) {
|
||||
physicsWorldStep(&world, &body, DUSK_TIME_STEP);
|
||||
assert_float_equal(body.position[2], 0.0f, 0.0001f);
|
||||
}
|
||||
|
||||
assert_float_equal(body.velocity[0], 5.0f, 0.0001f);
|
||||
assert_true(body.position[0] > 1.5f);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_physicsWorldStepStraightLineNoObstacles),
|
||||
cmocka_unit_test(test_physicsWorldStepBlockedHorizontally),
|
||||
cmocka_unit_test(test_physicsWorldStepGravitySettlesOnFloor),
|
||||
cmocka_unit_test(test_physicsWorldStepFallsThroughHole),
|
||||
cmocka_unit_test(test_physicsWorldStepTerminalVelocityClamp),
|
||||
cmocka_unit_test(test_physicsWorldStepBlockedByCeiling),
|
||||
cmocka_unit_test(test_physicsWorldStepMultiColumnFootprint),
|
||||
cmocka_unit_test(test_physicsWorldStepRampTreatedAsFlatGround),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
Reference in New Issue
Block a user