diff --git a/crates/controller/src/command.rs b/crates/controller/src/command.rs index 9df4eac6..c5116210 100644 --- a/crates/controller/src/command.rs +++ b/crates/controller/src/command.rs @@ -1,39 +1,57 @@ use bevy::{ - input::mouse::MouseButtonInput, - prelude::{ - App, Entity, EventReader, EventWriter, MouseButton, ParallelSystemDescriptorCoercion, - Plugin, Query, Res, SystemSet, With, - }, + input::{mouse::MouseButtonInput, ElementState}, + prelude::*, }; use de_core::{objects::MovableSolid, projection::ToFlat}; use de_pathing::UpdateEntityPath; +use iyes_loopless::prelude::*; -use crate::{pointer::Pointer, selection::Selected, Labels}; +use crate::{ + pointer::Pointer, + selection::{SelectEvent, Selected, SelectionMode}, + Labels, +}; pub(crate) struct CommandPlugin; impl Plugin for CommandPlugin { fn build(&self, app: &mut App) { - app.add_system_set( - SystemSet::new().with_system( - mouse_click_handler - .label(Labels::InputUpdate) - .after(Labels::PreInputUpdate), - ), + app.add_system_set_to_stage( + CoreStage::PreUpdate, + SystemSet::new() + .with_system( + right_click_handler + .run_if(on_pressed(MouseButton::Right)) + .label(Labels::InputUpdate) + .after(Labels::PreInputUpdate), + ) + .with_system( + left_click_handler + .run_if(on_pressed(MouseButton::Left)) + .label(Labels::InputUpdate) + .after(Labels::PreInputUpdate), + ), ); } } -fn mouse_click_handler( - mut click_events: EventReader, +fn on_pressed(button: MouseButton) -> impl Fn(EventReader) -> bool { + move |mut events: EventReader| { + // It is desirable to exhaust the iterator, thus .filter().count() is + // used instead of .any() + events + .iter() + .filter(|e| e.button == button && e.state == ElementState::Pressed) + .count() + > 0 + } +} + +fn right_click_handler( mut path_events: EventWriter, selected: Query, With)>, pointer: Res, ) { - if !click_events.iter().any(|e| e.button == MouseButton::Right) { - return; - } - let target = match pointer.terrain_point() { Some(point) => point.to_flat(), None => return, @@ -43,3 +61,20 @@ fn mouse_click_handler( path_events.send(UpdateEntityPath::new(entity, target)); } } + +fn left_click_handler( + mut events: EventWriter, + keys: Res>, + pointer: Res, +) { + let selection_mode = if keys.pressed(KeyCode::LControl) { + SelectionMode::Add + } else { + SelectionMode::Replace + }; + let event = match pointer.entity() { + Some(entity) => SelectEvent::single(entity, selection_mode), + None => SelectEvent::none(selection_mode), + }; + events.send(event); +} diff --git a/crates/controller/src/pointer.rs b/crates/controller/src/pointer.rs index 737d8129..63620c67 100644 --- a/crates/controller/src/pointer.rs +++ b/crates/controller/src/pointer.rs @@ -1,10 +1,5 @@ use bevy::{ - ecs::system::SystemParam, - input::mouse::MouseMotion, - prelude::{ - App, Camera, Entity, EventReader, GlobalTransform, Plugin, Query, Res, ResMut, With, - }, - render::camera::Camera3d, + ecs::system::SystemParam, input::mouse::MouseMotion, prelude::*, render::camera::Camera3d, window::Windows, }; use de_core::{objects::Playable, state::GameState}; @@ -20,7 +15,8 @@ pub(crate) struct PointerPlugin; impl Plugin for PointerPlugin { fn build(&self, app: &mut App) { - app.init_resource::().add_system( + app.init_resource::().add_system_to_stage( + CoreStage::PreUpdate, mouse_move_handler .run_in_state(GameState::Playing) .label(Labels::PreInputUpdate), diff --git a/crates/controller/src/selection.rs b/crates/controller/src/selection.rs index 4a48ac36..6158692f 100644 --- a/crates/controller/src/selection.rs +++ b/crates/controller/src/selection.rs @@ -2,35 +2,60 @@ use std::collections::HashSet; use bevy::{ ecs::system::SystemParam, - input::{mouse::MouseButtonInput, ElementState, Input}, - prelude::{ - App, Commands, Component, Entity, EventReader, KeyCode, MouseButton, Plugin, Query, Res, - With, - }, + prelude::{App, Commands, Component, CoreStage, Entity, EventReader, Plugin, Query, With}, }; use de_core::state::GameState; use iyes_loopless::prelude::*; -use crate::{pointer::Pointer, Labels}; +use crate::Labels; pub(crate) struct SelectionPlugin; impl Plugin for SelectionPlugin { fn build(&self, app: &mut App) { - app.add_system( - mouse_click_handler + app.add_event::().add_system_to_stage( + CoreStage::PreUpdate, + update_selection .run_in_state(GameState::Playing) - .label(Labels::InputUpdate) - .after(Labels::PreInputUpdate), + .after(Labels::InputUpdate), ); } } +pub(crate) struct SelectEvent { + entities: Vec, + mode: SelectionMode, +} + +impl SelectEvent { + pub(crate) fn none(mode: SelectionMode) -> Self { + Self { + entities: Vec::new(), + mode, + } + } + + pub(crate) fn single(entity: Entity, mode: SelectionMode) -> Self { + Self { + entities: vec![entity], + mode, + } + } + + fn entities(&self) -> &[Entity] { + self.entities.as_slice() + } + + fn mode(&self) -> SelectionMode { + self.mode + } +} + #[derive(Component)] pub(crate) struct Selected; #[derive(Clone, Copy, PartialEq)] -enum SelectionMode { +pub(crate) enum SelectionMode { Replace, Add, } @@ -42,14 +67,6 @@ struct Selector<'w, 's> { } impl<'w, 's> Selector<'w, 's> { - fn select_single(&mut self, entity: Option, mode: SelectionMode) { - let entities = match entity { - Some(entity) => vec![entity], - None => Vec::new(), - }; - self.select(&entities, mode); - } - fn select(&mut self, entities: &[Entity], mode: SelectionMode) { let selected: HashSet = self.selected.iter().collect(); let desired: HashSet = entities.iter().cloned().collect(); @@ -65,24 +82,8 @@ impl<'w, 's> Selector<'w, 's> { } } -fn mouse_click_handler( - mut event: EventReader, - keys: Res>, - pointer: Res, - mut selector: Selector, -) { - if !event - .iter() - .any(|e| e.button == MouseButton::Left && e.state == ElementState::Pressed) - { - return; +fn update_selection(mut events: EventReader, mut selector: Selector) { + for event in events.iter() { + selector.select(event.entities(), event.mode()); } - - let mode = if keys.pressed(KeyCode::LControl) { - SelectionMode::Add - } else { - SelectionMode::Replace - }; - - selector.select_single(pointer.entity(), mode); } diff --git a/crates/core/src/objects.rs b/crates/core/src/objects.rs index 64b5f0bf..f689ee9a 100644 --- a/crates/core/src/objects.rs +++ b/crates/core/src/objects.rs @@ -25,6 +25,15 @@ pub enum ObjectType { Inactive(InactiveObjectType), } +impl fmt::Display for ObjectType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Active(active) => write!(f, "Active -> {}", active), + Self::Inactive(inactive) => write!(f, "Inactive -> {}", inactive), + } + } +} + #[derive(Copy, Clone, Debug, Component, Serialize, Deserialize, PartialEq, Enum)] pub enum InactiveObjectType { Tree, diff --git a/crates/index/benches/ray.rs b/crates/index/benches/ray.rs index 11eda106..05724406 100644 --- a/crates/index/benches/ray.rs +++ b/crates/index/benches/ray.rs @@ -9,7 +9,7 @@ use criterion::{ criterion_group, criterion_main, AxisScale, BenchmarkId, Criterion, PlotConfiguration, Throughput, }; -use de_index::{EntityIndex, SpatialQuery}; +use de_index::{EntityIndex, LocalCollider, SpatialQuery}; use de_objects::ObjectCollider; use glam::Vec2; use parry3d::{ @@ -77,9 +77,11 @@ fn setup_world(num_entities: u32, max_distance: f32) -> World { let mut index = EntityIndex::new(); for (i, point) in points.iter().enumerate() { - let collider = ObjectCollider::new(Cuboid::new(Vector::new(3., 3., 4.)).into()); - let position = Isometry::new(Vector::new(point.x, 0., point.y), Vector::identity()); - index.insert(Entity::from_raw(i as u32), collider, position); + let collider = LocalCollider::new( + ObjectCollider::new(Cuboid::new(Vector::new(3., 3., 4.)).into()), + Isometry::new(Vector::new(point.x, 0., point.y), Vector::identity()), + ); + index.insert(Entity::from_raw(i as u32), collider); } let mut rays = Rays::new(); diff --git a/crates/index/src/aabb.rs b/crates/index/src/aabb.rs new file mode 100644 index 00000000..ddf8551e --- /dev/null +++ b/crates/index/src/aabb.rs @@ -0,0 +1,110 @@ +use ahash::AHashSet; +use bevy::prelude::Entity; +use parry3d::bounding_volume::AABB; + +use crate::{grid::TileGrid, range::TileRange}; + +/// An iterator over unique entity IDs withing a box. +pub(crate) struct AabbCandidates<'a> { + grid: &'a TileGrid, + tiles: TileRange, + row: Option, + prev_row: AHashSet, + current_row: AHashSet, +} + +impl<'a> AabbCandidates<'a> { + /// Creates a new iterator of entities potentially colliding with a given + /// AABB. + pub(crate) fn new(grid: &'a TileGrid, aabb: &AABB) -> Self { + Self { + grid, + tiles: TileRange::from_aabb(aabb), + row: None, + prev_row: AHashSet::new(), + current_row: AHashSet::new(), + } + } +} + +impl<'a> Iterator for AabbCandidates<'a> { + type Item = AHashSet; + + fn next(&mut self) -> Option> { + loop { + let tile_coords = match self.tiles.next() { + Some(tile_coords) => tile_coords, + None => return None, + }; + + let row = Some(tile_coords.y); + if self.row != row { + std::mem::swap(&mut self.prev_row, &mut self.current_row); + self.current_row.clear(); + self.row = row; + } + + if let Some(entities) = self.grid.get_tile_entities(tile_coords) { + debug_assert!(!entities.is_empty()); + + let mut new_entities = entities.to_owned(); + for entity in self.current_row.iter() { + new_entities.remove(entity); + } + self.current_row.extend(&new_entities); + for entity in self.prev_row.iter() { + new_entities.remove(entity); + } + + if !new_entities.is_empty() { + return Some(new_entities); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use parry3d::math::Point; + + use super::*; + use crate::TILE_SIZE; + + #[test] + fn test_aabb() { + let entity_a = Entity::from_raw(1); + let aabb_a = AABB::new( + Point::new(0.5 * TILE_SIZE, 0., 1.1 * TILE_SIZE), + Point::new(3.7 * TILE_SIZE, 3., 1.6 * TILE_SIZE), + ); + let entity_b = Entity::from_raw(2); + let aabb_b = AABB::new( + Point::new(-TILE_SIZE * 0.7, -100.5, -TILE_SIZE * 3.5), + Point::new(-TILE_SIZE * 0.6, 3.5, -TILE_SIZE * 3.2), + ); + let entity_c = Entity::from_raw(3); + let aabb_c = AABB::new( + Point::new(TILE_SIZE * 20.1, 0.5, TILE_SIZE * 20.5), + Point::new(TILE_SIZE * 20., 0.5, TILE_SIZE * 20.2), + ); + + let mut grid = TileGrid::new(); + grid.insert(entity_a, &aabb_a); + grid.insert(entity_b, &aabb_b); + grid.insert(entity_c, &aabb_c); + + let mut candidates = AabbCandidates::new( + &grid, + &AABB::new( + Point::new(-TILE_SIZE * 0.1, 0.5, -TILE_SIZE * 5.5), + Point::new(TILE_SIZE * 0.9, 0.5, TILE_SIZE * 1.2), + ), + ); + let first = candidates.next().unwrap(); + assert_eq!(first, AHashSet::from_iter(vec![entity_a])); + let second = candidates.next().unwrap(); + assert_eq!(second, AHashSet::from_iter(vec![entity_b])); + assert!(candidates.next().is_none()); + } +} diff --git a/crates/index/src/collider.rs b/crates/index/src/collider.rs new file mode 100644 index 00000000..0277660a --- /dev/null +++ b/crates/index/src/collider.rs @@ -0,0 +1,60 @@ +use de_objects::ObjectCollider; +use parry3d::{ + bounding_volume::{BoundingVolume, AABB}, + math::Isometry, + query::{Ray, RayCast}, +}; + +/// Entity collider with cached entity-space and world-space AABBs for fast +/// query pre-filtering. +pub struct LocalCollider { + object_collider: ObjectCollider, + /// World-space position of the collider. + position: Isometry, + /// Collider-space AABB. + local_aabb: AABB, + /// World-space AABB. It is kept for fast geometric pre-filtering. + world_aabb: AABB, +} + +impl LocalCollider { + /// Creates a new entity collider from entity shape and position. + pub fn new(object_collider: ObjectCollider, position: Isometry) -> Self { + let local_aabb = object_collider.compute_aabb(); + let world_aabb = local_aabb.transform_by(&position); + + Self { + object_collider, + position, + local_aabb, + world_aabb, + } + } + + pub(crate) fn world_aabb(&self) -> &AABB { + &self.world_aabb + } + + /// Updates position of cached world-space AABB of the collider. + pub(crate) fn update_position(&mut self, position: Isometry) { + self.world_aabb = self.local_aabb.transform_by(&position); + self.position = position; + } + + pub(crate) fn cast_ray(&self, ray: &Ray, max_toi: f32) -> Option { + if self.world_aabb.intersects_local_ray(ray, max_toi) { + self.object_collider.cast_ray(&self.position, ray, max_toi) + } else { + None + } + } + + pub(crate) fn intersects(&self, rhs: &Self) -> bool { + if self.world_aabb.intersects(&rhs.world_aabb) { + self.object_collider + .intersects(&self.position, &rhs.object_collider, &rhs.position) + } else { + false + } + } +} diff --git a/crates/index/src/grid.rs b/crates/index/src/grid.rs index d4b251ec..e1bcefc8 100644 --- a/crates/index/src/grid.rs +++ b/crates/index/src/grid.rs @@ -3,11 +3,10 @@ use ahash::{AHashMap, AHashSet}; use bevy::prelude::Entity; -use de_core::projection::ToFlat; -use glam::{IVec2, Vec2}; +use glam::IVec2; use parry3d::bounding_volume::AABB; -use super::TILE_SIZE; +use crate::range::TileRange; /// Rectangular (2D) grid of sets of Bevy ECS entities. /// @@ -143,92 +142,13 @@ impl TileGrid { } } -/// Iterable rectangular range of tiles. -struct TileRange { - a: IVec2, - b: IVec2, - x: i32, - y: i32, - exhausted: bool, -} - -impl TileRange { - /// Creates minimum tile range covers a given AABB. - /// - /// Tiles are assumed to be topologically closed. In other words, both - /// touching and intersecting tiles are included in the range. - fn from_aabb(aabb: &AABB) -> Self { - let aabb = aabb.to_flat(); - let min_flat: Vec2 = aabb.mins.into(); - let max_flat: Vec2 = aabb.maxs.into(); - let start = (min_flat / TILE_SIZE).floor().as_ivec2(); - let stop = (max_flat / TILE_SIZE).floor().as_ivec2(); - Self::new(start, stop) - } - - /// # Arguments - /// - /// * `a` - inclusive range start. - /// - /// * `b` - inclusive range end. - fn new(a: IVec2, b: IVec2) -> Self { - Self { - a, - b, - x: a.x, - y: a.y, - exhausted: a.cmpgt(b).any(), - } - } - - /// Returns true if the given point is not contained in the tile range. - fn excludes(&self, point: IVec2) -> bool { - self.a.cmpgt(point).any() || self.b.cmplt(point).any() - } - - /// Returns intersecting tile range. The result might be empty. - fn intersection(&self, other: &TileRange) -> TileRange { - Self::new(self.a.max(other.a), self.b.min(other.b)) - } -} - -impl PartialEq for TileRange { - fn eq(&self, other: &Self) -> bool { - self.a == other.a && self.b == other.b - } -} - -impl Eq for TileRange {} - -impl Iterator for TileRange { - type Item = IVec2; - - fn next(&mut self) -> Option { - if self.exhausted { - return None; - } - - let next = Some(IVec2::new(self.x, self.y)); - if self.x == self.b.x { - if self.y == self.b.y { - self.exhausted = true; - } else { - self.x = self.a.x; - self.y += 1; - } - } else { - self.x += 1; - } - next - } -} - #[cfg(test)] mod tests { use ahash::AHashSet; use parry3d::math::Point; use super::*; + use crate::TILE_SIZE; #[test] fn test_grid() { diff --git a/crates/index/src/index.rs b/crates/index/src/index.rs index 08c65dab..cf9bfe70 100644 --- a/crates/index/src/index.rs +++ b/crates/index/src/index.rs @@ -11,15 +11,16 @@ use bevy::{ }, prelude::{Entity, Query, Res}, }; -use de_objects::ObjectCollider; use parry3d::{ bounding_volume::{BoundingVolume, AABB}, math::{Isometry, Point}, - query::{Ray, RayCast}, + query::Ray, shape::Segment, }; use super::{grid::TileGrid, segment::SegmentCandidates}; +use crate::aabb::AabbCandidates; +use crate::LocalCollider; /// 2D rectangular grid based spatial index of entities. pub struct EntityIndex { @@ -38,13 +39,7 @@ impl EntityIndex { } } - pub fn insert( - &mut self, - entity: Entity, - object_collider: ObjectCollider, - position: Isometry, - ) { - let collider = LocalCollider::new(object_collider, position); + pub fn insert(&mut self, entity: Entity, collider: LocalCollider) { self.grid.insert(entity, collider.world_aabb()); self.world_bounds.merge(collider.world_aabb()); self.colliders.insert(entity, collider); @@ -89,6 +84,11 @@ impl EntityIndex { Some(SegmentCandidates::new(&self.grid, segment)) } + /// Returns an iterator of potentially intersecting entities. + fn query_aabb<'a>(&'a self, aabb: &AABB) -> AabbCandidates<'a> { + AabbCandidates::new(&self.grid, aabb) + } + fn get_collider(&self, entity: Entity) -> &LocalCollider { self.colliders .get(&entity) @@ -102,51 +102,6 @@ impl Default for EntityIndex { } } -/// Entity collider with cached entity-space and world-space AABBs for fast -/// query pre-filtering. -struct LocalCollider { - object_collider: ObjectCollider, - /// World-space position of the collider. - position: Isometry, - /// Collider-space AABB. - local_aabb: AABB, - /// World-space AABB. It is kept for fast geometric pre-filtering. - world_aabb: AABB, -} - -impl LocalCollider { - /// Creates a new entity collider from entity shape and position. - fn new(object_collider: ObjectCollider, position: Isometry) -> Self { - let local_aabb = object_collider.compute_aabb(); - let world_aabb = local_aabb.transform_by(&position); - - Self { - object_collider, - position, - local_aabb, - world_aabb, - } - } - - fn world_aabb(&self) -> &AABB { - &self.world_aabb - } - - /// Updates position of cached world-space AABB of the collider. - fn update_position(&mut self, position: Isometry) { - self.world_aabb = self.local_aabb.transform_by(&position); - self.position = position; - } - - fn cast_ray(&self, ray: &Ray, max_toi: f32) -> Option { - if self.world_aabb.intersects_local_ray(ray, max_toi) { - self.object_collider.cast_ray(&self.position, ray, max_toi) - } else { - None - } - } -} - /// System parameter implementing various spatial queries. /// /// Only entities automatically indexed by systems from @@ -203,6 +158,17 @@ where None } + + /// Returns true if queried solid object on the map, as indexed by + /// [`super::systems::IndexPlugin`], intersects with the given collider. + pub fn collides(&self, collider: &LocalCollider) -> bool { + let candidate_sets = self.index.query_aabb(collider.world_aabb()); + candidate_sets.flatten().any(|candidate| { + self.entities.get(candidate).map_or(false, |_| { + self.index.get_collider(candidate).intersects(collider) + }) + }) + } } pub struct RayEntityIntersection { @@ -262,6 +228,7 @@ impl Eq for RayEntityIntersection {} #[cfg(test)] mod tests { use ahash::AHashSet; + use de_objects::ObjectCollider; use parry3d::{ bounding_volume::AABB, math::{Isometry, Point, Vector}, @@ -273,15 +240,21 @@ mod tests { #[test] fn test_entity_index() { let entity_a = Entity::from_raw(1); - let collider_a = ObjectCollider::new(Cuboid::new(Vector::new(1., 2., 3.)).into()); - let position_a = Isometry::new(Vector::new(7., 0., 0.), Vector::new(0., 0., 0.)); + let collider_a = LocalCollider::new( + ObjectCollider::new(Cuboid::new(Vector::new(1., 2., 3.)).into()), + Isometry::new(Vector::new(7., 0., 0.), Vector::new(0., 0., 0.)), + ); let entity_b = Entity::from_raw(2); - let collider_b = ObjectCollider::new(Cuboid::new(Vector::new(2., 1., 2.)).into()); - let position_b_1 = Isometry::new(Vector::new(7., 1000., 0.), Vector::new(0.1, 0., 0.)); + let collider_b = LocalCollider::new( + ObjectCollider::new(Cuboid::new(Vector::new(2., 1., 2.)).into()), + Isometry::new(Vector::new(7., 1000., 0.), Vector::new(0.1, 0., 0.)), + ); let position_b_2 = Isometry::new(Vector::new(7., 1000., -200.), Vector::new(0., 0., 0.)); let entity_c = Entity::from_raw(3); - let collider_c = ObjectCollider::new(Cuboid::new(Vector::new(2., 1., 2.)).into()); - let position_c = Isometry::new(Vector::new(7., 1000., 1000.), Vector::new(0.1, 0., 0.)); + let collider_c = LocalCollider::new( + ObjectCollider::new(Cuboid::new(Vector::new(2., 1., 2.)).into()), + Isometry::new(Vector::new(7., 1000., 1000.), Vector::new(0.1, 0., 0.)), + ); let ray_a = Ray::new(Point::new(0., 0.1, 0.), Vector::new(1., 0., 0.)); let ray_b = Ray::new(Point::new(-10., 0.1, 0.), Vector::new(-1., 0., 0.)); @@ -289,9 +262,9 @@ mod tests { let mut index = EntityIndex::new(); assert!(index.cast_ray(&ray_a, 120.).is_none()); - index.insert(entity_a, collider_a, position_a); - index.insert(entity_b, collider_b, position_b_1); - index.insert(entity_c, collider_c, position_c); + index.insert(entity_a, collider_a); + index.insert(entity_b, collider_b); + index.insert(entity_c, collider_c); assert_eq!( index.get_collider(entity_a).world_aabb(), diff --git a/crates/index/src/lib.rs b/crates/index/src/lib.rs index 4a35f2b4..ccc91ae8 100644 --- a/crates/index/src/lib.rs +++ b/crates/index/src/lib.rs @@ -5,15 +5,21 @@ //! Newly spawned entities are automatically added, despawned entities removed //! and moved entities updated by systems added by //! [`self::IndexPlugin`]. +mod aabb; +mod collider; mod grid; mod index; +mod range; mod segment; mod systems; use bevy::{app::PluginGroupBuilder, prelude::PluginGroup}; use systems::IndexPlugin; -pub use self::index::{EntityIndex, RayEntityIntersection, SpatialQuery}; +pub use self::{ + collider::LocalCollider, + index::{EntityIndex, RayEntityIntersection, SpatialQuery}, +}; /// Size (in world-space) of a single square tile where entities are kept. const TILE_SIZE: f32 = 10.; diff --git a/crates/index/src/range.rs b/crates/index/src/range.rs new file mode 100644 index 00000000..73fbfa26 --- /dev/null +++ b/crates/index/src/range.rs @@ -0,0 +1,88 @@ +use de_core::projection::ToFlat; +use glam::{IVec2, Vec2}; +use parry3d::bounding_volume::AABB; + +use crate::TILE_SIZE; + +/// Iterable rectangular range of tiles. +/// +/// The tiles are iterated row-by-row, for example: (1, 1) -> (2, 1) -> (1, 2) +/// -> (2, 2). +pub(crate) struct TileRange { + a: IVec2, + b: IVec2, + x: i32, + y: i32, + exhausted: bool, +} + +impl TileRange { + /// Creates minimum tile range covers a given AABB. + /// + /// Tiles are assumed to be topologically closed. In other words, both + /// touching and intersecting tiles are included in the range. + pub(crate) fn from_aabb(aabb: &AABB) -> Self { + let aabb = aabb.to_flat(); + let min_flat: Vec2 = aabb.mins.into(); + let max_flat: Vec2 = aabb.maxs.into(); + let start = (min_flat / TILE_SIZE).floor().as_ivec2(); + let stop = (max_flat / TILE_SIZE).floor().as_ivec2(); + Self::new(start, stop) + } + + /// # Arguments + /// + /// * `a` - inclusive range start. + /// + /// * `b` - inclusive range end. + pub(crate) fn new(a: IVec2, b: IVec2) -> Self { + Self { + a, + b, + x: a.x, + y: a.y, + exhausted: a.cmpgt(b).any(), + } + } + + /// Returns true if the given point is not contained in the tile range. + pub(crate) fn excludes(&self, point: IVec2) -> bool { + self.a.cmpgt(point).any() || self.b.cmplt(point).any() + } + + /// Returns intersecting tile range. The result might be empty. + pub(crate) fn intersection(&self, other: &TileRange) -> TileRange { + Self::new(self.a.max(other.a), self.b.min(other.b)) + } +} + +impl PartialEq for TileRange { + fn eq(&self, other: &Self) -> bool { + self.a == other.a && self.b == other.b + } +} + +impl Eq for TileRange {} + +impl Iterator for TileRange { + type Item = IVec2; + + fn next(&mut self) -> Option { + if self.exhausted { + return None; + } + + let next = Some(IVec2::new(self.x, self.y)); + if self.x == self.b.x { + if self.y == self.b.y { + self.exhausted = true; + } else { + self.x = self.a.x; + self.y += 1; + } + } else { + self.x += 1; + } + next + } +} diff --git a/crates/index/src/systems.rs b/crates/index/src/systems.rs index b7eabf1a..e166d328 100644 --- a/crates/index/src/systems.rs +++ b/crates/index/src/systems.rs @@ -11,6 +11,7 @@ use iyes_loopless::prelude::*; use parry3d::math::Isometry; use super::index::EntityIndex; +use crate::collider::LocalCollider; type SolidEntityQuery<'w, 's> = Query< 'w, @@ -89,8 +90,8 @@ fn insert( transform.translation.into(), transform.rotation.to_scaled_axis().into(), ); - let collider = cache.get_collider(*object_type).clone(); - index.insert(entity, collider, position); + let collider = LocalCollider::new(cache.get_collider(*object_type).clone(), position); + index.insert(entity, collider); commands.entity(entity).insert(Indexed); } } diff --git a/crates/loader/src/map.rs b/crates/loader/src/map.rs index 09159c89..8bfeb990 100644 --- a/crates/loader/src/map.rs +++ b/crates/loader/src/map.rs @@ -5,15 +5,19 @@ use bevy::{ }; use de_camera::MoveFocusEvent; use de_core::{ - assets::asset_path, gconfig::GameConfig, log_full_error, objects::ActiveObjectType, - projection::ToMsl, state::GameState, + assets::asset_path, + gconfig::GameConfig, + log_full_error, + objects::{ActiveObjectType, ObjectType}, + projection::ToMsl, + state::GameState, }; use de_map::{ description::{InnerObject, Map}, io::{load_map, MapLoadingError}, size::MapBounds, }; -use de_objects::SpawnEvent; +use de_objects::SpawnBundle; use de_terrain::Terrain; use futures_lite::future; use iyes_loopless::prelude::*; @@ -51,7 +55,6 @@ fn spawn_map( task: Option>, mut meshes: ResMut>, mut materials: ResMut>, - mut spawn_events: EventWriter, mut move_focus_events: EventWriter, game_config: Res, ) -> Progress { @@ -98,7 +101,22 @@ fn spawn_map( setup_light(&mut commands); setup_terrain(&mut commands, &mut meshes, &mut materials, map.bounds()); - spawn_events.send_batch(map.objects().iter().cloned().map(SpawnEvent::new)); + + for object in map.objects() { + let mut entity_commands = commands.spawn(); + let object_type = match object.inner() { + InnerObject::Active(object) => { + entity_commands.insert(object.player()); + ObjectType::Active(object.object_type()) + } + InnerObject::Inactive(object) => ObjectType::Inactive(object.object_type()), + }; + entity_commands.insert_bundle(SpawnBundle::new( + object_type, + object.placement().to_transform(), + )); + } + commands.insert_resource(map.bounds()); true.into() } diff --git a/crates/objects/src/collider.rs b/crates/objects/src/collider.rs index dadbe65e..dd6d089e 100644 --- a/crates/objects/src/collider.rs +++ b/crates/objects/src/collider.rs @@ -2,7 +2,7 @@ use de_core::objects::ObjectType; use parry3d::{ bounding_volume::AABB, math::{Isometry, Point}, - query::{Ray, RayCast}, + query::{intersection_test, Ray, RayCast}, shape::{Shape, TriMesh, TriMeshFlags}, }; @@ -35,6 +35,15 @@ impl ObjectCollider { pub fn cast_ray(&self, position: &Isometry, ray: &Ray, max_toi: f32) -> Option { self.shape.cast_ray(position, ray, max_toi, true) } + + pub fn intersects( + &self, + position: &Isometry, + rhs: &Self, + rhs_position: &Isometry, + ) -> bool { + intersection_test(position, &self.shape, rhs_position, &rhs.shape).unwrap() + } } impl From<&TriMeshShape> for ObjectCollider { diff --git a/crates/objects/src/lib.rs b/crates/objects/src/lib.rs index 5a86eb51..a367e67f 100644 --- a/crates/objects/src/lib.rs +++ b/crates/objects/src/lib.rs @@ -6,7 +6,7 @@ use cache::CachePlugin; pub use cache::ObjectCache; pub use collider::{ColliderCache, ObjectCollider}; pub use ichnography::{Ichnography, IchnographyCache}; -pub use spawner::SpawnEvent; +pub use spawner::SpawnBundle; use spawner::SpawnerPlugin; mod cache; diff --git a/crates/objects/src/spawner.rs b/crates/objects/src/spawner.rs index 24f9e2b8..980bbb69 100644 --- a/crates/objects/src/spawner.rs +++ b/crates/objects/src/spawner.rs @@ -1,11 +1,10 @@ -use bevy::{ecs::system::EntityCommands, prelude::*}; +use bevy::prelude::*; use de_core::{ - events::ResendEventPlugin, gconfig::GameConfig, objects::{ActiveObjectType, MovableSolid, ObjectType, Playable, StaticSolid}, + player::Player, state::GameState, }; -use de_map::description::{ActiveObject, InnerObject, Object}; use iyes_loopless::prelude::*; use crate::cache::ObjectCache; @@ -14,64 +13,62 @@ pub(crate) struct SpawnerPlugin; impl Plugin for SpawnerPlugin { fn build(&self, app: &mut App) { - app.add_event::() - .add_plugin(ResendEventPlugin::::default()) - .add_system(spawn.run_in_state(GameState::Playing)); + app.add_system(spawn.run_in_state(GameState::Playing)); } } -pub struct SpawnEvent { - object: Object, +#[derive(Bundle)] +pub struct SpawnBundle { + object_type: ObjectType, + transform: Transform, + global_transform: GlobalTransform, + spawn: Spawn, } -impl SpawnEvent { - pub fn new(object: Object) -> Self { - Self { object } +impl SpawnBundle { + pub fn new(object_type: ObjectType, transform: Transform) -> Self { + Self { + object_type, + transform, + global_transform: transform.into(), + spawn: Spawn, + } } } +#[derive(Component)] +struct Spawn; + fn spawn( mut commands: Commands, game_config: Res, cache: Res, - mut events: EventReader, + to_spawn: Query<(Entity, &ObjectType, Option<&Player>), With>, ) { - for event in events.iter() { - let object = &event.object; - - let transform = object.placement().to_transform(); - let global_transform = GlobalTransform::from(transform); - let mut entity_commands = commands.spawn_bundle((global_transform, transform)); + for (entity, &object_type, player) in to_spawn.iter() { + info!("Spawning object {}", object_type); - let object_type = match object.inner() { - InnerObject::Active(object) => { - spawn_active(game_config.as_ref(), &mut entity_commands, object); - ObjectType::Active(object.object_type()) - } - InnerObject::Inactive(object) => { - info!("Spawning inactive object {}", object.object_type()); - entity_commands.insert(StaticSolid); - ObjectType::Inactive(object.object_type()) - } - }; - - entity_commands.insert(object_type).with_children(|parent| { + let mut entity_commands = commands.entity(entity); + entity_commands.remove::().with_children(|parent| { parent.spawn_scene(cache.get(object_type).scene()); }); - } -} - -fn spawn_active(game_config: &GameConfig, commands: &mut EntityCommands, object: &ActiveObject) { - info!("Spawning active object {}", object.object_type()); - commands.insert(object.player()); - if object.player() == game_config.player() { - commands.insert(Playable); - } + match object_type { + ObjectType::Active(active_type) => { + let player = *player.expect("Active object without an associated was spawned."); + if player == game_config.player() { + entity_commands.insert(Playable); + } - if object.object_type() == ActiveObjectType::Attacker { - commands.insert(MovableSolid); - } else { - commands.insert(StaticSolid); + if active_type == ActiveObjectType::Attacker { + entity_commands.insert(MovableSolid); + } else { + entity_commands.insert(StaticSolid); + } + } + ObjectType::Inactive(_) => { + entity_commands.insert(StaticSolid); + } + } } }