use std::{
sync::{Arc, RwLock},
time::Duration,
};
use bevy::{
math::Vec3Swizzles,
prelude::*,
sprite::MaterialMesh2dBundle,
tasks::AsyncComputeTaskPool,
window::{PrimaryWindow, WindowResized},
};
use bevy_pathmesh::{PathMesh, PathMeshPlugin};
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()
}),
PathMeshPlugin,
))
.add_systems(Startup, setup)
.add_systems(
Update,
(
on_mesh_change,
mesh_change,
on_click,
compute_paths,
poll_path_tasks,
move_navigator,
display_path,
),
)
.run();
}
#[derive(Resource)]
struct Meshes {
simple: Handle<PathMesh>,
arena: Handle<PathMesh>,
aurora: Handle<PathMesh>,
}
enum CurrentMesh {
Simple,
Arena,
Aurora,
}
#[derive(Resource)]
struct MeshDetails {
mesh: CurrentMesh,
size: Vec2,
}
const SIMPLE: MeshDetails = MeshDetails {
mesh: CurrentMesh::Simple,
size: Vec2::new(13.0, 8.0),
};
const ARENA: MeshDetails = MeshDetails {
mesh: CurrentMesh::Arena,
size: Vec2::new(49.0, 49.0),
};
const AURORA: MeshDetails = MeshDetails {
mesh: CurrentMesh::Aurora,
size: Vec2::new(1024.0, 768.0),
};
fn setup(
mut commands: Commands,
mut pathmeshes: ResMut<Assets<PathMesh>>,
asset_server: Res<AssetServer>,
) {
commands.spawn(Camera2dBundle::default());
commands.insert_resource(Meshes {
simple: pathmeshes.add(PathMesh::from_polyanya_mesh(polyanya::Mesh::new(
vec![
polyanya::Vertex::new(Vec2::new(0., 6.), vec![0, -1]),
polyanya::Vertex::new(Vec2::new(2., 5.), vec![0, -1, 2]),
polyanya::Vertex::new(Vec2::new(5., 7.), vec![0, 2, -1]),
polyanya::Vertex::new(Vec2::new(5., 8.), vec![0, -1]),
polyanya::Vertex::new(Vec2::new(0., 8.), vec![0, -1]),
polyanya::Vertex::new(Vec2::new(1., 4.), vec![1, -1]),
polyanya::Vertex::new(Vec2::new(2., 1.), vec![1, -1]),
polyanya::Vertex::new(Vec2::new(4., 1.), vec![1, -1]),
polyanya::Vertex::new(Vec2::new(4., 2.), vec![1, -1, 2]),
polyanya::Vertex::new(Vec2::new(2., 4.), vec![1, 2, -1]),
polyanya::Vertex::new(Vec2::new(7., 4.), vec![2, -1, 4]),
polyanya::Vertex::new(Vec2::new(10., 7.), vec![2, 4, 6, -1, 3]),
polyanya::Vertex::new(Vec2::new(7., 7.), vec![2, 3, -1]),
polyanya::Vertex::new(Vec2::new(11., 8.), vec![3, -1]),
polyanya::Vertex::new(Vec2::new(7., 8.), vec![3, -1]),
polyanya::Vertex::new(Vec2::new(7., 0.), vec![5, 4, -1]),
polyanya::Vertex::new(Vec2::new(11., 3.), vec![4, 5, -1]),
polyanya::Vertex::new(Vec2::new(11., 5.), vec![4, -1, 6]),
polyanya::Vertex::new(Vec2::new(12., 0.), vec![5, -1]),
polyanya::Vertex::new(Vec2::new(12., 3.), vec![5, -1]),
polyanya::Vertex::new(Vec2::new(13., 5.), vec![6, -1]),
polyanya::Vertex::new(Vec2::new(13., 7.), vec![6, -1]),
polyanya::Vertex::new(Vec2::new(1., 3.), vec![1, -1]),
],
vec![
polyanya::Polygon::new(vec![0, 1, 2, 3, 4], true),
polyanya::Polygon::new(vec![5, 22, 6, 7, 8, 9], true),
polyanya::Polygon::new(vec![1, 9, 8, 10, 11, 12, 2], false),
polyanya::Polygon::new(vec![12, 11, 13, 14], true),
polyanya::Polygon::new(vec![10, 15, 16, 17, 11], false),
polyanya::Polygon::new(vec![15, 18, 19, 16], true),
polyanya::Polygon::new(vec![11, 17, 20, 21], true),
],
))),
arena: asset_server.load("arena-merged.polyanya.mesh"),
aurora: asset_server.load("aurora-merged.polyanya.mesh"),
});
commands.insert_resource(AURORA);
}
fn on_mesh_change(
mesh: Res<MeshDetails>,
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
pathmeshes: Res<Assets<PathMesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
path_meshes: Res<Meshes>,
mut current_mesh_entity: Local<Option<Entity>>,
primary_window: Query<&Window, With<PrimaryWindow>>,
navigator: Query<Entity, With<Navigator>>,
window_resized: EventReader<WindowResized>,
asset_server: Res<AssetServer>,
text: Query<Entity, With<Text>>,
mut wait_for_mesh: Local<bool>,
) {
if mesh.is_changed() || !window_resized.is_empty() || *wait_for_mesh {
let handle = match mesh.mesh {
CurrentMesh::Simple => &path_meshes.simple,
CurrentMesh::Arena => &path_meshes.arena,
CurrentMesh::Aurora => &path_meshes.aurora,
};
if let Some(pathmesh) = pathmeshes.get(handle) {
*wait_for_mesh = false;
if let Some(entity) = *current_mesh_entity {
commands.entity(entity).despawn();
}
if let Ok(entity) = navigator.get_single() {
commands.entity(entity).despawn();
}
let window = primary_window.single();
let factor = (window.width() / mesh.size.x).min(window.height() / mesh.size.y);
*current_mesh_entity = Some(
commands
.spawn(MaterialMesh2dBundle {
mesh: meshes.add(pathmesh.to_mesh()).into(),
transform: Transform::from_translation(Vec3::new(
-mesh.size.x / 2.0 * factor,
-mesh.size.y / 2.0 * factor,
0.0,
))
.with_scale(Vec3::splat(factor)),
material: materials.add(ColorMaterial::from(Color::BLUE)),
..default()
})
.id(),
);
if let Ok(entity) = text.get_single() {
commands.entity(entity).despawn();
}
let font = asset_server.load("fonts/FiraMono-Medium.ttf");
commands.spawn(TextBundle {
text: Text::from_sections([
TextSection::new(
match mesh.mesh {
CurrentMesh::Simple => "Simple\n",
CurrentMesh::Arena => "Arena\n",
CurrentMesh::Aurora => "Aurora\n",
},
TextStyle {
font: font.clone_weak(),
font_size: 30.0,
color: Color::WHITE,
},
),
TextSection::new(
"Press spacebar or long touch to switch mesh\n",
TextStyle {
font: font.clone_weak(),
font_size: 15.0,
color: Color::WHITE,
},
),
TextSection::new(
"Click to find a path",
TextStyle {
font: font.clone_weak(),
font_size: 15.0,
color: Color::WHITE,
},
),
]),
style: Style {
position_type: PositionType::Absolute,
margin: UiRect {
top: Val::Px(5.0),
left: Val::Px(5.0),
..default()
},
..default()
},
..default()
});
} else {
*wait_for_mesh = true;
}
}
}
fn mesh_change(
mut mesh: ResMut<MeshDetails>,
keyboard_input: Res<ButtonInput<KeyCode>>,
mouse_input: Res<ButtonInput<MouseButton>>,
time: Res<Time>,
mut pressed_since: Local<Option<Duration>>,
) {
let mut touch_triggered = false;
if mouse_input.just_pressed(MouseButton::Left) {
*pressed_since = Some(time.elapsed());
}
if mouse_input.just_released(MouseButton::Left) {
*pressed_since = None;
}
if let Some(started) = *pressed_since {
if (time.elapsed() - started).as_secs() > 1 {
touch_triggered = true;
*pressed_since = None;
}
}
if keyboard_input.just_pressed(KeyCode::Space) || touch_triggered {
match mesh.mesh {
CurrentMesh::Simple => *mesh = ARENA,
CurrentMesh::Arena => *mesh = AURORA,
CurrentMesh::Aurora => *mesh = SIMPLE,
}
}
}
#[derive(Component)]
struct Navigator {
speed: f32,
}
#[derive(Component)]
struct Target {
target: Vec2,
pathmesh: Handle<PathMesh>,
}
#[derive(Component)]
struct Path {
path: Vec<Vec2>,
}
fn on_click(
mouse_button_input: Res<ButtonInput<MouseButton>>,
primary_window: Query<&Window, With<PrimaryWindow>>,
camera_q: Query<(&Camera, &GlobalTransform)>,
mesh: Res<MeshDetails>,
meshes: Res<Meshes>,
mut commands: Commands,
query: Query<Entity, With<Navigator>>,
pathmeshes: Res<Assets<PathMesh>>,
) {
if mouse_button_input.just_pressed(MouseButton::Left) {
let (camera, camera_transform) = camera_q.single();
let window = primary_window.single();
if let Some(position) = window
.cursor_position()
.and_then(|cursor| camera.viewport_to_world(camera_transform, cursor))
.map(|ray| ray.origin.truncate())
{
let screen = Vec2::new(window.width(), window.height());
let factor = (screen.x / mesh.size.x).min(screen.y / mesh.size.y);
let in_mesh = position / factor + mesh.size / 2.0;
if pathmeshes
.get(match mesh.mesh {
CurrentMesh::Simple => &meshes.simple,
CurrentMesh::Arena => &meshes.arena,
CurrentMesh::Aurora => &meshes.aurora,
})
.map(|mesh| mesh.is_in_mesh(in_mesh))
.unwrap_or_default()
{
if let Ok(navigator) = query.get_single() {
info!("going to {}", in_mesh);
commands.entity(navigator).insert(Target {
target: in_mesh,
pathmesh: match mesh.mesh {
CurrentMesh::Simple => meshes.simple.clone_weak(),
CurrentMesh::Arena => meshes.arena.clone_weak(),
CurrentMesh::Aurora => meshes.aurora.clone_weak(),
},
});
} else {
info!("spawning at {}", in_mesh);
commands.spawn((
SpriteBundle {
sprite: Sprite {
color: Color::RED,
custom_size: Some(Vec2::ONE),
..default()
},
transform: Transform::from_translation(
position.extend(1.0),
)
.with_scale(Vec3::splat(5.0)),
..default()
},
Navigator { speed: 100.0 },
));
}
} else {
info!("clicked outside of mesh");
}
}
}
}
#[derive(Component)]
struct FindingPath(Arc<RwLock<(Option<polyanya::Path>, bool)>>);
fn compute_paths(
mut commands: Commands,
with_target: Query<(Entity, &Target, &Transform), Changed<Target>>,
meshes: Res<Assets<PathMesh>>,
mesh: Res<MeshDetails>,
primary_window: Query<&Window, With<PrimaryWindow>>,
) {
let window = primary_window.single();
let factor = (window.width() / mesh.size.x).min(window.height() / mesh.size.y);
for (entity, target, transform) in &with_target {
let in_mesh = transform.translation.truncate() / factor + mesh.size / 2.0;
let mesh = meshes.get(&target.pathmesh).unwrap();
let to = target.target;
let mesh = mesh.clone();
let finding = FindingPath(Arc::new(RwLock::new((None, false))));
let writer = finding.0.clone();
AsyncComputeTaskPool::get()
.spawn(async move {
let path = mesh.path(in_mesh, to);
*writer.write().unwrap() = (path, true);
})
.detach();
commands.entity(entity).insert(finding);
}
}
fn poll_path_tasks(mut commands: Commands, computing: Query<(Entity, &FindingPath)>) {
for (entity, task) in &computing {
let mut task = task.0.write().unwrap();
if task.1 {
if let Some(path) = task.0.take() {
commands
.entity(entity)
.insert(Path { path: path.path })
.remove::<FindingPath>();
} else {
info!("no path found");
}
}
}
}
fn move_navigator(
mut query: Query<(Entity, &mut Transform, &mut Path, &Navigator)>,
mesh: Res<MeshDetails>,
primary_window: Query<&Window, With<PrimaryWindow>>,
time: Res<Time>,
mut commands: Commands,
) {
let window = primary_window.single();
let factor = (window.width() / mesh.size.x).min(window.height() / mesh.size.y);
for (entity, mut transform, mut path, navigator) in &mut query {
let next = (path.path[0] - mesh.size / 2.0) * factor;
let toward = next - transform.translation.xy();
if toward.length() < time.delta_seconds() * navigator.speed {
path.path.remove(0);
if path.path.is_empty() {
debug!("reached target");
commands.entity(entity).remove::<Path>();
} else {
debug!("reached next step");
}
}
transform.translation +=
(toward.normalize() * time.delta_seconds() * navigator.speed).extend(0.0);
}
}
fn display_path(
query: Query<(&Transform, &Path)>,
mut gizmos: Gizmos,
mesh: Res<MeshDetails>,
primary_window: Query<&Window, With<PrimaryWindow>>,
) {
let window = primary_window.single();
let factor = (window.width() / mesh.size.x).min(window.height() / mesh.size.y);
for (transform, path) in &query {
if path.path.is_empty() {
continue;
}
gizmos.linestrip_2d(
path.path.iter().map(|p| (*p - mesh.size / 2.0) * factor),
Color::ORANGE,
);
if let Some(next) = path.path.first() {
gizmos.line_2d(
transform.translation.truncate(),
(*next - mesh.size / 2.0) * factor,
Color::YELLOW,
);
}
}
}