Skip to main content

nightshade_api/
navigation.rs

1//! Pathfinding in three calls: bake, spawn a walker, tell it where to go.
2//! The engine's navmesh systems run in the frame schedule, so walkers move on
3//! their own once given a destination.
4
5use nightshade::prelude::nalgebra_glm::Mat4;
6use nightshade::prelude::*;
7
8/// Bakes a navigation mesh from the world's current static geometry: every
9/// visible mesh that is not a dynamic physics body, an agent, or api
10/// scaffolding, transformed into world space. Call once after the level is
11/// built, and again after the level changes.
12pub fn bake_navmesh(world: &mut World) {
13    let mut sources: Vec<(Entity, String, Mat4)> = Vec::new();
14    world
15        .core
16        .query()
17        .with(RENDER_MESH | GLOBAL_TRANSFORM)
18        .without(NAVMESH_AGENT | CLOTH)
19        .iter(|entity, table, index| {
20            sources.push((
21                entity,
22                table.render_mesh[index].name.clone(),
23                table.global_transform[index].0,
24            ));
25        });
26
27    let mut vertices: Vec<[f32; 3]> = Vec::new();
28    let mut indices: Vec<[u32; 3]> = Vec::new();
29    for (entity, source_mesh_name, matrix) in sources {
30        if world
31            .core
32            .get_visibility(entity)
33            .is_some_and(|visibility| !visibility.visible)
34        {
35            continue;
36        }
37        if world
38            .core
39            .get_name(entity)
40            .is_some_and(|name| name.0.starts_with(crate::runner::RESERVED_PREFIX))
41        {
42            continue;
43        }
44        #[cfg(feature = "physics")]
45        if world.core.get_rigid_body(entity).is_some_and(|body| {
46            !matches!(
47                body.body_type,
48                nightshade::ecs::physics::types::RigidBodyType::Fixed
49            )
50        }) {
51            continue;
52        }
53        let Some(mesh) = registry_entry_by_name(
54            &world.resources.assets.mesh_cache.registry,
55            &source_mesh_name,
56        ) else {
57            continue;
58        };
59        let base_index = vertices.len() as u32;
60        for vertex in &mesh.vertices {
61            let position = matrix
62                * vec4(
63                    vertex.position[0],
64                    vertex.position[1],
65                    vertex.position[2],
66                    1.0,
67                );
68            vertices.push([position.x, position.y, position.z]);
69        }
70        for triangle in mesh.indices.chunks_exact(3) {
71            indices.push([
72                base_index + triangle[0],
73                base_index + triangle[1],
74                base_index + triangle[2],
75            ]);
76        }
77    }
78
79    if let Some(navmesh) =
80        generate_navmesh_recast(&vertices, &indices, &RecastNavMeshConfig::default())
81    {
82        world.resources.navmesh = navmesh;
83    }
84}
85
86/// Spawns an agent that walks the baked navmesh at three units per second,
87/// adjustable with [`set_walk_speed`]. Returns the agent entity, whose
88/// [`position`](crate::prelude::position) tracks it as it moves and which
89/// renders as a capsule. Parent your own mesh to it with
90/// [`set_parent`](crate::prelude::set_parent) for a custom look.
91pub fn spawn_walker(world: &mut World, position: Vec3) -> Entity {
92    spawn_navmesh_agent(world, position, 3.0)
93}
94
95/// Sends the agent toward `destination` along the navmesh.
96#[inline]
97pub fn walk_to(world: &mut World, agent: Entity, destination: Vec3) {
98    set_agent_destination(world, agent, destination);
99}
100
101/// Sets the agent's movement speed in units per second.
102#[inline]
103pub fn set_walk_speed(world: &mut World, agent: Entity, speed: f32) {
104    set_agent_speed(world, agent, speed);
105}
106
107/// Stops the agent where it stands.
108#[inline]
109pub fn stop_walking(world: &mut World, agent: Entity) {
110    stop_agent(world, agent);
111}