feat(nav): 17.3 - a map in square tiles (nav_tiled, nav_tile_build; a tile must be a whole number of cells or its seams never join, and the shim refuses one that is not; 64-bit polygon refs for 16384 tiles), ground as a NavGround with boulders as convex footprints, one save format for one tile or many, and nav_random_near - a reachable point from the caller's seed, bit-identical on the Mac and the PC. 10 tests on both
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9b8606483d
commit
e6893f58be
16 changed files with 436 additions and 109 deletions
|
|
@ -14,15 +14,21 @@ import "ludic.nav"
|
|||
`NAV_LARGE` (an elk, a bear, a horse) and `NAV_SMALL` (a hare, a marmot) each have their own,
|
||||
because what a hare slips between a bear walks round. A `NavConfig` says what a mesh is built
|
||||
for: its voxels and its walker's height, radius, step and steepest slope.
|
||||
- **Built from triangles, or loaded from a bake.** `nav_build` rasterizes triangles - each with an
|
||||
area byte, 0 not walkable and 1..62 a kind of ground - and marks every cylinder given (a trunk, a
|
||||
post) as somewhere nothing stands; ground steeper than the slope is cleared whatever its area. A
|
||||
game bakes its maps once and loads the bytes (`nav_save_file` / `nav_load_file`): a map is never
|
||||
built at start-up.
|
||||
- **Built from ground, or loaded from a bake.** A `NavGround` is triangles - each with an area
|
||||
byte, 0 not walkable and 1..62 a kind of ground - and what nothing stands in: cylinders (a trunk,
|
||||
a post) and convex footprints (a boulder). Ground steeper than the slope is cleared whatever its
|
||||
area. `nav_build` makes one mesh of it; a map is square tiles (`nav_tiled`, then `nav_tile_build`
|
||||
for each tile from the ground under it and a few metres round), and a tile must be a whole number
|
||||
of cells across or its seams never join - the shim refuses one that is not. A game bakes its maps
|
||||
once and loads the bytes (`nav_save_file` / `nav_load_file`, one format for one tile or many): a
|
||||
map is never built at start-up. Polygon refs are 64-bit, so a map may have 16384 tiles.
|
||||
- **A path is corners.** `nav_path` answers the way from one point to another as the corners
|
||||
where it turns, the first the start and the last the end. When the end cannot be reached the
|
||||
path stops as near it as the mesh allows and `nav_partial` says so - a walker goes there and
|
||||
gives up, never through a river. Either end off the mesh is `-1`.
|
||||
- **A wander's goal is somewhere it can go.** `nav_random_near` answers a point about r away that a
|
||||
walker standing at the start can reach - never across a river without a ford - from a seed the
|
||||
caller draws from its own `Rng`, so the same seed is the same point on every machine.
|
||||
- **A kind of ground has a cost** (`nav_area_cost`): a trail cheaper than a meadow, scree dearer.
|
||||
- **The same question gets the same answer.** Detour is deterministic for the same mesh and the
|
||||
same points, so co-op's order of dice is untouched by asking it.
|
||||
|
|
@ -32,12 +38,15 @@ import "ludic.nav"
|
|||
| | |
|
||||
| --- | --- |
|
||||
| `NAV_PERSON`, `NAV_LARGE`, `NAV_SMALL`, `NAV_CORNERS` | the kinds of walker, and a path's most corners (256) |
|
||||
| `NavConfig { cell, cell_h, height, radius, climb, slope }` | what a mesh is built for (a person by default) |
|
||||
| `nav_build(kind, v, nv, t, nt, area, cyl, nc, config) -> bool`, `nav_polygons(kind)` | a mesh from triangles and cylinders (`x, y, z, r, h` each) |
|
||||
| `NavConfig { cell, cell_h, height, radius, climb, slope }` | what a mesh is built for (a person by default, 0.25 m cells) |
|
||||
| `NavGround { v, nv, t, nt, area, cyl, nc, foot, nf }` | what it is built from: triangles, an area byte each, cylinders (`x, y, z, r, h`) and footprints (`n`, n points `x z`, `ymin`, `ymax`) |
|
||||
| `nav_build(kind, ground, config) -> bool`, `nav_polygons(kind)` | one mesh over all of it |
|
||||
| `nav_tiled(kind, ox, oz, tile, max_tiles, max_polys) -> bool`, `nav_tile_build(kind, tx, tz, ground, config) -> int` | a map of square tiles, a tile at a time (its polygons, 0 none, -1 failed) |
|
||||
| `nav_save_file(kind, path)`, `nav_load_file(kind, path)`, `nav_reset()` | a baked mesh written and read; every mesh let go |
|
||||
| `nav_nearest(kind, x, y, z) -> bool`, `nav_near_x/y/z()` | the nearest walkable point within a couple of metres |
|
||||
| `nav_path(kind, sx, sy, sz, ex, ey, ez) -> int`, `nav_corners()`, `nav_corner_x/y/z(i)`, `nav_partial(kind)` | a way as corners |
|
||||
| `nav_straight(kind, sx, sy, sz, ex, ez) -> bool` | does the straight line stay walkable |
|
||||
| `nav_random_near(kind, x, y, z, r, seed) -> bool`, `nav_near_x/y/z()` | a reachable point about r away, from the caller's seed |
|
||||
| `nav_area_cost(kind, area, cost)` | how dear a kind of ground is to cross |
|
||||
|
||||
## Tests
|
||||
|
|
@ -46,8 +55,10 @@ import "ludic.nav"
|
|||
ludic test packages/ludic.nav
|
||||
```
|
||||
|
||||
A 40 m meadow built by hand: a path round a post, over a river's ford, stopping at the bank of a
|
||||
river with none, the nearest point, and a saved mesh answering as the built one did.
|
||||
A 40 m meadow built by hand: a path round a post and a boulder's footprint, over a river's ford,
|
||||
stopping at the bank of a river with none, the nearest point, random points that never cross the
|
||||
river, and a saved mesh answering as the built one did. And a 128 m meadow as four tiles: a path
|
||||
across the seams, a tile under water with no polygons, and the set saved and loaded.
|
||||
|
||||
## The native library
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
# build.ludic - a kind's mesh from triangles, or from a file a bake wrote
|
||||
# v: nv points (x, y, z); t: nt triangles (three indices each); area: one byte per triangle, 0 not
|
||||
# walkable, 1..62 a kind of ground; cyl: nc cylinders (x, y, z, r, h) nothing stands in
|
||||
export function nav_build(nav_st: mut NavState, kind: int, v: []float, nv: int, t: []int, nt: int, area: []byte, cyl: []float, nc: int, c: NavConfig) -> bool {
|
||||
if len(v) < nv * 3 or len(t) < nt * 3 or len(area) < nt or len(cyl) < nc * 5 { return false }
|
||||
# build.ludic - a kind's mesh: whole from its ground, or tile by tile (a map), or from a bake's file
|
||||
function nv_ground_ok(g: NavGround) -> bool {
|
||||
if g.v == null or g.t == null or g.area == null { return false }
|
||||
if len(g.v) < g.nv * 3 or len(g.t) < g.nt * 3 or len(g.area) < g.nt { return false }
|
||||
if g.nc > 0 and (g.cyl == null or len(g.cyl) < g.nc * 5) { return false }
|
||||
return g.nf == 0 or (g.foot != null and len(g.foot) >= g.nf)
|
||||
}
|
||||
function nv_cfg(c: NavConfig) -> []float {
|
||||
let cfg = floats(6)
|
||||
cfg[0] = c.cell
|
||||
cfg[1] = c.cell_h
|
||||
|
|
@ -10,7 +13,28 @@ export function nav_build(nav_st: mut NavState, kind: int, v: []float, nv: int,
|
|||
cfg[3] = c.radius
|
||||
cfg[4] = c.climb
|
||||
cfg[5] = c.slope
|
||||
return nv_set(nav_st, kind, nvc_build(v, nv, t, nt, area, cyl, nc, cfg))
|
||||
return cfg
|
||||
}
|
||||
function nv_or_none(xs: []float) -> []float {
|
||||
if xs == null { return floats(1) }
|
||||
return xs
|
||||
}
|
||||
|
||||
# one mesh over the whole of g
|
||||
export function nav_build(nav_st: mut NavState, kind: int, g: NavGround, c: NavConfig) -> bool {
|
||||
if not nv_ground_ok(g) { return false }
|
||||
return nv_set(nav_st, kind, nvc_build(g.v, g.nv, g.t, g.nt, g.area, nv_or_none(g.cyl), g.nc, nv_or_none(g.foot), g.nf, nv_cfg(c)))
|
||||
}
|
||||
|
||||
# an empty mesh of square tiles `tile` metres across from (ox, oz), for a map built a tile at a time
|
||||
export function nav_tiled(nav_st: mut NavState, kind: int, ox: float, oz: float, tile: float, max_tiles: int, max_polys: int) -> bool {
|
||||
return nv_set(nav_st, kind, nvc_tiled(ox, oz, tile, max_tiles, max_polys))
|
||||
}
|
||||
# tile (tx, tz) from ground g covering it and a few metres round it: its polygons, 0 none, -1 failed
|
||||
export function nav_tile_build(nav_st: NavState, kind: int, tx: int, tz: int, g: NavGround, c: NavConfig) -> int {
|
||||
let h = nv_mesh(nav_st, kind)
|
||||
if h == null or not nv_ground_ok(g) { return -1 }
|
||||
return nvc_tile_build(h, tx, tz, g.v, g.nv, g.t, g.nt, g.area, nv_or_none(g.cyl), g.nc, nv_or_none(g.foot), g.nf, nv_cfg(c))
|
||||
}
|
||||
|
||||
# the polygons a kind's mesh has (0: none)
|
||||
|
|
@ -25,6 +49,7 @@ export function nav_save_file(nav_st: NavState, kind: int, path: string) -> bool
|
|||
let h = nv_mesh(nav_st, kind)
|
||||
if h == null { return false }
|
||||
let n = nvc_save(h, null, 0)
|
||||
if n <= 0 { return false }
|
||||
let buf = buffer(n)
|
||||
if nvc_save(h, buf, n) != n { return false }
|
||||
return Fs.write_bytes(path, buf, n)
|
||||
|
|
|
|||
BIN
packages/ludic.nav/lib/macos-arm64/libludicnav.dylib
(Stored with Git LFS)
BIN
packages/ludic.nav/lib/macos-arm64/libludicnav.dylib
(Stored with Git LFS)
Binary file not shown.
BIN
packages/ludic.nav/lib/windows-x64/ludicnav.dll
(Stored with Git LFS)
BIN
packages/ludic.nav/lib/windows-x64/ludicnav.dll
(Stored with Git LFS)
Binary file not shown.
BIN
packages/ludic.nav/lib/windows-x64/ludicnav.lib
(Stored with Git LFS)
BIN
packages/ludic.nav/lib/windows-x64/ludicnav.lib
(Stored with Git LFS)
Binary file not shown.
|
|
@ -1,6 +1,8 @@
|
|||
# native.ludic - the shim's symbols (native/shim/nav_shim.cpp). A navmesh is a handle the package
|
||||
# keeps; points come back through a float buffer it owns. None of this is exported.
|
||||
extern function nvc_build(v: pointer, nv: int, t: pointer, nt: int, area: pointer, cyl: pointer, nc: int, c: pointer) -> pointer = "nav_build"
|
||||
extern function nvc_build(v: pointer, nv: int, t: pointer, nt: int, area: pointer, cyl: pointer, nc: int, foot: pointer, nf: int, c: pointer) -> pointer = "nav_build"
|
||||
extern function nvc_tiled(ox: float, oz: float, tile: float, max_tiles: int, max_polys: int) -> pointer = "nav_tiled"
|
||||
extern function nvc_tile_build(h: pointer, tx: int, tz: int, v: pointer, nv: int, t: pointer, nt: int, area: pointer, cyl: pointer, nc: int, foot: pointer, nf: int, c: pointer) -> int = "nav_tile_build"
|
||||
extern function nvc_save(h: pointer, buf: pointer, cap: int) -> int = "nav_save"
|
||||
extern function nvc_load(buf: pointer, size: int) -> pointer = "nav_load"
|
||||
extern function nvc_free(h: pointer) -> void = "nav_free"
|
||||
|
|
@ -10,3 +12,4 @@ extern function nvc_area_cost(h: pointer, area: int, cost: float) -> void = "nav
|
|||
extern function nvc_path(h: pointer, sx: float, sy: float, sz: float, ex: float, ey: float, ez: float, out: pointer, max: int) -> int = "nav_path"
|
||||
extern function nvc_partial(h: pointer) -> int = "nav_partial"
|
||||
extern function nvc_raycast(h: pointer, sx: float, sy: float, sz: float, ex: float, ez: float) -> int = "nav_raycast"
|
||||
extern function nvc_random_near(h: pointer, x: float, y: float, z: float, r: float, seed: int, out: pointer) -> int = "nav_random_near"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
#!/bin/sh
|
||||
# builds lib/<target>/ for ludic.nav: Recast & Detour at a pinned tag, and the shim over them.
|
||||
# Recast builds the mesh, Detour answers it; DetourTileCache and DetourCrowd come with phases 17.8
|
||||
# and 18. Objects go to build/native; a few seconds on eight cores.
|
||||
# and 18. Polygon refs are 64-bit (DT_POLYREF64): an 8 km map in 64 m tiles is 16384 tiles, which
|
||||
# 32-bit refs would leave 256 polygons each. Objects go to build/native; a few seconds on eight cores.
|
||||
set -eu
|
||||
PKG="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
. "$PKG/../../tools/native/lib.sh"
|
||||
|
|
@ -14,7 +15,7 @@ OBJ="$PKG/build/native/$(native_target)"
|
|||
mkdir -p "$OBJ"
|
||||
CXX="$(native_cxx)"
|
||||
INC="-I$SRC/Recast/Include -I$SRC/Detour/Include"
|
||||
FLAGS="$(native_cflags) -std=c++17 -ffp-contract=off -fno-exceptions -fno-rtti -DNDEBUG $INC"
|
||||
FLAGS="$(native_cflags) -std=c++17 -ffp-contract=off -fno-exceptions -fno-rtti -DNDEBUG -DDT_POLYREF64 $INC"
|
||||
case "$(native_target)" in windows-*) FLAGS="$FLAGS -D_CRT_SECURE_NO_WARNINGS" ;; esac
|
||||
JOBS="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// nav_build.inl - a navmesh from triangles: Recast's solo pipeline (rasterize, filter, erode,
|
||||
// regions, contours, polygons, detail) and one Detour tile. c[] is the configuration:
|
||||
// cell size, cell height, agent height, agent radius, max climb, max slope (degrees).
|
||||
// nav_build.inl - Recast's pipeline, shared by a whole mesh and a tile: rasterize the triangles,
|
||||
// filter, mark what nothing stands in, erode, regions, contours, polygons, detail, Detour's data.
|
||||
// c[] is the configuration: cell size, cell height, agent height, radius, max climb, max slope.
|
||||
|
||||
// cylinders (x, y, z, r, h each) no agent may stand in: trunks, posts, tents
|
||||
static void nav_mark_cylinders(rcContext &ctx, rcCompactHeightfield &chf, const float *cyl, int nc) {
|
||||
|
|
@ -10,12 +10,21 @@ static void nav_mark_cylinders(rcContext &ctx, rcCompactHeightfield &chf, const
|
|||
}
|
||||
}
|
||||
|
||||
// v: nv points (x, y, z); t: nt triangles; area: one per triangle, 0 not walkable, 1..62 a kind of
|
||||
// ground (its cost is the filter's); the slope clears what is too steep whatever its area
|
||||
NAV_SHIM void *nav_build(const float *v, int nv, const int *t, int nt, const unsigned char *area,
|
||||
const float *cyl, int nc, const float *c) {
|
||||
rcContext ctx(false);
|
||||
rcConfig cfg;
|
||||
// convex footprints no agent may stand in (a boulder): each is n, n points (x, z), ymin, ymax
|
||||
static void nav_mark_footprints(rcContext &ctx, rcCompactHeightfield &chf, const float *f, int nf) {
|
||||
float pts[3 * 64];
|
||||
int i = 0;
|
||||
while (i < nf) {
|
||||
int n = (int)f[i];
|
||||
if (n < 3 || n > 64 || i + 1 + n * 2 + 2 > nf) return;
|
||||
float y0 = f[i + 1 + n * 2], y1 = f[i + 2 + n * 2];
|
||||
for (int k = 0; k < n; ++k) { pts[k * 3] = f[i + 1 + k * 2]; pts[k * 3 + 1] = y0; pts[k * 3 + 2] = f[i + 2 + k * 2]; }
|
||||
rcMarkConvexPolyArea(&ctx, pts, n, y0, y1, RC_NULL_AREA, chf);
|
||||
i += 3 + n * 2;
|
||||
}
|
||||
}
|
||||
|
||||
static void nav_config(rcConfig &cfg, const float *c) {
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
cfg.cs = c[0];
|
||||
cfg.ch = c[1];
|
||||
|
|
@ -30,11 +39,18 @@ NAV_SHIM void *nav_build(const float *v, int nv, const int *t, int nt, const uns
|
|||
cfg.maxVertsPerPoly = 6;
|
||||
cfg.detailSampleDist = cfg.cs * 6.0f;
|
||||
cfg.detailSampleMaxError = cfg.ch;
|
||||
rcCalcBounds(v, nv, cfg.bmin, cfg.bmax);
|
||||
rcCalcGridSize(cfg.bmin, cfg.bmax, cfg.cs, &cfg.width, &cfg.height);
|
||||
}
|
||||
|
||||
std::vector<unsigned char> areas(area, area + nt);
|
||||
rcClearUnwalkableTriangles(&ctx, cfg.walkableSlopeAngle, v, nv, t, nt, areas.data());
|
||||
struct NavIn {
|
||||
const float *v; int nv; const int *t; int nt; const unsigned char *area;
|
||||
const float *cyl; int nc; const float *foot; int nf; const float *c;
|
||||
};
|
||||
|
||||
// the pipeline over cfg's box; the tile's Detour data into data / size. 0 polygons is not an error
|
||||
static bool nav_pipeline(rcConfig &cfg, const NavIn &in, int tx, int tz, unsigned char **data, int *size, int *polys) {
|
||||
rcContext ctx(false);
|
||||
std::vector<unsigned char> areas(in.area, in.area + in.nt);
|
||||
rcClearUnwalkableTriangles(&ctx, cfg.walkableSlopeAngle, in.v, in.nv, in.t, in.nt, areas.data());
|
||||
rcHeightfield *hf = rcAllocHeightfield();
|
||||
rcCompactHeightfield *chf = rcAllocCompactHeightfield();
|
||||
rcContourSet *cs = rcAllocContourSet();
|
||||
|
|
@ -42,7 +58,7 @@ NAV_SHIM void *nav_build(const float *v, int nv, const int *t, int nt, const uns
|
|||
rcPolyMeshDetail *dm = rcAllocPolyMeshDetail();
|
||||
bool ok = hf && chf && cs && pm && dm &&
|
||||
rcCreateHeightfield(&ctx, *hf, cfg.width, cfg.height, cfg.bmin, cfg.bmax, cfg.cs, cfg.ch) &&
|
||||
rcRasterizeTriangles(&ctx, v, nv, t, areas.data(), nt, *hf, cfg.walkableClimb);
|
||||
rcRasterizeTriangles(&ctx, in.v, in.nv, in.t, areas.data(), in.nt, *hf, cfg.walkableClimb);
|
||||
if (ok) {
|
||||
rcFilterLowHangingWalkableObstacles(&ctx, cfg.walkableClimb, *hf);
|
||||
rcFilterLedgeSpans(&ctx, cfg.walkableHeight, cfg.walkableClimb, *hf);
|
||||
|
|
@ -50,16 +66,16 @@ NAV_SHIM void *nav_build(const float *v, int nv, const int *t, int nt, const uns
|
|||
ok = rcBuildCompactHeightfield(&ctx, cfg.walkableHeight, cfg.walkableClimb, *hf, *chf);
|
||||
}
|
||||
if (ok) {
|
||||
nav_mark_cylinders(ctx, *chf, cyl, nc);
|
||||
nav_mark_cylinders(ctx, *chf, in.cyl, in.nc);
|
||||
nav_mark_footprints(ctx, *chf, in.foot, in.nf);
|
||||
ok = rcErodeWalkableArea(&ctx, cfg.walkableRadius, *chf) && rcBuildDistanceField(&ctx, *chf) &&
|
||||
rcBuildRegions(&ctx, *chf, 0, cfg.minRegionArea, cfg.mergeRegionArea) &&
|
||||
rcBuildRegions(&ctx, *chf, cfg.borderSize, cfg.minRegionArea, cfg.mergeRegionArea) &&
|
||||
rcBuildContours(&ctx, *chf, cfg.maxSimplificationError, cfg.maxEdgeLen, *cs) &&
|
||||
rcBuildPolyMesh(&ctx, *cs, cfg.maxVertsPerPoly, *pm) &&
|
||||
rcBuildPolyMeshDetail(&ctx, *pm, *chf, cfg.detailSampleDist, cfg.detailSampleMaxError, *dm);
|
||||
}
|
||||
unsigned char *data = nullptr;
|
||||
int size = 0;
|
||||
if (ok) {
|
||||
*polys = ok ? pm->npolys : 0;
|
||||
if (ok && pm->npolys > 0) {
|
||||
for (int i = 0; i < pm->npolys; ++i) pm->flags[i] = pm->areas[i] ? 1 : 0;
|
||||
dtNavMeshCreateParams p;
|
||||
memset(&p, 0, sizeof(p));
|
||||
|
|
@ -67,30 +83,28 @@ NAV_SHIM void *nav_build(const float *v, int nv, const int *t, int nt, const uns
|
|||
p.polyFlags = pm->flags; p.polyCount = pm->npolys; p.nvp = pm->nvp;
|
||||
p.detailMeshes = dm->meshes; p.detailVerts = dm->verts; p.detailVertsCount = dm->nverts;
|
||||
p.detailTris = dm->tris; p.detailTriCount = dm->ntris;
|
||||
p.walkableHeight = c[2]; p.walkableRadius = c[3]; p.walkableClimb = c[4];
|
||||
p.walkableHeight = in.c[2]; p.walkableRadius = in.c[3]; p.walkableClimb = in.c[4];
|
||||
p.tileX = tx; p.tileY = tz;
|
||||
rcVcopy(p.bmin, pm->bmin); rcVcopy(p.bmax, pm->bmax);
|
||||
p.cs = cfg.cs; p.ch = cfg.ch; p.buildBvTree = true;
|
||||
ok = dtCreateNavMeshData(&p, &data, &size);
|
||||
ok = dtCreateNavMeshData(&p, data, size);
|
||||
}
|
||||
rcFreeHeightField(hf); rcFreeCompactHeightfield(chf); rcFreeContourSet(cs); rcFreePolyMesh(pm); rcFreePolyMeshDetail(dm);
|
||||
if (!ok) return nullptr;
|
||||
return nav_from(data, size);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// the tile's bytes, to write to a file: first the size (buf null), then the copy
|
||||
NAV_SHIM int nav_save(void *h, unsigned char *buf, int cap) {
|
||||
Nav *n = static_cast<Nav *>(h);
|
||||
if (!buf) return n->size;
|
||||
if (cap < n->size) return -1;
|
||||
memcpy(buf, n->data, n->size);
|
||||
return n->size;
|
||||
}
|
||||
|
||||
// a navmesh from bytes nav_save wrote
|
||||
NAV_SHIM void *nav_load(const unsigned char *buf, int size) {
|
||||
unsigned char *data = static_cast<unsigned char *>(dtAlloc(size, DT_ALLOC_PERM));
|
||||
if (!data) return nullptr;
|
||||
memcpy(data, buf, size);
|
||||
// one mesh over all the triangles given: v nv points (x, y, z); t nt triangles; area a byte each,
|
||||
// 0 not walkable and 1..62 a kind of ground; the slope clears what is too steep whatever its area
|
||||
NAV_SHIM void *nav_build(const float *v, int nv, const int *t, int nt, const unsigned char *area,
|
||||
const float *cyl, int nc, const float *foot, int nf, const float *c) {
|
||||
rcConfig cfg;
|
||||
nav_config(cfg, c);
|
||||
rcCalcBounds(v, nv, cfg.bmin, cfg.bmax);
|
||||
rcCalcGridSize(cfg.bmin, cfg.bmax, cfg.cs, &cfg.width, &cfg.height);
|
||||
NavIn in = {v, nv, t, nt, area, cyl, nc, foot, nf, c};
|
||||
unsigned char *data = nullptr;
|
||||
int size = 0, polys = 0;
|
||||
if (!nav_pipeline(cfg, in, 0, 0, &data, &size, &polys) || polys == 0) return nullptr;
|
||||
return nav_from(data, size);
|
||||
}
|
||||
|
||||
|
|
@ -102,7 +116,13 @@ NAV_SHIM void nav_free(void *h) {
|
|||
delete n;
|
||||
}
|
||||
|
||||
// every polygon in every tile
|
||||
NAV_SHIM int nav_polys(void *h) {
|
||||
const dtMeshTile *t = static_cast<const dtNavMesh *>(static_cast<Nav *>(h)->mesh)->getTile(0);
|
||||
return t && t->header ? t->header->polyCount : 0;
|
||||
const dtNavMesh *m = static_cast<Nav *>(h)->mesh;
|
||||
int total = 0;
|
||||
for (int i = 0; i < m->getMaxTiles(); ++i) {
|
||||
const dtMeshTile *t = m->getTile(i);
|
||||
if (t && t->header) total += t->header->polyCount;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,3 +51,22 @@ NAV_SHIM int nav_raycast(void *h, float sx, float sy, float sz, float ex, float
|
|||
if (dtStatusFailed(n->query->raycast(a, so, e, &n->filter, &t, nrm, n->polys, &np, NAV_MAX_POLYS))) return -1;
|
||||
return t >= 1.0f ? 1 : 0;
|
||||
}
|
||||
|
||||
// a point about r from (x, y, z) that a walker there can reach, into out; 0 when there is none.
|
||||
// Detour asks a function for its dice: this one is seeded by the caller, so the same seed from the
|
||||
// package's Rng gives the same point on every machine
|
||||
static unsigned int nav_rs = 1;
|
||||
static float nav_frand() {
|
||||
nav_rs = nav_rs * 1664525u + 1013904223u;
|
||||
return (float)(nav_rs >> 8) * (1.0f / 16777216.0f);
|
||||
}
|
||||
NAV_SHIM int nav_random_near(void *h, float x, float y, float z, float r, int seed, float *out) {
|
||||
Nav *n = static_cast<Nav *>(h);
|
||||
float p[3] = {x, y, z}, on[3];
|
||||
dtPolyRef start = nav_poly_at(n, p, on);
|
||||
if (!start) return 0;
|
||||
nav_rs = (unsigned int)seed * 2654435761u + 1u;
|
||||
dtPolyRef ref = 0;
|
||||
if (dtStatusFailed(n->query->findRandomPointAroundCircle(start, on, r, &n->filter, nav_frand, &ref, out))) return 0;
|
||||
return ref ? 1 : 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,24 +27,26 @@ struct Nav {
|
|||
dtQueryFilter filter;
|
||||
float ext[3] = {2.0f, 4.0f, 2.0f}; // how far to look for the mesh around a point
|
||||
dtPolyRef polys[NAV_MAX_POLYS];
|
||||
unsigned char *data = nullptr; // the tile as built or loaded (the mesh frees it)
|
||||
int size = 0;
|
||||
int partial = 0; // the last path stopped short of its end
|
||||
};
|
||||
|
||||
Nav *nav_from(unsigned char *data, int size) {
|
||||
// a navmesh with its query, from params (a set of tiles) or from one tile's data
|
||||
Nav *nav_open(dtNavMesh *mesh) {
|
||||
Nav *n = new Nav();
|
||||
n->mesh = dtAllocNavMesh();
|
||||
if (!n->mesh || dtStatusFailed(n->mesh->init(data, size, DT_TILE_FREE_DATA))) { dtFree(data); dtFreeNavMesh(n->mesh); delete n; return nullptr; }
|
||||
n->data = data;
|
||||
n->size = size;
|
||||
n->mesh = mesh;
|
||||
n->query = dtAllocNavMeshQuery();
|
||||
if (!n->query || dtStatusFailed(n->query->init(n->mesh, NAV_MAX_NODES))) { dtFreeNavMeshQuery(n->query); dtFreeNavMesh(n->mesh); delete n; return nullptr; }
|
||||
n->filter.setIncludeFlags(0xffff);
|
||||
n->filter.setExcludeFlags(0);
|
||||
return n;
|
||||
}
|
||||
Nav *nav_from(unsigned char *data, int size) {
|
||||
dtNavMesh *mesh = dtAllocNavMesh();
|
||||
if (!mesh || dtStatusFailed(mesh->init(data, size, DT_TILE_FREE_DATA))) { dtFree(data); dtFreeNavMesh(mesh); return nullptr; }
|
||||
return nav_open(mesh);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
#include "nav_build.inl"
|
||||
#include "nav_tiles.inl"
|
||||
#include "nav_query.inl"
|
||||
|
|
|
|||
102
packages/ludic.nav/native/shim/nav_tiles.inl
Normal file
102
packages/ludic.nav/native/shim/nav_tiles.inl
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
// nav_tiles.inl - a map as square tiles: a tiled mesh, one tile built from the triangles under it
|
||||
// and a border round it, and every tile written and read back as one set of bytes
|
||||
|
||||
// an empty mesh of tiles `tile` metres across, the first at (ox, oz)
|
||||
NAV_SHIM void *nav_tiled(float ox, float oz, float tile, int max_tiles, int max_polys) {
|
||||
dtNavMeshParams p;
|
||||
memset(&p, 0, sizeof(p));
|
||||
p.orig[0] = ox; p.orig[1] = 0.0f; p.orig[2] = oz;
|
||||
p.tileWidth = tile; p.tileHeight = tile;
|
||||
p.maxTiles = max_tiles; p.maxPolys = max_polys;
|
||||
dtNavMesh *mesh = dtAllocNavMesh();
|
||||
if (!mesh || dtStatusFailed(mesh->init(&p))) { dtFreeNavMesh(mesh); return nullptr; }
|
||||
return nav_open(mesh);
|
||||
}
|
||||
|
||||
// tile (tx, tz) built from triangles that cover it and a border of the walker's radius and three
|
||||
// cells round it, replacing whatever was there; its polygons, 0 for none, -1 when it failed
|
||||
NAV_SHIM int nav_tile_build(void *h, int tx, int tz, const float *v, int nv, const int *t, int nt,
|
||||
const unsigned char *area, const float *cyl, int nc, const float *foot, int nf, const float *c) {
|
||||
Nav *n = static_cast<Nav *>(h);
|
||||
const dtNavMeshParams *mp = n->mesh->getParams();
|
||||
rcConfig cfg;
|
||||
nav_config(cfg, c);
|
||||
cfg.tileSize = (int)(mp->tileWidth / cfg.cs + 0.5f);
|
||||
// a tile a cell short of its width leaves a seam its neighbour's polygons never meet
|
||||
if (fabsf(cfg.tileSize * cfg.cs - mp->tileWidth) > 0.001f) return -1;
|
||||
cfg.borderSize = cfg.walkableRadius + 3;
|
||||
cfg.width = cfg.height = cfg.tileSize + cfg.borderSize * 2;
|
||||
float lo[3], hi[3];
|
||||
rcCalcBounds(v, nv, lo, hi);
|
||||
cfg.bmin[0] = mp->orig[0] + tx * mp->tileWidth - cfg.borderSize * cfg.cs;
|
||||
cfg.bmin[2] = mp->orig[2] + tz * mp->tileHeight - cfg.borderSize * cfg.cs;
|
||||
cfg.bmax[0] = mp->orig[0] + (tx + 1) * mp->tileWidth + cfg.borderSize * cfg.cs;
|
||||
cfg.bmax[2] = mp->orig[2] + (tz + 1) * mp->tileHeight + cfg.borderSize * cfg.cs;
|
||||
cfg.bmin[1] = lo[1] - 1.0f;
|
||||
cfg.bmax[1] = hi[1] + c[2] + 1.0f;
|
||||
NavIn in = {v, nv, t, nt, area, cyl, nc, foot, nf, c};
|
||||
unsigned char *data = nullptr;
|
||||
int size = 0, polys = 0;
|
||||
n->mesh->removeTile(n->mesh->getTileRefAt(tx, tz, 0), nullptr, nullptr);
|
||||
if (!nav_pipeline(cfg, in, tx, tz, &data, &size, &polys)) return -1;
|
||||
if (polys == 0) return 0;
|
||||
if (dtStatusFailed(n->mesh->addTile(data, size, DT_TILE_FREE_DATA, 0, nullptr))) { dtFree(data); return -1; }
|
||||
return polys;
|
||||
}
|
||||
|
||||
// the set's bytes: "NAVT", a version, the mesh's params, then each tile's ref, size and data.
|
||||
// First call with buf null for the size, then with a buffer that big
|
||||
static const int NAV_MAGIC = 'N' << 24 | 'A' << 16 | 'V' << 8 | 'T';
|
||||
static const int NAV_VERSION = 1;
|
||||
static void nav_put(unsigned char *buf, int &at, const void *p, int n) {
|
||||
if (buf) memcpy(buf + at, p, n);
|
||||
at += n;
|
||||
}
|
||||
NAV_SHIM int nav_save(void *h, unsigned char *buf, int cap) {
|
||||
const dtNavMesh *m = static_cast<Nav *>(h)->mesh;
|
||||
const dtNavMeshParams *p = m->getParams();
|
||||
int at = 0, count = 0;
|
||||
for (int i = 0; i < m->getMaxTiles(); ++i) {
|
||||
const dtMeshTile *t = m->getTile(i);
|
||||
if (t && t->header && t->dataSize) count++;
|
||||
}
|
||||
nav_put(buf, at, &NAV_MAGIC, 4); nav_put(buf, at, &NAV_VERSION, 4); nav_put(buf, at, &count, 4);
|
||||
nav_put(buf, at, p->orig, 12); nav_put(buf, at, &p->tileWidth, 4); nav_put(buf, at, &p->tileHeight, 4);
|
||||
nav_put(buf, at, &p->maxTiles, 4); nav_put(buf, at, &p->maxPolys, 4);
|
||||
for (int i = 0; i < m->getMaxTiles(); ++i) {
|
||||
const dtMeshTile *t = m->getTile(i);
|
||||
if (!t || !t->header || !t->dataSize) continue;
|
||||
unsigned long long ref = (unsigned long long)m->getTileRef(t);
|
||||
if (buf && at + 12 + t->dataSize > cap) return -1;
|
||||
nav_put(buf, at, &ref, 8); nav_put(buf, at, &t->dataSize, 4); nav_put(buf, at, t->data, t->dataSize);
|
||||
}
|
||||
return at;
|
||||
}
|
||||
|
||||
// a mesh from bytes nav_save wrote; null for anything else
|
||||
NAV_SHIM void *nav_load(const unsigned char *buf, int size) {
|
||||
int at = 0, magic = 0, version = 0, count = 0;
|
||||
if (size < 40) return nullptr;
|
||||
memcpy(&magic, buf, 4); memcpy(&version, buf + 4, 4); memcpy(&count, buf + 8, 4);
|
||||
if (magic != NAV_MAGIC || version != NAV_VERSION) return nullptr;
|
||||
dtNavMeshParams p;
|
||||
memset(&p, 0, sizeof(p));
|
||||
memcpy(p.orig, buf + 12, 12); memcpy(&p.tileWidth, buf + 24, 4); memcpy(&p.tileHeight, buf + 28, 4);
|
||||
memcpy(&p.maxTiles, buf + 32, 4); memcpy(&p.maxPolys, buf + 36, 4);
|
||||
at = 40;
|
||||
dtNavMesh *mesh = dtAllocNavMesh();
|
||||
if (!mesh || dtStatusFailed(mesh->init(&p))) { dtFreeNavMesh(mesh); return nullptr; }
|
||||
for (int i = 0; i < count; ++i) {
|
||||
unsigned long long ref = 0;
|
||||
int n = 0;
|
||||
if (at + 12 > size) break;
|
||||
memcpy(&ref, buf + at, 8); memcpy(&n, buf + at + 8, 4);
|
||||
at += 12;
|
||||
if (n <= 0 || at + n > size) break;
|
||||
unsigned char *data = static_cast<unsigned char *>(dtAlloc(n, DT_ALLOC_PERM));
|
||||
memcpy(data, buf + at, n);
|
||||
at += n;
|
||||
if (dtStatusFailed(mesh->addTile(data, n, DT_TILE_FREE_DATA, (dtTileRef)ref, nullptr))) dtFree(data);
|
||||
}
|
||||
return nav_open(mesh);
|
||||
}
|
||||
|
|
@ -11,6 +11,14 @@ export function nav_near_x(nav_st: NavState) -> float { return nav_st.nv_near[0]
|
|||
export function nav_near_y(nav_st: NavState) -> float { return nav_st.nv_near[1] }
|
||||
export function nav_near_z(nav_st: NavState) -> float { return nav_st.nv_near[2] }
|
||||
|
||||
# a point about r from (x, y, z) that a walker standing there can reach (a wander's goal), read back
|
||||
# as the nearest is; the seed is the caller's dice, so the same seed is the same point everywhere
|
||||
export function nav_random_near(nav_st: NavState, kind: int, x: float, y: float, z: float, r: float, seed: int) -> bool {
|
||||
let h = nv_mesh(nav_st, kind)
|
||||
if h == null { return false }
|
||||
return nvc_random_near(h, x, y, z, r, seed, nav_st.nv_near) == 1
|
||||
}
|
||||
|
||||
# a way from one point to another: its corners, first the start and last the end (or as near the end
|
||||
# as the mesh reaches - nav_partial says which); -1 when either end is off the mesh
|
||||
export function nav_path(nav_st: mut NavState, kind: int, sx: float, sy: float, sz: float, ex: float, ey: float, ez: float) -> int {
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ export const NAV_KINDS: int = 3
|
|||
export const NAV_CORNERS: int = 256 # a path's corners at most; a longer one is cut there
|
||||
|
||||
# what a mesh is built for: its voxels (cell across, cell high) and its walker (height, radius,
|
||||
# the step it takes whatever its gradient, the steepest ground it walks, degrees)
|
||||
# the step it takes whatever its gradient, the steepest ground it walks, degrees). A tile must be a
|
||||
# whole number of cells across, or its seams never join: 0.25 m is 256 to a 64 m tile
|
||||
export property NavConfig {
|
||||
cell: float = 0.3
|
||||
cell: float = 0.25
|
||||
cell_h: float = 0.2
|
||||
height: float = 1.8
|
||||
radius: float = 0.35
|
||||
|
|
@ -17,6 +18,21 @@ export property NavConfig {
|
|||
slope: float = 45.0
|
||||
}
|
||||
|
||||
# what a mesh is built from: v holds nv points (x, y, z) and t nt triangles (three indices each);
|
||||
# area a byte per triangle, 0 not walkable and 1..62 a kind of ground; cyl nc cylinders (x, y, z,
|
||||
# r, h) and foot nf floats of convex footprints (n, n points x z, ymin, ymax) nothing stands in
|
||||
export property NavGround {
|
||||
v: []float = null
|
||||
nv: int = 0
|
||||
t: []int = null
|
||||
nt: int = 0
|
||||
area: []byte = null
|
||||
cyl: []float = null
|
||||
nc: int = 0
|
||||
foot: []float = null
|
||||
nf: int = 0
|
||||
}
|
||||
|
||||
export state NavState {
|
||||
nv_meshes: []pointer = nv_none()
|
||||
nv_out: []float = floats(NAV_CORNERS * 3)
|
||||
|
|
|
|||
61
packages/ludic.nav/tests/fakes/meadow.ludic
Normal file
61
packages/ludic.nav/tests/fakes/meadow.ludic
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# fakes/meadow.ludic - ground built by hand for ludic.nav's tests: n metre squares a side, flat at
|
||||
# y = 0; river 1 crosses x 18..22 with a ford at z 30..34, river 2 without one, river 0 is none
|
||||
function ground(n: int, river: int) -> NavGround {
|
||||
let g = new NavGround
|
||||
g.nv = (n + 1) * (n + 1)
|
||||
g.v = floats(g.nv * 3)
|
||||
for j in 0 .. n + 1 {
|
||||
for i in 0 .. n + 1 {
|
||||
let o = (j * (n + 1) + i) * 3
|
||||
g.v[o] = float(i)
|
||||
g.v[o + 2] = float(j)
|
||||
}
|
||||
}
|
||||
g.nt = n * n * 2
|
||||
g.t = new []int
|
||||
g.area = buffer(g.nt)
|
||||
for j in 0 .. n {
|
||||
for i in 0 .. n {
|
||||
let a = j * (n + 1) + i
|
||||
push(g.t, a)
|
||||
push(g.t, a + n + 1)
|
||||
push(g.t, a + 1)
|
||||
push(g.t, a + 1)
|
||||
push(g.t, a + n + 1)
|
||||
push(g.t, a + n + 2)
|
||||
var k = 1
|
||||
if river > 0 and i >= 18 and i < 22 and not (river == 1 and j >= 30 and j < 34) { k = 0 }
|
||||
g.area[(j * n + i) * 2] = k
|
||||
g.area[(j * n + i) * 2 + 1] = k
|
||||
}
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
# a post r wide at (x, z), from under the ground to over a walker's head
|
||||
function post_at(x: float, z: float, r: float) -> []float {
|
||||
let c = floats(5)
|
||||
c[0] = x
|
||||
c[1] = -1.0
|
||||
c[2] = z
|
||||
c[3] = r
|
||||
c[4] = 4.0
|
||||
return c
|
||||
}
|
||||
|
||||
# a square footprint 2h across at (x, z), as a boulder's is written: n, the corners, ymin, ymax
|
||||
function square_at(x: float, z: float, h: float) -> []float {
|
||||
let f = floats(11)
|
||||
f[0] = 4.0
|
||||
f[1] = x - h
|
||||
f[2] = z - h
|
||||
f[3] = x + h
|
||||
f[4] = z - h
|
||||
f[5] = x + h
|
||||
f[6] = z + h
|
||||
f[7] = x - h
|
||||
f[8] = z + h
|
||||
f[9] = -1.0
|
||||
f[10] = 3.0
|
||||
return f
|
||||
}
|
||||
|
|
@ -3,47 +3,15 @@
|
|||
# the ford, stops at the bank when there is none, and a saved mesh answers as the built one did.
|
||||
import "ludic.nav"
|
||||
import "ludic.base"
|
||||
import "fakes/meadow.ludic"
|
||||
program NavTest {
|
||||
numbers float
|
||||
|
||||
# the meadow, flat at y = 0; river 1 has a ford at z 30..34, river 2 none, river 0 is no river
|
||||
function meadow(nav_st: mut NavState, kind: int, river: int, post: bool) -> bool {
|
||||
let n = 40
|
||||
let v = floats((n + 1) * (n + 1) * 3)
|
||||
for j in 0 .. n + 1 {
|
||||
for i in 0 .. n + 1 {
|
||||
let o = (j * (n + 1) + i) * 3
|
||||
v[o] = float(i)
|
||||
v[o + 1] = 0.0
|
||||
v[o + 2] = float(j)
|
||||
}
|
||||
}
|
||||
let t = new []int
|
||||
let area = buffer(n * n * 2)
|
||||
for j in 0 .. n {
|
||||
for i in 0 .. n {
|
||||
let a = j * (n + 1) + i
|
||||
push(t, a)
|
||||
push(t, a + n + 1)
|
||||
push(t, a + 1)
|
||||
push(t, a + 1)
|
||||
push(t, a + n + 1)
|
||||
push(t, a + n + 2)
|
||||
var k = 1
|
||||
if river > 0 and i >= 18 and i < 22 and not (river == 1 and j >= 30 and j < 34) { k = 0 }
|
||||
area[(j * n + i) * 2] = k
|
||||
area[(j * n + i) * 2 + 1] = k
|
||||
}
|
||||
}
|
||||
let cyl = floats(5)
|
||||
cyl[0] = 20.0
|
||||
cyl[1] = -1.0
|
||||
cyl[2] = 20.0
|
||||
cyl[3] = 2.0
|
||||
cyl[4] = 4.0
|
||||
var nc = 0
|
||||
if post { nc = 1 }
|
||||
return nav_build(nav_st, kind, v, (n + 1) * (n + 1), t, n * n * 2, area, cyl, nc, new NavConfig)
|
||||
let g = ground(40, river)
|
||||
if post { g.cyl = post_at(20.0, 20.0, 2.0) }
|
||||
if post { g.nc = 1 }
|
||||
return nav_build(nav_st, kind, g, new NavConfig)
|
||||
}
|
||||
function last_x(nav_st: NavState) -> float { return nav_corner_x(nav_st, nav_corners(nav_st) - 1) }
|
||||
function last_z(nav_st: NavState) -> float { return nav_corner_z(nav_st, nav_corners(nav_st) - 1) }
|
||||
|
|
@ -92,6 +60,31 @@ program NavTest {
|
|||
expect_eq(nav_path(nav_st, NAV_LARGE, 5.0, 0.0, 5.0, 10.0, 0.0, 10.0), -1)
|
||||
}
|
||||
|
||||
test "a random point near is always one a walker there can reach, and a seed is always the same point" (nav_st: mut NavState) {
|
||||
expect(meadow(nav_st, NAV_PERSON, 2, false))
|
||||
for seed in 0 .. 50 {
|
||||
expect(nav_random_near(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 30.0, seed))
|
||||
expect(nav_near_x(nav_st) < 18.0)
|
||||
}
|
||||
nav_random_near(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 30.0, 7)
|
||||
let x7 = nav_near_x(nav_st)
|
||||
let z7 = nav_near_z(nav_st)
|
||||
nav_random_near(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 30.0, 8)
|
||||
expect(Math.abs(nav_near_x(nav_st) - x7) + Math.abs(nav_near_z(nav_st) - z7) > 0.01)
|
||||
nav_random_near(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 30.0, 7)
|
||||
expect_eq(nav_near_x(nav_st), x7)
|
||||
expect_eq(nav_near_z(nav_st), z7)
|
||||
}
|
||||
|
||||
test "a boulder's footprint is walked round" (nav_st: mut NavState) {
|
||||
let g = ground(40, 0)
|
||||
g.foot = square_at(20.0, 20.0, 3.0)
|
||||
g.nf = len(g.foot)
|
||||
expect(nav_build(nav_st, NAV_PERSON, g, new NavConfig))
|
||||
expect(not nav_straight(nav_st, NAV_PERSON, 5.0, 0.0, 20.0, 35.0, 20.0))
|
||||
expect(nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 20.0, 35.0, 0.0, 20.0) >= 3)
|
||||
}
|
||||
|
||||
test "a mesh saved and loaded answers as the built one did" (nav_st: mut NavState) {
|
||||
expect(meadow(nav_st, NAV_PERSON, 1, true))
|
||||
let path = Os.temp_dir() + "/ludic_nav_test.navmesh"
|
||||
|
|
|
|||
66
packages/ludic.nav/tests/tiles_test.ludic
Normal file
66
packages/ludic.nav/tests/tiles_test.ludic
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# tiles_test.ludic - ludic.nav's map in tiles: a 128 m meadow as four 64 m tiles, one of them under
|
||||
# water. A path crosses the tiles' seams, the water's tile has no polygons and nothing inside it is
|
||||
# a way's end, and the set saved and loaded answers as the built one did.
|
||||
import "ludic.nav"
|
||||
import "ludic.base"
|
||||
import "fakes/meadow.ludic"
|
||||
program TilesTest {
|
||||
numbers float
|
||||
|
||||
# four tiles over ground(128), the far corner's squares (x and z past 64) under water when wet
|
||||
function map(nav_st: mut NavState, wet: bool) -> int {
|
||||
let g = ground(128, 0)
|
||||
if wet {
|
||||
for j in 64 .. 128 { for i in 64 .. 128 { g.area[(j * 128 + i) * 2] = 0 } }
|
||||
for j in 64 .. 128 { for i in 64 .. 128 { g.area[(j * 128 + i) * 2 + 1] = 0 } }
|
||||
}
|
||||
g.cyl = post_at(96.0, 20.0, 2.0)
|
||||
g.nc = 1
|
||||
expect(nav_tiled(nav_st, NAV_PERSON, 0.0, 0.0, 64.0, 16, 4096))
|
||||
var empty = 0
|
||||
for tz in 0 .. 2 {
|
||||
for tx in 0 .. 2 {
|
||||
let p = nav_tile_build(nav_st, NAV_PERSON, tx, tz, g, new NavConfig)
|
||||
expect(p >= 0)
|
||||
if p == 0 { empty += 1 }
|
||||
}
|
||||
}
|
||||
return empty
|
||||
}
|
||||
function last_x(nav_st: NavState) -> float { return nav_corner_x(nav_st, nav_corners(nav_st) - 1) }
|
||||
function last_z(nav_st: NavState) -> float { return nav_corner_z(nav_st, nav_corners(nav_st) - 1) }
|
||||
|
||||
test "a path crosses the tiles' seams and goes round the post in the far tile" (nav_st: mut NavState) {
|
||||
expect_eq(map(nav_st, false), 0)
|
||||
expect(nav_polygons(nav_st, NAV_PERSON) > 0)
|
||||
let n = nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 20.0, 120.0, 0.0, 20.0)
|
||||
expect(n >= 3)
|
||||
expect(not nav_partial(nav_st, NAV_PERSON))
|
||||
expect(Math.abs(last_x(nav_st) - 120.0) < 0.1)
|
||||
let d = nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 10.0, 120.0, 0.0, 120.0)
|
||||
expect(d >= 2)
|
||||
expect(not nav_partial(nav_st, NAV_PERSON))
|
||||
}
|
||||
|
||||
test "a tile under water has no polygons: a way into it is none, the shore beside it is reached" (nav_st: mut NavState) {
|
||||
expect_eq(map(nav_st, true), 1)
|
||||
expect(not nav_nearest(nav_st, NAV_PERSON, 100.0, 0.0, 100.0))
|
||||
expect_eq(nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 10.0, 70.0, 0.0, 70.0), -1)
|
||||
expect(nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 10.0, 62.0, 0.0, 70.0) >= 2)
|
||||
expect(not nav_partial(nav_st, NAV_PERSON))
|
||||
expect(Math.abs(last_z(nav_st) - 70.0) < 0.1)
|
||||
}
|
||||
|
||||
test "the tiles saved and loaded answer as the built ones did" (nav_st: mut NavState) {
|
||||
map(nav_st, true)
|
||||
let path = Os.temp_dir() + "/ludic_nav_tiles.navmesh"
|
||||
expect(nav_save_file(nav_st, NAV_PERSON, path))
|
||||
expect(nav_load_file(nav_st, NAV_LARGE, path))
|
||||
expect_eq(nav_polygons(nav_st, NAV_LARGE), nav_polygons(nav_st, NAV_PERSON))
|
||||
let a = nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 20.0, 120.0, 0.0, 20.0)
|
||||
let ax = last_x(nav_st)
|
||||
let b = nav_path(nav_st, NAV_LARGE, 10.0, 0.0, 20.0, 120.0, 0.0, 20.0)
|
||||
expect_eq(a, b)
|
||||
expect_near(last_x(nav_st), ax, 0.0001)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue