make Target into a typed struct. delineate the clear difference in Target versus Interactable. comments and naming cleanup, for more explicitness.

This commit is contained in:
anomaluridae 2021-08-10 20:13:51 -07:00
parent 51f38df169
commit 48cc5d3b08
3 changed files with 200 additions and 160 deletions

View File

@ -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<Target>,
@ -47,107 +47,117 @@ pub(super) fn select_interactable(
scene: &Scene,
) -> Option<Interactable> {
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::<comp::Pos>();
let player_pos = positions.get(player_entity)?.0;
let scales = ecs.read_storage::<comp::Scale>();
let colliders = ecs.read_storage::<comp::Collider>();
let char_states = ecs.read_storage::<comp::CharacterState>();
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::<i32>::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::<comp::Pos>();
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::<comp::Scale>();
let colliders = ecs.read_storage::<comp::Collider>();
let char_states = ecs.read_storage::<comp::CharacterState>();
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::<i32>::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)))
}
}

View File

@ -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>| {
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);
}
},

View File

@ -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<f32>, Vec3<f32>, f32), // (solid_pos, build_pos, dist)
Collectable(Vec3<f32>, f32), // (pos, dist)
Entity(specs::Entity, Vec3<f32>, f32), // (e, pos, dist)
Mine(Vec3<f32>, f32), // (pos, dist)
pub enum TargetType {
Build(Vec3<f32>),
Collectable,
Entity(specs::Entity),
Mine,
}
#[derive(Clone, Copy, Debug)]
pub struct Target {
pub typed: TargetType,
pub distance: f32,
pub position: Vec3<f32>,
}
impl Target {
pub fn entity(self) -> Option<specs::Entity> {
match self {
Self::Entity(e, _, _) => Some(e),
_ => None,
pub fn position_int(self) -> Vec3<i32> { self.position.map(|p| p.floor() as i32) }
pub fn make_interactable(self, client: &Client) -> Option<Interactable> {
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<f32> {
match self {
Self::Collectable(sp, _)
| Self::Entity(_, sp, _)
| Self::Mine(sp, _)
| Self::Build(sp, _, _) => sp,
}
}
pub fn position_int(self) -> Vec3<i32> { 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,
)
}