use bevy::{ asset::LoadState, gltf::{Gltf, GltfMesh}, math::Vec3Swizzles, pbr::NotShadowCaster, prelude::*, window::PrimaryWindow, }; use bevy_pathmesh::{PathMesh, PathMeshPlugin}; use rand::Rng; use std::f32::consts::FRAC_PI_2; const HANDLE_TRIMESH_OPTIMIZED: Handle = Handle::weak_from_u128(0); fn main() { App::new() .insert_resource(Msaa::default()) .insert_resource(ClearColor(Color::rgb(0., 0., 0.01))) .add_plugins(( DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { title: "Navmesh with Polyanya".to_string(), ..default() }), ..default() }), PathMeshPlugin, )) .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) .add_systems( Update, ( give_target_auto, give_target_on_click, move_object, move_hover, target_activity, trigger_navmesh_visibility, ) .run_if(in_state(AppState::Playing)), ) .run(); } #[derive(Debug, Clone, PartialEq, Eq, Hash, States, Default)] enum AppState { #[default] Setup, Playing, } #[derive(Resource, Default, Deref)] struct GltfHandle(Handle); #[derive(Resource)] 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: 100.0, }); commands.spawn(Camera3dBundle { camera: Camera { #[cfg(not(target_arch = "wasm32"))] hdr: true, ..default() }, transform: Transform::from_xyz(0.0, 70.0, 5.0) .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), ..Default::default() }); let font = asset_server.load("fonts/FiraSans-Bold.ttf"); commands.spawn(TextBundle { style: Style { align_self: AlignSelf::FlexStart, margin: UiRect::all(Val::Px(15.0)), ..Default::default() }, text: Text { sections: vec![ TextSection { value: "".to_string(), style: TextStyle { font: font.clone_weak(), font_size: 30.0, color: Color::GOLD, }, }, TextSection { value: " to display the navmesh, ".to_string(), style: TextStyle { font: font.clone_weak(), font_size: 30.0, color: Color::WHITE, }, }, TextSection { value: "click".to_string(), style: TextStyle { font: font.clone_weak(), font_size: 30.0, color: Color::GOLD, }, }, TextSection { value: " to set the destination".to_string(), style: TextStyle { font, font_size: 30.0, color: Color::WHITE, }, }, ], ..Default::default() }, ..Default::default() }); commands.insert_resource(CurrentMesh(HANDLE_TRIMESH_OPTIMIZED)); } fn check_textures( mut next_state: ResMut>, gltf: ResMut, asset_server: Res, ) { if let Some(LoadState::Loaded) = asset_server.get_load_state(gltf.id()) { next_state.set(AppState::Playing); } } #[derive(Component)] struct Path { current: Vec3, next: Vec, } #[derive(Component)] struct Object(Option); #[derive(Component)] struct Target; #[derive(Component)] struct Hover(Vec2); #[derive(Component, Clone)] struct NavMeshDisp(Handle); fn setup_scene( mut commands: Commands, gltf: Res, gltfs: Res>, gltf_meshes: Res>, mut meshes: ResMut>, mut materials: ResMut>, mut pathmeshes: 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.id()) { let mesh = gltf_meshes.get(&gltf.named_meshes["obstacles"]).unwrap(); let mut material: StandardMaterial = Color::GRAY.into(); material.perceptual_roughness = 1.0; commands.spawn(PbrBundle { mesh: mesh.primitives[0].mesh.clone(), material: materials.add(material), ..default() }); let mesh = gltf_meshes.get(&gltf.named_meshes["plane"]).unwrap(); commands.spawn(PbrBundle { mesh: mesh.primitives[0].mesh.clone(), transform: Transform::from_xyz(0.0, 0.1, 0.0), material: ground_material.clone(), ..default() }); } { #[cfg(target_arch = "wasm32")] const NB_HOVER: usize = 5; #[cfg(not(target_arch = "wasm32"))] const NB_HOVER: usize = 10; for _i in 0..NB_HOVER { commands.spawn(( SpotLightBundle { spot_light: SpotLight { intensity: 1000000.0, color: Color::SEA_GREEN, shadows_enabled: true, inner_angle: 0.5, outer_angle: 0.8, range: 250.0, ..default() }, transform: Transform::from_xyz( rand::thread_rng().gen_range(-50.0..50.0), 20.0, rand::thread_rng().gen_range(-25.0..25.0), ) .with_rotation(Quat::from_rotation_x(-FRAC_PI_2)), ..default() }, Hover(Vec2::new( rand::thread_rng().gen_range(-50.0..50.0), rand::thread_rng().gen_range(-25.0..25.0), )), )); } } if let Some(gltf) = gltfs.get(gltf.id()) { { let navmesh = bevy_pathmesh::PathMesh::from_bevy_mesh( meshes .get( &gltf_meshes .get(&gltf.named_meshes["navmesh"]) .unwrap() .primitives[0] .mesh, ) .unwrap(), ); let mut material: StandardMaterial = Color::ANTIQUE_WHITE.into(); material.unlit = true; commands.spawn(( PbrBundle { mesh: meshes.add(navmesh.to_wireframe_mesh()), material: materials.add(material), transform: Transform::from_xyz(0.0, 0.2, 0.0), visibility: Visibility::Hidden, ..Default::default() }, NavMeshDisp(HANDLE_TRIMESH_OPTIMIZED), )); pathmeshes.insert(HANDLE_TRIMESH_OPTIMIZED, navmesh); } commands .spawn(( PbrBundle { 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() }, Object(None), NotShadowCaster, )) .with_children(|object| { object.spawn(PointLightBundle { point_light: PointLight { color: Color::BLUE, range: 500.0, intensity: 100000.0, shadows_enabled: true, ..default() }, transform: Transform::from_xyz(0.0, 1.2, 0.0), ..default() }); }); } } fn give_target_auto( mut commands: Commands, mut object_query: Query<(Entity, &Transform, &mut Object), Without>, navmeshes: Res>, mut meshes: ResMut>, mut materials: ResMut>, current_mesh: Res, ) { for (entity, transform, mut object) in object_query.iter_mut() { let navmesh = navmeshes.get(¤t_mesh.0).unwrap(); let mut x; let mut z; loop { x = rand::thread_rng().gen_range(-50.0..50.0); z = rand::thread_rng().gen_range(-25.0..25.0); if navmesh.transformed_is_in_mesh(Vec3::new(x, 0.0, z)) { break; } } let Some(path) = navmesh.transformed_path(transform.translation, Vec3::new(x, 0.0, z)) else { break; }; if let Some((first, remaining)) = path.path.split_first() { let mut remaining = remaining.to_vec(); remaining.reverse(); let target_id = commands .spawn(( PbrBundle { mesh: meshes.add(Mesh::from(Sphere { radius: 0.5, ..default() })), material: materials.add(StandardMaterial { base_color: Color::RED, emissive: Color::RED * 50.0, ..default() }), transform: Transform::from_xyz(x, 0.0, z), ..Default::default() }, NotShadowCaster, Target, )) .with_children(|target| { target.spawn(PointLightBundle { point_light: PointLight { color: Color::RED, shadows_enabled: true, range: 10.0, ..default() }, transform: Transform::from_xyz(0.0, 1.5, 0.0), ..default() }); }) .id(); commands.entity(entity).insert(Path { current: first.clone(), next: remaining, }); object.0 = Some(target_id); } } } fn give_target_on_click( mut commands: Commands, mut object_query: Query<(Entity, &Transform, &mut Object)>, targets: Query>, navmeshes: Res>, mut meshes: ResMut>, mut materials: ResMut>, current_mesh: Res, mouse_buttons: Res>, primary_window: Query<&Window, With>, camera: Query<(&Camera, &GlobalTransform)>, ) { if mouse_buttons.just_pressed(MouseButton::Left) { let navmesh = navmeshes.get(¤t_mesh.0).unwrap(); let Some(target) = (|| { 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.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; }; for (entity, transform, mut object) in object_query.iter_mut() { 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(); remaining.reverse(); let target_id = commands .spawn(( PbrBundle { mesh: meshes.add(Mesh::from(Sphere { radius: 0.5, ..default() })), material: materials.add(StandardMaterial { base_color: Color::RED, emissive: Color::RED * 50.0, ..default() }), transform: Transform::from_translation(target), ..Default::default() }, NotShadowCaster, Target, )) .with_children(|target| { target.spawn(PointLightBundle { point_light: PointLight { color: Color::RED, shadows_enabled: true, range: 10.0, ..default() }, transform: Transform::from_xyz(0.0, 1.5, 0.0), ..default() }); }) .id(); commands.entity(entity).insert(Path { current: first.clone(), next: remaining, }); object.0 = Some(target_id); } } for entity in &targets { commands.entity(entity).despawn_recursive(); } } } fn move_object( mut commands: Commands, mut object_query: Query<(&mut Transform, &mut Path, Entity, &mut Object)>, time: Res