diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7b0c9a7..a730975 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -31,18 +31,24 @@ jobs: - name: Build run: | - cargo build --target wasm32-unknown-unknown --release --example moving + cargo build --target wasm32-unknown-unknown --release --example moving --features bevy/webgl2 wasm-bindgen --no-typescript --out-dir wasm --target web target/wasm32-unknown-unknown/release/examples/moving.wasm - cargo build --target wasm32-unknown-unknown --release --example lines + cargo build --target wasm32-unknown-unknown --release --example lines --features bevy/webgl2 wasm-bindgen --no-typescript --out-dir wasm --target web target/wasm32-unknown-unknown/release/examples/lines.wasm - cargo build --target wasm32-unknown-unknown --release --example many + cargo build --target wasm32-unknown-unknown --release --example many --features bevy/webgl2 wasm-bindgen --no-typescript --out-dir wasm --target web target/wasm32-unknown-unknown/release/examples/many.wasm - cargo build --target wasm32-unknown-unknown --release --example gltf + cargo build --target wasm32-unknown-unknown --release --example gltf --features bevy/webgl2 wasm-bindgen --no-typescript --out-dir wasm --target web target/wasm32-unknown-unknown/release/examples/gltf.wasm + cargo build --target wasm32-unknown-unknown --release --package interactive --features bevy/webgl2 + wasm-bindgen --no-typescript --out-dir wasm --target web target/wasm32-unknown-unknown/release/interactive.wasm + + cargo build --target wasm32-unknown-unknown --release --package physics_xpbd --features bevy/webgl2 + wasm-bindgen --no-typescript --out-dir wasm --target web target/wasm32-unknown-unknown/release/physics_xpbd.wasm + - name: Copy Assets run: cp -r assets wasm/ diff --git a/Cargo.toml b/Cargo.toml index 3504c5a..94c55b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,25 +1,29 @@ [package] -name = "bevy_pathmesh" -version = "0.5.0" -authors = ["François Mockers "] +name = "vleue_navigator" +version = "0.7.1" +authors = ["François Mockers "] edition = "2021" license = "MIT OR Apache-2.0" -keywords = ["pathfinding", "bevy", "navmesh"] +keywords = ["pathfinding", "bevy", "navmesh", "navigation"] readme = "README.md" description = "Navmesh plugin for Bevy" -repository = "https://github.com/vleue/bevy_pathmesh" -homepage = "https://github.com/vleue/bevy_pathmesh" -documentation = "https://docs.rs/bevy_pathmesh" +repository = "https://github.com/vleue/vleue_navigator" +homepage = "https://github.com/vleue/vleue_navigator" +documentation = "https://docs.rs/vleue_navigator" categories = ["game-development"] [dependencies] -itertools = "0.11" +itertools = "0.12" +tracing = { version = "0.1", optional = true } [dependencies.polyanya] -version = "0.4" +version = "0.5" +# path = "../polyanya" +git = "https://github.com/vleue/polyanya" +branch = "radius-baking" [dependencies.bevy] -version = "0.11" +version = "0.13" features = ["bevy_render", "bevy_asset"] default-features = false @@ -27,7 +31,7 @@ default-features = false rand = "0.8" [dev-dependencies.bevy] -version = "0.11" +version = "0.13" features = [ "bevy_ui", "bevy_text", @@ -38,10 +42,10 @@ features = [ "bevy_pbr", "bevy_sprite", "bevy_gizmos", - # Required for TonyMcMapface "tonemapping_luts", "ktx2", "zstd", + "multi-threaded", ] default-features = false @@ -51,3 +55,6 @@ linuxci = ["bevy/x11"] [profile.dev.package."*"] opt-level = 3 + +[workspace] +members = ["examples/interactive", "examples/physics_xpbd"] diff --git a/README.md b/README.md index 6d971c8..59eaf0e 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ -# NavMesh for Bevy +# Navigation for Bevy with NavMesh ![MIT/Apache 2.0](https://img.shields.io/badge/license-MIT%2FApache-blue.svg) -[![Release Doc](https://docs.rs/bevy_pathmesh/badge.svg)](https://docs.rs/bevy_pathmesh) -[![Crate](https://img.shields.io/crates/v/bevy_pathmesh.svg)](https://crates.io/crates/bevy_pathmesh) +[![Release Doc](https://docs.rs/vleue_navigator/badge.svg)](https://docs.rs/vleue_navigator) +[![Crate](https://img.shields.io/crates/v/vleue_navigator.svg)](https://crates.io/crates/vleue_navigator) Navigation mesh for [Bevy](http://github.com/bevyengine/bevy) using [Polyanya](https://github.com/vleue/polyanya). -![map with many points finding their paths](https://raw.githubusercontent.com/vleue/bevy_pathmesh/main/screenshots/many.png) +![map with many points finding their paths](https://raw.githubusercontent.com/vleue/vleue_navigator/main/screenshots/many.png) -Check out the [WASM demo](https://vleue.github.io/bevy_pathmesh/) +Check out the [WASM demo](https://vleue.github.io/vleue_navigator/) ## Usage -Loading a mesh from a gLTF file, then building a `PathMesh` from it and using it for getting paths between random points. +Loading a mesh from a gLTF file, then building a `NavMesh` from it and using it for getting paths between random points. ```rust,no_run use bevy::{ @@ -20,19 +20,19 @@ use bevy::{ prelude::*, }; -use bevy_pathmesh::{PathMesh, PathMeshPlugin}; +use vleue_navigator::{NavMesh, VleueNavigatorPlugin}; use rand::Rng; fn main() { App::new() - .add_plugins((DefaultPlugins, PathMeshPlugin)) + .add_plugins((DefaultPlugins, VleueNavigatorPlugin)) .add_systems(Startup, load) .add_systems(Update, get_path) .run() } #[derive(Resource)] -struct Handles(Handle, Option>); +struct Handles(Handle, Option>); fn load(mut commands: Commands, asset_server: Res) { commands.insert_resource(Handles(asset_server.load("navmesh.glb"), None)); @@ -43,7 +43,7 @@ fn get_path( gltfs: Res>, gltf_meshes: Res>, meshes: Res>, - mut path_meshes: ResMut>, + mut navmeshes: ResMut>, ) { if handles.1.is_none() { // Get the gltf struct loaded from the file @@ -58,11 +58,11 @@ fn get_path( let Some(mesh) = meshes.get(&gltf_mesh.primitives[0].mesh) else { return }; - // Build a `PathMesh` from that mesh, then save it as an asset - handles.1 = Some(path_meshes.add(PathMesh::from_bevy_mesh(mesh))); + // Build a `NavMesh` from that mesh, then save it as an asset + handles.1 = Some(navmeshes.add(NavMesh::from_bevy_mesh(mesh))); } else { - // Get the path mesh, then search for a path - let Some(path_mesh) = path_meshes.get(handles.1.as_ref().unwrap()) else { + // Get the navmesh, then search for a path + let Some(navmesh) = navmeshes.get(handles.1.as_ref().unwrap()) else { return }; // Find two random point @@ -74,7 +74,7 @@ fn get_path( rand::thread_rng().gen_range(-50.0..50.0), rand::thread_rng().gen_range(-50.0..50.0), ); - if let Some(path) = path_mesh.path(from, to) { + if let Some(path) = navmesh.path(from, to) { info!("path from {} to {}: {:?}", from, to, path); } else { info!("no path between {} and {}", from, to) @@ -83,7 +83,6 @@ fn get_path( } ``` -|Bevy|bevy_pathmesh| +|Bevy|vleue_navigator| |---|---| -|0.11|0.5| -|0.10|0.4| +|0.13|0.7| diff --git a/examples/gltf.rs b/examples/gltf.rs index e643e2f..d4f4937 100644 --- a/examples/gltf.rs +++ b/examples/gltf.rs @@ -4,15 +4,13 @@ use bevy::{ math::Vec3Swizzles, pbr::NotShadowCaster, prelude::*, - reflect::TypeUuid, window::PrimaryWindow, }; -use bevy_pathmesh::{PathMesh, PathMeshPlugin}; use rand::Rng; use std::f32::consts::FRAC_PI_2; +use vleue_navigator::{NavMesh, VleueNavigatorPlugin}; -const HANDLE_TRIMESH_OPTIMIZED: HandleUntyped = - HandleUntyped::weak_from_u64(PathMesh::TYPE_UUID, 0); +const HANDLE_TRIMESH_OPTIMIZED: Handle = Handle::weak_from_u128(72919684653223358031); fn main() { App::new() @@ -22,14 +20,13 @@ fn main() { DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { title: "Navmesh with Polyanya".to_string(), - fit_canvas_to_parent: true, ..default() }), ..default() }), - PathMeshPlugin, + VleueNavigatorPlugin, )) - .add_state::() + .init_state::() .add_systems(OnEnter(AppState::Setup), setup) .add_systems(Update, check_textures.run_if(in_state(AppState::Setup))) .add_systems(OnExit(AppState::Setup), setup_scene) @@ -59,14 +56,14 @@ enum AppState { struct GltfHandle(Handle); #[derive(Resource)] -struct CurrentMesh(Handle); +struct CurrentMesh(Handle); fn setup(mut commands: Commands, asset_server: Res) { commands.insert_resource(GltfHandle(asset_server.load("meshes/navmesh.glb"))); commands.insert_resource(AmbientLight { color: Color::SEA_GREEN, - brightness: 0.05, + brightness: 100.0, }); commands.spawn(Camera3dBundle { @@ -127,7 +124,7 @@ fn setup(mut commands: Commands, asset_server: Res) { ..Default::default() }); - commands.insert_resource(CurrentMesh(HANDLE_TRIMESH_OPTIMIZED.typed())); + commands.insert_resource(CurrentMesh(HANDLE_TRIMESH_OPTIMIZED)); } fn check_textures( @@ -135,7 +132,7 @@ fn check_textures( gltf: ResMut, asset_server: Res, ) { - if let LoadState::Loaded = asset_server.get_load_state(gltf.id()) { + if let Some(LoadState::Loaded) = asset_server.get_load_state(gltf.id()) { next_state.set(AppState::Playing); } } @@ -156,7 +153,7 @@ struct Target; struct Hover(Vec2); #[derive(Component, Clone)] -struct NavMeshDisp(Handle); +struct NavMeshDisp(Handle); fn setup_scene( mut commands: Commands, @@ -165,12 +162,12 @@ fn setup_scene( gltf_meshes: Res>, mut meshes: ResMut>, mut materials: ResMut>, - mut pathmeshes: ResMut>, + mut navmeshes: ResMut>, ) { let mut material: StandardMaterial = Color::ALICE_BLUE.into(); material.perceptual_roughness = 1.0; let ground_material = materials.add(material); - if let Some(gltf) = gltfs.get(&gltf) { + if let Some(gltf) = gltfs.get(gltf.id()) { let mesh = gltf_meshes.get(&gltf.named_meshes["obstacles"]).unwrap(); let mut material: StandardMaterial = Color::GRAY.into(); material.perceptual_roughness = 1.0; @@ -199,7 +196,7 @@ fn setup_scene( commands.spawn(( SpotLightBundle { spot_light: SpotLight { - intensity: 800.0, + intensity: 1000000.0, color: Color::SEA_GREEN, shadows_enabled: true, inner_angle: 0.5, @@ -223,9 +220,9 @@ fn setup_scene( } } - if let Some(gltf) = gltfs.get(&gltf) { + if let Some(gltf) = gltfs.get(gltf.id()) { { - let navmesh = bevy_pathmesh::PathMesh::from_bevy_mesh( + let navmesh = vleue_navigator::NavMesh::from_bevy_mesh( meshes .get( &gltf_meshes @@ -248,16 +245,20 @@ fn setup_scene( visibility: Visibility::Hidden, ..Default::default() }, - NavMeshDisp(HANDLE_TRIMESH_OPTIMIZED.typed()), + NavMeshDisp(HANDLE_TRIMESH_OPTIMIZED), )); - pathmeshes.set_untracked(HANDLE_TRIMESH_OPTIMIZED, navmesh); + navmeshes.insert(HANDLE_TRIMESH_OPTIMIZED, navmesh); } commands .spawn(( PbrBundle { - mesh: meshes.add(Mesh::from(shape::Capsule { ..default() })), - material: materials.add(Color::BLUE.into()), + mesh: meshes.add(Mesh::from(Capsule3d { ..default() })), + material: materials.add(StandardMaterial { + base_color: Color::BLUE, + emissive: Color::BLUE * 50.0, + ..default() + }), transform: Transform::from_xyz(-1.0, 0.0, -2.0), ..Default::default() }, @@ -269,7 +270,7 @@ fn setup_scene( point_light: PointLight { color: Color::BLUE, range: 500.0, - intensity: 2000.0, + intensity: 100000.0, shadows_enabled: true, ..default() }, @@ -283,7 +284,7 @@ fn setup_scene( fn give_target_auto( mut commands: Commands, mut object_query: Query<(Entity, &Transform, &mut Object), Without>, - navmeshes: Res>, + navmeshes: Res>, mut meshes: ResMut>, mut materials: ResMut>, current_mesh: Res, @@ -303,7 +304,7 @@ fn give_target_auto( let Some(path) = navmesh.transformed_path(transform.translation, Vec3::new(x, 0.0, z)) else { - break + break; }; if let Some((first, remaining)) = path.path.split_first() { let mut remaining = remaining.to_vec(); @@ -311,11 +312,15 @@ fn give_target_auto( let target_id = commands .spawn(( PbrBundle { - mesh: meshes.add(Mesh::from(shape::UVSphere { + mesh: meshes.add(Mesh::from(Sphere { radius: 0.5, ..default() })), - material: materials.add(Color::RED.into()), + material: materials.add(StandardMaterial { + base_color: Color::RED, + emissive: Color::RED * 50.0, + ..default() + }), transform: Transform::from_xyz(x, 0.0, z), ..Default::default() }, @@ -327,7 +332,7 @@ fn give_target_auto( point_light: PointLight { color: Color::RED, shadows_enabled: true, - range: 3.0, + range: 10.0, ..default() }, transform: Transform::from_xyz(0.0, 1.5, 0.0), @@ -348,11 +353,11 @@ fn give_target_on_click( mut commands: Commands, mut object_query: Query<(Entity, &Transform, &mut Object)>, targets: Query>, - navmeshes: Res>, + navmeshes: Res>, mut meshes: ResMut>, mut materials: ResMut>, current_mesh: Res, - mouse_buttons: Res>, + mouse_buttons: Res>, primary_window: Query<&Window, With>, camera: Query<(&Camera, &GlobalTransform)>, ) { @@ -362,18 +367,17 @@ fn give_target_on_click( let position = primary_window.single().cursor_position()?; let (camera, transform) = camera.get_single().ok()?; let ray = camera.viewport_to_world(transform, position)?; - let denom = Vec3::Y.dot(ray.direction); - let t = (Vec3::ZERO - ray.origin).dot(Vec3::Y) / denom; + let denom = Vec3::Y.dot(ray.direction.into()); + let t = (Vec3::ZERO - ray.origin).dot(Vec3::Y) / denom; let target = ray.origin + ray.direction * t; navmesh.transformed_is_in_mesh(target).then_some(target) })() else { - return + return; }; for (entity, transform, mut object) in object_query.iter_mut() { - let Some(path) = navmesh.transformed_path(transform.translation, target) - else { - break + let Some(path) = navmesh.transformed_path(transform.translation, target) else { + break; }; if let Some((first, remaining)) = path.path.split_first() { let mut remaining = remaining.to_vec(); @@ -381,11 +385,15 @@ fn give_target_on_click( let target_id = commands .spawn(( PbrBundle { - mesh: meshes.add(Mesh::from(shape::UVSphere { + mesh: meshes.add(Mesh::from(Sphere { radius: 0.5, ..default() })), - material: materials.add(Color::RED.into()), + material: materials.add(StandardMaterial { + base_color: Color::RED, + emissive: Color::RED * 50.0, + ..default() + }), transform: Transform::from_translation(target), ..Default::default() }, @@ -397,7 +405,7 @@ fn give_target_on_click( point_light: PointLight { color: Color::RED, shadows_enabled: true, - range: 3.0, + range: 10.0, ..default() }, transform: Transform::from_xyz(0.0, 1.5, 0.0), @@ -440,7 +448,7 @@ fn move_object( fn trigger_navmesh_visibility( mut query: Query<(&mut Visibility, &NavMeshDisp)>, - keyboard_input: ResMut>, + keyboard_input: ResMut>, current_mesh: Res, ) { if keyboard_input.just_pressed(KeyCode::Space) { @@ -463,7 +471,7 @@ fn target_activity( ) { for children in &target { point_light.get_mut(children[0]).unwrap().intensity = - (time.elapsed_seconds() * 10.0).sin().abs() * 100.0; + (time.elapsed_seconds() * 10.0).sin().abs() * 100000.0; } } diff --git a/examples/interactive/Cargo.toml b/examples/interactive/Cargo.toml new file mode 100644 index 0000000..4b02fa2 --- /dev/null +++ b/examples/interactive/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "interactive" +version = "0.1.0" +edition = "2021" +publish = false +license = "MIT OR Apache-2.0" + +[dependencies.bevy] +version = "0.13" +default-features = false +features = [ + "bevy_asset", + "bevy_winit", + "bevy_core_pipeline", + "bevy_gizmos", + "bevy_pbr", + "bevy_render", + "bevy_sprite", + "bevy_text", + "bevy_ui", + "png", + "hdr", + "default_font", + "tonemapping_luts", + "ktx2", + "zstd", +] + +[dependencies] +vleue_navigator = { path = "../../" } +rand = "0.8" +bevy_vector_shapes = "0.7" diff --git a/examples/interactive/src/build_navmesh.rs b/examples/interactive/src/build_navmesh.rs new file mode 100644 index 0000000..9beba46 --- /dev/null +++ b/examples/interactive/src/build_navmesh.rs @@ -0,0 +1,95 @@ +use bevy::{prelude::*, render::primitives::Aabb}; +use vleue_navigator::{ + updater::{NavMeshStatus, NavmeshUpdaterPlugin}, + NavMesh, +}; + +use crate::{ + ui::{Slider, UiButton, UiInfo}, + Agent, Obstacle, Path, Target, HANDLE_NAVMESH_MESH, HANDLE_NAVMESH_WIREFRAME, +}; + +pub struct BuilderPlugin; + +impl Plugin for BuilderPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(NavmeshUpdaterPlugin::::default()) + .add_systems(Update, update); + } +} + +fn update( + mut commands: Commands, + obstacles: Query<(Ref, &Aabb), With>, + mut meshes: ResMut>, + pathmeshes: Res>, + mut agents: Query<(Entity, &mut Agent)>, + targets: Query<&Transform, With>, + mut text_info: Query<(&mut Text, &UiInfo)>, + mut sliders: Query<(&mut Slider, &UiButton)>, + navmeshes: Query<(&Handle, Ref)>, +) { + for (handle, status) in &navmeshes { + if status.is_changed() { + let pathmesh = pathmeshes.get(handle).unwrap(); + + for (agent_entity, mut agent) in &mut agents { + commands.entity(agent_entity).remove::(); + if let Some(target_entity) = agent.target { + if let Ok(target_transform) = targets.get(target_entity) { + if !pathmesh.transformed_is_in_mesh(target_transform.translation) { + commands.entity(target_entity).despawn_recursive(); + agent.target = None; + } + } + } + } + + for (mut text, info) in &mut text_info { + match info { + UiInfo::PolygonCount => { + text.sections[1].value = pathmesh.get().polygons.len().to_string(); + } + UiInfo::ObstacleCount => { + text.sections[1].value = obstacles.iter().count().to_string(); + } + UiInfo::Simplification => { + text.sections[0].style.color = Color::WHITE; + } + _ => (), + } + } + for (mut slider, button) in &mut sliders { + match button { + UiButton::Simplification => slider.line_color = Color::GREEN, + _ => (), + } + } + + meshes.insert(HANDLE_NAVMESH_WIREFRAME, pathmesh.to_wireframe_mesh()); + meshes.insert(HANDLE_NAVMESH_MESH, pathmesh.to_mesh()); + } + match *status { + NavMeshStatus::Failed => { + for (mut text, info) in &mut text_info { + match info { + UiInfo::ObstacleCount => { + text.sections[1].value = obstacles.iter().count().to_string(); + } + UiInfo::Simplification => { + text.sections[0].style.color = Color::RED; + } + _ => (), + } + } + for (mut slider, button) in &mut sliders { + match button { + UiButton::Simplification => slider.line_color = Color::RED, + _ => (), + } + } + } + _ => (), + } + } +} diff --git a/examples/interactive/src/helpers.rs b/examples/interactive/src/helpers.rs new file mode 100644 index 0000000..8823232 --- /dev/null +++ b/examples/interactive/src/helpers.rs @@ -0,0 +1,28 @@ +use bevy::prelude::*; + +use crate::Obstacle; + +pub struct HelperPlugin; + +impl Plugin for HelperPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Update, block_moving_obstacles_out); + } +} + +fn block_moving_obstacles_out(mut obstacle_transforms: Query<&mut Transform, With>) { + for mut transform in &mut obstacle_transforms { + if transform.translation.x < -4.4 { + transform.translation.x = -4.4; + } + if transform.translation.x > 4.4 { + transform.translation.x = 4.4; + } + if transform.translation.z < -4.4 { + transform.translation.z = -4.4; + } + if transform.translation.z > 4.4 { + transform.translation.z = 4.4; + } + } +} diff --git a/examples/interactive/src/main.rs b/examples/interactive/src/main.rs new file mode 100644 index 0000000..818f0c4 --- /dev/null +++ b/examples/interactive/src/main.rs @@ -0,0 +1,373 @@ +use std::f32::consts::FRAC_PI_2; + +use bevy::{ + math::{vec2, vec3}, + pbr::NotShadowCaster, + prelude::*, + render::view::{RenderLayers, VisibilitySystems}, +}; +use bevy_vector_shapes::Shape2dPlugin; +use rand::Rng; +use vleue_navigator::{ + updater::{NavMeshBundle, NavMeshSettings, NavMeshStatus, NavMeshUpdateMode}, + NavMesh, PolyanyaTriangulation, VleueNavigatorPlugin, +}; + +mod build_navmesh; +mod helpers; +mod ui; + +const HANDLE_NAVMESH_WIREFRAME: Handle = Handle::weak_from_u128(1); +const HANDLE_NAVMESH_MESH: Handle = Handle::weak_from_u128(2); + +const HANDLE_OBSTACLE_MESH: Handle = Handle::weak_from_u128(3); +const HANDLE_AGENT_MESH: Handle = Handle::weak_from_u128(4); +const HANDLE_TARGET_MESH: Handle = Handle::weak_from_u128(5); + +const HANDLE_OBSTACLE_MATERIAL: Handle = Handle::weak_from_u128(1); +const HANDLE_AGENT_MATERIAL: Handle = Handle::weak_from_u128(2); +const HANDLE_TARGET_MATERIAL: Handle = Handle::weak_from_u128(3); + +const BOARD_LIMIT: f32 = 4.4; + +fn main() { + App::new() + .insert_resource(ClearColor(Color::BLACK)) + .add_plugins(( + DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "Navmesh with Polyanya".to_string(), + ..default() + }), + ..default() + }), + Shape2dPlugin::default(), + VleueNavigatorPlugin, + )) + .add_plugins(( + ui::UiPlugin, + helpers::HelperPlugin, + build_navmesh::BuilderPlugin, + )) + .add_systems(Startup, setup) + .add_systems(Update, (give_target_auto, move_agent, display_path)) + .add_systems( + PostUpdate, + (find_path_to_target, apply_deferred) + .chain() + .before(VisibilitySystems::CalculateBounds), + ) + .insert_gizmo_group( + DefaultGizmoConfigGroup, + GizmoConfig { + depth_bias: -1.0, + render_layers: RenderLayers::layer(1), + ..default() + }, + ) + // .insert_resource( + //GizmoConfig { + // depth_bias: -1.0, + // render_layers: RenderLayers::layer(1), + // ..default() + // } + // ) + .run(); +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut pathmeshes: ResMut>, +) { + meshes.insert( + HANDLE_OBSTACLE_MESH, + Mesh::from(Cuboid { + half_size: Vec3::splat(0.2), + }), + ); + meshes.insert( + HANDLE_AGENT_MESH, + Mesh::from(Capsule3d { + radius: 0.07, + half_length: 0.1, + ..default() + }), + ); + meshes.insert( + HANDLE_TARGET_MESH, + Mesh::from(Sphere { + radius: 0.05, + ..default() + }), + ); + materials.insert( + HANDLE_OBSTACLE_MATERIAL, + StandardMaterial { + base_color: Color::RED, + // alpha_mode: AlphaMode::Blend, + ..default() + }, + ); + materials.insert( + HANDLE_AGENT_MATERIAL, + StandardMaterial { + base_color: Color::GREEN, + ..default() + }, + ); + materials.insert( + HANDLE_TARGET_MATERIAL, + StandardMaterial { + base_color: Color::YELLOW, + unlit: true, + ..default() + }, + ); + + let mut pathmesh = NavMesh::from_edge_and_obstacles( + vec![vec2(-5., -5.), vec2(5., -5.), vec2(5., 5.), vec2(-5., 5.)], + vec![], + ); + pathmesh.set_transform(Transform::from_rotation(Quat::from_rotation_x(FRAC_PI_2))); + meshes.insert(HANDLE_NAVMESH_WIREFRAME, pathmesh.to_wireframe_mesh()); + meshes.insert(HANDLE_NAVMESH_MESH, pathmesh.to_mesh()); + commands.spawn(( + NavMeshBundle { + settings: NavMeshSettings { + simplify: 0.0, + merge_steps: 0, + unit_radius: 0.0, + default_delta: 0.01, + fixed: PolyanyaTriangulation::from_outer_edges(&vec![ + vec2(-5., -5.), + vec2(5., -5.), + vec2(5., 5.), + vec2(-5., 5.), + ]), + }, + status: NavMeshStatus::Building, + handle: pathmeshes.add(pathmesh), + transform: Transform::from_rotation(Quat::from_rotation_x(FRAC_PI_2)), + update_mode: NavMeshUpdateMode::Debounced(0.025), + // update_mode: NavMeshUpdateMode::Direct, + }, + // NavMeshUpdateModeBlocking, + )); + commands.spawn(( + PbrBundle { + mesh: meshes.add(Plane3d::default().mesh().size(10.0, 10.0)), + material: materials.add(StandardMaterial { + base_color: Color::BEIGE, + perceptual_roughness: 1.0, + metallic: 0.0, + ..default() + }), + transform: Transform::from_xyz(0.0, -0.05, 0.0), + ..default() + }, + RenderLayers::layer(1), + )); + commands.spawn(( + PbrBundle { + mesh: HANDLE_NAVMESH_MESH, + material: materials.add(StandardMaterial { + base_color: Color::MIDNIGHT_BLUE, + perceptual_roughness: 1.0, + metallic: 0.0, + ..default() + }), + ..default() + }, + RenderLayers::layer(1), + )); + commands.spawn(( + PbrBundle { + mesh: HANDLE_NAVMESH_WIREFRAME, + transform: Transform::from_translation(Vec3::new(0., 0.01, 0.)), + material: materials.add(StandardMaterial { + base_color: Color::RED, + unlit: true, + ..default() + }), + ..default() + }, + RenderLayers::layer(1), + )); + + // light + commands.spawn(( + PointLightBundle { + transform: Transform::from_xyz(0.0, 8.0, 0.0), + ..Default::default() + }, + RenderLayers::layer(1), + )); + // camera + commands.spawn(( + Camera3dBundle { + transform: Transform::from_xyz(-5.0, 10.0, 0.0) + .looking_at(Vec3::new(-0.8, 0.0, 0.0), Vec3::Y), + ..Default::default() + }, + // UiCameraConfig { show_ui: false }, + // bevy_mod_picking::backends::raycast::RaycastPickCamera::default(), + // bevy_transform_gizmo::GizmoPickSource::default(), + RenderLayers::layer(1), + )); + commands.spawn(Camera2dBundle { + camera: Camera { + order: 1, + ..default() + }, + // camera_2d: Camera2d { + // // clear_color: ClearColorConfig::None, + // }, + ..default() + }); +} + +#[derive(Component)] +struct Obstacle; + +#[derive(Component)] +struct Agent { + target: Option, +} + +#[derive(Component)] +struct Target; + +#[derive(Component)] +struct Path { + current: Vec3, + next: Vec, +} + +fn give_target_auto( + mut commands: Commands, + mut object_query: Query<&mut Agent, Without>, + navmeshes: Res>, + navmesh: Query<&Handle>, +) { + for mut agent in object_query.iter_mut() { + if agent.target.is_some() { + continue; + } + let navmesh = navmeshes.get(navmesh.single()).unwrap(); + let mut x; + let mut z; + loop { + x = rand::thread_rng().gen_range(-4.75..4.75); + z = rand::thread_rng().gen_range(-4.75..4.75); + + if navmesh.transformed_is_in_mesh(Vec3::new(x, 0.0, z)) { + break; + } + } + let target_id = commands + .spawn(( + PbrBundle { + mesh: HANDLE_TARGET_MESH, + material: HANDLE_TARGET_MATERIAL, + transform: Transform::from_xyz(x, 0.0, z), + ..Default::default() + }, + NotShadowCaster, + Target, + RenderLayers::layer(1), + )) + .id(); + agent.target = Some(target_id); + } +} + +fn find_path_to_target( + mut commands: Commands, + agents: Query<(Entity, &Transform, &Agent), (With, Without)>, + targets: Query<&Transform, With>, + mut navmeshes: ResMut>, + navmesh: Query<(&Handle, &NavMeshSettings)>, +) { + let (navmesh_handle, settings) = navmesh.single(); + let navmesh = navmeshes.get(navmesh_handle).unwrap(); + let current_delta = navmesh.delta(); + let mut has_unreachable = false; + + for (agent_entity, from, agent) in &agents { + if agent.target.is_none() { + continue; + } + let Ok(target) = targets.get(agent.target.unwrap()) else { + continue; + }; + let Some(path) = navmesh.transformed_path(from.translation, target.translation) else { + has_unreachable = true; + continue; + }; + if let Some((first, remaining)) = path.path.split_first() { + let mut remaining = remaining.to_vec(); + remaining.reverse(); + + commands.entity(agent_entity).insert(Path { + current: first.clone(), + next: remaining, + }); + } + } + if has_unreachable { + let navmesh = navmeshes.get_mut(navmesh_handle).unwrap(); + warn!( + "some agents have an unreachable target, increasing delta to {}", + (current_delta * 3.0).min(settings.default_delta * 1000.0) + ); + navmesh.set_delta((current_delta * 3.0).min(settings.default_delta * 1000.0)); + } else { + if current_delta != settings.default_delta { + info!("resetting delta"); + let navmesh = navmeshes.get_mut(navmesh_handle).unwrap(); + navmesh.set_delta(settings.default_delta); + } + } +} + +fn move_agent( + mut commands: Commands, + mut object_query: Query<(&mut Transform, &mut Path, Entity, &mut Agent)>, + time: Res