Fixed projectile collisions, hitboxes, better aiming

This commit is contained in:
Joshua Barretto 2020-08-24 18:24:44 +01:00
parent 90735f1ef7
commit 6a4c5a05d0
10 changed files with 330 additions and 194 deletions

View File

@ -139,43 +139,62 @@ impl Body {
pub fn radius(&self) -> f32 {
// TODO: Improve these values (some might be reliant on more info in inner type)
match self {
Body::Humanoid(_) => 0.2,
Body::QuadrupedSmall(_) => 0.3,
Body::Humanoid(_) => 0.6,
Body::QuadrupedSmall(_) => 0.6,
Body::QuadrupedMedium(_) => 0.9,
Body::Critter(_) => 0.2,
Body::QuadrupedLow(_) => 1.0,
Body::Critter(_) => 0.5,
Body::BirdMedium(_) => 0.5,
Body::FishMedium(_) => 0.5,
Body::Dragon(_) => 2.5,
Body::BirdSmall(_) => 0.2,
Body::FishSmall(_) => 0.2,
Body::BipedLarge(_) => 3.0,
Body::Golem(_) => 2.5,
Body::QuadrupedLow(_) => 1.0,
Body::Object(_) => 0.3,
Body::BirdSmall(_) => 0.4,
Body::FishSmall(_) => 0.4,
Body::BipedLarge(_) => 2.0,
Body::Golem(_) => 1.75,
Body::Object(_) => 0.4,
}
}
pub fn height(&self) -> f32 {
match self {
Body::Humanoid(humanoid) => match humanoid.species {
humanoid::Species::Danari => 0.8,
humanoid::Species::Dwarf => 0.9,
humanoid::Species::Orc => 1.14,
humanoid::Species::Undead => 0.95,
humanoid::Species::Danari => 0.5,
humanoid::Species::Dwarf => 1.55,
humanoid::Species::Orc => 1.95,
_ => 1.8,
},
Body::QuadrupedSmall(body) => match body.species {
quadruped_small::Species::Dodarock => 1.5,
quadruped_small::Species::Holladon => 1.5,
quadruped_small::Species::Truffler => 2.0,
_ => 1.0,
},
Body::QuadrupedSmall(_) => 0.6,
Body::QuadrupedMedium(_) => 0.5,
Body::Critter(_) => 0.4,
Body::BirdMedium(_) => 1.2,
Body::FishMedium(_) => 1.0,
Body::Dragon(_) => 5.0,
Body::BirdSmall(_) => 0.4,
Body::FishSmall(_) => 0.4,
Body::BipedLarge(_) => 5.0,
Body::Golem(_) => 5.0,
Body::QuadrupedLow(_) => 0.5,
Body::Object(_) => 0.6,
Body::QuadrupedMedium(body) => match body.species {
quadruped_medium::Species::Tarasque => 2.5,
quadruped_medium::Species::Lion => 1.8,
quadruped_medium::Species::Saber => 1.8,
quadruped_medium::Species::Catoblepas => 2.8,
_ => 1.6,
},
Body::QuadrupedLow(body) => match body.species {
quadruped_low::Species::Monitor => 1.5,
quadruped_low::Species::Tortoise => 2.0,
quadruped_low::Species::Rocksnapper => 2.0,
quadruped_low::Species::Maneater => 4.0,
_ => 1.3,
},
Body::Critter(_) => 0.7,
Body::BirdMedium(body) => match body.species {
bird_medium::Species::Cockatrice => 1.8,
_ => 1.1,
},
Body::FishMedium(_) => 1.1,
Body::Dragon(_) => 20.0,
Body::BirdSmall(_) => 1.1,
Body::FishSmall(_) => 0.9,
Body::BipedLarge(_) => 4.5,
Body::Golem(_) => 5.8,
Body::Object(_) => 1.0,
}
}

View File

@ -55,6 +55,22 @@ pub enum Collider {
Point,
}
impl Collider {
pub fn get_radius(&self) -> f32 {
match self {
Collider::Box { radius, .. } => *radius,
Collider::Point => 0.0,
}
}
pub fn get_z_limits(&self) -> (f32, f32) {
match self {
Collider::Box { z_min, z_max, .. } => (*z_min, *z_max),
Collider::Point => (0.0, 0.0),
}
}
}
impl Component for Collider {
type Storage = FlaggedStorage<Self, IdvStorage<Self>>;
}
@ -74,16 +90,26 @@ impl Component for Sticky {
}
// PhysicsState
#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct PhysicsState {
pub on_ground: bool,
pub on_ceiling: bool,
pub on_wall: Option<Vec3<f32>>,
pub touch_entity: Option<Uid>,
pub touch_entities: Vec<Uid>,
pub in_fluid: Option<f32>, // Depth
}
impl PhysicsState {
pub fn clear(&mut self) {
// Avoid allocation overhead!
let mut touch_entities = std::mem::take(&mut self.touch_entities);
touch_entities.clear();
*self = Self {
touch_entities,
..Self::default()
}
}
pub fn on_surface(&self) -> Option<Vec3<f32>> {
self.on_ground
.then_some(-Vec3::unit_z())

View File

@ -33,7 +33,7 @@ impl CharacterBehavior for Data {
}
}
if self.buildup_duration != Duration::default() && data.physics.touch_entity.is_none() {
if self.buildup_duration != Duration::default() && data.physics.touch_entities.is_empty() {
// Build up (this will move you forward)
update.vel.0 = Vec3::new(0.0, 0.0, update.vel.0.z)
+ (update.vel.0 * Vec3::new(1.0, 1.0, 0.0)

View File

@ -113,7 +113,7 @@ impl CharacterBehavior for Data {
// Handling movement
if stage_time_active < Duration::from_millis(STAGE_DURATION / 3) {
let adjusted_accel = match (self.stage, data.physics.touch_entity.is_none()) {
let adjusted_accel = match (self.stage, data.physics.touch_entities.is_empty()) {
(Stage::First, true) => INITIAL_ACCEL,
(Stage::Second, true) => INITIAL_ACCEL * 0.75,
(Stage::Third, true) => INITIAL_ACCEL * 0.75,

View File

@ -11,8 +11,8 @@ use crate::{
};
use rayon::iter::ParallelIterator;
use specs::{
saveload::MarkerAllocator, Entities, Join, ParJoin, Read, ReadExpect, ReadStorage, System,
WriteStorage,
saveload::MarkerAllocator, storage, Entities, Join, ParJoin, Read, ReadExpect, ReadStorage,
System, WriteStorage,
};
use std::ops::Range;
use vek::*;
@ -95,7 +95,7 @@ impl<'a> System<'a> for Sys {
) {
let mut event_emitter = event_bus.emitter();
// Add physics state components
// Add/reset physics state components
for entity in (
&entities,
!&physics_states,
@ -108,7 +108,153 @@ impl<'a> System<'a> for Sys {
.map(|(e, _, _, _, _, _)| e)
.collect::<Vec<_>>()
{
let _ = physics_states.insert(entity, Default::default());
match physics_states.entry(entity) {
Ok(storage::StorageEntry::Occupied(mut o)) => o.get_mut().clear(),
Ok(storage::StorageEntry::Vacant(v)) => {
v.insert(Default::default());
},
Err(_) => {},
}
}
// Apply pushback
//
// Note: We now do this first because we project velocity ahead. This is slighty
// imperfect and implies that we might get edge-cases where entities
// standing right next to the edge of a wall may get hit by projectiles
// fired into the wall very close to them. However, this sort of thing is
// already possible with poorly-defined hitboxes anyway so it's not too
// much of a concern.
//
// Actually, the aforementioned case can't happen, but only because wall
// collision is checked prior to entity collision in the projectile
// code.
//
// If this situation becomes a problem, this code should be integrated with the
// terrain collision code below, although that's not trivial to do since
// it means the step needs to take into account the speeds of both
// entities.
for (entity, pos, scale, mass, collider, _, _, physics, projectile) in (
&entities,
&positions,
scales.maybe(),
masses.maybe(),
colliders.maybe(),
!&mountings,
stickies.maybe(),
&mut physics_states,
// TODO: if we need to avoid collisions for other things consider moving whether it
// should interact into the collider component or into a separate component
projectiles.maybe(),
)
.join()
.filter(|(_, _, _, _, _, _, sticky, physics, _)| {
sticky.is_none() || (physics.on_wall.is_none() && !physics.on_ground)
})
{
let scale = scale.map(|s| s.0).unwrap_or(1.0);
let radius = collider.map(|c| c.get_radius()).unwrap_or(0.5);
let z_limits = collider.map(|c| c.get_z_limits()).unwrap_or((-0.5, 0.5));
let mass = mass.map(|m| m.0).unwrap_or(scale);
// Group to ignore collisions with
let ignore_group = projectile
.and_then(|p| p.owner)
.and_then(|uid| uid_allocator.retrieve_entity_internal(uid.into()))
.and_then(|e| groups.get(e));
for (
entity_other,
other,
pos_other,
scale_other,
mass_other,
collider_other,
_,
group,
) in (
&entities,
&uids,
&positions,
scales.maybe(),
masses.maybe(),
colliders.maybe(),
!&mountings,
groups.maybe(),
)
.join()
{
if ignore_group.is_some() && ignore_group == group {
continue;
}
let scale_other = scale_other.map(|s| s.0).unwrap_or(1.0);
let radius_other = collider_other.map(|c| c.get_radius()).unwrap_or(0.5);
let z_limits_other = collider_other
.map(|c| c.get_z_limits())
.unwrap_or((-0.5, 0.5));
let mass_other = mass_other.map(|m| m.0).unwrap_or(scale_other);
if mass_other == 0.0 {
continue;
}
let collision_dist = scale * radius + scale_other * radius_other;
let vel = velocities.get(entity).copied().unwrap_or_default().0;
let vel_other = velocities.get(entity_other).copied().unwrap_or_default().0;
// Sanity check: don't try colliding entities that are too far from each other
// Note: I think this catches all cases. If you get entitiy collision problems,
// try removing this!
if (pos.0 - pos_other.0).magnitude()
> ((vel + vel_other) * dt.0).magnitude() + collision_dist
{
continue;
}
let min_collision_dist = 0.3;
// Ideally we'd deal with collision speed to minimise work here, but for not
// taking the maximum velocity of the two is fine.
let increments = (vel
.magnitude_squared()
.max(vel_other.magnitude_squared())
.sqrt()
* dt.0
/ min_collision_dist)
.ceil() as usize;
let step_delta = 1.0 / increments as f32;
let mut collided = false;
for i in 0..increments {
let factor = i as f32 * step_delta;
let pos = pos.0 + vel * dt.0 * factor;
let pos_other = pos_other.0 + vel_other * dt.0 * factor;
let diff = pos.xy() - pos_other.xy();
if diff.magnitude_squared() <= collision_dist.powf(2.0)
&& pos.z + z_limits.1 * scale
>= pos_other.z + z_limits_other.0 * scale_other
&& pos.z + z_limits.0 * scale
<= pos_other.z + z_limits_other.1 * scale_other
{
if !collided {
physics.touch_entities.push(*other);
}
if diff.magnitude_squared() > 0.0 {
let force = 40.0 * (collision_dist - diff.magnitude()) * mass_other
/ (mass + mass_other);
// Change velocity
velocities.get_mut(entity).map(|vel| {
vel.0 += Vec3::from(diff.normalized()) * force * dt.0 * step_delta
});
}
collided = true;
}
}
}
}
// Apply movement inputs
@ -116,7 +262,7 @@ impl<'a> System<'a> for Sys {
&entities,
scales.maybe(),
stickies.maybe(),
&colliders,
colliders.maybe(),
&mut positions,
&mut velocities,
&mut orientations,
@ -176,16 +322,17 @@ impl<'a> System<'a> for Sys {
Vec3::zero()
};
match collider {
match collider.copied().unwrap_or(Collider::Point) {
Collider::Box {
radius,
z_min,
z_max,
} => {
// Scale collider
let radius = *radius; // * scale;
let z_min = *z_min; // * scale;
let z_max = *z_max; // * scale;
// TODO: Use scale when pathfinding is good enough to manage irregular entity sizes
let radius = radius; // * scale;
let z_min = z_min; // * scale;
let z_max = z_max.max(1.0); // * scale;
// Probe distances
let hdist = radius.ceil() as i32;
@ -514,74 +661,5 @@ impl<'a> System<'a> for Sys {
land_on_grounds.into_iter().for_each(|(entity, vel)| {
event_emitter.emit(ServerEvent::LandOnGround { entity, vel: vel.0 });
});
// Apply pushback
for (pos, scale, mass, vel, _, _, _, physics, projectile) in (
&positions,
scales.maybe(),
masses.maybe(),
&mut velocities,
&colliders,
!&mountings,
stickies.maybe(),
&mut physics_states,
// TODO: if we need to avoid collisions for other things consider moving whether it
// should interact into the collider component or into a separate component
projectiles.maybe(),
)
.join()
.filter(|(_, _, _, _, _, _, sticky, physics, _)| {
sticky.is_none() || (physics.on_wall.is_none() && !physics.on_ground)
})
{
physics.touch_entity = None;
let scale = scale.map(|s| s.0).unwrap_or(1.0);
let mass = mass.map(|m| m.0).unwrap_or(scale);
// Group to ignore collisions with
let ignore_group = projectile
.and_then(|p| p.owner)
.and_then(|uid| uid_allocator.retrieve_entity_internal(uid.into()))
.and_then(|e| groups.get(e));
for (other, pos_other, scale_other, mass_other, _, _, group) in (
&uids,
&positions,
scales.maybe(),
masses.maybe(),
&colliders,
!&mountings,
groups.maybe(),
)
.join()
{
if ignore_group.is_some() && ignore_group == group {
continue;
}
let scale_other = scale_other.map(|s| s.0).unwrap_or(1.0);
let mass_other = mass_other.map(|m| m.0).unwrap_or(scale_other);
if mass_other == 0.0 {
continue;
}
let diff = Vec2::<f32>::from(pos.0 - pos_other.0);
let collision_dist = 0.55 * (scale + scale_other);
if diff.magnitude_squared() > 0.0
&& diff.magnitude_squared() < collision_dist.powf(2.0)
&& pos.0.z + 1.6 * scale > pos_other.0.z
&& pos.0.z < pos_other.0.z + 1.6 * scale_other
{
let force = (collision_dist - diff.magnitude()) * 2.0 * mass_other
/ (mass + mass_other);
vel.0 += Vec3::from(diff.normalized()) * force;
physics.touch_entity = Some(*other);
}
}
}
}
}

View File

@ -81,81 +81,91 @@ impl<'a> System<'a> for Sys {
_ => {},
}
}
}
// Hit entity
else if let Some(other) = physics.touch_entity {
for effect in projectile.hit_entity.drain(..) {
match effect {
projectile::Effect::Damage(healthchange) => {
let owner_uid = projectile.owner.unwrap();
let mut damage = Damage {
healthchange: healthchange as f32,
source: DamageSource::Projectile,
};
} else {
// Hit entity
for other in physics.touch_entities.iter().copied() {
if projectile.owner == Some(other) {
continue;
}
let other_entity = uid_allocator.retrieve_entity_internal(other.into());
if let Some(loadout) = other_entity.and_then(|e| loadouts.get(e)) {
damage.modify_damage(false, loadout);
}
for effect in projectile.hit_entity.iter().cloned() {
match effect {
projectile::Effect::Damage(healthchange) => {
let owner_uid = projectile.owner.unwrap();
let mut damage = Damage {
healthchange: healthchange as f32,
source: DamageSource::Projectile,
};
if other != owner_uid {
server_emitter.emit(ServerEvent::Damage {
uid: other,
change: HealthChange {
amount: damage.healthchange as i32,
cause: HealthSource::Attack { by: owner_uid },
},
});
}
},
projectile::Effect::Knockback(knockback) => {
if let Some(entity) =
uid_allocator.retrieve_entity_internal(other.into())
{
local_emitter.emit(LocalEvent::ApplyForce {
entity,
force: knockback
* *Dir::slerp(ori.0, Dir::new(Vec3::unit_z()), 0.5),
});
}
},
projectile::Effect::RewardEnergy(energy) => {
if let Some(energy_mut) = projectile
.owner
.and_then(|o| uid_allocator.retrieve_entity_internal(o.into()))
.and_then(|o| energies.get_mut(o))
{
energy_mut.change_by(energy as i32, EnergySource::HitEnemy);
}
},
projectile::Effect::Explode { power } => {
server_emitter.emit(ServerEvent::Explosion {
pos: pos.0,
power,
owner: projectile.owner,
friendly_damage: false,
reagent: None,
})
},
projectile::Effect::Vanish => server_emitter.emit(ServerEvent::Destroy {
entity,
cause: HealthSource::World,
}),
projectile::Effect::Possess => {
if other != projectile.owner.unwrap() {
if let Some(owner) = projectile.owner {
server_emitter.emit(ServerEvent::Possess(owner, other));
let other_entity =
uid_allocator.retrieve_entity_internal(other.into());
if let Some(loadout) = other_entity.and_then(|e| loadouts.get(e)) {
damage.modify_damage(false, loadout);
}
}
},
_ => {},
if other != owner_uid {
server_emitter.emit(ServerEvent::Damage {
uid: other,
change: HealthChange {
amount: damage.healthchange as i32,
cause: HealthSource::Attack { by: owner_uid },
},
});
}
},
projectile::Effect::Knockback(knockback) => {
if let Some(entity) =
uid_allocator.retrieve_entity_internal(other.into())
{
local_emitter.emit(LocalEvent::ApplyForce {
entity,
force: knockback
* *Dir::slerp(ori.0, Dir::new(Vec3::unit_z()), 0.5),
});
}
},
projectile::Effect::RewardEnergy(energy) => {
if let Some(energy_mut) = projectile
.owner
.and_then(|o| uid_allocator.retrieve_entity_internal(o.into()))
.and_then(|o| energies.get_mut(o))
{
energy_mut.change_by(energy as i32, EnergySource::HitEnemy);
}
},
projectile::Effect::Explode { power } => {
server_emitter.emit(ServerEvent::Explosion {
pos: pos.0,
power,
owner: projectile.owner,
friendly_damage: false,
reagent: None,
})
},
projectile::Effect::Vanish => {
server_emitter.emit(ServerEvent::Destroy {
entity,
cause: HealthSource::World,
})
},
projectile::Effect::Possess => {
if other != projectile.owner.unwrap() {
if let Some(owner) = projectile.owner {
server_emitter.emit(ServerEvent::Possess(owner, other));
}
}
},
_ => {},
}
}
}
} else if let Some(dir) = velocities
.get(entity)
.and_then(|vel| vel.0.try_normalized())
{
ori.0 = dir.into();
if let Some(dir) = velocities
.get(entity)
.and_then(|vel| vel.0.try_normalized())
{
ori.0 = dir.into();
}
}
if projectile.time_left == Duration::default() {

View File

@ -102,20 +102,15 @@ impl StateExt for State {
.with(comp::Vel(Vec3::zero()))
.with(comp::Ori::default())
.with(comp::Collider::Box {
radius: 0.4,
radius: body.radius(),
z_min: 0.0,
z_max: 1.75,
z_max: body.height(),
})
.with(comp::Controller::default())
.with(body)
.with(stats)
.with(comp::Alignment::Npc)
.with(comp::Energy::new(500))
.with(comp::Collider::Box {
radius: 0.4,
z_min: 0.0,
z_max: 1.75,
})
.with(comp::Gravity(1.0))
.with(comp::CharacterState::default())
.with(loadout)
@ -127,13 +122,13 @@ impl StateExt for State {
.with(pos)
.with(comp::Vel(Vec3::zero()))
.with(comp::Ori::default())
.with(comp::Body::Object(object))
.with(comp::Mass(5.0))
.with(comp::Collider::Box {
radius: 0.4,
radius: comp::Body::Object(object).radius(),
z_min: 0.0,
z_max: 0.9,
z_max: comp::Body::Object(object).height(),
})
.with(comp::Body::Object(object))
.with(comp::Gravity(1.0))
}
@ -223,6 +218,11 @@ impl StateExt for State {
}),
));
self.write_component(entity, comp::Collider::Box {
radius: body.radius(),
z_min: 0.0,
z_max: body.height(),
});
self.write_component(entity, body);
self.write_component(entity, stats);
self.write_component(entity, inventory);

View File

@ -476,7 +476,7 @@ impl Scene {
player_scale * 1.65
}
},
CameraMode::ThirdPerson if scene_data.is_aiming => player_scale * 2.1,
CameraMode::ThirdPerson if scene_data.is_aiming => player_scale * 2.2,
CameraMode::ThirdPerson => player_scale * 1.65,
CameraMode::Freefly => 0.0,
};

View File

@ -238,7 +238,7 @@ impl PlayState for SessionState {
(
is_aiming,
if is_aiming {
Vec3::unit_z() * 0.025
Vec3::unit_z() * 0.05
} else {
Vec3::zero()
},

View File

@ -361,11 +361,14 @@ impl<'a> BlockGen<'a> {
Some(Block::new(
BlockKind::Rock,
stone_col.map2(Rgb::new(
field0.get(wpos) as u8 % 16,
field1.get(wpos) as u8 % 16,
field2.get(wpos) as u8 % 16,
), |stone, x| stone.saturating_sub(x)),
stone_col.map2(
Rgb::new(
field0.get(wpos) as u8 % 16,
field1.get(wpos) as u8 % 16,
field2.get(wpos) as u8 % 16,
),
|stone, x| stone.saturating_sub(x),
),
))
} else {
None