summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKonAki <konaki@anokonaki.org>2026-10-11 16:44:20 +0800
committerKonAki <konaki@anokonaki.org>2026-10-11 16:44:20 +0800
commit22348718dfdc48e417f53d5b4b4d471b622846e2 (patch)
treec9b4f0b6a541a2eaa05152c89b1e3ea5f6ad2502
parente3f48813fc53057ebd04084b2f71b526c6df80d3 (diff)
world: bodies move through sectors, terrain, tiles and custom colliders
-rw-r--r--README.md1
-rwxr-xr-xbuild.sh5
-rw-r--r--kon/konworld.c483
-rw-r--r--kon/konworld.h121
4 files changed, 608 insertions, 2 deletions
diff --git a/README.md b/README.md
index a6e20a8..7aaed7d 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,7 @@ The engine modules are a `.h` and a `.c` pair, they depend on each other a bit,
| tile | tilemaps and sprite sheets |
| 3d | first person kit: mouse look body, walls from a tilemap, editable hill terrain (adds tile) |
| sector | maps made of rooms (vertices, floors, ceilings, walls) with entities like doors and buttons, in a text format |
+| world | one place to move bodies through sector maps, terrain, tilemaps and your own boxes, cylinders, spheres and planes (needs 3d, sector) |
| pack | encrypted asset packs |
| assets | named asset cache with reference counts, loose files or a pack |
| image | stb_image so assets can load pngs |
diff --git a/build.sh b/build.sh
index b792931..34fc37d 100755
--- a/build.sh
+++ b/build.sh
@@ -12,7 +12,7 @@ developer=0
usage()
{
echo "usage: build.sh [-d] [-D] [-p platform] [-m module,...] [-o objdir] [target ...]"
- echo "modules: font toml tile pack ui debug assets image audio mic model 3d sector"
+ echo "modules: font toml tile pack ui debug assets image audio mic model 3d sector world"
echo "platforms: native linux bsd windows"
echo "targets: all run clean tools pack examples"
exit 1
@@ -77,7 +77,7 @@ addobj()
for m in $modules; do
DEFS="$DEFS -DKON_MODULE_$(echo "$m" | tr 'a-z' 'A-Z')"
case $m in
- font|toml|tile|pack|ui|debug|3d|sector) addobj "kon$m" ;;
+ font|toml|tile|pack|ui|debug|3d|sector|world) addobj "kon$m" ;;
assets) addobj konassets ;;
model) addobj konmodel; addobj konassets ;;
image) addobj stb_image ;;
@@ -105,6 +105,7 @@ need()
# modules that use other modules
case " $modules " in *" debug "*) need font; need assets ;; esac
case " $modules " in *" ui "*) need font ;; esac
+case " $modules " in *" world "*) need 3d; need sector; need tile ;; esac
case " $modules " in *" 3d "*) need tile ;; esac
case " $modules " in *" model "*) need assets ;; esac
case " $DEFS " in *" -DKON_MODULE_ASSETS "*) need pack ;; esac
diff --git a/kon/konworld.c b/kon/konworld.c
new file mode 100644
index 0000000..1806099
--- /dev/null
+++ b/kon/konworld.c
@@ -0,0 +1,483 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * konworld.c
+ *
+ * Copyright (c) 2026 KonAki
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include "konworld.h"
+
+#include <math.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define KON_SECTOR_SET 48
+
+/*** shapes ***/
+
+kon_collider_t kon_colliderBox(kon_vec3_t min, kon_vec3_t max)
+{
+ kon_collider_t c = {0};
+
+ c.type = KON_COLLIDER_BOX;
+ c.enabled = 1;
+ c.min = min;
+ c.max = max;
+ return c;
+}
+
+kon_collider_t kon_colliderCylinder(kon_vec3_t bottom, float radius, float height)
+{
+ kon_collider_t c = {0};
+
+ c.type = KON_COLLIDER_CYLINDER;
+ c.enabled = 1;
+ c.center = bottom;
+ c.radius = radius;
+ c.height = height;
+ return c;
+}
+
+kon_collider_t kon_colliderSphere(kon_vec3_t center, float radius)
+{
+ kon_collider_t c = {0};
+
+ c.type = KON_COLLIDER_SPHERE;
+ c.enabled = 1;
+ c.center = center;
+ c.radius = radius;
+ return c;
+}
+
+kon_collider_t kon_colliderPlane(kon_vec3_t point, kon_vec3_t normal)
+{
+ kon_collider_t c = {0};
+
+ c.type = KON_COLLIDER_PLANE;
+ c.enabled = 1;
+ c.normal = kon_vec3Normalize(normal);
+ c.offset = kon_vec3Dot(c.normal, point);
+ return c;
+}
+
+/*** the world ***/
+
+void kon_worldInit(kon_world_t *world)
+{
+ memset(world, 0, sizeof(*world));
+ world->tileHeight = 1.0f;
+ world->gravity = 20.0f;
+ world->stepHeight = 0.35f;
+ world->maxSlope = 1.0f;
+}
+
+void kon_worldFree(kon_world_t *world)
+{
+ free(world->colliders);
+ world->colliders = NULL;
+ world->colliderCount = world->colliderCap = 0;
+}
+
+int kon_worldAdd(kon_world_t *world, kon_collider_t collider)
+{
+ int i;
+
+ for (i = 0; i < world->colliderCount; i++) {
+ if (world->colliders[i].type == KON_COLLIDER_NONE) {
+ world->colliders[i] = collider;
+ return i;
+ }
+ }
+ if (world->colliderCount == world->colliderCap) {
+ int cap = world->colliderCap ? world->colliderCap * 2 : 16;
+ kon_collider_t *grown = realloc(world->colliders, (size_t)cap * sizeof(*grown));
+
+ if (!grown) return -1;
+ world->colliders = grown;
+ world->colliderCap = cap;
+ }
+ world->colliders[world->colliderCount] = collider;
+ return world->colliderCount++;
+}
+
+kon_collider_t *kon_worldCollider(kon_world_t *world, int index)
+{
+ if (index < 0 || index >= world->colliderCount || world->colliders[index].type == KON_COLLIDER_NONE) return NULL;
+ return &world->colliders[index];
+}
+
+void kon_worldRemove(kon_world_t *world, int index)
+{
+ if (index < 0 || index >= world->colliderCount) return;
+ memset(&world->colliders[index], 0, sizeof(world->colliders[index]));
+}
+
+/*** ground ***/
+
+static float kon_colliderTop_(const kon_collider_t *c, float x, float z)
+{
+ switch (c->type) {
+ case KON_COLLIDER_BOX:
+ if (x >= c->min.x && x <= c->max.x && z >= c->min.z && z <= c->max.z) return c->max.y;
+ break;
+ case KON_COLLIDER_CYLINDER: {
+ float dx = x - c->center.x, dz = z - c->center.z;
+
+ if (dx * dx + dz * dz <= c->radius * c->radius) return c->center.y + c->height;
+ break;
+ }
+ case KON_COLLIDER_PLANE:
+ if (c->normal.y >= 0.7f) return (c->offset - c->normal.x * x - c->normal.z * z) / c->normal.y;
+ break;
+ default:
+ break;
+ }
+ return KON_NO_GROUND;
+}
+
+float kon_worldGround(const kon_world_t *world, float x, float z, float feet, int hint, int *sector)
+{
+ float ground = KON_NO_GROUND;
+ int s = -1, i, any = 0;
+
+ if (world->sectors) {
+ any = 1;
+ s = kon_sectorAt(world->sectors, x, z, hint);
+ if (s >= 0) ground = world->sectors->sectors[s].floor;
+ }
+ if (s < 0 && world->terrain) {
+ any = 1;
+ if (x >= 0.0f && z >= 0.0f && x <= (float)world->terrain->columns && z <= (float)world->terrain->rows) ground = kon_terrainHeight(world->terrain, x, z);
+ }
+ if (s < 0 && ground == KON_NO_GROUND && world->tiles) {
+ any = 1;
+ if (x >= 0.0f && z >= 0.0f && x < (float)world->tiles->columns && z < (float)world->tiles->rows) ground = 0.0f;
+ }
+ if (!any) ground = 0.0f;
+
+ for (i = 0; i < world->colliderCount; i++) {
+ const kon_collider_t *c = &world->colliders[i];
+ float top;
+
+ if (!c->enabled || c->type == KON_COLLIDER_NONE) continue;
+ top = kon_colliderTop_(c, x, z);
+ if (top != KON_NO_GROUND && top <= feet + world->stepHeight && top > ground) ground = top;
+ }
+ if (sector) *sector = s;
+ return ground;
+}
+
+/*** pushing out ***/
+
+static float kon_clampf_(float v, float lo, float hi)
+{
+ return v < lo ? lo : (v > hi ? hi : v);
+}
+
+/* the body can't step onto something that ends above its step, and something that starts above its head is no problem */
+static int kon_blocks_(const kon_world_t *world, const kon_body_t *body, float lo, float hi)
+{
+ return hi > body->position.y + world->stepHeight && lo < body->position.y + body->height;
+}
+
+static int kon_pushSegment_(float *x, float *z, float r, float ax, float az, float bx, float bz, float nx, float nz)
+{
+ float ex = bx - ax, ez = bz - az, len2 = ex * ex + ez * ez;
+ float t = len2 > 0.0f ? ((*x - ax) * ex + (*z - az) * ez) / len2 : 0.0f;
+ float cx, cz, dx, dz, d;
+
+ t = kon_clampf_(t, 0.0f, 1.0f);
+ cx = ax + ex * t;
+ cz = az + ez * t;
+ dx = *x - cx;
+ dz = *z - cz;
+ d = sqrtf(dx * dx + dz * dz);
+ if (d >= r) return 0;
+ if (d < 1.0e-5f) {
+ dx = nx;
+ dz = nz;
+ d = 0.0f;
+ } else {
+ dx /= d;
+ dz /= d;
+ }
+ *x += dx * (r - d);
+ *z += dz * (r - d);
+ return 1;
+}
+
+static int kon_pushBox_(float *x, float *z, float r, float minX, float minZ, float maxX, float maxZ)
+{
+ float cx = kon_clampf_(*x, minX, maxX), cz = kon_clampf_(*z, minZ, maxZ);
+ float dx = *x - cx, dz = *z - cz, d2 = dx * dx + dz * dz;
+
+ if (d2 >= r * r) return 0;
+ if (d2 > 1.0e-10f) {
+ float d = sqrtf(d2);
+
+ *x += dx / d * (r - d);
+ *z += dz / d * (r - d);
+ } else {
+ /* the middle is inside, leave through the nearest side */
+ float left = *x - minX, right = maxX - *x, up = *z - minZ, down = maxZ - *z;
+ float best = left;
+
+ if (right < best) best = right;
+ if (up < best) best = up;
+ if (down < best) best = down;
+ if (best == left) *x = minX - r;
+ else if (best == right) *x = maxX + r;
+ else if (best == up) *z = minZ - r;
+ else *z = maxZ + r;
+ }
+ return 1;
+}
+
+static int kon_pushCircle_(float *x, float *z, float r, float cx, float cz, float cr)
+{
+ float dx = *x - cx, dz = *z - cz, d = sqrtf(dx * dx + dz * dz), reach = r + cr;
+
+ if (d >= reach) return 0;
+ if (d < 1.0e-5f) {
+ *x = cx + reach;
+ return 1;
+ }
+ *x = cx + dx / d * reach;
+ *z = cz + dz / d * reach;
+ return 1;
+}
+
+/* is the way from one room into the next blocked for this body, because of the step, or because it would not fit */
+static int kon_gapBlocks_(const kon_world_t *world, const kon_body_t *body, const kon_sector_t *from, const kon_sector_t *to)
+{
+ float stand = body->position.y > to->floor ? body->position.y : to->floor;
+
+ if (to->floor - body->position.y > world->stepHeight) return 1;
+ if (to->ceiling - stand < body->height) return 1;
+ if (from->ceiling - stand < body->height) return 1;
+ return 0;
+}
+
+static int kon_sectorSet_(const kon_sectorMap_t *map, int start, int *set)
+{
+ int n = 0, ring, i, k, j;
+
+ set[n++] = start;
+ for (ring = 0; ring < 2; ring++) {
+ int count = n;
+
+ for (i = 0; i < count; i++) {
+ const kon_sector_t *sector = &map->sectors[set[i]];
+
+ for (k = 0; k < sector->count; k++) {
+ int p = map->edges[sector->first + k].portal;
+
+ if (p < 0) continue;
+ for (j = 0; j < n; j++) if (set[j] == p) break;
+ if (j == n && n < KON_SECTOR_SET) set[n++] = p;
+ }
+ }
+ }
+ return n;
+}
+
+/* one pass over everything the body can bump into. touching is set to the last collider that moved it */
+static int kon_pushAll_(const kon_world_t *world, kon_body_t *body, float *x, float *z)
+{
+ float r = body->radius, feet = body->position.y, top = feet + body->height;
+ int moved = 0, i;
+
+ if (world->sectors && body->sector >= 0) {
+ const kon_sectorMap_t *map = world->sectors;
+ int set[KON_SECTOR_SET], n = kon_sectorSet_(map, body->sector, set), a, k;
+
+ for (a = 0; a < n; a++) {
+ const kon_sector_t *sector = &map->sectors[set[a]];
+
+ for (k = 0; k < sector->count; k++) {
+ kon_vec2_t p = kon_sectorCorner(map, set[a], k), q = kon_sectorCorner(map, set[a], k + 1);
+ float ex = q.x - p.x, ez = q.y - p.y, len = sqrtf(ex * ex + ez * ez);
+ int portal = map->edges[sector->first + k].portal;
+
+ if (len <= 0.0f) continue;
+ if (portal < 0) {
+ if (!(feet < sector->ceiling && top > sector->floor)) continue;
+ } else if (!kon_gapBlocks_(world, body, sector, &map->sectors[portal])) {
+ continue;
+ }
+ moved |= kon_pushSegment_(x, z, r, p.x, p.y, q.x, q.y, ez / len, -ex / len);
+ }
+ }
+ }
+
+ if (world->tiles) {
+ const kon_tilemap_t *tiles = world->tiles;
+ int column, row, c0 = (int)floorf(*x - r), c1 = (int)floorf(*x + r), r0 = (int)floorf(*z - r), r1 = (int)floorf(*z + r);
+
+ if (kon_blocks_(world, body, 0.0f, world->tileHeight)) {
+ for (row = r0; row <= r1; row++) {
+ for (column = c0; column <= c1; column++) {
+ int solid = 1;
+
+ if (column >= 0 && row >= 0 && column < tiles->columns && row < tiles->rows) {
+ uint16_t id = kon_tilemapAt(tiles, column, row);
+
+ solid = id < world->solidCount && world->solid[id];
+ }
+ if (solid) moved |= kon_pushBox_(x, z, r, (float)column, (float)row, (float)column + 1.0f, (float)row + 1.0f);
+ }
+ }
+ }
+ }
+
+ for (i = 0; i < world->colliderCount; i++) {
+ const kon_collider_t *c = &world->colliders[i];
+ int hit = 0;
+
+ if (!c->enabled) continue;
+ switch (c->type) {
+ case KON_COLLIDER_BOX:
+ if (kon_blocks_(world, body, c->min.y, c->max.y)) hit = kon_pushBox_(x, z, r, c->min.x, c->min.z, c->max.x, c->max.z);
+ break;
+ case KON_COLLIDER_CYLINDER:
+ if (kon_blocks_(world, body, c->center.y, c->center.y + c->height)) hit = kon_pushCircle_(x, z, r, c->center.x, c->center.z, c->radius);
+ break;
+ case KON_COLLIDER_SPHERE:
+ if (kon_blocks_(world, body, c->center.y - c->radius, c->center.y + c->radius)) hit = kon_pushCircle_(x, z, r, c->center.x, c->center.z, c->radius);
+ break;
+ case KON_COLLIDER_PLANE:
+ if (c->normal.y < 0.7f) {
+ float nh = sqrtf(c->normal.x * c->normal.x + c->normal.z * c->normal.z);
+ float d = c->normal.x * *x + c->normal.y * (feet + body->height * 0.5f) + c->normal.z * *z - c->offset;
+
+ if (nh > 1.0e-4f && d < r * nh) {
+ float t = (r * nh - d) / (nh * nh);
+
+ *x += c->normal.x * t;
+ *z += c->normal.z * t;
+ hit = 1;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ if (hit) body->touching = i;
+ moved |= hit;
+ }
+ return moved;
+}
+
+/*** moving ***/
+
+/* tries to stand at x, z. the body is only changed if it works */
+static int kon_try_(const kon_world_t *world, kon_body_t *body, float x, float z, float oldX, float oldZ)
+{
+ kon_body_t probe = *body;
+ float ground, dist;
+ int pass, s;
+
+ probe.sector = body->sector;
+ for (pass = 0; pass < 4; pass++) {
+ if (!kon_pushAll_(world, &probe, &x, &z)) break;
+ }
+ ground = kon_worldGround(world, x, z, body->position.y, body->sector, &s);
+ if (ground == KON_NO_GROUND) return 0;
+
+ if (world->terrain && s < 0 && kon_sectorAt(world->sectors, oldX, oldZ, -1) < 0) {
+ float before = kon_terrainHeight(world->terrain, oldX, oldZ), after = kon_terrainHeight(world->terrain, x, z);
+
+ dist = sqrtf((x - oldX) * (x - oldX) + (z - oldZ) * (z - oldZ));
+ if (after > body->position.y && after - before > world->maxSlope * dist + 1.0e-4f) return 0;
+ }
+ body->position.x = x;
+ body->position.z = z;
+ body->touching = probe.touching;
+ if (s >= 0) body->sector = s;
+ return 1;
+}
+
+void kon_bodyInit(kon_body_t *body, float x, float y, float z, float radius, float height)
+{
+ memset(body, 0, sizeof(*body));
+ body->position = KON_VEC3(x, y, z);
+ body->radius = radius;
+ body->height = height;
+ body->sector = -1;
+ body->touching = -1;
+}
+
+void kon_bodyJump(kon_body_t *body, float speed)
+{
+ if (!body->onGround) return;
+ body->vy = speed;
+ body->onGround = 0;
+}
+
+void kon_worldPlace(const kon_world_t *world, kon_body_t *body)
+{
+ int s;
+ float ground = kon_worldGround(world, body->position.x, body->position.z, body->position.y, body->sector, &s);
+
+ if (s >= 0) body->sector = s;
+ if (ground != KON_NO_GROUND) body->position.y = ground;
+ body->vy = 0.0f;
+ body->onGround = 1;
+}
+
+void kon_worldMove(kon_world_t *world, kon_body_t *body, float dx, float dz, double dt)
+{
+ float x = body->position.x, z = body->position.z, ground, ceiling = 1.0e30f;
+ int s;
+
+ body->touching = -1;
+ if (dx != 0.0f || dz != 0.0f) {
+ if (!kon_try_(world, body, x + dx, z + dz, x, z)) {
+ if (!kon_try_(world, body, x + dx, z, x, z)) kon_try_(world, body, x, z + dz, x, z);
+ }
+ }
+
+ ground = kon_worldGround(world, body->position.x, body->position.z, body->position.y, body->sector, &s);
+ if (s >= 0) {
+ body->sector = s;
+ ceiling = world->sectors->sectors[s].ceiling;
+ }
+ if (ground == KON_NO_GROUND) ground = body->position.y;
+
+ if (body->vy <= 0.0f && body->position.y - ground <= world->stepHeight) {
+ body->position.y = ground;
+ body->vy = 0.0f;
+ body->onGround = 1;
+ } else {
+ body->vy -= world->gravity * (float)dt;
+ body->position.y += body->vy * (float)dt;
+ body->onGround = 0;
+ if (body->vy <= 0.0f && body->position.y <= ground) {
+ body->position.y = ground;
+ body->vy = 0.0f;
+ body->onGround = 1;
+ }
+ }
+ if (body->position.y + body->height > ceiling) {
+ body->position.y = ceiling - body->height;
+ if (body->vy > 0.0f) body->vy = 0.0f;
+ }
+}
diff --git a/kon/konworld.h b/kon/konworld.h
new file mode 100644
index 0000000..4be1d1c
--- /dev/null
+++ b/kon/konworld.h
@@ -0,0 +1,121 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * konworld.h
+ *
+ * Copyright (c) 2026 KonAki
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#ifndef KONWORLD_H
+#define KONWORLD_H
+
+#include "konmath.h"
+#include "kontile.h"
+#include "kon3d.h"
+#include "konsector.h"
+
+/*** definition ***/
+
+/* one place to ask "can I stand here, can I walk there" for everything a 3d game is built from: the rooms of a sector map,
+ a terrain, a tilemap, and any shapes you add yourself. x and z are the plan, y is up. a world only points at the maps and the
+ terrain, they stay yours and can change between frames. nothing here knows what a door or a monster is, a door is a box that
+ the game moves */
+
+#define KON_NO_GROUND (-1.0e30f)
+
+typedef enum kon_colliderType {
+ KON_COLLIDER_NONE = 0,
+ KON_COLLIDER_BOX, /* min and max, sides line up with the axes */
+ KON_COLLIDER_CYLINDER, /* standing on the ground, center is the middle of its bottom */
+ KON_COLLIDER_SPHERE, /* blocks only, you can't stand on one */
+ KON_COLLIDER_PLANE /* everything behind the plane is solid. a plane that faces mostly up is a floor, a ramp if it is tilted */
+} kon_colliderType_t;
+
+typedef struct kon_collider {
+ int type;
+ int enabled; /* 0 and it is ignored, 1 */
+ int user; /* yours, like the entity this belongs to */
+ kon_vec3_t min, max; /* box */
+ kon_vec3_t center; /* cylinder, sphere */
+ float radius, height; /* cylinder, sphere (no height) */
+ kon_vec3_t normal; /* plane, the side it faces, length 1. points p with dot(normal, p) < offset are solid */
+ float offset;
+} kon_collider_t;
+
+kon_collider_t kon_colliderBox(kon_vec3_t min, kon_vec3_t max);
+kon_collider_t kon_colliderCylinder(kon_vec3_t bottom, float radius, float height);
+kon_collider_t kon_colliderSphere(kon_vec3_t center, float radius);
+kon_collider_t kon_colliderPlane(kon_vec3_t point, kon_vec3_t normal);
+
+typedef struct kon_world {
+ const kon_sectorMap_t *sectors; /* any of these three can be NULL */
+ const kon_terrain_t *terrain;
+ const kon_tilemap_t *tiles; /* one unit per tile like the rest of kon3d */
+ const uint8_t *solid; /* indexed by tile id like kon_tilemapMove */
+ int solidCount;
+ float tileHeight; /* how tall the solid tiles are, 1 */
+
+ kon_collider_t *colliders;
+ int colliderCount, colliderCap;
+
+ float gravity; /* 20 */
+ float stepHeight; /* the biggest step you walk up without jumping, 0.35 */
+ float maxSlope; /* rise over run you can climb on terrain, 1 is 45 degrees */
+} kon_world_t;
+
+/* a thing that walks. position is where the feet are, the body is a cylinder going up from there */
+typedef struct kon_body {
+ kon_vec3_t position;
+ float radius, height; /* 0.2 and 1.0 are fine for a person in this scale */
+ float vy; /* up speed, only jumps and falls change it */
+ int onGround;
+ int sector; /* the room it is in, -1 if none, kept up to date by kon_worldMove */
+ int touching; /* the collider it was pushed by in the last move, -1 if none. a monster hitting a door, say */
+} kon_body_t;
+
+/*** the world ***/
+
+void kon_worldInit(kon_world_t *world);
+void kon_worldFree(kon_world_t *world);
+
+/* returns the index, or -1 if there is no memory. the index stays the same until the collider is removed */
+int kon_worldAdd(kon_world_t *world, kon_collider_t collider);
+
+/* NULL if the index is not a collider. change it any time, a door opens by raising min.y */
+kon_collider_t *kon_worldCollider(kon_world_t *world, int index);
+void kon_worldRemove(kon_world_t *world, int index);
+
+/* the height of the highest thing to stand on at x, z that is not more than a step above feet, or KON_NO_GROUND outside the world.
+ hint is the sector you were in, -1 if you have none. sector gets the room the point is in, it can be NULL */
+float kon_worldGround(const kon_world_t *world, float x, float z, float feet, int hint, int *sector);
+
+/*** bodies ***/
+
+void kon_bodyInit(kon_body_t *body, float x, float y, float z, float radius, float height);
+void kon_bodyJump(kon_body_t *body, float speed);
+
+/* puts the feet on the ground under them right away */
+void kon_worldPlace(const kon_world_t *world, kon_body_t *body);
+
+/* moves by dx, dz and slides along whatever is in the way: walls, shapes, steps that are too high, slopes that are too steep, the edge
+ of the world. then gravity: steps up are taken at once and drops are falls. dt is seconds */
+void kon_worldMove(kon_world_t *world, kon_body_t *body, float dx, float dz, double dt);
+
+#endif