Improved archery with feedback sfx and particles

This commit is contained in:
Joshua Barretto 2021-03-29 00:29:48 +01:00
parent 88f99986af
commit b0acbda236
16 changed files with 236 additions and 47 deletions

View File

@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Generated a net world map
- Overhauled clouds for more verticality and performance
- New tooltip for items with stats comparison
- Improved bow feedback, added arrow particles
### Removed

BIN
assets/voxygen/audio/sfx/arrow_hit.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/audio/sfx/arrow_miss.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -62,6 +62,7 @@ const int EXPLOSION = 20;
const int ICE = 21;
const int LIFESTEAL_BEAM = 22;
const int CULTIST_FLAME = 23;
const int STATIC_SMOKE = 24;
// meters per second squared (acceleration)
const float earth_gravity = 9.807;
@ -402,6 +403,13 @@ void main() {
vec4(purp_color, 0.0, purp_color, 1),
spin_in_axis(vec3(rand6, rand7, rand8), percent() * 10 + 3 * rand9)
);
} else if (inst_mode == STATIC_SMOKE) {
attr = Attr(
vec3(0),
vec3((0.5 * (1 - slow_start(0.8)))),
vec4(1.0),
spin_in_axis(vec3(rand6, rand7, rand8), rand9)
);
} else {
attr = Attr(
linear_motion(
@ -424,7 +432,7 @@ void main() {
vec4 normals[6] = vec4[](vec4(-1,0,0,0), vec4(1,0,0,0), vec4(0,-1,0,0), vec4(0,1,0,0), vec4(0,0,-1,0), vec4(0,0,1,0));
f_norm =
// inst_pos *
((normals[(v_norm_ao >> 0) & 0x7u]) * attr.rot).xyz;
normalize(((normals[(v_norm_ao >> 0) & 0x7u]) * attr.rot).xyz);
//vec3 col = vec3((uvec3(v_col) >> uvec3(0, 8, 16)) & uvec3(0xFFu)) / 255.0;
f_col = vec4(attr.col.rgb, attr.col.a);

View File

@ -1033,14 +1033,15 @@ impl Client {
pub fn loaded_distance(&self) -> f32 { self.loaded_distance }
pub fn current_chunk(&self) -> Option<Arc<TerrainChunk>> {
let chunk_pos = Vec2::from(
pub fn position(&self) -> Option<Vec3<f32>> {
self.state
.read_storage::<comp::Pos>()
.get(self.entity())
.cloned()?
.0,
)
.map(|v| v.0)
}
pub fn current_chunk(&self) -> Option<Arc<TerrainChunk>> {
let chunk_pos = Vec2::from(self.position()?)
.map2(TerrainChunkSize::RECT_SIZE, |e: f32, sz| {
(e as u32).div_euclid(sz) as i32
});

View File

@ -8,13 +8,25 @@ use specs_idvs::IdvStorage;
use tracing::warn;
use vek::ops::{Lerp, Slerp};
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct InterpBuffer<T> {
pub buf: [(f64, T); 4],
pub i: usize,
}
impl<T: Clone> InterpBuffer<T> {
pub fn new(x: T) -> Self {
Self {
buf: [
(0.0, x.clone()),
(0.0, x.clone()),
(0.0, x.clone()),
(0.0, x),
],
i: 0,
}
}
fn push(&mut self, time: f64, x: T) {
let InterpBuffer {
ref mut buf,
@ -54,6 +66,8 @@ impl InterpolatableComponent for Pos {
type InterpData = InterpBuffer<Pos>;
type ReadData = InterpBuffer<Vel>;
fn new_data(x: Self) -> Self::InterpData { InterpBuffer::new(x) }
fn update_component(&self, interp_data: &mut Self::InterpData, time: f64, force_update: bool) {
interp_data.update(time, *self, force_update);
}
@ -108,6 +122,8 @@ impl InterpolatableComponent for Vel {
type InterpData = InterpBuffer<Vel>;
type ReadData = ();
fn new_data(x: Self) -> Self::InterpData { InterpBuffer::new(x) }
fn update_component(&self, interp_data: &mut Self::InterpData, time: f64, force_update: bool) {
interp_data.update(time, *self, force_update);
}
@ -143,6 +159,8 @@ impl InterpolatableComponent for Ori {
type InterpData = InterpBuffer<Ori>;
type ReadData = ();
fn new_data(x: Self) -> Self::InterpData { InterpBuffer::new(x) }
fn update_component(&self, interp_data: &mut Self::InterpData, time: f64, force_update: bool) {
interp_data.update(time, *self, force_update);
}

View File

@ -47,20 +47,21 @@ pub fn handle_remove<C: Component>(entity: Entity, world: &World) {
}
pub trait InterpolatableComponent: Component {
type InterpData: Component + Default;
type InterpData: Component;
type ReadData;
fn new_data(x: Self) -> Self::InterpData;
fn update_component(&self, data: &mut Self::InterpData, time: f64, force_update: bool);
fn interpolate(self, data: &Self::InterpData, time: f64, read_data: &Self::ReadData) -> Self;
}
pub fn handle_interp_insert<C: InterpolatableComponent>(
pub fn handle_interp_insert<C: InterpolatableComponent + Clone>(
comp: C,
entity: Entity,
world: &World,
force_update: bool,
) {
let mut interp_data = C::InterpData::default();
let mut interp_data = C::new_data(comp.clone());
let time = world.read_resource::<Time>().0;
comp.update_component(&mut interp_data, time, force_update);
handle_insert(comp, entity, world);

View File

@ -22,6 +22,13 @@ pub enum Outcome {
body: comp::Body,
vel: Vec3<f32>,
},
ProjectileHit {
pos: Vec3<f32>,
body: comp::Body,
vel: Vec3<f32>,
source: Option<Uid>,
target: Option<Uid>,
},
Beam {
pos: Vec3<f32>,
specifier: beam::FrontendSpecifier,
@ -56,6 +63,7 @@ impl Outcome {
match self {
Outcome::Explosion { pos, .. }
| Outcome::ProjectileShot { pos, .. }
| Outcome::ProjectileHit { pos, .. }
| Outcome::Beam { pos, .. }
| Outcome::SkillPointGain { pos, .. }
| Outcome::SummonedCreature { pos, .. } => Some(*pos),

View File

@ -10,6 +10,7 @@ use common::{
},
consts::{FRIC_GROUND, GRAVITY},
event::{EventBus, ServerEvent},
outcome::Outcome,
resources::DeltaTime,
terrain::{Block, TerrainGrid},
uid::Uid,
@ -20,7 +21,7 @@ use common_ecs::{Job, Origin, ParMode, Phase, PhysicsMetrics, System};
use rayon::iter::ParallelIterator;
use specs::{
shred::{ResourceId, World},
Entities, Entity, Join, ParJoin, Read, ReadExpect, ReadStorage, SystemData, WriteExpect,
Entities, Entity, Join, ParJoin, Read, ReadExpect, ReadStorage, SystemData, Write, WriteExpect,
WriteStorage,
};
use std::ops::Range;
@ -106,6 +107,7 @@ pub struct PhysicsWrite<'a> {
pos_vel_defers: WriteStorage<'a, PosVelDefer>,
orientations: WriteStorage<'a, Ori>,
previous_phys_cache: WriteStorage<'a, PreviousPhysCache>,
outcomes: Write<'a, Vec<Outcome>>,
}
#[derive(SystemData)]
@ -590,7 +592,7 @@ impl<'a> PhysicsData<'a> {
let velocities = &write.velocities;
// Second pass: resolve collisions
let land_on_grounds = (
let (land_on_grounds, mut outcomes) = (
&read.entities,
read.scales.maybe(),
read.stickies.maybe(),
@ -628,16 +630,12 @@ impl<'a> PhysicsData<'a> {
_,
)| {
let mut land_on_ground = None;
let mut outcomes = Vec::new();
// Defer the writes of positions and velocities to allow an inner loop over
// terrain-like entities
let old_vel = *vel;
let mut vel = *vel;
if sticky.is_some() && physics_state.on_surface().is_some() {
vel.0 = physics_state.ground_vel;
return land_on_ground;
}
let scale = if let Collider::Voxel { .. } = collider {
scale.map(|s| s.0).unwrap_or(1.0)
} else {
@ -738,17 +736,45 @@ impl<'a> PhysicsData<'a> {
Collider::Point => {
let mut pos = *pos;
let (dist, block) = if let Some(block) = read
.terrain
.get(pos.0.map(|e| e.floor() as i32))
.ok()
.filter(|b| b.is_filled())
// TODO: `is_solid`, when arrows are special-cased
{
(0.0, Some(block))
} else {
let (dist, block) = read
.terrain
.ray(pos.0, pos.0 + pos_delta)
.until(|block: &Block| block.is_filled())
.ignore_error()
.cast();
(dist, block.unwrap()) // Can't fail since we do ignore_error above
};
pos.0 += pos_delta.try_normalized().unwrap_or_else(Vec3::zero) * dist;
// Can't fail since we do ignore_error above
if block.unwrap().is_some() {
// TODO: Not all projectiles should count as sticky!
if sticky.is_some() {
if let Some((projectile, body)) = read
.projectiles
.get(entity)
.filter(|_| vel.0.magnitude_squared() > 1.0 && block.is_some())
.zip(read.bodies.get(entity).copied())
{
outcomes.push(Outcome::ProjectileHit {
pos: pos.0,
body,
vel: vel.0,
source: projectile.owner,
target: None,
});
}
}
if block.is_some() {
let block_center = pos.0.map(|e| e.floor()) + 0.5;
let block_rpos = (pos.0 - block_center)
.try_normalized()
@ -774,6 +800,11 @@ impl<'a> PhysicsData<'a> {
Vec3::unit_y() * -block_rpos.y.signum()
});
}
// Sticky things shouldn't move
if sticky.is_some() {
vel.0 = Vec3::zero();
}
}
physics_state.in_liquid = read
@ -978,20 +1009,31 @@ impl<'a> PhysicsData<'a> {
pos_vel_defer.vel = None;
}
land_on_ground
(land_on_ground, outcomes)
},
)
.fold(Vec::new, |mut land_on_grounds, land_on_ground| {
.fold(
|| (Vec::new(), Vec::new()),
|(mut land_on_grounds, mut all_outcomes), (land_on_ground, mut outcomes)| {
land_on_ground.map(|log| land_on_grounds.push(log));
land_on_grounds
})
.reduce(Vec::new, |mut land_on_grounds_a, mut land_on_grounds_b| {
all_outcomes.append(&mut outcomes);
(land_on_grounds, all_outcomes)
},
)
.reduce(
|| (Vec::new(), Vec::new()),
|(mut land_on_grounds_a, mut outcomes_a),
(mut land_on_grounds_b, mut outcomes_b)| {
land_on_grounds_a.append(&mut land_on_grounds_b);
land_on_grounds_a
});
outcomes_a.append(&mut outcomes_b);
(land_on_grounds_a, outcomes_a)
},
);
drop(guard);
job.cpu_stats.measure(ParMode::Single);
write.outcomes.append(&mut outcomes);
prof_span!(guard, "write deferred pos and vel");
for (_, pos, vel, pos_vel_defer) in (
&read.entities,
@ -1279,6 +1321,7 @@ fn box_voxel_collision<'a, T: BaseVol<Vox = Block> + ReadVol>(
if d * e.signum() < 0.0 { 0.0 } else { e }
},
);
pos_delta *= resolve_dir.map(|e| if e != 0.0 { 0.0 } else { 1.0 });
}

View File

@ -1,21 +1,23 @@
use common::{
combat::{AttackerInfo, TargetInfo},
comp::{
projectile, Combo, Energy, Group, Health, HealthSource, Inventory, Ori, PhysicsState, Pos,
Projectile, Stats, Vel,
projectile, Body, Combo, Energy, Group, Health, HealthSource, Inventory, Ori, PhysicsState,
Pos, Projectile, Stats, Vel,
},
event::{EventBus, ServerEvent},
outcome::Outcome,
resources::DeltaTime,
uid::UidAllocator,
uid::{Uid, UidAllocator},
util::Dir,
GroupTarget,
};
use common_ecs::{Job, Origin, Phase, System};
use specs::{
saveload::MarkerAllocator, shred::ResourceId, Entities, Join, Read, ReadStorage, SystemData,
World, WriteStorage,
World, Write, WriteStorage,
};
use std::time::Duration;
use vek::*;
#[derive(SystemData)]
pub struct ReadData<'a> {
@ -23,6 +25,7 @@ pub struct ReadData<'a> {
dt: Read<'a, DeltaTime>,
uid_allocator: Read<'a, UidAllocator>,
server_bus: Read<'a, EventBus<ServerEvent>>,
uids: ReadStorage<'a, Uid>,
positions: ReadStorage<'a, Pos>,
physics_states: ReadStorage<'a, PhysicsState>,
velocities: ReadStorage<'a, Vel>,
@ -32,6 +35,7 @@ pub struct ReadData<'a> {
stats: ReadStorage<'a, Stats>,
combos: ReadStorage<'a, Combo>,
healths: ReadStorage<'a, Health>,
bodies: ReadStorage<'a, Body>,
}
/// This system is responsible for handling projectile effect triggers
@ -42,13 +46,17 @@ impl<'a> System<'a> for Sys {
ReadData<'a>,
WriteStorage<'a, Ori>,
WriteStorage<'a, Projectile>,
Write<'a, Vec<Outcome>>,
);
const NAME: &'static str = "projectile";
const ORIGIN: Origin = Origin::Common;
const PHASE: Phase = Phase::Create;
fn run(_job: &mut Job<Self>, (read_data, mut orientations, mut projectiles): Self::SystemData) {
fn run(
_job: &mut Job<Self>,
(read_data, mut orientations, mut projectiles, mut outcomes): Self::SystemData,
) {
let mut server_emitter = read_data.server_bus.emitter();
// Attacks
@ -123,6 +131,19 @@ impl<'a> System<'a> for Sys {
health: read_data.healths.get(target),
};
if let Some(&body) = read_data.bodies.get(entity) {
outcomes.push(Outcome::ProjectileHit {
pos: pos.0,
body,
vel: read_data
.velocities
.get(entity)
.map_or(Vec3::zero(), |v| v.0),
source: projectile.owner,
target: read_data.uids.get(target).copied(),
});
}
attack.apply_attack(
target_group,
attacker_info,

View File

@ -289,7 +289,12 @@ impl SfxMgr {
);
}
pub fn handle_outcome(&mut self, outcome: &Outcome, audio: &mut AudioFrontend) {
pub fn handle_outcome(
&mut self,
outcome: &Outcome,
audio: &mut AudioFrontend,
client: &Client,
) {
if !audio.sfx_enabled() {
return;
}
@ -350,6 +355,33 @@ impl SfxMgr {
},
}
},
Outcome::ProjectileHit {
pos,
body,
source,
target,
..
} => match body {
Body::Object(
object::Body::Arrow
| object::Body::MultiArrow
| object::Body::ArrowSnake
| object::Body::ArrowTurret,
) => {
if target.is_none() {
audio.play_sfx("voxygen.audio.sfx.arrow_miss", *pos, Some(2.0));
} else if *source == client.uid() {
audio.play_sfx(
"voxygen.audio.sfx.arrow_hit",
client.position().unwrap_or(*pos),
Some(2.0),
);
} else {
audio.play_sfx("voxygen.audio.sfx.arrow_hit", *pos, Some(2.0));
}
},
_ => {},
},
Outcome::SkillPointGain { pos, .. } => {
let file_ref = "voxygen.audio.sfx.character.level_up_sound_-_shorter_wind_up";
audio.play_sfx(file_ref, *pos, None);

View File

@ -41,8 +41,8 @@ impl<'a> System<'a> for Sys {
)
.join()
{
// Update interpolation values
if i.pos.distance_squared(pos.0) < 64.0 * 64.0 {
// Update interpolation values, but don't interpolate far things or objects
if i.pos.distance_squared(pos.0) < 64.0 * 64.0 && !matches!(body, Body::Object(_)) {
i.pos = Lerp::lerp(i.pos, pos.0 + vel.0 * 0.03, 10.0 * dt.0);
i.ori = Ori::slerp(i.ori, *ori, base_ori_interp(body) * dt.0);
} else {

View File

@ -120,6 +120,7 @@ pub enum ParticleMode {
Ice = 21,
LifestealBeam = 22,
CultistFlame = 23,
StaticSmoke = 24,
}
impl ParticleMode {

View File

@ -108,6 +108,7 @@ pub struct Scene {
}
pub struct SceneData<'a> {
pub client: &'a Client,
pub state: &'a State,
pub player_entity: specs::Entity,
pub target_entity: Option<specs::Entity>,
@ -398,7 +399,8 @@ impl Scene {
) {
span!(_guard, "handle_outcome", "Scene::handle_outcome");
self.particle_mgr.handle_outcome(&outcome, &scene_data);
self.sfx_mgr.handle_outcome(&outcome, audio);
self.sfx_mgr
.handle_outcome(&outcome, audio, scene_data.client);
match outcome {
Outcome::Explosion {

View File

@ -196,6 +196,18 @@ impl ParticleMgr {
},
_ => {},
},
Outcome::ProjectileHit { pos, target, .. } => {
if target.is_some() {
self.particles.resize_with(self.particles.len() + 30, || {
Particle::new(
Duration::from_millis(100),
time,
ParticleMode::Shrapnel,
*pos,
)
});
}
},
Outcome::ProjectileShot { .. }
| Outcome::Beam { .. }
| Outcome::ExpChange { .. }
@ -257,6 +269,12 @@ impl ParticleMgr {
Body::Object(object::Body::CampfireLit) => {
self.maintain_campfirelit_particles(scene_data, pos, vel)
},
Body::Object(
object::Body::Arrow
| object::Body::MultiArrow
| object::Body::ArrowSnake
| object::Body::ArrowTurret,
) => self.maintain_arrow_particles(scene_data, pos, vel),
Body::Object(object::Body::BoltFire) => {
self.maintain_boltfire_particles(scene_data, pos, vel)
},
@ -313,6 +331,33 @@ impl ParticleMgr {
}
}
fn maintain_arrow_particles(&mut self, scene_data: &SceneData, pos: &Pos, vel: Option<&Vel>) {
const MIN_SPEED: f32 = 15.0;
// Don't emit particles for immobile arrows
if vel.map_or(true, |v| v.0.magnitude_squared() < MIN_SPEED.powi(2)) {
return;
}
span!(
_guard,
"arrow_particles",
"ParticleMgr::maintain_arrow_particles"
);
let time = scene_data.state.get_time();
let dt = scene_data.state.get_delta_time();
let count = self.scheduler.heartbeats(Duration::from_millis(2));
for i in 0..count {
let proportion = i as f32 / count as f32;
self.particles.push(Particle::new(
Duration::from_millis(200),
time,
ParticleMode::StaticSmoke,
pos.0 + vel.map_or(Vec3::zero(), |v| -v.0 * dt * proportion),
));
}
}
fn maintain_boltfire_particles(
&mut self,
scene_data: &SceneData,

View File

@ -1510,6 +1510,7 @@ impl PlayState for SessionState {
{
let client = self.client.borrow();
let scene_data = SceneData {
client: &client,
state: client.state(),
player_entity: client.entity(),
// Only highlight if interactable
@ -1577,6 +1578,7 @@ impl PlayState for SessionState {
let client = self.client.borrow();
let scene_data = SceneData {
client: &client,
state: client.state(),
player_entity: client.entity(),
// Only highlight if interactable