Fix beam offsets

- Introduce notion of min and max radius for Body instead of old
  `radius()` function (which is renamed to `max_radius()`).
This commit is contained in:
juliancoffee 2021-09-16 13:42:07 +03:00
parent 7d97fe7ec5
commit c069a3523d
13 changed files with 224 additions and 190 deletions

View File

@ -400,9 +400,17 @@ impl Body {
// Note: This is used for collisions, but it's not very accurate for shapes that
// are very much not cylindrical. Eventually this ought to be replaced by more
// accurate collision shapes.
pub fn radius(&self) -> f32 {
pub fn max_radius(&self) -> f32 {
let dim = self.dimensions();
dim.x.max(dim.y) / 2.0
let (x, y) = (dim.x, dim.y);
x.max(y) / 2.0
}
pub fn min_radius(&self) -> f32 {
let (_p0, _p1, radius) = self.sausage();
radius
}
/// Base of our Capsule Prism used for collisions.
@ -453,7 +461,7 @@ impl Body {
// lead to that both entities will try to keep 5.0 units away from each
// other.
pub fn spacing_radius(&self) -> f32 {
self.radius()
self.max_radius()
+ match self {
Body::QuadrupedSmall(body) => match body.species {
quadruped_small::Species::Rat => 0.0,

View File

@ -232,8 +232,9 @@ fn height_offset(body: &Body, look_dir: Dir) -> f32 {
}
pub fn beam_offsets(body: &Body, look_dir: Dir, ori: Vec3<f32>) -> Vec3<f32> {
let body_radius = body.radius();
let body_radius = body.min_radius();
let body_offsets_z = height_offset(body, look_dir);
Vec3::new(
body_radius * ori.x * 1.1,
body_radius * ori.y * 1.1,

View File

@ -196,7 +196,7 @@ impl CharacterBehavior for Data {
Outcome::GroundSlam {
pos: data.pos.0
+ *data.ori.look_dir()
* (data.body.radius() + self.static_data.range),
* (data.body.max_radius() + self.static_data.range),
},
));
}

View File

@ -641,7 +641,7 @@ pub fn handle_manipulate_loadout(
// MAX_PICKUP_RANGE and the radius of the body
let sprite_range_check = |pos: Vec3<f32>| {
(sprite_pos_f32 - pos).magnitude_squared()
< (MAX_PICKUP_RANGE + data.body.radius()).powi(2)
< (MAX_PICKUP_RANGE + data.body.max_radius()).powi(2)
};
// Checks if player's feet or head is near to sprite

View File

@ -82,174 +82,193 @@ impl<'a> System<'a> for Sys {
&beam_segments,
)
.par_join()
.fold(|| (Vec::new(), Vec::new(), Vec::new()),
|(mut server_events, mut add_hit_entities, mut outcomes),
(entity, pos, ori, beam_segment)|
{
let creation_time = match beam_segment.creation {
Some(time) => time,
// Skip newly created beam segments
None => return (server_events, add_hit_entities, outcomes),
};
let end_time = creation_time + beam_segment.duration.as_secs_f64();
let beam_owner = beam_segment
.owner
.and_then(|uid| read_data.uid_allocator.retrieve_entity_internal(uid.into()));
let mut rng = thread_rng();
if rng.gen_bool(0.005) {
server_events.push(ServerEvent::Sound {
sound: Sound::new(SoundKind::Beam, pos.0, 7.0, time),
});
}
// If beam segment is out of time emit destroy event but still continue since it
// may have traveled and produced effects a bit before reaching its
// end point
if end_time < time {
server_events.push(ServerEvent::Delete(entity));
}
// Determine area that was covered by the beam in the last tick
let frame_time = dt.min((end_time - time) as f32);
if frame_time <= 0.0 {
return (server_events, add_hit_entities, outcomes);
}
// Note: min() probably uneeded
let time_since_creation = (time - creation_time) as f32;
let frame_start_dist =
(beam_segment.speed * (time_since_creation - frame_time)).max(0.0);
let frame_end_dist = (beam_segment.speed * time_since_creation).max(frame_start_dist);
// Group to ignore collisions with
// Might make this more nuanced if beams are used for non damage effects
let group = beam_owner.and_then(|e| read_data.groups.get(e));
let hit_entities = if let Some(beam) = beam_owner.and_then(|e| beams.get(e)) {
&beam.hit_entities
} else {
return (server_events, add_hit_entities, outcomes);
};
// Go through all affectable entities by querying the spatial grid
let target_iter = read_data
.cached_spatial_grid
.0
.in_circle_aabr(pos.0.xy(), frame_end_dist - frame_start_dist)
.filter_map(|target|{
read_data
.positions
.get(target)
.and_then(|l| read_data.healths.get(target).map(|r| (l,r)))
.and_then(|l| read_data.uids.get(target).map(|r| (l,r)))
.and_then(|l| read_data.bodies.get(target).map(|r| (l,r)))
.map(|(((pos_b, health_b), uid_b), body_b)| {
(target, uid_b, pos_b, health_b, body_b)
})
});
target_iter.for_each(|(target, uid_b, pos_b, health_b, body_b)| {
// Check to see if entity has already been hit recently
if hit_entities.iter().any(|&uid| uid == *uid_b) {
return;
}
// Scales
let scale_b = read_data.scales.get(target).map_or(1.0, |s| s.0);
let rad_b = body_b.radius() * scale_b;
let height_b = body_b.height() * scale_b;
// Check if it is a hit
let hit = entity != target && !health_b.is_dead
// Collision shapes
&& sphere_wedge_cylinder_collision(pos.0, frame_start_dist, frame_end_dist, *ori.look_dir(), beam_segment.angle, pos_b.0, rad_b, height_b);
// Finally, ensure that a hit has actually occurred by performing a raycast. We do this last because
// it's likely to be the most expensive operation.
let tgt_dist = pos.0.distance(pos_b.0);
let hit = hit && read_data.terrain
.ray(pos.0, pos.0 + *ori.look_dir() * (tgt_dist + 1.0))
.until(|b| b.is_filled())
.cast().0 >= tgt_dist;
if hit {
// See if entities are in the same group
let same_group = group
.map(|group_a| Some(group_a) == read_data.groups.get(target))
.unwrap_or(Some(*uid_b) == beam_segment.owner);
let target_group = if same_group {
GroupTarget::InGroup
} else {
GroupTarget::OutOfGroup
.fold(
|| (Vec::new(), Vec::new(), Vec::new()),
|(mut server_events, mut add_hit_entities, mut outcomes),
(entity, pos, ori, beam_segment)| {
let creation_time = match beam_segment.creation {
Some(time) => time,
// Skip newly created beam segments
None => return (server_events, add_hit_entities, outcomes),
};
let end_time = creation_time + beam_segment.duration.as_secs_f64();
// If owner, shouldn't heal or damage
if Some(*uid_b) == beam_segment.owner {
return;
let beam_owner = beam_segment.owner.and_then(|uid| {
read_data.uid_allocator.retrieve_entity_internal(uid.into())
});
let mut rng = thread_rng();
if rng.gen_bool(0.005) {
server_events.push(ServerEvent::Sound {
sound: Sound::new(SoundKind::Beam, pos.0, 7.0, time),
});
}
let attacker_info =
beam_owner
.zip(beam_segment.owner)
.map(|(entity, uid)| AttackerInfo {
entity,
uid,
energy: read_data.energies.get(entity),
combo: read_data.combos.get(entity),
inventory: read_data.inventories.get(entity),
});
// If beam segment is out of time emit destroy event but still continue since it
// may have traveled and produced effects a bit before reaching its
// end point
if end_time < time {
server_events.push(ServerEvent::Delete(entity));
}
let target_info = TargetInfo {
entity: target,
uid: *uid_b,
inventory: read_data.inventories.get(target),
stats: read_data.stats.get(target),
health: read_data.healths.get(target),
pos: pos_b.0,
ori: read_data.orientations.get(target),
char_state: read_data.character_states.get(target),
// Determine area that was covered by the beam in the last tick
let frame_time = dt.min((end_time - time) as f32);
if frame_time <= 0.0 {
return (server_events, add_hit_entities, outcomes);
}
// Note: min() probably uneeded
let time_since_creation = (time - creation_time) as f32;
let frame_start_dist =
(beam_segment.speed * (time_since_creation - frame_time)).max(0.0);
let frame_end_dist =
(beam_segment.speed * time_since_creation).max(frame_start_dist);
// Group to ignore collisions with
// Might make this more nuanced if beams are used for non damage effects
let group = beam_owner.and_then(|e| read_data.groups.get(e));
let hit_entities = if let Some(beam) = beam_owner.and_then(|e| beams.get(e)) {
&beam.hit_entities
} else {
return (server_events, add_hit_entities, outcomes);
};
// Go through all affectable entities by querying the spatial grid
let target_iter = read_data
.cached_spatial_grid
.0
.in_circle_aabr(pos.0.xy(), frame_end_dist - frame_start_dist)
.filter_map(|target| {
read_data
.positions
.get(target)
.and_then(|l| read_data.healths.get(target).map(|r| (l, r)))
.and_then(|l| read_data.uids.get(target).map(|r| (l, r)))
.and_then(|l| read_data.bodies.get(target).map(|r| (l, r)))
.map(|(((pos_b, health_b), uid_b), body_b)| {
(target, uid_b, pos_b, health_b, body_b)
})
});
target_iter.for_each(|(target, uid_b, pos_b, health_b, body_b)| {
// Check to see if entity has already been hit recently
if hit_entities.iter().any(|&uid| uid == *uid_b) {
return;
}
// PvP check
let may_harm = combat::may_harm(
&read_data.alignments,
&read_data.players,
&read_data.uid_allocator,
beam_owner,
target,
);
let attack_options = AttackOptions {
// No luck with dodging beams
target_dodging: false,
may_harm,
target_group,
};
// Scales
let scale_b = read_data.scales.get(target).map_or(1.0, |s| s.0);
let rad_b = body_b.max_radius() * scale_b;
let height_b = body_b.height() * scale_b;
beam_segment.properties.attack.apply_attack(
attacker_info,
target_info,
ori.look_dir(),
attack_options,
1.0,
AttackSource::Beam,
|e| server_events.push(e),
|o| outcomes.push(o),
);
// Check if it is a hit
// TODO: use Capsule Prism instead of cylinder
let hit = entity != target
&& !health_b.is_dead
&& sphere_wedge_cylinder_collision(
pos.0,
frame_start_dist,
frame_end_dist,
*ori.look_dir(),
beam_segment.angle,
pos_b.0,
rad_b,
height_b,
);
add_hit_entities.push((beam_owner, *uid_b));
}
});
(server_events, add_hit_entities, outcomes)
}).reduce(|| (Vec::new(), Vec::new(), Vec::new()),
|(mut events_a, mut hit_entities_a, mut outcomes_a),
(mut events_b, mut hit_entities_b, mut outcomes_b)| {
events_a.append(&mut events_b);
hit_entities_a.append(&mut hit_entities_b);
outcomes_a.append(&mut outcomes_b);
(events_a, hit_entities_a, outcomes_a)
});
// Finally, ensure that a hit has actually occurred by performing a raycast.
// We do this last because it's likely to be the
// most expensive operation.
let tgt_dist = pos.0.distance(pos_b.0);
let hit = hit
&& read_data
.terrain
.ray(pos.0, pos.0 + *ori.look_dir() * (tgt_dist + 1.0))
.until(|b| b.is_filled())
.cast()
.0
>= tgt_dist;
if hit {
// See if entities are in the same group
let same_group = group
.map(|group_a| Some(group_a) == read_data.groups.get(target))
.unwrap_or(Some(*uid_b) == beam_segment.owner);
let target_group = if same_group {
GroupTarget::InGroup
} else {
GroupTarget::OutOfGroup
};
// If owner, shouldn't heal or damage
if Some(*uid_b) == beam_segment.owner {
return;
}
let attacker_info =
beam_owner.zip(beam_segment.owner).map(|(entity, uid)| {
AttackerInfo {
entity,
uid,
energy: read_data.energies.get(entity),
combo: read_data.combos.get(entity),
inventory: read_data.inventories.get(entity),
}
});
let target_info = TargetInfo {
entity: target,
uid: *uid_b,
inventory: read_data.inventories.get(target),
stats: read_data.stats.get(target),
health: read_data.healths.get(target),
pos: pos_b.0,
ori: read_data.orientations.get(target),
char_state: read_data.character_states.get(target),
};
// PvP check
let may_harm = combat::may_harm(
&read_data.alignments,
&read_data.players,
&read_data.uid_allocator,
beam_owner,
target,
);
let attack_options = AttackOptions {
// No luck with dodging beams
target_dodging: false,
may_harm,
target_group,
};
beam_segment.properties.attack.apply_attack(
attacker_info,
target_info,
ori.look_dir(),
attack_options,
1.0,
AttackSource::Beam,
|e| server_events.push(e),
|o| outcomes.push(o),
);
add_hit_entities.push((beam_owner, *uid_b));
}
});
(server_events, add_hit_entities, outcomes)
},
)
.reduce(
|| (Vec::new(), Vec::new(), Vec::new()),
|(mut events_a, mut hit_entities_a, mut outcomes_a),
(mut events_b, mut hit_entities_b, mut outcomes_b)| {
events_a.append(&mut events_b);
hit_entities_a.append(&mut hit_entities_b);
outcomes_a.append(&mut outcomes_b);
(events_a, hit_entities_a, outcomes_a)
},
);
job.cpu_stats.measure(ParMode::Single);
outcomes.append(&mut new_outcomes);

View File

@ -80,7 +80,8 @@ impl<'a> System<'a> for Sys {
// Scales
let eye_pos = pos.0 + Vec3::unit_z() * body.eye_height();
let scale = read_data.scales.get(attacker).map_or(1.0, |s| s.0);
let rad = body.radius() * scale;
// TODO: use Capsule Prisms instead of Cylinders
let rad = body.max_radius() * scale;
// Mine blocks broken by the attack
if let Some((block_pos, tool)) = melee_attack.break_block {
@ -115,7 +116,7 @@ impl<'a> System<'a> for Sys {
// Scales
let scale_b = read_data.scales.get(target).map_or(1.0, |s| s.0);
let rad_b = body_b.radius() * scale_b;
let rad_b = body_b.max_radius() * scale_b;
// Check if entity is dodging
let target_dodging = read_data

View File

@ -228,8 +228,7 @@ impl<'a> PhysicsData<'a> {
// Move center to the middle between OLD and OLD+VEL_DT
// so that we can reduce the collision_boundary.
phys_cache.center = entity_center + phys_cache.velocity_dt / 2.0;
phys_cache.collision_boundary = radius
+ (phys_cache.velocity_dt / 2.0).magnitude();
phys_cache.collision_boundary = radius + (phys_cache.velocity_dt / 2.0).magnitude();
phys_cache.scale = scale;
phys_cache.scaled_radius = flat_radius;
@ -459,7 +458,6 @@ impl<'a> PhysicsData<'a> {
let mut collision_registered = false;
for i in 0..increments {
let factor = i as f32 * step_delta;
match try_e2e_collision(
@ -1919,7 +1917,8 @@ struct ColliderContext<'a> {
previous_cache: &'a PreviousPhysCache,
}
/// Find pushback vector and collision_distance we assume between this colliders.
/// Find pushback vector and collision_distance we assume between this
/// colliders.
fn projection_between(c0: ColliderContext, c1: ColliderContext) -> (Vec2<f32>, f32) {
// "Proper" way to do this would be handle the case when both our colliders
// are capsule prisms by building origins from p0, p1 offsets and our

View File

@ -152,7 +152,8 @@ impl<'a> System<'a> for Sys {
// Scales
let scale_b = read_data.scales.get(target).map_or(1.0, |s| s.0);
let rad_b = body_b.radius() * scale_b;
// TODO: use Capsule Prism instead of Cylinder
let rad_b = body_b.max_radius() * scale_b;
// Angle checks
let pos_b_ground = Vec3::new(pos_b.0.x, pos_b.0.y, pos.0.z);

View File

@ -715,8 +715,8 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
cyl_body: Body,
) -> f32 {
// 2d check
let horiz_dist =
Vec2::<f32>::from(sphere_pos - cyl_pos).distance(Vec2::default()) - cyl_body.radius();
let horiz_dist = Vec2::<f32>::from(sphere_pos - cyl_pos).distance(Vec2::default())
- cyl_body.max_radius();
// z check
let half_body_height = cyl_body.height() / 2.0;
let vert_distance =

View File

@ -249,7 +249,7 @@ impl StateExt for State {
.with(body.mass())
.with(body.density())
.with(comp::Collider::Box {
radius: body.radius(),
radius: body.max_radius(),
z_min: 0.0,
z_max: body.height(),
})
@ -315,7 +315,7 @@ impl StateExt for State {
projectile_base = projectile_base.with(comp::Collider::Point)
} else {
projectile_base = projectile_base.with(comp::Collider::Box {
radius: body.radius(),
radius: body.max_radius(),
z_min: 0.0,
z_max: body.height(),
})
@ -392,7 +392,7 @@ impl StateExt for State {
.with(comp::Vel(Vec3::zero()))
.with(comp::Ori::default())
.with(comp::Collider::Box {
radius: comp::Body::Object(object).radius(),
radius: comp::Body::Object(object).max_radius(),
z_min: 0.0,
z_max: comp::Body::Object(object).height()
})
@ -510,7 +510,7 @@ impl StateExt for State {
// commands, so we can assume that all of these calls succeed,
// justifying ignoring the result of insertion.
self.write_component_ignore_entity_dead(entity, comp::Collider::Box {
radius: body.radius(),
radius: body.max_radius(),
z_min: 0.0,
z_max: body.height(),
});

View File

@ -1783,9 +1783,9 @@ impl<'a> AgentData<'a> {
// Wield the weapon as running towards the target
controller.actions.push(ControlAction::Wield);
let min_attack_dist = (self.body.map_or(0.5, |b| b.radius()) + DEFAULT_ATTACK_RANGE)
let min_attack_dist = (self.body.map_or(0.5, |b| b.max_radius()) + DEFAULT_ATTACK_RANGE)
* self.scale
+ tgt_data.body.map_or(0.5, |b| b.radius()) * tgt_data.scale.map_or(1.0, |s| s.0);
+ tgt_data.body.map_or(0.5, |b| b.max_radius()) * tgt_data.scale.map_or(1.0, |s| s.0);
let dist_sqrd = self.pos.0.distance_squared(tgt_data.pos.0);
let angle = self
.ori
@ -3676,7 +3676,7 @@ impl<'a> AgentData<'a> {
) {
const BIRD_ATTACK_RANGE: f32 = 4.0;
const BIRD_CHARGE_DISTANCE: f32 = 15.0;
let bird_attack_distance = self.body.map_or(0.0, |b| b.radius()) + BIRD_ATTACK_RANGE;
let bird_attack_distance = self.body.map_or(0.0, |b| b.max_radius()) + BIRD_ATTACK_RANGE;
// Increase action timer
agent.action_state.timer += read_data.dt.0;
// If higher than 2 blocks
@ -3748,7 +3748,7 @@ impl<'a> AgentData<'a> {
const MINOTAUR_ATTACK_RANGE: f32 = 5.0;
const MINOTAUR_CHARGE_DISTANCE: f32 = 15.0;
let minotaur_attack_distance =
self.body.map_or(0.0, |b| b.radius()) + MINOTAUR_ATTACK_RANGE;
self.body.map_or(0.0, |b| b.max_radius()) + MINOTAUR_ATTACK_RANGE;
let health_fraction = self.health.map_or(1.0, |h| h.fraction());
// Sets action counter at start of combat
if agent.action_state.counter < MINOTAUR_FRENZY_THRESHOLD
@ -3815,7 +3815,7 @@ impl<'a> AgentData<'a> {
const GOLEM_LASER_RANGE: f32 = 30.0;
const GOLEM_LONG_RANGE: f32 = 50.0;
const GOLEM_TARGET_SPEED: f32 = 8.0;
let golem_melee_range = self.body.map_or(0.0, |b| b.radius()) + GOLEM_MELEE_RANGE;
let golem_melee_range = self.body.map_or(0.0, |b| b.max_radius()) + GOLEM_MELEE_RANGE;
// Fraction of health, used for activation of shockwave
// If golem don't have health for some reason, assume it's full
let health_fraction = self.health.map_or(1.0, |h| h.fraction());

View File

@ -198,7 +198,7 @@ impl ParticleMgr {
0.0,
)
.normalized()
* (body.radius() + 4.0)
* (body.max_radius() + 4.0)
+ Vec3::unit_z() * (body.height() + 2.0) * rng.gen::<f32>();
Particle::new_directed(
@ -688,7 +688,7 @@ impl ParticleMgr {
0.0,
)
.normalized()
* (body.radius() + 2.0)
* (body.max_radius() + 2.0)
+ Vec3::unit_z() * body.height() * rng.gen::<f32>();
let (start_pos, end_pos) =
@ -720,8 +720,8 @@ impl ParticleMgr {
|| {
let start_pos = pos.0
+ Vec3::new(
body.radius(),
body.radius(),
body.max_radius(),
body.max_radius(),
body.height() / 2.0,
)
.map(|d| d * rng.gen_range(-1.0..1.0));
@ -1021,8 +1021,12 @@ impl ParticleMgr {
+ usize::from(self.scheduler.heartbeats(Duration::from_millis(15))),
|| {
let start_pos = pos.0
+ Vec3::new(body.radius(), body.radius(), body.height() / 2.0)
.map(|d| d * rng.gen_range(-1.0..1.0));
+ Vec3::new(
body.max_radius(),
body.max_radius(),
body.height() / 2.0,
)
.map(|d| d * rng.gen_range(-1.0..1.0));
let end_pos = start_pos
+ Vec3::unit_z() * body.height()
+ Vec3::<f32>::zero()

View File

@ -1612,12 +1612,13 @@ fn under_cursor(
.filter_map(|(e, p, s, b, i)| {
const RADIUS_SCALE: f32 = 3.0;
// TODO: use collider radius instead of body radius?
let radius = s.map_or(1.0, |s| s.0) * b.radius() * RADIUS_SCALE;
let radius = s.map_or(1.0, |s| s.0) * b.max_radius() * RADIUS_SCALE;
// Move position up from the feet
let pos = Vec3::new(p.0.x, p.0.y, p.0.z + radius);
// Distance squared from camera to the entity
let dist_sqr = pos.distance_squared(cam_pos);
// We only care about interacting with entities that contain items, or are not inanimate (to trade with)
// We only care about interacting with entities that contain items,
// or are not inanimate (to trade with)
if i.is_some() || !matches!(b, comp::Body::Object(_)) {
Some((e, pos, radius, dist_sqr))
} else {