From 074189b73e2bbc37afe40da2f503fab44eaea44d Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Mon, 28 Sep 2026 00:47:47 +0300 Subject: [PATCH] ludic.nav (18.5): a walker the crowd avoids and never moves nav_crowd_add_fixed adds a walker with no steering, and nav_crowd_place puts it (snapped to the mesh) where the player is, with the velocity they have, each frame. The others plan round it by that velocity, and whatever it was pushed by is undone at the next placement. crowd_test: six walkers aimed through a standing player come no closer than 1.71 m and all get past, and the player stays where it was put. nav 16/16 on the Mac and the PC; DLL KERNEL32 only. Co-Authored-By: Claude Opus 5.5 --- packages/ludic.nav/README.md | 3 +- packages/ludic.nav/crowd.ludic | 11 ++++++ .../lib/macos-arm64/libludicnav.dylib | 4 +- .../ludic.nav/lib/windows-x64/ludicnav.dll | 4 +- .../ludic.nav/lib/windows-x64/ludicnav.lib | 4 +- packages/ludic.nav/native.ludic | 2 + packages/ludic.nav/native/shim/nav_crowd.inl | 37 +++++++++++++++++++ packages/ludic.nav/tests/crowd_test.ludic | 35 ++++++++++++++++++ 8 files changed, 93 insertions(+), 7 deletions(-) diff --git a/packages/ludic.nav/README.md b/packages/ludic.nav/README.md index a873c946..0eabe81e 100644 --- a/packages/ludic.nav/README.md +++ b/packages/ludic.nav/README.md @@ -78,6 +78,7 @@ the PC (Git Bash, the LLVM installer's clang). `native/LICENSE-recastnavigation` its filters. `nav_crowd_add(kind, x, y, z, radius, height, speed, filter, separation)` adds a walker (snapped to the mesh, -1 off it), `nav_crowd_target` and `nav_crowd_speed` say where and how fast, and `nav_crowd_step(dt)` steps every crowd. `nav_crowd_read` puts a walker into `nav_agent_x/y/z/vx/vz`, -and `nav_crowd_us()` is what the stepping has cost. The mechanic decides and the crowd moves, and +and `nav_crowd_us()` is what the stepping has cost. `nav_crowd_add_fixed` adds a walker the crowd avoids +but never steers (a player), which `nav_crowd_place` puts where it is, and how fast it goes, each frame. The mechanic decides and the crowd moves, and dropping a mesh drops its crowd first. On the test meadow, twenty walkers crossing head-on never come closer than their two radii, and a step costs about 12 us. diff --git a/packages/ludic.nav/crowd.ludic b/packages/ludic.nav/crowd.ludic index 519ead41..b3801389 100644 --- a/packages/ludic.nav/crowd.ludic +++ b/packages/ludic.nav/crowd.ludic @@ -27,6 +27,17 @@ export function nav_crowd_add(nav_st: NavState, kind: int, x: float, y: float, z if c == null { return -1 } return nvc_crowd_add(c, x, y, z, radius, height, speed, filter, separation) } +# a walker the crowd avoids but never moves (a player): put where it is each frame with nav_crowd_place +export function nav_crowd_add_fixed(nav_st: NavState, kind: int, x: float, y: float, z: float, radius: float, height: float) -> int { + let c = nv_crowd(nav_st, kind) + if c == null { return -1 } + return nvc_crowd_add_fixed(c, x, y, z, radius, height) +} +# walker i put at (x, y, z) going (vx, vz); false off the mesh +export function nav_crowd_place(nav_st: NavState, kind: int, i: int, x: float, y: float, z: float, vx: float, vz: float) -> bool { + let c = nv_crowd(nav_st, kind) + return c != null and i >= 0 and nvc_crowd_place(c, i, x, y, z, vx, vz) == 1 +} export function nav_crowd_remove(nav_st: NavState, kind: int, i: int) -> void { let c = nv_crowd(nav_st, kind) if c != null and i >= 0 { nvc_crowd_remove(c, i) } diff --git a/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib b/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib index 474332e5..9bfb38c7 100755 --- a/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib +++ b/packages/ludic.nav/lib/macos-arm64/libludicnav.dylib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:82c2b394bc9a6640055be28ce5172057b665759828e499d057df0cffe94aa05a -size 198320 +oid sha256:422b71e66182f0c12a32fb4660c536b46d0665bfdd2fa3e6f751d16c14761b55 +size 198464 diff --git a/packages/ludic.nav/lib/windows-x64/ludicnav.dll b/packages/ludic.nav/lib/windows-x64/ludicnav.dll index 253cf5f0..678bc410 100644 --- a/packages/ludic.nav/lib/windows-x64/ludicnav.dll +++ b/packages/ludic.nav/lib/windows-x64/ludicnav.dll @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a0eea4d94669a548e4874c07bc23caa321bf891f7a724331163e64f10006099 -size 374784 +oid sha256:14a3b0a5f6e6a6af79f8a6c205baeee57fc47d42148dec4190a8b3fdff633223 +size 375296 diff --git a/packages/ludic.nav/lib/windows-x64/ludicnav.lib b/packages/ludic.nav/lib/windows-x64/ludicnav.lib index 32d4c3af..e6dca282 100644 --- a/packages/ludic.nav/lib/windows-x64/ludicnav.lib +++ b/packages/ludic.nav/lib/windows-x64/ludicnav.lib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:754f4ea0ce90f7b512d9e6e12f59c5babaf1d071dfd8a30faff93bba8af52cee -size 5804 +oid sha256:382674cb7e3141c20b852cd25b43c6348de433fad36141b97335c0c44de9b5c7 +size 6228 diff --git a/packages/ludic.nav/native.ludic b/packages/ludic.nav/native.ludic index 0f1db0f2..b7f59072 100644 --- a/packages/ludic.nav/native.ludic +++ b/packages/ludic.nav/native.ludic @@ -18,6 +18,8 @@ extern function nvc_stage(size: int) -> pointer = "nav_stage" extern function nvc_crowd_new(h: pointer, max: int, max_radius: float) -> pointer = "nav_crowd_new" extern function nvc_crowd_free(c: pointer) -> void = "nav_crowd_free" extern function nvc_crowd_add(c: pointer, x: float, y: float, z: float, radius: float, height: float, speed: float, filter: int, separation: float) -> int = "nav_crowd_add" +extern function nvc_crowd_add_fixed(c: pointer, x: float, y: float, z: float, radius: float, height: float) -> int = "nav_crowd_add_fixed" +extern function nvc_crowd_place(c: pointer, i: int, x: float, y: float, z: float, vx: float, vz: float) -> int = "nav_crowd_place" extern function nvc_crowd_remove(c: pointer, i: int) -> void = "nav_crowd_remove" extern function nvc_crowd_target(c: pointer, i: int, x: float, y: float, z: float) -> int = "nav_crowd_target" extern function nvc_crowd_speed(c: pointer, i: int, speed: float) -> void = "nav_crowd_speed" diff --git a/packages/ludic.nav/native/shim/nav_crowd.inl b/packages/ludic.nav/native/shim/nav_crowd.inl index 178df676..62a44c6d 100644 --- a/packages/ludic.nav/native/shim/nav_crowd.inl +++ b/packages/ludic.nav/native/shim/nav_crowd.inl @@ -46,6 +46,43 @@ NAV_SHIM int nav_crowd_add(void *p, float x, float y, float z, float radius, flo if (!ref) return -1; return c->crowd->addAgent(near, &ap); } +// a walker the crowd avoids but does not steer (a player): placed by nav_crowd_place each frame +NAV_SHIM int nav_crowd_add_fixed(void *p, float x, float y, float z, float radius, float height) { + Crowd *c = static_cast(p); + dtCrowdAgentParams ap; + memset(&ap, 0, sizeof(ap)); + ap.radius = radius; + ap.height = height; + ap.maxSpeed = 20.0f; + ap.maxAcceleration = 1000.0f; + ap.collisionQueryRange = radius * 12.0f; + ap.pathOptimizationRange = radius * 30.0f; + ap.separationWeight = 0.0f; + ap.updateFlags = 0; + float pos[3] = {x, y, z}, near[3]; + dtPolyRef ref = 0; + c->nav->query->findNearestPoly(pos, c->nav->ext, c->crowd->getFilter(0), &ref, near); + if (!ref) return -1; + return c->crowd->addAgent(near, &ap); +} +// walker i put at (x, y, z) going (vx, vz), snapped to the mesh; 0 when it is off it (left where it was) +NAV_SHIM int nav_crowd_place(void *p, int i, float x, float y, float z, float vx, float vz) { + Crowd *c = static_cast(p); + dtCrowdAgent *a = c->crowd->getEditableAgent(i); + if (!a || !a->active) return 0; + float pos[3] = {x, y, z}, near[3]; + dtPolyRef ref = 0; + c->nav->query->findNearestPoly(pos, c->nav->ext, c->crowd->getFilter(0), &ref, near); + if (!ref) return 0; + a->corridor.reset(ref, near); + dtVcopy(a->npos, near); + float v[3] = {vx, 0.0f, vz}; + dtVcopy(a->vel, v); + dtVcopy(a->dvel, v); + dtVcopy(a->nvel, v); + a->desiredSpeed = dtVlen(v); + return 1; +} NAV_SHIM void nav_crowd_remove(void *p, int i) { static_cast(p)->crowd->removeAgent(i); } // send walker i toward (x, y, z), snapped to the mesh; 0 when the point is off it diff --git a/packages/ludic.nav/tests/crowd_test.ludic b/packages/ludic.nav/tests/crowd_test.ludic index 183e87cd..8f3ca4f3 100644 --- a/packages/ludic.nav/tests/crowd_test.ludic +++ b/packages/ludic.nav/tests/crowd_test.ludic @@ -81,4 +81,39 @@ program CrowdTest { expect(not nav_crowd_read(nav_st, NAV_PERSON, a)) expect_eq(nav_crowd_add(nav_st, NAV_PERSON, 5.0, 0.0, 5.0, 0.35, 1.8, 1.4, 0, 2.0), -1) } + + test "a player standing in the way is walked round and never moved" (nav_st: mut NavState) { + expect(nav_build(nav_st, NAV_PERSON, ground(40, 0), new NavConfig)) + expect(nav_crowd_start(nav_st, NAV_PERSON, 16, 0.6)) + let me = nav_crowd_add_fixed(nav_st, NAV_PERSON, 20.0, 0.0, 20.0, 0.4, 1.8) + expect(me >= 0) + let ids = new []int + for i in 0 .. 6 { + let id = nav_crowd_add(nav_st, NAV_PERSON, 4.0, 0.0, 19.0 + float(i % 3), 0.35, 1.8, 1.4, 0, 2.0) + expect(nav_crowd_target(nav_st, NAV_PERSON, id, 36.0, 0.0, 19.0 + float(i % 3))) + push(ids, id) + } + var worst = 1000.0 + for k in 0 .. 1800 { + expect(nav_crowd_place(nav_st, NAV_PERSON, me, 20.0, 0.0, 20.0, 0.0, 0.0)) + nav_crowd_step(nav_st, 1.0 / 60.0) + for i in 0 .. len(ids) { + nav_crowd_read(nav_st, NAV_PERSON, ids[i]) + let dx = nav_agent_x(nav_st) - 20.0 + let dz = nav_agent_z(nav_st) - 20.0 + worst = Math.min(worst, Math.sqrt(dx * dx + dz * dz)) + } + } + nav_crowd_place(nav_st, NAV_PERSON, me, 20.0, 0.0, 20.0, 0.0, 0.0) + nav_crowd_read(nav_st, NAV_PERSON, me) + expect_near(nav_agent_x(nav_st), 20.0, 0.01) + var past = 0 + for i in 0 .. len(ids) { + nav_crowd_read(nav_st, NAV_PERSON, ids[i]) + if nav_agent_x(nav_st) > 34.0 { past += 1 } + } + print(`crowd: a walker came within {worst} m of the player (radii 0.75); {past} of 6 got past`) + expect(worst > 0.65) + expect_eq(past, 6) + } }