From 48cc5d3b080fe9936ffe730f03fd64b0907c8421 Mon Sep 17 00:00:00 2001 From: anomaluridae Date: Tue, 10 Aug 2021 20:13:51 -0700 Subject: [PATCH] make Target into a typed struct. delineate the clear difference in Target versus Interactable. comments and naming cleanup, for more explicitness. --- voxygen/src/session/interactable.rs | 216 +++++++++++++++------------- voxygen/src/session/mod.rs | 28 ++-- voxygen/src/session/target.rs | 116 +++++++++------ 3 files changed, 200 insertions(+), 160 deletions(-) diff --git a/voxygen/src/session/interactable.rs b/voxygen/src/session/interactable.rs index 842503bc6c..eca3d9b073 100644 --- a/voxygen/src/session/interactable.rs +++ b/voxygen/src/session/interactable.rs @@ -33,12 +33,12 @@ impl Interactable { /// Select interactable to hightlight, display interaction text for, and to /// interact with if the interact key is pressed /// Selected in the following order -/// 1) Targeted entity (if interactable) (entities can't be target through -/// blocks) -/// 2) Selected block -/// (a) if collectable -/// (b) if can be mined -/// 3) Closest of nearest interactable entity/block +/// 1) Targeted items, in order of preference: +/// (a) entity (if within range) +/// (b) collectable +/// (c) can be mined +/// 2) outside of targeted cam ray +/// -> closest of nearest interactable entity/block pub(super) fn select_interactable( client: &Client, collect_target: Option, @@ -47,107 +47,117 @@ pub(super) fn select_interactable( scene: &Scene, ) -> Option { span!(_guard, "select_interactable"); - // TODO: once there are multiple distances for different types of interactions - // this code will need to be revamped to cull things by varying distances - // based on the types of interactions available for those things use common::{spiral::Spiral2d, terrain::TerrainChunk, vol::RectRasterableVol}; - entity_target - .and_then(|x| if let Target::Entity(entity, _, dist) = x { - (dist < MAX_PICKUP_RANGE).then_some(Interactable::Entity(entity)) - } else { None }) + if let Some(interactable) = entity_target + .and_then(|t| { + if t.distance < MAX_PICKUP_RANGE { + t.make_interactable(client) + } else { + None + } + }) .or_else(|| { - collect_target.and_then(|ct| { - client.state().terrain().get(ct.position_int()).ok().copied() - .map(|b| Interactable::Block(b, ct.position_int(), Some(Interaction::Collect))) + collect_target + .map(|t| t.make_interactable(client)) + .unwrap_or(None) + }) + .or_else(|| { + mine_target + .map(|t| t.make_interactable(client)) + .unwrap_or(None) + }) + { + Some(interactable) + } else { + let ecs = client.state().ecs(); + let player_entity = client.entity(); + let positions = ecs.read_storage::(); + let player_pos = positions.get(player_entity)?.0; + + let scales = ecs.read_storage::(); + let colliders = ecs.read_storage::(); + let char_states = ecs.read_storage::(); + + let player_cylinder = Cylinder::from_components( + player_pos, + scales.get(player_entity).copied(), + colliders.get(player_entity), + char_states.get(player_entity), + ); + + let closest_interactable_entity = ( + &ecs.entities(), + &positions, + scales.maybe(), + colliders.maybe(), + char_states.maybe(), + ) + .join() + .filter(|(e, _, _, _, _)| *e != player_entity) + .map(|(e, p, s, c, cs)| { + let cylinder = Cylinder::from_components(p.0, s.copied(), c, cs); + (e, cylinder) }) - }) - .or_else(|| { - mine_target.and_then(|mt| { - client.state().terrain().get(mt.position_int()).ok().copied() - .map(|b| Interactable::Block(b, mt.position_int(), None)) + // Roughly filter out entities farther than interaction distance + .filter(|(_, cylinder)| player_cylinder.approx_in_range(*cylinder, MAX_PICKUP_RANGE)) + .map(|(e, cylinder)| (e, player_cylinder.min_distance(cylinder))) + .min_by_key(|(_, dist)| OrderedFloat(*dist)); + + // Only search as far as closest interactable entity + let search_dist = closest_interactable_entity.map_or(MAX_PICKUP_RANGE, |(_, dist)| dist); + let player_chunk = player_pos.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| { + (e.floor() as i32).div_euclid(sz as i32) + }); + let terrain = scene.terrain(); + + // Find closest interactable block + // TODO: consider doing this one first? + let closest_interactable_block_pos = Spiral2d::new() + // TODO: this formula for the number to take was guessed + // Note: assume RECT_SIZE.x == RECT_SIZE.y + .take(((search_dist / TerrainChunk::RECT_SIZE.x as f32).ceil() as usize * 2 + 1).pow(2)) + .flat_map(|offset| { + let chunk_pos = player_chunk + offset; + let chunk_voxel_pos = + Vec3::::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32)); + terrain.get(chunk_pos).map(|data| (data, chunk_voxel_pos)) }) - }) - .or_else(|| { - let ecs = client.state().ecs(); - let player_entity = client.entity(); - let positions = ecs.read_storage::(); - let player_pos = positions.get(player_entity)?.0; + // TODO: maybe we could make this more efficient by putting the + // interactables is some sort of spatial structure + .flat_map(|(chunk_data, chunk_pos)| { + chunk_data + .blocks_of_interest + .interactables + .iter() + .map(move |(block_offset, interaction)| (chunk_pos + block_offset, interaction)) + }) + .map(|(block_pos, interaction)| ( + block_pos, + block_pos.map(|e| e as f32 + 0.5) + .distance_squared(player_pos), + interaction, + )) + .min_by_key(|(_, dist_sqr, _)| OrderedFloat(*dist_sqr)) + .map(|(block_pos, _, interaction)| (block_pos, interaction)); - let scales = ecs.read_storage::(); - let colliders = ecs.read_storage::(); - let char_states = ecs.read_storage::(); - - let player_cylinder = Cylinder::from_components( - player_pos, - scales.get(player_entity).copied(), - colliders.get(player_entity), - char_states.get(player_entity), - ); - - let closest_interactable_entity = ( - &ecs.entities(), - &positions, - scales.maybe(), - colliders.maybe(), - char_states.maybe(), - ) - .join() - .filter(|(e, _, _, _, _)| *e != player_entity) - .map(|(e, p, s, c, cs)| { - let cylinder = Cylinder::from_components(p.0, s.copied(), c, cs); - (e, cylinder) - }) - // Roughly filter out entities farther than interaction distance - .filter(|(_, cylinder)| player_cylinder.approx_in_range(*cylinder, MAX_PICKUP_RANGE)) - .map(|(e, cylinder)| (e, player_cylinder.min_distance(cylinder))) - .min_by_key(|(_, dist)| OrderedFloat(*dist)); - - // Only search as far as closest interactable entity - let search_dist = closest_interactable_entity - .map_or(MAX_PICKUP_RANGE, |(_, dist)| dist); - let player_chunk = player_pos.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| { - (e.floor() as i32).div_euclid(sz as i32) - }); - let terrain = scene.terrain(); - - // Find closest interactable block - // TODO: consider doing this one first? - let closest_interactable_block_pos = Spiral2d::new() - // TODO: this formula for the number to take was guessed - // Note: assume RECT_SIZE.x == RECT_SIZE.y - .take(((search_dist / TerrainChunk::RECT_SIZE.x as f32).ceil() as usize * 2 + 1).pow(2)) - .flat_map(|offset| { - let chunk_pos = player_chunk + offset; - let chunk_voxel_pos = - Vec3::::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32)); - terrain.get(chunk_pos).map(|data| (data, chunk_voxel_pos)) - }) - // TODO: maybe we could make this more efficient by putting the - // interactables is some sort of spatial structure - .flat_map(|(chunk_data, chunk_pos)| { - chunk_data - .blocks_of_interest - .interactables - .iter() - .map(move |(block_offset, interaction)| (chunk_pos + block_offset, interaction)) - }) - .map(|(block_pos, interaction)| ( - block_pos, - block_pos.map(|e| e as f32 + 0.5) - .distance_squared(player_pos), - interaction, - )) - .min_by_key(|(_, dist_sqr, _)| OrderedFloat(*dist_sqr)) - .map(|(block_pos, _, interaction)| (block_pos, interaction)); - - // Pick closer one if they exist - closest_interactable_block_pos - .filter(|(block_pos, _)| player_cylinder.min_distance(Cube { min: block_pos.as_(), side_length: 1.0}) < search_dist) - .and_then(|(block_pos, interaction)| - client.state().terrain().get(block_pos).ok().copied() - .map(|b| Interactable::Block(b, block_pos, Some(*interaction))) - ) - .or_else(|| closest_interactable_entity.map(|(e, _)| Interactable::Entity(e))) - }) + // return the closest, and the 2 closest inertactable options (entity or block) + closest_interactable_block_pos + .filter(|(block_pos, _)| { + player_cylinder.min_distance(Cube { + min: block_pos.as_(), + side_length: 1.0, + }) < search_dist + }) + .and_then(|(block_pos, interaction)| { + client + .state() + .terrain() + .get(block_pos) + .ok() + .copied() + .map(|b| Interactable::Block(b, block_pos, Some(*interaction))) + }) + .or_else(|| closest_interactable_entity.map(|(e, _)| Interactable::Entity(e))) + } } diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index 64378f9e7e..1b4d846e27 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -50,7 +50,7 @@ use crate::{ use hashbrown::HashMap; use interactable::{select_interactable, Interactable}; use settings_change::Language::ChangeLanguage; -use target::{targets_under_cursor, Target}; +use target::{targets_under_cursor, Target, TargetType}; #[cfg(feature = "egui-ui")] use voxygen_egui::EguiDebugInfo; @@ -433,7 +433,7 @@ impl PlayState for SessionState { let is_nearest_target = |target: Option| { target - .map(|t| (t.distance() <= shortest_dist)) + .map(|t| (t.distance <= shortest_dist)) .unwrap_or(false) }; @@ -444,11 +444,17 @@ impl PlayState for SessionState { build_target.map(|bt| self.scene.set_select_pos(Some(bt.position_int()))); } else { self.scene.set_select_pos(None); - self.inputs.select_pos = entity_target.map(|et| et.position()); + self.inputs.select_pos = entity_target.map(|et| et.position); } // Throw out distance info, it will be useful in the future - self.target_entity = entity_target.and_then(Target::entity); + let entity_under_target = + if let Some(TargetType::Entity(e)) = entity_target.map(|t| t.typed) { + Some(e) + } else { + None + }; + self.target_entity = entity_under_target; macro_rules! entity_event_handler { ($input: expr, $pressed: expr) => { @@ -457,7 +463,7 @@ impl PlayState for SessionState { $input, $pressed, self.inputs.select_pos, - entity_target.map(Target::entity).unwrap_or(None), + entity_under_target, ); }; } @@ -483,10 +489,10 @@ impl PlayState for SessionState { match input { GameInput::Primary => { if is_mining && is_nearest_target(mine_target) { - self.inputs.select_pos = mine_target.map(Target::position); + self.inputs.select_pos = mine_target.map(|t| t.position); entity_event_handler!(InputKind::Primary, state); } else if state && can_build && is_nearest_target(build_target) { - self.inputs.select_pos = build_target.map(Target::position); + self.inputs.select_pos = build_target.map(|t| t.position); let mut client = self.client.borrow_mut(); client.remove_block(build_target.unwrap().position_int()); } else { @@ -496,7 +502,7 @@ impl PlayState for SessionState { GameInput::Secondary => { if state && can_build && is_nearest_target(build_target) { if let Some(build_target) = build_target { - self.inputs.select_pos = Some(build_target.position()); + self.inputs.select_pos = Some(build_target.position); let mut client = self.client.borrow_mut(); client.place_block( build_target.position_int(), @@ -523,7 +529,7 @@ impl PlayState for SessionState { .copied() }) { self.inputs.select_pos = - build_target.map(Target::position); + build_target.map(|t| t.position); self.selected_block = block; } } @@ -682,8 +688,8 @@ impl PlayState for SessionState { match interaction { Some(Interaction::Collect) => { if block.is_collectible() { - self.inputs.select_pos = collect_target - .map(Target::position); + self.inputs.select_pos = + collect_target.map(|t| t.position); client.collect_block(pos); } }, diff --git a/voxygen/src/session/target.rs b/voxygen/src/session/target.rs index 049d67d91f..22bc932fca 100644 --- a/voxygen/src/session/target.rs +++ b/voxygen/src/session/target.rs @@ -1,6 +1,8 @@ use specs::{Join, WorldExt}; use vek::*; +use super::interactable::Interactable; +use crate::scene::terrain::Interaction; use client::{self, Client}; use common::{ comp, @@ -13,40 +15,43 @@ use common::{ use common_base::span; #[derive(Clone, Copy, Debug)] -pub enum Target { - Build(Vec3, Vec3, f32), // (solid_pos, build_pos, dist) - Collectable(Vec3, f32), // (pos, dist) - Entity(specs::Entity, Vec3, f32), // (e, pos, dist) - Mine(Vec3, f32), // (pos, dist) +pub enum TargetType { + Build(Vec3), + Collectable, + Entity(specs::Entity), + Mine, +} + +#[derive(Clone, Copy, Debug)] +pub struct Target { + pub typed: TargetType, + pub distance: f32, + pub position: Vec3, } impl Target { - pub fn entity(self) -> Option { - match self { - Self::Entity(e, _, _) => Some(e), - _ => None, + pub fn position_int(self) -> Vec3 { self.position.map(|p| p.floor() as i32) } + + pub fn make_interactable(self, client: &Client) -> Option { + match self.typed { + TargetType::Collectable => client + .state() + .terrain() + .get(self.position_int()) + .ok() + .copied() + .map(|b| Interactable::Block(b, self.position_int(), Some(Interaction::Collect))), + TargetType::Entity(e) => Some(Interactable::Entity(e)), + TargetType::Mine => client + .state() + .terrain() + .get(self.position_int()) + .ok() + .copied() + .map(|b| Interactable::Block(b, self.position_int(), None)), + TargetType::Build(_) => None, } } - - pub fn distance(self) -> f32 { - match self { - Self::Collectable(_, d) - | Self::Entity(_, _, d) - | Self::Mine(_, d) - | Self::Build(_, _, d) => d, - } - } - - pub fn position(self) -> Vec3 { - match self { - Self::Collectable(sp, _) - | Self::Entity(_, sp, _) - | Self::Mine(sp, _) - | Self::Build(sp, _, _) => sp, - } - } - - pub fn position_int(self) -> Vec3 { self.position().map(|p| p.floor() as i32) } } /// Max distance an entity can be "targeted" @@ -130,13 +135,14 @@ pub(super) fn targets_under_cursor( // FIXME: the `solid_pos` is used in the remove_block(). is this correct? let (solid_pos, build_pos, cam_ray_2) = find_pos(|b: Block| b.is_solid()); - // collectables can be in the Air. so using solely solid_pos is not correct. - // so, use a minimum distance of all 3 + // find shortest cam_dist of non-entity targets + // note that some of these targets can technically be in Air, such as the + // collectable. let mut cam_rays = vec![&cam_ray_0, &cam_ray_2]; if is_mining { cam_rays.push(&cam_ray_1); } - let cam_dist = cam_rays + let shortest_cam_dist = cam_rays .iter() .filter_map(|x| match **x { (d, Ok(Some(_))) => Some(d), @@ -146,10 +152,10 @@ pub(super) fn targets_under_cursor( .unwrap_or(MAX_PICKUP_RANGE); // See if ray hits entities - // Currently treated as spheres - // Don't cast through blocks - // Could check for intersection with entity from last frame to narrow this down - let cast_dist = cam_dist.min(MAX_TARGET_RANGE); + // Don't cast through blocks, (hence why use shortest_cam_dist from non-entity + // targets) Could check for intersection with entity from last frame to + // narrow this down + let cast_dist = shortest_cam_dist.min(MAX_TARGET_RANGE); // Need to raycast by distance to cam // But also filter out by distance to the player (but this only needs to be done @@ -191,7 +197,7 @@ pub(super) fn targets_under_cursor( let seg_ray = LineSegment3 { start: cam_pos, - end: cam_pos + cam_dir * cam_dist, + end: cam_pos + cam_dir * shortest_cam_dist, }; // TODO: fuzzy borders let entity_target = nearby @@ -209,30 +215,48 @@ pub(super) fn targets_under_cursor( ); let dist_to_player = player_cylinder.min_distance(target_cylinder); - (dist_to_player < MAX_TARGET_RANGE).then_some(Target::Entity(*e, p, dist_to_player)) + if dist_to_player < MAX_TARGET_RANGE { + Some(Target { + typed: TargetType::Entity(*e), + position: p, + distance: dist_to_player, + }) + } else { None } }); - let build_target = if can_build { - solid_pos.map(|p| Target::Build(p, build_pos.unwrap(), cam_ray_2.0)) + let build_target = if let (true, Some(position), Some(bp)) = (can_build, solid_pos, build_pos) { + Some(Target { + typed: TargetType::Build(bp), + distance: cam_ray_2.0, + position, + }) } else { None }; - let mine_target = if is_mining { - mine_pos.map(|p| Target::Mine(p, cam_ray_1.0)) + let collect_target = collect_pos.map(|position| Target { + typed: TargetType::Collectable, + distance: cam_ray_0.0, + position, + }); + + let mine_target = if let (true, Some(position)) = (is_mining, mine_pos) { + Some(Target { + typed: TargetType::Mine, + distance: cam_ray_1.0, + position, + }) } else { None }; - let shortest_distance = cam_dist; - // Return multiple possible targets // GameInput events determine which target to use. ( build_target, - collect_pos.map(|p| Target::Collectable(p, cam_ray_0.0)), + collect_target, entity_target, mine_target, - shortest_distance, + shortest_cam_dist, ) }