feat(npc, nav): 17.5 - a person walks by the world's way where it has one (NpcWorld.way: the next corner toward the target, asked every half second, on reaching it or for a new target; the errand begins only at the target itself; the step round and the give-up stay the fallback), way_test round a wall; and ludic.nav reads a mesh file into the shim's own memory and frees it once parsed, so loading a map leaves nothing in Ludic's never-freed heap
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4add35b4ca
commit
ee0768bf3e
12 changed files with 184 additions and 39 deletions
|
|
@ -47,8 +47,8 @@ export function nav_polygons(nav_st: NavState, kind: int) -> int {
|
|||
return nvc_polys(h)
|
||||
}
|
||||
|
||||
# a kind's mesh written to a file, and read back. Reading holds the file's bytes for good (a slice
|
||||
# is never given back), so a mesh is loaded once per map, never per frame
|
||||
# a kind's mesh written to a file, and read back - through the asset pack when it is in one - into the
|
||||
# shim's own memory, which it frees once the tiles are made, so a map swap leaves nothing behind
|
||||
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 }
|
||||
|
|
@ -59,8 +59,14 @@ export function nav_save_file(nav_st: NavState, kind: int, path: string) -> bool
|
|||
return Fs.write_bytes(path, buf, n)
|
||||
}
|
||||
export function nav_load_file(nav_st: mut NavState, kind: int, path: string) -> bool {
|
||||
if not Fs.exists(path) { return false }
|
||||
let buf = Fs.read_bytes(path)
|
||||
if buf == null or len(buf) == 0 { return false }
|
||||
return nv_set(nav_st, kind, nvc_load(buf, len(buf)))
|
||||
let f = file_open(path, "rb")
|
||||
if f == null { return false }
|
||||
file_seek(f, 0, 2)
|
||||
let n = file_tell(f)
|
||||
file_seek(f, 0, 0)
|
||||
let buf = nvc_stage(n)
|
||||
var got = 0
|
||||
if buf != null { got = file_read(f, buf, n) }
|
||||
file_close(f)
|
||||
return nv_set(nav_st, kind, nvc_load_staged(buf, got))
|
||||
}
|
||||
|
|
|
|||
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.
|
|
@ -13,3 +13,5 @@ extern function nvc_path(h: pointer, f: int, sx: float, sy: float, sz: float, ex
|
|||
extern function nvc_partial(h: pointer) -> int = "nav_partial"
|
||||
extern function nvc_raycast(h: pointer, f: int, sx: float, sy: float, sz: float, ex: float, ez: float) -> int = "nav_raycast"
|
||||
extern function nvc_random_near(h: pointer, f: int, x: float, y: float, z: float, r: float, seed: int, out: pointer) -> int = "nav_random_near"
|
||||
extern function nvc_stage(size: int) -> pointer = "nav_stage"
|
||||
extern function nvc_load_staged(buf: pointer, size: int) -> pointer = "nav_load_staged"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include <DetourNavMeshQuery.h>
|
||||
#include <DetourCommon.h>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
|
|
|
|||
|
|
@ -100,3 +100,12 @@ NAV_SHIM void *nav_load(const unsigned char *buf, int size) {
|
|||
}
|
||||
return nav_open(mesh);
|
||||
}
|
||||
|
||||
// a file read straight into the shim's own memory rather than a Ludic slice (which is never given
|
||||
// back): nav_stage hands out the room, nav_load_staged parses it and frees it, whatever the outcome
|
||||
NAV_SHIM unsigned char *nav_stage(int size) { return size > 0 ? static_cast<unsigned char *>(malloc(size)) : nullptr; }
|
||||
NAV_SHIM void *nav_load_staged(unsigned char *buf, int size) {
|
||||
void *h = buf ? nav_load(buf, size) : nullptr;
|
||||
free(buf);
|
||||
return h;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,10 +87,18 @@ export port NpcWorld { # every member has a default: a flat dry field at n
|
|||
next_id: fn() -> int, body_of: fn(int) -> int # an id across machines; a look seed's body
|
||||
taken: fn(string) -> bool # a name the place keeps
|
||||
born / gone: fn(NpcPerson) -> void # draw it, stop drawing it
|
||||
way: fn(NpcPerson, x, z) -> bool, way_x, way_z # the next corner of its way toward (x, z)
|
||||
}
|
||||
export port NpcTalk { say: fn(NpcPerson, int) -> string, greeting: fn(NpcPerson) -> string }
|
||||
```
|
||||
|
||||
**A person walks by the world's way where the world has one.** `way` is asked for the next corner
|
||||
toward where it is going - every half second, on reaching the corner, and at once for a new target -
|
||||
and the person walks corner to corner, but begins its errand only at the target itself. With no
|
||||
answer (the default) it walks straight and steps round what is in the way, as before; the step
|
||||
round and the twenty seconds' give-up stay as the fallback. Maroon Lake binds `way` to its navmesh
|
||||
by a person's taste.
|
||||
|
||||
## API
|
||||
|
||||
| | |
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ export property NpcPerson {
|
|||
via_on: bool = false # a step round whatever is in the way
|
||||
vx: float = 0.0
|
||||
vz: float = 0.0
|
||||
wx: float = 0.0 # the corner of its way it walks to now (NpcWorld.way)
|
||||
wz: float = 0.0
|
||||
wtx: float = 0.0 # the target that corner was asked for: a new target asks again
|
||||
wtz: float = 0.0
|
||||
way_t: float = 0.0 # seconds until it asks the way again
|
||||
detours: int = 0
|
||||
fails: int = 0
|
||||
stall: float = 0.0 # seconds walking without moving
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import "ground.ludic"
|
|||
import "spots.ludic"
|
||||
import "routine.ludic"
|
||||
import "walk.ludic"
|
||||
import "way.ludic"
|
||||
import "act.ludic"
|
||||
import "names.ludic"
|
||||
import "census.ludic"
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@ export port NpcWorld {
|
|||
taken: fn(string) -> bool = fn nw_free # a name the place already uses
|
||||
born: fn(NpcPerson) -> void = fn nw_nothing # a new one (or a slot reused): draw it
|
||||
gone: fn(NpcPerson) -> void = fn nw_nothing # left: stop drawing it
|
||||
# (p, x, z): the next corner of this person's way toward (x, z) into way_x / _z; false, the
|
||||
# straight line with its steps round (a navmesh's path by a person's taste, in Maroon Lake)
|
||||
way: fn(NpcPerson, float, float) -> bool = fn nw_no_way
|
||||
way_x: fn() -> float = fn nw_asked_x
|
||||
way_z: fn() -> float = fn nw_asked_z
|
||||
}
|
||||
|
||||
# what the story says through them: the words of a line whose `fill` is set ("" when it has nothing
|
||||
|
|
@ -76,3 +81,4 @@ export function npc_near(npc_st: mut NpcState, x: float, z: float) -> float {
|
|||
}
|
||||
|
||||
export function npc_near_i(npc_st: NpcState) -> int { return npc_st.np_near_i }
|
||||
function nw_no_way(p: NpcPerson, x: float, z: float) -> bool { return false }
|
||||
|
|
|
|||
82
packages/ludic.npc/tests/way_test.ludic
Normal file
82
packages/ludic.npc/tests/way_test.ludic
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# way_test.ludic - a person walks by the world's way where the world has one: round a wall the
|
||||
# straight line runs into, by the corners NpcWorld.way gives, and arrives only where it was going -
|
||||
# never at a corner
|
||||
import "ludic.npc"
|
||||
import "ludic.base"
|
||||
import "fake"
|
||||
program NpcWayTest {
|
||||
numbers float
|
||||
bind NpcWorld {
|
||||
ground: fn fk_ground
|
||||
water: fn fk_water
|
||||
allowed: fn fk_allowed
|
||||
push: fn fk_push
|
||||
pushed_x: fn fk_pushed_x
|
||||
pushed_z: fn fk_pushed_z
|
||||
clear: fn fk_clear
|
||||
hour: fn fk_hour_now
|
||||
day: fn fk_day_now
|
||||
players: fn fk_players
|
||||
player_on: fn fk_player_on
|
||||
player_x: fn fk_x
|
||||
player_z: fn fk_z
|
||||
want: fn fk_wanted
|
||||
extra: fn fk_extra_kind
|
||||
taken: fn fk_taken
|
||||
born: fn fk_on_born
|
||||
gone: fn fk_on_gone
|
||||
way: fn wt_way
|
||||
way_x: fn wt_x
|
||||
way_z: fn wt_z
|
||||
}
|
||||
bind NpcTalk { say: fn tk_say, greeting: fn tk_greeting }
|
||||
|
||||
def NpcActs sit { }
|
||||
def NpcActs fish { turns: false }
|
||||
def NpcKinds from "kinds.lres"
|
||||
def NpcNames from "names.lres"
|
||||
def NpcLines from "lines.lres"
|
||||
|
||||
function tk_say(p: NpcPerson, line: int) -> string { return "" }
|
||||
function tk_greeting(p: NpcPerson) -> string { return "" }
|
||||
function tk_start(npc_st: mut NpcState, p: NpcPerson) -> void { npc_start_near(npc_st, p, 0.0, 0.0, 0.0, 10.0) }
|
||||
|
||||
state WayTestState {
|
||||
wx: float = 0.0
|
||||
wz: float = 0.0
|
||||
asked: int = 0
|
||||
}
|
||||
# a wall across x = 20 for |z| < 15 (a straight walk to (38, -25) meets it): the way goes round its
|
||||
# north end, then on to the target - which is clear of where the fake place forbids stopping
|
||||
function wt_way(way_test_st: mut WayTestState, p: NpcPerson, x: float, z: float) -> bool {
|
||||
way_test_st.asked += 1
|
||||
way_test_st.wx = x
|
||||
way_test_st.wz = z
|
||||
if p.x < 19.0 and p.z < 16.0 {
|
||||
way_test_st.wx = 20.0
|
||||
way_test_st.wz = 17.0
|
||||
}
|
||||
return true
|
||||
}
|
||||
function wt_x(way_test_st: WayTestState) -> float { return way_test_st.wx }
|
||||
function wt_z(way_test_st: WayTestState) -> float { return way_test_st.wz }
|
||||
|
||||
test "it walks round the wall by the corners it is given and begins only where it was going" (npc_st: mut NpcState, npc_tests_fake_st: mut NpcTestsFakeState, way_test_st: WayTestState) {
|
||||
fk_setup(npc_st, npc_tests_fake_st)
|
||||
npc_tests_fake_st.fk_want = 1
|
||||
let p = npc_spawn(npc_st, NPCK_HIKER)
|
||||
npc_set_at(p, 0.0, 0.0)
|
||||
npc_go(npc_st, p, 38.0, -25.0, NPCA_SIT, 30.0)
|
||||
var north = -100.0
|
||||
var k = 0
|
||||
while p.act == NPCA_WALK and k < 2000 {
|
||||
npc_tick(npc_st, 0.05)
|
||||
if p.x > 19.0 and p.x < 21.0 { north = Math.max(north, p.z) }
|
||||
k += 1
|
||||
}
|
||||
expect(north > 14.0)
|
||||
expect_eq(p.act, NPCA_SIT)
|
||||
expect(npc_d(p.x, p.z, 38.0, -25.0) < 1.3)
|
||||
expect(way_test_st.asked > 5)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# walk.ludic - walking to the target: turn toward it, step, slid round a trunk or a tent, never into
|
||||
# the water and never up a scramble - step round it instead - and give up on a target that gets no
|
||||
# closer for twenty seconds
|
||||
# walk.ludic - walking to the target by the world's way where it has one (way.ludic): turn toward the
|
||||
# next corner, step, slid round a trunk or a tent, never into the water and never up a scramble - step
|
||||
# round it instead - and give up on a target that gets no closer for twenty seconds
|
||||
const NP_BODY_R: float = 0.35 # a person's width, for the still things
|
||||
|
||||
export function npc_walk(npc_st: mut NpcState, p: NpcPerson, dt: float) -> void {
|
||||
|
|
@ -10,12 +10,17 @@ export function npc_walk(npc_st: mut NpcState, p: NpcPerson, dt: float) -> void
|
|||
gx = p.vx
|
||||
gz = p.vz
|
||||
}
|
||||
let dx = gx - p.x
|
||||
let dz = gz - p.z
|
||||
if Math.sqrt(dx * dx + dz * dz) < 1.2 {
|
||||
if npc_d(p.x, p.z, gx, gz) < 1.2 {
|
||||
np_arrive(npc_st, p)
|
||||
return
|
||||
}
|
||||
if not p.via_on {
|
||||
np_way(p, dt)
|
||||
gx = p.wx
|
||||
gz = p.wz
|
||||
}
|
||||
let dx = gx - p.x
|
||||
let dz = gz - p.z
|
||||
let dg = npc_d(p.x, p.z, p.tx, p.tz)
|
||||
if dg < p.best - 1.0 {
|
||||
p.best = dg
|
||||
|
|
@ -79,28 +84,3 @@ function np_arrive(npc_st: mut NpcState, p: NpcPerson) -> void {
|
|||
p.stall = 0.0
|
||||
if p.act != NPCA_WALK { np_fact(npc_st, NPC_F_ARRIVED, p, -1) }
|
||||
}
|
||||
|
||||
function np_detour(p: NpcPerson, want: float) -> bool {
|
||||
for k in 0 .. 8 {
|
||||
let ang = want + float((k / 2 + 1) * (1 - 2 * (k % 2))) * 0.6
|
||||
let x = p.x - Math.sin(ang) * 6.0
|
||||
let z = p.z - Math.cos(ang) * 6.0
|
||||
if npc_line_ok(p.x, p.z, x, z) {
|
||||
p.via_on = true
|
||||
p.vx = x
|
||||
p.vz = z
|
||||
p.yaw = ang
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function np_stuck(npc_st: mut NpcState, p: NpcPerson) -> void {
|
||||
p.fails += 1
|
||||
p.via_on = false
|
||||
if p.fails >= 3 {
|
||||
p.fails = 0
|
||||
npc_wander(npc_st, p)
|
||||
} else { npc_plan(npc_st, p) }
|
||||
}
|
||||
|
|
|
|||
45
packages/ludic.npc/way.ludic
Normal file
45
packages/ludic.npc/way.ludic
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# way.ludic - where a person walks when the straight line will not do: the world's way (NpcWorld.way,
|
||||
# a navmesh by a person's taste in Maroon Lake), and without one a step round what is in the way,
|
||||
# and giving the target up after three tries
|
||||
|
||||
# the next corner toward the target, asked again every half second, on reaching it, or for a new target
|
||||
const NP_WAY_EVERY: float = 0.5
|
||||
function np_way(p: NpcPerson, dt: float) -> void {
|
||||
p.way_t = p.way_t - dt
|
||||
let fresh = p.wtx != p.tx or p.wtz != p.tz
|
||||
if p.way_t > 0.0 and not fresh and npc_d(p.x, p.z, p.wx, p.wz) >= 1.0 { return }
|
||||
p.way_t = NP_WAY_EVERY
|
||||
p.wtx = p.tx
|
||||
p.wtz = p.tz
|
||||
p.wx = p.tx
|
||||
p.wz = p.tz
|
||||
if NpcWorld.way(p, p.tx, p.tz) {
|
||||
p.wx = NpcWorld.way_x()
|
||||
p.wz = NpcWorld.way_z()
|
||||
}
|
||||
}
|
||||
|
||||
function np_detour(p: NpcPerson, want: float) -> bool {
|
||||
for k in 0 .. 8 {
|
||||
let ang = want + float((k / 2 + 1) * (1 - 2 * (k % 2))) * 0.6
|
||||
let x = p.x - Math.sin(ang) * 6.0
|
||||
let z = p.z - Math.cos(ang) * 6.0
|
||||
if npc_line_ok(p.x, p.z, x, z) {
|
||||
p.via_on = true
|
||||
p.vx = x
|
||||
p.vz = z
|
||||
p.yaw = ang
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function np_stuck(npc_st: mut NpcState, p: NpcPerson) -> void {
|
||||
p.fails += 1
|
||||
p.via_on = false
|
||||
if p.fails >= 3 {
|
||||
p.fails = 0
|
||||
npc_wander(npc_st, p)
|
||||
} else { npc_plan(npc_st, p) }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue