Entropie git · main
Entropie festival game; first steps
C 88.5% Markdown 9.1% GLSL 2.3%git clone https://git.christianimmanuel.de/games/Entropie.gitwget https://git.christianimmanuel.de/games/Entropie/archive/Entropie.tar.gzentropie/src/world/nav.c raw
/*
* Walking map: a grid over the site in two levels - where one stands (the
* ground, or a building's floor or step) and, under a floor, the level below
* it (a basement). A cell of a level is blocked where something fixed stands
* between knee and head height over it. Ways are found with A* (8
* directions, no cutting corners, no more up or down from cell to cell than a
* step or two, from one level to the other where they meet: stairs) and then
* straightened: only the turns are kept.
*/
#include "world/nav.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
#define NAV_N ((int)(TERRAIN_SIZE / NAV_CELL))
#define NAV_MARGIN 0.18f /* clearance from walls (sideways): an 0.8 m landing or doorway still has a row of free cells */
#define STEP_OVER 0.25f /* lower than this above the ground: stepped over */
#define HEADROOM 1.8f /* higher than this: walked under */
#define MOST_STEPS 90000 /* cells looked at for one way, at most */
#define FREE_SEARCH 6 /* cells round a blocked start or end to find a free one */
#define WALK_ON_MOST 1.2f /* building floors and steps (OBJ_WALL) up to this over the natural ground are walked on ... */
#define NAV_RISE 0.38f /* ... from one cell to the next at most this much up or down (a stair: 1-2 steps) */
#define BELOW_ROOM 2.0f /* a level this far under the top one (or more): a second one there (a basement) */
#define NO_FLOOR -1e9f
#define N2 ((size_t)NAV_N * NAV_N) /* cells in one level; node = level * N2 + cell */
static unsigned char *g_blocked; /* [2 * N2] */
static float *g_floor; /* [2 * N2] where one stands: the ground, a floor or step of a building; NO_FLOOR: no lower level */
static bool g_ready;
static double g_signature = -1.0;
/* Search state (stamped, so nothing needs clearing between searches) */
static float *g_cost;
static int *g_from;
static unsigned *g_seen, *g_done;
static unsigned g_stamp;
typedef struct { float f; int cell; } HeapItem;
static HeapItem *g_heap;
static int g_heap_n, g_heap_cap;
static int cellOf(float v) { return (int)floorf((v + TERRAIN_SIZE * 0.5f) / NAV_CELL); }
static float centerOf(int i) { return ((float)i + 0.5f) * NAV_CELL - TERRAIN_SIZE * 0.5f; }
static bool inside(int x, int z) { return x >= 0 && z >= 0 && x < NAV_N && z < NAV_N; }
static int node(int level, int x, int z) { return (int)((size_t)level * N2) + z * NAV_N + x; }
static bool openAt(int level, int x, int z) { return inside(x, z) && !g_blocked[node(level, x, z)]; }
static bool open(int x, int z) { return openAt(0, x, z) || openAt(1, x, z); }
/* Point in the collider, with clearance sideways only (not up and down). */
static bool inCollider(const Object *o, int i, Vec3 p) {
const Collider *c = &o->colliders[i];
Vec3 rel = vec3_sub(p, Object_colliderCenter(o, i));
const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z };
for (int k = 0; k < 3; k++) {
float level = sqrtf(fmaxf(0.0f, 1.0f - c->axes[k].y * c->axes[k].y));
if (fabsf(vec3_dot(rel, c->axes[k])) > half[k] + NAV_MARGIN * level) return false;
}
return true;
}
/* Cells an object's colliders may block (with the clearance), clipped to the map. */
typedef struct { int x0, z0, x1, z1; } CellRect;
static CellRect boundsOf(const Object *o) {
CellRect r = { NAV_N, NAV_N, -1, -1 };
for (int i = 0; i < o->collider_count; i++) {
const Collider *c = &o->colliders[i];
Vec3 center = Object_colliderCenter(o, i);
float ex = 0.0f, ez = 0.0f;
const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z };
for (int k = 0; k < 3; k++) {
ex += fabsf(c->axes[k].x) * half[k];
ez += fabsf(c->axes[k].z) * half[k];
}
int x0 = cellOf(center.x - ex - NAV_MARGIN), x1 = cellOf(center.x + ex + NAV_MARGIN);
int z0 = cellOf(center.z - ez - NAV_MARGIN), z1 = cellOf(center.z + ez + NAV_MARGIN);
if (x0 < r.x0) r.x0 = x0;
if (z0 < r.z0) r.z0 = z0;
if (x1 > r.x1) r.x1 = x1;
if (z1 > r.z1) r.z1 = z1;
}
if (r.x0 < 0) r.x0 = 0;
if (r.z0 < 0) r.z0 = 0;
if (r.x1 >= NAV_N) r.x1 = NAV_N - 1;
if (r.z1 >= NAV_N) r.z1 = NAV_N - 1;
return r;
}
static bool overlaps(CellRect a, CellRect b) { return a.x0 <= b.x1 && b.x0 <= a.x1 && a.z0 <= b.z1 && b.z0 <= a.z1; }
/* A building's floor slab or step (level, its top within reach of the natural ground): stood on. */
static bool walkOn(const Object *o, int i, const Terrain *terrain, float *top) {
if (o->type != OBJ_WALL || o->variant == 3) return false; /* (building parts; not roofs) */
const Collider *c = &o->colliders[i];
if (fabsf(c->axes[1].y) < 0.99f || c->half_extents.x < 0.1f || c->half_extents.z < 0.1f) return false;
Vec3 center = Object_colliderCenter(o, i);
*top = center.y + c->half_extents.y;
return *top <= Terrain_naturalHeight(terrain, center.x, center.z) + WALK_ON_MOST;
}
static void raiseFloor(const Object *o, int i, const Terrain *terrain, CellRect clip, int level) {
float top;
if (!walkOn(o, i, terrain, &top)) return;
const Collider *c = &o->colliders[i];
Vec3 center = Object_colliderCenter(o, i);
float ex = fabsf(c->axes[0].x) * c->half_extents.x + fabsf(c->axes[2].x) * c->half_extents.z;
float ez = fabsf(c->axes[0].z) * c->half_extents.x + fabsf(c->axes[2].z) * c->half_extents.z;
int x0 = cellOf(center.x - ex), x1 = cellOf(center.x + ex), z0 = cellOf(center.z - ez), z1 = cellOf(center.z + ez);
if (x0 < clip.x0) x0 = clip.x0;
if (z0 < clip.z0) z0 = clip.z0;
if (x1 > clip.x1) x1 = clip.x1;
if (z1 > clip.z1) z1 = clip.z1;
for (int z = z0; z <= z1; z++)
for (int x = x0; x <= x1; x++) {
Vec3 rel = vec3_sub(vec3(centerOf(x), center.y, centerOf(z)), center);
if (fabsf(vec3_dot(rel, c->axes[0])) > c->half_extents.x || fabsf(vec3_dot(rel, c->axes[2])) > c->half_extents.z) continue;
float *f = &g_floor[node(level, x, z)];
if (level == 1 && top > g_floor[node(0, x, z)] - BELOW_ROOM) continue; /* (the level below: under the top one) */
if (top > *f) *f = top;
}
}
static void blockCollider(const Object *o, int i, CellRect clip) {
const Collider *c = &o->colliders[i];
Vec3 center = Object_colliderCenter(o, i);
float ex = 0.0f, ey = 0.0f, ez = 0.0f;
const float half[3] = { c->half_extents.x, c->half_extents.y, c->half_extents.z };
for (int k = 0; k < 3; k++) {
ex += fabsf(c->axes[k].x) * half[k];
ey += fabsf(c->axes[k].y) * half[k];
ez += fabsf(c->axes[k].z) * half[k];
}
int x0 = cellOf(center.x - ex - NAV_MARGIN), x1 = cellOf(center.x + ex + NAV_MARGIN);
int z0 = cellOf(center.z - ez - NAV_MARGIN), z1 = cellOf(center.z + ez + NAV_MARGIN);
if (x0 < clip.x0) x0 = clip.x0;
if (z0 < clip.z0) z0 = clip.z0;
if (x1 > clip.x1) x1 = clip.x1;
if (z1 > clip.z1) z1 = clip.z1;
for (int level = 0; level < 2; level++)
for (int z = z0; z <= z1; z++)
for (int x = x0; x <= x1; x++) {
int n = node(level, x, z);
if (g_blocked[n]) continue;
float px = centerOf(x), pz = centerOf(z);
float ground = g_floor[n];
if (center.y + ey < ground + STEP_OVER || center.y - ey > ground + HEADROOM) continue;
for (int k = 0; k < 3; k++)
if (inCollider(o, i, vec3(px, ground + 0.4f + 0.6f * (float)k, pz))) {
g_blocked[n] = 1;
break;
}
}
}
/* What each fixed thing was at the last build (by pointer, open addressing):
when something changes only the cells round it are done again. */
typedef struct { const Object *o; float x, z, yaw; int colliders; CellRect cells; unsigned seen; } NavEntry;
#define NAV_TABLE 16384
#define NAV_DIRTY 64 /* more changes at once than this: the whole map again */
static NavEntry *g_table, *g_table_new;
static unsigned g_build;
static NavEntry *slotFor(NavEntry *table, const Object *o, bool *found) {
size_t h = ((size_t)o >> 4) * 2654435761u;
for (size_t k = 0; k < NAV_TABLE; k++) {
NavEntry *e = &table[(h + k) & (NAV_TABLE - 1)];
if (e->o == o) { *found = true; return e; }
if (!e->o) { *found = false; return e; }
}
return NULL;
}
/* Clears the cells in r and blocks them again from every fixed thing over them. */
static void redo(Object *const *objects, int count, const Terrain *terrain, CellRect r) {
for (int z = r.z0; z <= r.z1; z++) {
memset(&g_blocked[node(0, r.x0, z)], 0, (size_t)(r.x1 - r.x0 + 1));
memset(&g_blocked[node(1, r.x0, z)], 0, (size_t)(r.x1 - r.x0 + 1));
for (int x = r.x0; x <= r.x1; x++) g_floor[node(0, x, z)] = Terrain_height(terrain, centerOf(x), centerOf(z));
}
for (int i = 0; i < count; i++) { /* floors and steps first: what's in the way is from there up */
const Object *o = objects[i];
if (!o->is_static || o->type != OBJ_WALL || !overlaps(boundsOf(o), r)) continue;
for (int c = 0; c < o->collider_count; c++) raiseFloor(o, c, terrain, r, 0);
}
/* The level below (a basement under a floor): the ground there, or a step, well under the top one */
for (int z = r.z0; z <= r.z1; z++)
for (int x = r.x0; x <= r.x1; x++) {
float ground = Terrain_height(terrain, centerOf(x), centerOf(z));
g_floor[node(1, x, z)] = ground <= g_floor[node(0, x, z)] - BELOW_ROOM ? ground : NO_FLOOR;
}
for (int i = 0; i < count; i++) {
const Object *o = objects[i];
if (!o->is_static || o->type != OBJ_WALL || !overlaps(boundsOf(o), r)) continue;
for (int c = 0; c < o->collider_count; c++) raiseFloor(o, c, terrain, r, 1);
}
for (int z = r.z0; z <= r.z1; z++)
for (int x = r.x0; x <= r.x1; x++)
if (g_floor[node(1, x, z)] == NO_FLOOR) g_blocked[node(1, x, z)] = 1;
for (int i = 0; i < count; i++) {
const Object *o = objects[i];
if (!o->is_static || o->collider_count == 0 || !overlaps(boundsOf(o), r)) continue;
for (int c = 0; c < o->collider_count; c++) blockCollider(o, c, r);
}
}
void Nav_update(Object *const *objects, int count, const Terrain *terrain) {
/* What the fixed things are and where: rebuild when that changes */
double signature = 0.0;
for (int i = 0; i < count; i++) {
const Object *o = objects[i];
if (!o->is_static || o->collider_count == 0) continue;
signature += 1.0 + (double)o->position.x * 0.731 + (double)o->position.z * 1.379 + (double)o->rotation.y * 0.17 +
(double)o->collider_count * 3.1;
}
if (g_ready && signature == g_signature) return;
if (!g_blocked) {
size_t n = 2 * N2;
g_blocked = calloc(n, 1);
g_floor = calloc(n, sizeof *g_floor);
g_cost = malloc(n * sizeof *g_cost);
g_from = malloc(n * sizeof *g_from);
g_seen = calloc(n, sizeof *g_seen);
g_done = calloc(n, sizeof *g_done);
g_heap_cap = MOST_STEPS * 8;
g_heap = malloc((size_t)g_heap_cap * sizeof *g_heap);
if (!g_blocked || !g_floor || !g_cost || !g_from || !g_seen || !g_done || !g_heap) return;
}
if (!g_table) {
g_table = calloc(NAV_TABLE, sizeof *g_table);
g_table_new = calloc(NAV_TABLE, sizeof *g_table_new);
if (!g_table || !g_table_new) return;
}
/* What changed since the last build: the cells it was on and is on now */
CellRect dirty[NAV_DIRTY];
int n_dirty = 0;
bool whole = !g_ready;
g_build++;
memset(g_table_new, 0, NAV_TABLE * sizeof *g_table_new);
for (int i = 0; i < count; i++) {
const Object *o = objects[i];
if (!o->is_static || o->collider_count == 0) continue;
bool found = false, was = false;
NavEntry *e = slotFor(g_table_new, o, &found);
if (!e) { whole = true; continue; }
NavEntry *old = slotFor(g_table, o, &was);
*e = (NavEntry){ o, o->position.x, o->position.z, o->rotation.y, o->collider_count, boundsOf(o), 0 };
if (was && old->x == e->x && old->z == e->z && old->yaw == e->yaw && old->colliders == e->colliders) { old->seen = g_build; continue; }
if (was) old->seen = g_build;
if (n_dirty + 2 > NAV_DIRTY) { whole = true; continue; }
if (was) dirty[n_dirty++] = old->cells;
dirty[n_dirty++] = e->cells;
}
for (int k = 0; k < NAV_TABLE && !whole; k++) { /* (gone since: its cells) */
const NavEntry *old = &g_table[k];
if (!old->o || old->seen == g_build) continue;
if (n_dirty + 1 > NAV_DIRTY) whole = true;
else dirty[n_dirty++] = old->cells;
}
NavEntry *swap = g_table;
g_table = g_table_new;
g_table_new = swap;
if (whole) {
CellRect all = { 0, 0, NAV_N - 1, NAV_N - 1 };
redo(objects, count, terrain, all);
} else {
for (int k = 0; k < n_dirty; k++)
if (dirty[k].x1 >= dirty[k].x0 && dirty[k].z1 >= dirty[k].z0) redo(objects, count, terrain, dirty[k]);
}
g_signature = signature;
g_ready = true;
}
bool Nav_free(Vec3 p) {
return !g_ready || open(cellOf(p.x), cellOf(p.z));
}
/* The level at (x, z) nearest the height y (the lower one only if it's there and nearer). */
static int levelFor(int x, int z, float y) {
if (!inside(x, z)) return 0;
float f0 = g_floor[node(0, x, z)], f1 = g_floor[node(1, x, z)];
return f1 != NO_FLOOR && fabsf(y - f1) < fabsf(y - f0) ? 1 : 0;
}
/* Nearest open cell of that level to (x, z) within a few cells; false if none. */
static bool nearestOpen(int level, int *x, int *z) {
if (openAt(level, *x, *z)) return true;
for (int r = 1; r <= FREE_SEARCH; r++) {
int best = -1;
float best_d = 1e9f;
for (int dz = -r; dz <= r; dz++)
for (int dx = -r; dx <= r; dx++) {
if (abs(dx) != r && abs(dz) != r) continue;
float d = (float)(dx * dx + dz * dz);
if (openAt(level, *x + dx, *z + dz) && d < best_d) {
best_d = d;
best = (*z + dz) * NAV_N + (*x + dx);
}
}
if (best >= 0) {
*x = best % NAV_N;
*z = best / NAV_N;
return true;
}
}
return false;
}
static void heapPush(float f, int cell) {
if (g_heap_n >= g_heap_cap) return;
int i = g_heap_n++;
while (i > 0) {
int up = (i - 1) / 2;
if (g_heap[up].f <= f) break;
g_heap[i] = g_heap[up];
i = up;
}
g_heap[i] = (HeapItem){ f, cell };
}
static HeapItem heapPop(void) {
HeapItem top = g_heap[0], last = g_heap[--g_heap_n];
int i = 0;
for (;;) {
int c = 2 * i + 1;
if (c >= g_heap_n) break;
if (c + 1 < g_heap_n && g_heap[c + 1].f < g_heap[c].f) c++;
if (g_heap[c].f >= last.f) break;
g_heap[i] = g_heap[c];
i = c;
}
if (g_heap_n > 0) g_heap[i] = last;
return top;
}
static float guess(int x, int z, int gx, int gz) {
float dx = fabsf((float)(x - gx)), dz = fabsf((float)(z - gz));
return (dx + dz) + (1.41421356f - 2.0f) * fminf(dx, dz);
}
/* The open node at (x, z) a step from height `from` (either level), or -1. */
static int stepTo(int x, int z, float from, bool diagonal) {
for (int level = 0; level < 2; level++) {
if (!openAt(level, x, z)) continue;
int n = node(level, x, z);
if (fabsf(g_floor[n] - from) <= (diagonal ? NAV_RISE * 1.42f : NAV_RISE)) return n;
}
return -1;
}
/* Straight line between two nodes over open cells, a step at a time up or down. */
static bool clearLine(int a, int b) {
int ac = (int)((size_t)a % N2), bc = (int)((size_t)b % N2);
int ax = ac % NAV_N, az = ac / NAV_N, bx = bc % NAV_N, bz = bc / NAV_N;
float dx = (float)(bx - ax), dz = (float)(bz - az);
int steps = (int)(fmaxf(fabsf(dx), fabsf(dz)) * 3.0f) + 1, last = ac;
float floor_y = g_floor[a];
for (int s = 1; s <= steps; s++) {
float t = (float)s / (float)steps;
float fx = (float)ax + 0.5f + dx * t, fz = (float)az + 0.5f + dz * t;
int cx = (int)floorf(fx), cz = (int)floorf(fz), here = cz * NAV_N + cx;
if (here != last) {
int n = stepTo(cx, cz, floor_y, true); /* (not off the edge of a floor) */
if (n < 0) return false;
floor_y = g_floor[n];
last = here;
}
/* (a little to each side too: no grazing corners) */
if (stepTo((int)floorf(fx + 0.3f), (int)floorf(fz + 0.3f), floor_y, true) < 0 ||
stepTo((int)floorf(fx - 0.3f), (int)floorf(fz - 0.3f), floor_y, true) < 0)
return false;
}
return fabsf(floor_y - g_floor[b]) <= NAV_RISE * 1.42f;
}
int Nav_findPath(Vec3 from, Vec3 to, Vec3 *out, int max) {
if (!g_ready || max < 1) return 0;
int sx = cellOf(from.x), sz = cellOf(from.z), gx = cellOf(to.x), gz = cellOf(to.z);
int s_level = levelFor(sx, sz, from.y), g_level = levelFor(gx, gz, to.y);
bool goal_open = openAt(g_level, gx, gz);
if (!nearestOpen(s_level, &sx, &sz) || !nearestOpen(g_level, &gx, &gz)) return 0;
int start = node(s_level, sx, sz), goal = node(g_level, gx, gz);
if (++g_stamp == 0) { /* wrapped: clear the stamps once */
memset(g_seen, 0, 2 * N2 * sizeof *g_seen);
memset(g_done, 0, 2 * N2 * sizeof *g_done);
g_stamp = 1;
}
g_heap_n = 0;
g_cost[start] = 0.0f;
g_from[start] = -1;
g_seen[start] = g_stamp;
heapPush(guess(sx, sz, gx, gz), start);
static const int DX[8] = { 1, -1, 0, 0, 1, 1, -1, -1 }, DZ[8] = { 0, 0, 1, -1, 1, -1, 1, -1 };
bool found = false;
for (int steps = 0; g_heap_n > 0 && steps < MOST_STEPS; steps++) {
HeapItem it = heapPop();
int cur = it.cell;
if (g_done[cur] == g_stamp) continue;
g_done[cur] = g_stamp;
if (cur == goal) { found = true; break; }
int cell = (int)((size_t)cur % N2), x = cell % NAV_N, z = cell / NAV_N;
float here = g_floor[cur];
for (int k = 0; k < 8; k++) {
int nx = x + DX[k], nz = z + DZ[k];
int n = stepTo(nx, nz, here, k >= 4); /* (up or down a floor's edge: no) */
if (n < 0) continue;
if (k >= 4 && stepTo(x + DX[k], z, here, false) < 0 && stepTo(x, z + DZ[k], here, false) < 0) continue; /* no squeezing between two corners (past one: the margin's room enough - a turned stair runs diagonally) */
if (g_done[n] == g_stamp) continue;
float cost = g_cost[cur] + (k < 4 ? 1.0f : 1.41421356f);
if (g_seen[n] == g_stamp && cost >= g_cost[n]) continue;
g_seen[n] = g_stamp;
g_cost[n] = cost;
g_from[n] = cur;
heapPush(cost + guess(nx, nz, gx, gz), n);
}
}
if (!found) return 0;
/* The nodes back from the goal, then only the turns that matter */
int length = 0;
for (int c = goal; c >= 0; c = g_from[c]) length++;
int *chain = malloc((size_t)length * sizeof *chain);
if (!chain) return 0;
int k = length;
for (int c = goal; c >= 0; c = g_from[c]) chain[--k] = c;
int n = 0, at = 0;
while (at < length - 1 && n < max) {
int next = at + 1; /* as far along as can be seen straight */
while (next + 1 < length && clearLine(chain[at], chain[next + 1])) next++;
at = next;
int cell = (int)((size_t)chain[at] % N2);
out[n++] = vec3(centerOf(cell % NAV_N), g_floor[chain[at]], centerOf(cell / NAV_N));
}
free(chain);
if (n == 0) out[n++] = vec3(centerOf(gx), g_floor[goal], centerOf(gz));
if (goal_open && at == length - 1) out[n - 1] = vec3(to.x, g_floor[goal], to.z); /* right to it */
return n;
}