blocks/npc.rs + sim/sense.rs. Utility scoring re-run every ~0.45s per NPC, staggered so a crowd doesn't re-plan on one tick. Candidates scored additively: visit a POI (tag appeal x distance falloff x novelty x jitter), loiter near someone (gated on sociability), go home (grows with time away), wander (the fallback, so a world with no POIs still moves). Three things do the legibility work. POIs, so NPCs walk to THINGS rather than coordinates. Per-NPC seeded personality (haste/patience/sociability/curiosity/ homebody) so identical config still yields unlike villagers. And a day clock with a per-NPC phase offset — benches read as afternoon, doors as evening — which is what stops ten villagers doing the same thing in unison. Activities are deliberately only four (Idle/Travel/Dwell/Follow); routines come from sequencing them, not from twenty verbs. Sensing reads THE SAME SOLID FILTER the mover sweep uses, so perception and collision cannot disagree. obstacle_ahead sweeps the NPC's own box rather than casting a ray, because a ray through a doorway reports "clear" for a body twice its width. Blocked -> jump if the top is in reach with landing room, else sidestep toward the side with clearance (blended with the goal so it curves rather than turning 90 degrees), else a stuck timer abandons the goal. Reading the existing tests caught a bug in the new logic: "low obstacle -> walk over it" is wrong, because the 0.55 step-up is a TERRAIN contract and sweep_axis blocks against static boxes at any height. That branch is gone — it was exactly the perception/physics disagreement this module exists to avoid. Two bugs the tests caught: - MUTUAL SOCIAL LOCK: two sociable NPCs each chose to loiter near the other, permanently. One moved exactly 0.0 units in 90 seconds. A social cooldown stops Follow being re-picked immediately - VILLAGE DRIFT: an unbiased random walk has no centre, and a trace showed a villager 43 units out with every POI inside 18. Wander steps past a 26-unit leash now aim home Cost against a 16.6 ms budget: 50 NPCs 0.007 ms/tick, 200 NPCs 0.046 ms/tick (full sim step — a pre_step-only figure would be a lie, since without step_world the NPCs never move and re-decide more often). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
144 lines
4.1 KiB
Rust
144 lines
4.1 KiB
Rust
//! Villager cost + behaviour probe: `cargo run --release --example village`.
|
|
//!
|
|
//! Prints per-tick cost at two crowd sizes and a readable trace of what a
|
|
//! handful of villagers actually did, since behaviour has no other inspection
|
|
//! surface without a screen.
|
|
|
|
use makepad_game_blocks::*;
|
|
use makepad_game_sim::*;
|
|
use makepad_math::*;
|
|
use std::time::Instant;
|
|
|
|
fn ground(world: &mut GameWorld) {
|
|
world.next_id += 1;
|
|
let id = world.next_id;
|
|
world.push_entity(Entity {
|
|
id,
|
|
kind: BodyKind::Static,
|
|
pos: vec3f(0.0, -1.0, 0.0),
|
|
half: vec3f(200.0, 1.0, 200.0),
|
|
collide: true,
|
|
scale: vec3f(1.0, 1.0, 1.0),
|
|
scale_target: vec3f(1.0, 1.0, 1.0),
|
|
density: 1.0,
|
|
friction: 0.6,
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
fn prop(world: &mut GameWorld, pos: Vec3f, half: Vec3f) {
|
|
world.next_id += 1;
|
|
let id = world.next_id;
|
|
world.push_entity(Entity {
|
|
id,
|
|
kind: BodyKind::Static,
|
|
pos,
|
|
half,
|
|
collide: true,
|
|
scale: vec3f(1.0, 1.0, 1.0),
|
|
scale_target: vec3f(1.0, 1.0, 1.0),
|
|
density: 1.0,
|
|
friction: 0.6,
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
fn village(count: usize) -> (GameWorld, Blocks) {
|
|
let mut world = GameWorld::new();
|
|
world.reset_content();
|
|
world.gravity = 30.0;
|
|
ground(&mut world);
|
|
// Some buildings to walk around.
|
|
for i in 0..12 {
|
|
let a = i as f32 * 0.52;
|
|
prop(
|
|
&mut world,
|
|
vec3f(a.cos() * 26.0, 2.0, a.sin() * 26.0),
|
|
vec3f(3.0, 2.0, 3.0),
|
|
);
|
|
}
|
|
let mut blocks = Blocks::new();
|
|
for (x, z, tag) in [
|
|
(10.0, 4.0, "bench"),
|
|
(-12.0, 7.0, "well"),
|
|
(5.0, -15.0, "door"),
|
|
(-8.0, -12.0, "market"),
|
|
(16.0, -6.0, "lamp"),
|
|
(0.0, 18.0, "work"),
|
|
] {
|
|
blocks
|
|
.pois
|
|
.push(Poi::new(vec3f(x, 0.0, z), tag).with_capacity(2));
|
|
}
|
|
for i in 0..count {
|
|
let a = i as f32 * 0.7;
|
|
let r = 6.0 + (i % 7) as f32 * 1.5;
|
|
let pos = vec3f(a.cos() * r, 1.0, a.sin() * r);
|
|
world.next_id += 1;
|
|
let id = world.next_id;
|
|
world.push_entity(Entity {
|
|
id,
|
|
kind: BodyKind::Mover,
|
|
pos,
|
|
half: vec3f(0.4, 0.8, 0.4),
|
|
collide: true,
|
|
scale: vec3f(1.0, 1.0, 1.0),
|
|
scale_target: vec3f(1.0, 1.0, 1.0),
|
|
gravity_scale: 1.0,
|
|
speed_mult: 1.0,
|
|
turn_rate: 9.0,
|
|
auto_face: true,
|
|
density: 1.0,
|
|
..Default::default()
|
|
});
|
|
blocks
|
|
.npcs
|
|
.push(Npc::new(id, NpcConfig::default(), pos, 500 + i as u64));
|
|
}
|
|
(world, blocks)
|
|
}
|
|
|
|
fn bench(count: usize, ticks: usize) {
|
|
let (mut world, mut blocks) = village(count);
|
|
// Warm the caches so the first tick's allocation isn't the headline.
|
|
for _ in 0..60 {
|
|
blocks.pre_step(&mut world);
|
|
step_world(&mut world);
|
|
blocks.post_step(&mut world);
|
|
}
|
|
let t = Instant::now();
|
|
for _ in 0..ticks {
|
|
blocks.pre_step(&mut world);
|
|
step_world(&mut world);
|
|
blocks.post_step(&mut world);
|
|
}
|
|
let per_tick = t.elapsed().as_secs_f64() * 1000.0 / ticks as f64;
|
|
// Isolate the NPC share by running the same world with the blocks phase only.
|
|
let t = Instant::now();
|
|
for _ in 0..ticks {
|
|
blocks.pre_step(&mut world);
|
|
}
|
|
let npc_only = t.elapsed().as_secs_f64() * 1000.0 / ticks as f64;
|
|
println!(
|
|
"{count:>4} npcs: {per_tick:.3} ms/tick full ({npc_only:.3} ms/tick in blocks::pre_step)"
|
|
);
|
|
}
|
|
|
|
fn main() {
|
|
bench(50, 600);
|
|
bench(200, 600);
|
|
|
|
println!("\n--- a few villagers over four minutes ---");
|
|
let (mut world, mut blocks) = village(6);
|
|
for t in 0..60 * 240 {
|
|
blocks.pre_step(&mut world);
|
|
step_world(&mut world);
|
|
blocks.post_step(&mut world);
|
|
if t % (60 * 20) == 0 {
|
|
println!("[t={:>4}s]", t / 60);
|
|
for n in blocks.npcs.iter().take(3) {
|
|
println!(" {}", n.trace(&world));
|
|
}
|
|
}
|
|
}
|
|
}
|