makepad/libs/game/blocks/examples/village.rs
andodeki 435135ed50 feat(arcade): port makepad game libs + arcade from makepad-internal onto the fork
Reintroduce apps/arcade and the ten game crates that were ripped out of
the fork (assets, audio, blocks, coedit, gen, net, pkg, render, script,
session), from makepad-internal/dev. Reconcile against the fork's layout:

- makepad-game-sim/math come from the fork's existing libs/sim + sim/math;
  the copied internal libs/game/{sim,math} are dropped and the kept game
  crates plus arcade point at ../../sim and ../../sim/math.
- arcade AI is rewritten off the removed makepad-ai agent API onto the
  makepad-ai-hub headless ChatProvider worker (example/cad pattern):
  AiWorker{send,cancel,poll}, ai_worker_loop driving
  ClaudeApiChatProvider(ClaudeCli), AiWorkerEvent availability/delta/
  done/error mapped into the chat feed and authoring land_edit, with
  main.rs send_message/cancel_request/event-drain rebuilt around it.
- the game-script sandbox is adapted to the fork's jailed splash storage:
  every isolate gets mod.fs = splash_storage jail; splaes splash_storage
  set_root_for_heap is now public so the ScriptHost (or Splash) can grant
  a per-game jail root. ENT font: build.rs/dispatch.rs entities fill new
  fork sim Entity/Part fields via ..Default::default(); world_raycast now
  borrows mutably and returns the material id.

cargo check -p makepad-arcade and the game + arcade test suites pass.
2026-09-05 05:49:12 +03:00

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));
}
}
}
}