Skip to main content

bevy_landmass/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::{marker::PhantomData, sync::Arc};
4
5use bevy_app::{FixedPreUpdate, Plugin};
6use bevy_asset::{Asset, AssetApp, Handle};
7use bevy_ecs::{
8  component::Component,
9  entity::Entity,
10  intern::Interned,
11  schedule::{IntoScheduleConfigs, ScheduleLabel, SystemSet},
12  system::{Query, Res},
13};
14use bevy_platform::collections::HashMap;
15use bevy_reflect::TypePath;
16use bevy_time::Time;
17use coords::{CoordinateSystem, ThreeD, TwoD};
18use landmass::{AgentId, AnimationLinkId, CharacterId, IslandId};
19
20mod agent;
21mod character;
22mod island;
23mod landmass_structs;
24mod link;
25
26pub use landmass::{
27  ArchipelagoOptions, FindPathError, FromAgentRadius, HeightNavigationMesh,
28  HeightPolygon, NavigationMesh, PathStep, PointSampleDistance3d,
29  SamplePointError, SetTypeIndexCostError, ValidNavigationMesh,
30  ValidationError,
31};
32
33pub use agent::*;
34pub use character::*;
35pub use island::*;
36pub use landmass_structs::*;
37pub use link::*;
38
39pub mod coords;
40pub mod debug;
41
42#[cfg(feature = "mesh-utils")]
43pub mod nav_mesh;
44
45pub mod prelude {
46  pub use crate::Agent2dBundle;
47  pub use crate::Agent3dBundle;
48  pub use crate::AgentDesiredVelocity2d;
49  pub use crate::AgentDesiredVelocity3d;
50  pub use crate::AgentSettings;
51  pub use crate::AgentState;
52  pub use crate::AgentTarget2d;
53  pub use crate::AgentTarget3d;
54  pub use crate::AnimationLink2d;
55  pub use crate::AnimationLink2dBundle;
56  pub use crate::AnimationLink3d;
57  pub use crate::AnimationLink3dBundle;
58  pub use crate::Archipelago2d;
59  pub use crate::Archipelago3d;
60  pub use crate::ArchipelagoOptions;
61  pub use crate::ArchipelagoRef2d;
62  pub use crate::ArchipelagoRef3d;
63  pub use crate::Character2dBundle;
64  pub use crate::Character3dBundle;
65  pub use crate::CharacterSettings;
66  pub use crate::FromAgentRadius;
67  pub use crate::HeightNavigationMesh2d;
68  pub use crate::HeightNavigationMesh3d;
69  pub use crate::Island;
70  pub use crate::Island2dBundle;
71  pub use crate::Island3dBundle;
72  pub use crate::Landmass2dPlugin;
73  pub use crate::Landmass3dPlugin;
74  pub use crate::LandmassSystems;
75  pub use crate::NavMesh2d;
76  pub use crate::NavMesh3d;
77  pub use crate::NavMeshHandle2d;
78  pub use crate::NavMeshHandle3d;
79  pub use crate::NavigationMesh2d;
80  pub use crate::NavigationMesh3d;
81  pub use crate::ReachedAnimationLink2d;
82  pub use crate::ReachedAnimationLink3d;
83  pub use crate::ValidNavigationMesh2d;
84  pub use crate::ValidNavigationMesh3d;
85  pub use crate::Velocity2d;
86  pub use crate::Velocity3d;
87  pub use crate::coords::CoordinateSystem;
88  pub use crate::coords::ThreeD;
89  pub use crate::coords::TwoD;
90}
91
92pub struct LandmassPlugin<CS: CoordinateSystem> {
93  schedule: Interned<dyn ScheduleLabel>,
94  _marker: PhantomData<CS>,
95}
96
97impl<CS: CoordinateSystem> Default for LandmassPlugin<CS> {
98  fn default() -> Self {
99    Self { schedule: FixedPreUpdate.intern(), _marker: Default::default() }
100  }
101}
102
103impl<CS: CoordinateSystem> LandmassPlugin<CS> {
104  /// Sets the schedule for running the plugin. Defaults to
105  /// [`FixedPreUpdate`].
106  #[must_use]
107  pub fn in_schedule(mut self, schedule: impl ScheduleLabel) -> Self {
108    self.schedule = schedule.intern();
109    self
110  }
111}
112
113pub type Landmass2dPlugin = LandmassPlugin<TwoD>;
114pub type Landmass3dPlugin = LandmassPlugin<ThreeD>;
115
116impl<CS: CoordinateSystem> Plugin for LandmassPlugin<CS> {
117  fn build(&self, app: &mut bevy_app::App) {
118    app.init_asset::<NavMesh<CS>>();
119    app.configure_sets(
120      self.schedule,
121      (
122        LandmassSystems::SyncExistence,
123        LandmassSystems::SyncValues,
124        LandmassSystems::Update,
125        LandmassSystems::Output,
126      )
127        .chain(),
128    );
129    app.add_systems(
130      self.schedule,
131      (
132        add_agents_to_archipelagos::<CS>,
133        sync_islands_to_archipelago::<CS>,
134        add_characters_to_archipelago::<CS>,
135        update_animation_links_to_archipelagos::<CS>,
136      )
137        .in_set(LandmassSystems::SyncExistence),
138    );
139    app.add_systems(
140      self.schedule,
141      (sync_agent_input_state::<CS>, sync_character_state::<CS>)
142        .in_set(LandmassSystems::SyncValues),
143    );
144    app.add_systems(
145      self.schedule,
146      update_archipelagos::<CS>.in_set(LandmassSystems::Update),
147    );
148    app.add_systems(
149      self.schedule,
150      (
151        sync_agent_state::<CS>,
152        sync_desired_velocity::<CS>,
153        sync_agent_reached_animation_link::<CS>,
154      )
155        .in_set(LandmassSystems::Output),
156    );
157
158    app.add_observer(on_remove_animation_link::<CS>);
159    app.add_observer(on_replace_archipelago_ref_from_animation_link::<CS>);
160  }
161}
162
163/// System set for `landmass` systems.
164#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
165pub enum LandmassSystems {
166  /// Systems for syncing the existence of components with the internal
167  /// `landmass` state. Ensure your `landmass` entities are setup before this
168  /// point (and not removed until [`LandmassSystemSet::Output`]).
169  SyncExistence,
170  /// Systems for syncing the values of components with the internal `landmass`
171  /// state.
172  SyncValues,
173  /// The actual `landmass` updating step.
174  Update,
175  /// Systems for returning the output of `landmass` back to users. Avoid
176  /// reading/mutating data from your `landmass` entities until after this
177  /// point.
178  Output,
179}
180
181/// An archipelago, holding the internal state of `landmass`.
182#[derive(Component)]
183pub struct Archipelago<CS: CoordinateSystem> {
184  /// The `landmass` archipelago.
185  archipelago: landmass::Archipelago<CS>,
186  /// A map from the Bevy entity to its associated island ID in
187  /// [`Self::archipelago`].
188  islands: HashMap<Entity, IslandId>,
189  /// A map from the island ID to its associated Bevy entity in
190  /// [`Self::archipelago`]. This is just the reverse of [`Self::islands`].
191  reverse_islands: HashMap<IslandId, Entity>,
192  /// A map from the Bevy entity to its associated animation link ID in
193  /// [`Self::archipelago`].
194  animation_links: HashMap<Entity, AnimationLinkId>,
195  /// A map from the animation link ID to its associated Bevy entity in
196  /// [`Self::archipelago`]. This is just the reverse of
197  /// [`Self::animation_links`].
198  reverse_animation_links: HashMap<AnimationLinkId, Entity>,
199  /// A map from the Bevy entity to its associated agent ID in
200  /// [`Self::archipelago`].
201  agents: HashMap<Entity, AgentId>,
202  /// A map from the agent ID to its associated Bevy entity in
203  /// [`Self::archipelago`]. This is just the reverse of [`Self::agents`].
204  reverse_agents: HashMap<AgentId, Entity>,
205  /// A map from the Bevy entity to its associated character ID in
206  /// [`Self::archipelago`].
207  characters: HashMap<Entity, CharacterId>,
208}
209
210pub type Archipelago2d = Archipelago<TwoD>;
211pub type Archipelago3d = Archipelago<ThreeD>;
212
213impl<CS: CoordinateSystem> Archipelago<CS> {
214  /// Creates an empty archipelago.
215  pub fn new(agent_options: ArchipelagoOptions<CS>) -> Self {
216    Self {
217      archipelago: landmass::Archipelago::new(agent_options),
218      islands: HashMap::new(),
219      reverse_islands: HashMap::new(),
220      animation_links: HashMap::new(),
221      reverse_animation_links: HashMap::new(),
222      agents: HashMap::new(),
223      reverse_agents: HashMap::new(),
224      characters: HashMap::new(),
225    }
226  }
227
228  /// Gets the agent options.
229  pub fn get_agent_options(&self) -> &ArchipelagoOptions<CS> {
230    &self.archipelago.archipelago_options
231  }
232
233  /// Gets a mutable borrow to the agent options.
234  pub fn get_agent_options_mut(&mut self) -> &mut ArchipelagoOptions<CS> {
235    &mut self.archipelago.archipelago_options
236  }
237
238  /// Sets the cost of `type_index` to `cost`. The cost is a multiplier on the
239  /// distance travelled along this node (essentially the cost per meter).
240  /// Agents will prefer to travel along low-cost terrain.
241  pub fn set_type_index_cost(
242    &mut self,
243    type_index: usize,
244    cost: f32,
245  ) -> Result<(), SetTypeIndexCostError> {
246    self.archipelago.set_type_index_cost(type_index, cost)
247  }
248
249  /// Gets the cost of `type_index`.
250  pub fn get_type_index_cost(&self, type_index: usize) -> Option<f32> {
251    self.archipelago.get_type_index_cost(type_index)
252  }
253
254  /// Gets the current registered type indices and their costs.
255  pub fn get_type_index_costs(
256    &self,
257  ) -> impl Iterator<Item = (usize, f32)> + '_ {
258    self.archipelago.get_type_index_costs()
259  }
260
261  /// Finds the nearest point on the navigation meshes to (and within
262  /// `distance_to_node` of) `point`.
263  pub fn sample_point(
264    &self,
265    point: CS::Coordinate,
266    point_sample_distance: &CS::SampleDistance,
267  ) -> Result<SampledPoint<'_, CS>, SamplePointError> {
268    let sampled_point =
269      self.archipelago.sample_point(point, point_sample_distance)?;
270    Ok(SampledPoint {
271      island: *self
272        .reverse_islands
273        .get(&sampled_point.island())
274        .expect("The island hasn't been removed from the archipelago."),
275      sampled_point,
276    })
277  }
278
279  /// Finds a path from `start_point` and `end_point` along the navigation
280  /// meshes. Only [`SampledPoint`]s from this archipelago are supported. This
281  /// should only be used for querying (e.g., finding the walking distance to an
282  /// object), not for controlling movement. For controlling movement, use
283  /// agents.
284  pub fn find_path(
285    &self,
286    start_point: &SampledPoint<'_, CS>,
287    end_point: &SampledPoint<'_, CS>,
288    override_type_index_costs: &std::collections::HashMap<usize, f32>,
289    permitted_animation_links: PermittedAnimationLinks,
290  ) -> Result<Vec<PathStep<CS>>, FindPathError> {
291    self.archipelago.find_path(
292      &start_point.sampled_point,
293      &end_point.sampled_point,
294      override_type_index_costs,
295      permitted_animation_links.to_landmass(),
296    )
297  }
298
299  /// Gets an agent.
300  fn get_agent(&self, entity: Entity) -> Option<&landmass::Agent<CS>> {
301    self
302      .agents
303      .get(&entity)
304      .and_then(|&agent_id| self.archipelago.get_agent(agent_id))
305  }
306
307  /// Gets a mutable borrow to an agent.
308  fn get_agent_mut(
309    &mut self,
310    entity: Entity,
311  ) -> Option<&mut landmass::Agent<CS>> {
312    self
313      .agents
314      .get(&entity)
315      .and_then(|&agent_id| self.archipelago.get_agent_mut(agent_id))
316  }
317
318  /// Gets a borrow to a character.
319  #[allow(unused)] // Used in tests.
320  fn get_character(&self, entity: Entity) -> Option<&landmass::Character<CS>> {
321    self
322      .characters
323      .get(&entity)
324      .and_then(|&character_id| self.archipelago.get_character(character_id))
325  }
326
327  /// Gets a mutable borrow to a character.
328  fn get_character_mut(
329    &mut self,
330    entity: Entity,
331  ) -> Option<&mut landmass::Character<CS>> {
332    self.characters.get(&entity).and_then(|&character_id| {
333      self.archipelago.get_character_mut(character_id)
334    })
335  }
336
337  /// Gets a borrow to a character.
338  #[allow(unused)] // Used in tests.
339  fn get_island(&self, entity: Entity) -> Option<&landmass::Island<CS>> {
340    self
341      .islands
342      .get(&entity)
343      .and_then(|&island_id| self.archipelago.get_island(island_id))
344  }
345
346  /// Gets a mutable borrow to an island (if present).
347  fn get_island_mut(
348    &mut self,
349    entity: Entity,
350  ) -> Option<landmass::IslandMut<'_, CS>> {
351    self
352      .islands
353      .get(&entity)
354      .and_then(|&island_id| self.archipelago.get_island_mut(island_id))
355  }
356}
357
358/// Updates the archipelago.
359fn update_archipelagos<CS: CoordinateSystem>(
360  time: Res<Time>,
361  mut archipelago_query: Query<&mut Archipelago<CS>>,
362) {
363  for mut archipelago in archipelago_query.iter_mut() {
364    archipelago.archipelago.update(time.delta_secs());
365  }
366}
367
368/// A reference to an archipelago.
369#[derive(Component)]
370pub struct ArchipelagoRef<CS: CoordinateSystem> {
371  pub entity: Entity,
372  pub marker: PhantomData<CS>,
373}
374
375pub type ArchipelagoRef2d = ArchipelagoRef<TwoD>;
376pub type ArchipelagoRef3d = ArchipelagoRef<ThreeD>;
377
378impl<CS: CoordinateSystem<Coordinate: std::fmt::Debug>> std::fmt::Debug
379  for ArchipelagoRef<CS>
380{
381  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
382    f.debug_struct("ArchipelagoRef")
383      .field("entity", &self.entity)
384      .field("marker", &self.marker)
385      .finish()
386  }
387}
388
389impl<CS: CoordinateSystem> ArchipelagoRef<CS> {
390  pub fn new(entity: Entity) -> Self {
391    Self { entity, marker: Default::default() }
392  }
393}
394
395///[`NavigationMesh`] using 2D Bevy coordinates.
396/// That means that the mesh expects vertices to be on the XY plane.
397///
398/// The expected winding order is *counter-clockwise*
399pub type NavigationMesh2d = NavigationMesh<TwoD>;
400///[`NavigationMesh`] using 3D Bevy coordinates.
401/// That means that the mesh expects vertices to be in
402/// - X right
403/// - Y up
404/// - Z forward
405///
406/// The expected winding order is *counter-clockwise*
407pub type NavigationMesh3d = NavigationMesh<ThreeD>;
408
409pub type HeightNavigationMesh2d = HeightNavigationMesh<TwoD>;
410pub type HeightNavigationMesh3d = HeightNavigationMesh<ThreeD>;
411
412pub type ValidNavigationMesh2d = ValidNavigationMesh<TwoD>;
413pub type ValidNavigationMesh3d = ValidNavigationMesh<ThreeD>;
414
415/// An asset holding a `landmass` nav mesh.
416#[derive(Asset, TypePath)]
417pub struct NavMesh<CS: CoordinateSystem> {
418  /// The nav mesh data.
419  pub nav_mesh: Arc<ValidNavigationMesh<CS>>,
420}
421
422pub type NavMesh2d = NavMesh<TwoD>;
423pub type NavMesh3d = NavMesh<ThreeD>;
424
425/// A handle to a navigation mesh for an [`Island`].
426#[derive(Component, Clone, Debug)]
427pub struct NavMeshHandle<CS: CoordinateSystem>(pub Handle<NavMesh<CS>>);
428
429impl<CS: CoordinateSystem> Default for NavMeshHandle<CS> {
430  fn default() -> Self {
431    Self(Default::default())
432  }
433}
434
435pub type NavMeshHandle2d = NavMeshHandle<TwoD>;
436pub type NavMeshHandle3d = NavMeshHandle<ThreeD>;
437
438/// A point on the navigation meshes.
439pub struct SampledPoint<'archipelago, CS: CoordinateSystem> {
440  /// The sampled point from landmass.
441  sampled_point: landmass::SampledPoint<'archipelago, CS>,
442  /// The island that the point is on.
443  island: Entity,
444}
445
446// Manual Clone impl for `SampledPoint` to avoid the Clone bound on CS.
447impl<CS: CoordinateSystem> Clone for SampledPoint<'_, CS> {
448  fn clone(&self) -> Self {
449    Self { sampled_point: self.sampled_point.clone(), island: self.island }
450  }
451}
452
453impl<CS: CoordinateSystem> SampledPoint<'_, CS> {
454  /// Gets the point on the navigation meshes.
455  pub fn point(&self) -> CS::Coordinate {
456    self.sampled_point.point()
457  }
458
459  /// Gets the island the sampled point is on.
460  pub fn island(&self) -> Entity {
461    self.island
462  }
463
464  /// Gets the type index of the sampled point.
465  pub fn type_index(&self) -> usize {
466    self.sampled_point.type_index()
467  }
468}
469
470#[cfg(test)]
471#[path = "lib_test.rs"]
472mod test;