From ff3f3d12713fae9fe5047fa980a8123ac668d482 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Thu, 11 Mar 2021 11:48:59 -0500 Subject: [PATCH 01/41] Draft of airships (spawn command, visuals, some physics refactoring, no collision yet). --- assets/common/npc_names.ron | 9 + assets/server/manifests/ship_manifest.ron | 16 + assets/server/voxel/Human_Airship.vox | Bin 0 -> 88100 bytes assets/server/voxel/airship.vox | Bin 0 -> 70176 bytes assets/server/voxel/propeller-l.vox | Bin 0 -> 1584 bytes assets/server/voxel/propeller-r.vox | Bin 0 -> 1584 bytes assets/voxygen/voxel/object/Human_Airship.vox | Bin 0 -> 78024 bytes assets/voxygen/voxel/object/airship.vox | 1 + assets/voxygen/voxel/object/propeller-l.vox | 1 + assets/voxygen/voxel/object/propeller-r.vox | 1 + common/src/cmd.rs | 4 + common/src/comp/agent.rs | 1 + common/src/comp/body.rs | 12 +- common/src/comp/body/ship.rs | 69 + common/src/comp/mod.rs | 2 +- common/src/comp/phys.rs | 9 +- common/src/states/utils.rs | 6 +- common/src/util/find_dist.rs | 2 +- common/sys/src/phys.rs | 1230 ++++++++++------- server/src/cmd.rs | 34 + server/src/events/inventory_manip.rs | 2 +- server/src/state_ext.rs | 13 + server/src/sys/sentinel.rs | 2 +- voxygen/anim/src/lib.rs | 1 + voxygen/anim/src/ship/idle.rs | 34 + voxygen/anim/src/ship/mod.rs | 71 + voxygen/src/render/renderer.rs | 2 +- voxygen/src/scene/figure/load.rs | 110 +- voxygen/src/scene/figure/mod.rs | 108 +- voxygen/src/session.rs | 8 +- 30 files changed, 1196 insertions(+), 552 deletions(-) create mode 100644 assets/server/manifests/ship_manifest.ron create mode 100644 assets/server/voxel/Human_Airship.vox create mode 100644 assets/server/voxel/airship.vox create mode 100644 assets/server/voxel/propeller-l.vox create mode 100644 assets/server/voxel/propeller-r.vox create mode 100644 assets/voxygen/voxel/object/Human_Airship.vox create mode 120000 assets/voxygen/voxel/object/airship.vox create mode 120000 assets/voxygen/voxel/object/propeller-l.vox create mode 120000 assets/voxygen/voxel/object/propeller-r.vox create mode 100644 common/src/comp/body/ship.rs create mode 100644 voxygen/anim/src/ship/idle.rs create mode 100644 voxygen/anim/src/ship/mod.rs diff --git a/assets/common/npc_names.ron b/assets/common/npc_names.ron index 49ceb5c023..aeefb77fee 100644 --- a/assets/common/npc_names.ron +++ b/assets/common/npc_names.ron @@ -969,6 +969,15 @@ ), species: () ), + ship: ( + body: ( + keyword: "ship", + names_0: [ + "Boaty McBoatface", + ], + ), + species: (), + ), biped_small: ( body: ( keyword: "biped_small", diff --git a/assets/server/manifests/ship_manifest.ron b/assets/server/manifests/ship_manifest.ron new file mode 100644 index 0000000000..42555fc051 --- /dev/null +++ b/assets/server/manifests/ship_manifest.ron @@ -0,0 +1,16 @@ +({ + DefaultAirship: ( + bone0: ( + offset: (-20.0, -35.0, 1.0), + central: ("object.Human_Airship"), + ), + bone1: ( + offset: (0.0, 0.0, 0.0), + central: ("propeller-l"), + ), + bone2: ( + offset: (0.0, 0.0, 0.0), + central: ("propeller-r"), + ), + ), +}) diff --git a/assets/server/voxel/Human_Airship.vox b/assets/server/voxel/Human_Airship.vox new file mode 100644 index 0000000000000000000000000000000000000000..687a8f45b3a6d2c4c65dabcfd9f2d4bb11449d16 GIT binary patch literal 88100 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 literal 0 HcmV?d00001 diff --git a/assets/server/voxel/airship.vox b/assets/server/voxel/airship.vox new file mode 100644 index 0000000000000000000000000000000000000000..10c79d32e7a205c4b2043529e3e8b40a0e8e9d9c GIT binary patch literal 70176 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 QkH&xW*b;qI_aDdZ-@RAK#sB~S literal 0 HcmV?d00001 diff --git a/assets/server/voxel/propeller-l.vox b/assets/server/voxel/propeller-l.vox new file mode 100644 index 0000000000000000000000000000000000000000..2dc7e554e4cc9bf5fb94d4ac40330400197039a4 GIT binary patch literal 1584 zcmc(eT})eL9EbmG$wNQ|f=QB5SJh$*TP zO-w1PWJMLC5>2w`X1bYfrkm*&x`l3`Tj*B0m2Rb5Q#Ot|F^HWwh?7`FVW+TD*eUE3 zb_zR%ox)CGr$k-EDk@u*t;$wqtFl$us%%xZDqEGU8g&!Rq>hMaDHF*eW?~^$q7ap6 zqK()%>ck*+;vi1qB5tCZv=NQih=?s}B3Z;tEW}C_q7qGPye=EB%f{=Xn`|Q{(b?#<8yQxoFxI37$1_Fi5`nddM3;qhQ?tp}Ok)!;IE zyLaKpp%%o)y0JVS#PZY;EYC#IxO@WcKpwn@eej*GhJUsh(rJZ6bjaKjko7#cdj$E% z1x1sBXRZmfh`^B$Mr-~D?o9$6y@F z6p|uXAwQ5`Nb;m$@ngZ-g5cJ5dO$GODfoK7VDl|Oh<#M}{v0B7mYsB<$JJ#NR9ly?pQC*gY*4h$uzfg%|1C5Au2@;0{vu_Jl zKNK_+7a&+wj=_c<80~1rKvf>X^@W(&Q-So*PIR``VBbs42))vV$l+eZPJ}S|UIgid zbJ)0X8R<)dSo>lGhGAe^PBwf#A8P!iXxQ;Io~!j^^4%gVepZSfZ&hLQPBkv^oauSN z?QaD?e=mrID{(PVhxM!N`2DATjGlL6;!6*%-}Pb>CI9>XudA!WM~$=CSu=;0@w4zI zyttVX+s4KZYEnwhP- zhzk>uz)Us~3_-IDH&Tek3nU9!mMqJHi!m`|S&Z?<3onfE?(e_ME0fJjoBZ;=|M&ep z-_!Ry^z1v}n-YGl?cPW*f7O*~V;VwlmwA?J=u3Vm4wY4x$j1XyTyfpy#0Hh*?CTtI$>G zDs&aP3SEV+LRX=y&{bk0Dt(o{N?)a~(pTxL^i}#QeU-jSpV^`g@-%Z6VkI_WCk~KqMX5+`vHH_0F#qFS^;jp)Q7I{WGDr?a2VemeW6&y+dQN&ydQN&yp2@}R zVs{4G}k3=GY2NQz5m)B9^{R@A1 zy;xi8K>BwzvW(95T{v>65s8s@ERRO8JaGie(=pU7pMWQngS^8&_)b?KIMV_ zWcCTjdJa4tg52YR!g0Yf*96*-z?BqaG3Q(n)O46=JMKaBOcp*^c@nJ&LD!_<%|*ef zyR7ZXN6~;ErHOLXra~wf_QF4wkMj9qRNmZ<++IQH8D4uz(6lO$SJ^+VK_=ahB`-#| zXJTPjHts!N0JJ@YNfE4&AIL9c@}yw#W5L>j;MR5KkYF|}_TK53SjD36%w!2VXD6cYwy2~U*`urTfxuY3*rL-TufGD{b~z-|EU|p=RFwv(u?bN^RS8H|NZ>c)z$c@ zZU#FmXVEx%7J11$+?*5KdqZ#~5I`^(M1RQ$Uf#O|Nyd>qoQ;*~htKeIKHr_9BD_;J ziQSD8IC}mZw)N%W&R1nviRNK~_v+vGKkcQjb+G$3hWmk2(+fy`p2CHt8~EhA4Llng z#qP5+X#aE--Pbk{3We}M0MQ8kgl$7`|C@*Z?>^tJ|JvtQxb>YqYD{lFYA?P1xV^0l IKfYf627r)Zg#Z8m literal 0 HcmV?d00001 diff --git a/assets/voxygen/voxel/object/Human_Airship.vox b/assets/voxygen/voxel/object/Human_Airship.vox new file mode 100644 index 0000000000000000000000000000000000000000..88e79f59a7a92962a6b023d576accc3a96ed2bcf GIT binary patch literal 78024 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 Y&;F$Em%~44c6t0yn|*KbpWZ+GA6s@|!vFvP literal 0 HcmV?d00001 diff --git a/assets/voxygen/voxel/object/airship.vox b/assets/voxygen/voxel/object/airship.vox new file mode 120000 index 0000000000..3479493953 --- /dev/null +++ b/assets/voxygen/voxel/object/airship.vox @@ -0,0 +1 @@ +../../../server/voxel/airship.vox \ No newline at end of file diff --git a/assets/voxygen/voxel/object/propeller-l.vox b/assets/voxygen/voxel/object/propeller-l.vox new file mode 120000 index 0000000000..a8105d8b1b --- /dev/null +++ b/assets/voxygen/voxel/object/propeller-l.vox @@ -0,0 +1 @@ +../../../server/voxel/propeller-l.vox \ No newline at end of file diff --git a/assets/voxygen/voxel/object/propeller-r.vox b/assets/voxygen/voxel/object/propeller-r.vox new file mode 120000 index 0000000000..647f3f66d0 --- /dev/null +++ b/assets/voxygen/voxel/object/propeller-r.vox @@ -0,0 +1 @@ +../../../server/voxel/propeller-r.vox \ No newline at end of file diff --git a/common/src/cmd.rs b/common/src/cmd.rs index f6771bdcd3..a74f0cab09 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -36,6 +36,7 @@ impl ChatCommandData { #[derive(Copy, Clone)] pub enum ChatCommand { Adminify, + Airship, Alias, Ban, Build, @@ -89,6 +90,7 @@ pub enum ChatCommand { // Thank you for keeping this sorted alphabetically :-) pub static CHAT_COMMANDS: &[ChatCommand] = &[ ChatCommand::Adminify, + ChatCommand::Airship, ChatCommand::Alias, ChatCommand::Ban, ChatCommand::Build, @@ -222,6 +224,7 @@ impl ChatCommand { "Temporarily gives a player admin permissions or removes them", Admin, ), + ChatCommand::Airship => cmd(vec![], "Spawns an airship", Admin), ChatCommand::Alias => cmd(vec![Any("name", Required)], "Change your alias", NoAdmin), ChatCommand::Ban => cmd( vec![Any("username", Required), Message(Optional)], @@ -449,6 +452,7 @@ impl ChatCommand { pub fn keyword(&self) -> &'static str { match self { ChatCommand::Adminify => "adminify", + ChatCommand::Airship => "airship", ChatCommand::Alias => "alias", ChatCommand::Ban => "ban", ChatCommand::Build => "build", diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 7d6c5a57df..9134e40091 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -168,6 +168,7 @@ impl<'a> From<&'a Body> for Psyche { Body::Golem(_) => 1.0, Body::Theropod(_) => 1.0, Body::Dragon(_) => 1.0, + Body::Ship(_) => 1.0, }, } } diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index bb331f3a54..1733eec1e1 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -11,6 +11,7 @@ pub mod object; pub mod quadruped_low; pub mod quadruped_medium; pub mod quadruped_small; +pub mod ship; pub mod theropod; use crate::{ @@ -44,6 +45,7 @@ make_case_elim!( Golem(body: golem::Body) = 11, Theropod(body: theropod::Body) = 12, QuadrupedLow(body: quadruped_low::Body) = 13, + Ship(body: ship::Body) = 14, } ); @@ -78,6 +80,7 @@ pub struct AllBodies { pub golem: BodyData>, pub theropod: BodyData>, pub quadruped_low: BodyData>, + pub ship: BodyData, } /// Can only retrieve body metadata by direct index. @@ -124,6 +127,7 @@ impl<'a, BodyMeta, SpeciesMeta> core::ops::Index<&'a Body> for AllBodies &self.golem.body, Body::Theropod(_) => &self.theropod.body, Body::QuadrupedLow(_) => &self.quadruped_low.body, + Body::Ship(_) => &self.ship.body, } } } @@ -218,6 +222,7 @@ impl Body { Body::Golem(_) => 2.5, Body::BipedSmall(_) => 0.75, Body::Object(_) => 0.4, + Body::Ship(_) => 1.0, } } @@ -294,6 +299,7 @@ impl Body { object::Body::Crossbow => 1.7, _ => 1.0, }, + Body::Ship(_) => 1.0, } } @@ -416,6 +422,7 @@ impl Body { quadruped_low::Species::Deadwood => 600, _ => 200, }, + Body::Ship(_) => 10000, } } @@ -508,12 +515,13 @@ impl Body { quadruped_low::Species::Deadwood => 30, _ => 20, }, + Body::Ship(_) => 500, } } pub fn immune_to(&self, buff: BuffKind) -> bool { match buff { - BuffKind::Bleeding => matches!(self, Body::Object(_) | Body::Golem(_)), + BuffKind::Bleeding => matches!(self, Body::Object(_) | Body::Golem(_) | Body::Ship(_)), _ => false, } } @@ -521,7 +529,7 @@ impl Body { /// Returns a multiplier representing increased difficulty not accounted for /// due to AI or not using an actual weapon // TODO: Match on species - pub fn combat_multiplier(&self) -> f32 { if let Body::Object(_) = self { 0.0 } else { 1.0 } } + pub fn combat_multiplier(&self) -> f32 { if let Body::Object(_) | Body::Ship(_) = self { 0.0 } else { 1.0 } } pub fn base_poise(&self) -> u32 { match self { diff --git a/common/src/comp/body/ship.rs b/common/src/comp/body/ship.rs new file mode 100644 index 0000000000..ef2e93e113 --- /dev/null +++ b/common/src/comp/body/ship.rs @@ -0,0 +1,69 @@ +use crate::{ + make_case_elim +}; +use serde::{Deserialize, Serialize}; + +make_case_elim!( + body, + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] + #[repr(u32)] + pub enum Body { + DefaultAirship = 0, + } +); + +impl From for super::Body { + fn from(body: Body) -> Self { super::Body::Ship(body) } +} + +impl Body { + pub fn manifest_id(&self) -> &'static str { + match self { + Body::DefaultAirship => "server.manifests.ship_manifest", + } + } +} + +/// Duplicate of some of the things defined in `voxygen::scene::figure::load` to avoid having to +/// refactor all of that to `common` for using voxels as collider geometry +pub mod figuredata { + use crate::{ + assets::{self, AssetExt, AssetHandle, DotVoxAsset, Ron}, + volumes::dyna::Dyna, + }; + use serde::Deserialize; + use hashbrown::HashMap; + + #[derive(Deserialize)] + pub struct VoxSimple(pub String); + + #[derive(Deserialize)] + pub struct ShipCentralSpec(pub HashMap); + + #[derive(Deserialize)] + pub struct SidedShipCentralVoxSpec { + pub bone0: ShipCentralSubSpec, + pub bone1: ShipCentralSubSpec, + pub bone2: ShipCentralSubSpec, + } + + #[derive(Deserialize)] + pub struct ShipCentralSubSpec { + pub offset: [f32; 3], + pub central: VoxSimple, + } + + /// manual instead of through `make_vox_spec!` so that it can be in `common` + #[derive(Clone)] + pub struct ShipSpec { + pub central: AssetHandle>, + } + + impl assets::Compound for ShipSpec { + fn load(_: &assets::AssetCache, _: &str) -> Result { + Ok(ShipSpec { + central: AssetExt::load("server.manifests.ship_manifest")? + }) + } + } +} diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index e493a527c9..d97e265919 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -48,7 +48,7 @@ pub use self::{ beam::{Beam, BeamSegment}, body::{ biped_large, biped_small, bird_medium, bird_small, dragon, fish_medium, fish_small, golem, - humanoid, object, quadruped_low, quadruped_medium, quadruped_small, theropod, AllBodies, + humanoid, object, quadruped_low, quadruped_medium, quadruped_small, theropod, ship, AllBodies, Body, BodyData, }, buff::{ diff --git a/common/src/comp/phys.rs b/common/src/comp/phys.rs index fdb8d10a05..3e06418792 100644 --- a/common/src/comp/phys.rs +++ b/common/src/comp/phys.rs @@ -55,9 +55,12 @@ impl Component for Mass { type Storage = DerefFlaggedStorage>; } -// Mass -#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +// Collider +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum Collider { + // TODO: pass the map from ids -> voxel data to get_radius and get_z_limits to compute a + // bounding cylinder + Voxel { id: String }, Box { radius: f32, z_min: f32, z_max: f32 }, Point, } @@ -65,6 +68,7 @@ pub enum Collider { impl Collider { pub fn get_radius(&self) -> f32 { match self { + Collider::Voxel { .. } => 0.0, Collider::Box { radius, .. } => *radius, Collider::Point => 0.0, } @@ -72,6 +76,7 @@ impl Collider { pub fn get_z_limits(&self, modifier: f32) -> (f32, f32) { match self { + Collider::Voxel { .. } => (0.0, 0.0), Collider::Box { z_min, z_max, .. } => (*z_min * modifier, *z_max * modifier), Collider::Point => (0.0, 0.0), } diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index bc43bb7643..ea780a1a38 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -5,7 +5,7 @@ use crate::{ item::{Hands, ItemKind, Tool, ToolKind}, quadruped_low, quadruped_medium, quadruped_small, skills::Skill, - theropod, Body, CharacterAbility, CharacterState, InputKind, InventoryAction, StateUpdate, + theropod, ship, Body, CharacterAbility, CharacterState, InputKind, InventoryAction, StateUpdate, }, consts::{FRIC_GROUND, GRAVITY}, event::{LocalEvent, ServerEvent}, @@ -117,6 +117,7 @@ impl Body { quadruped_low::Species::Basilisk => 120.0, quadruped_low::Species::Deadwood => 140.0, }, + Body::Ship(_) => 30.0, } } @@ -168,13 +169,14 @@ impl Body { quadruped_low::Species::Lavadrake => 4.0, _ => 6.0, }, + Body::Ship(_) => 10.0, } } pub fn can_fly(&self) -> bool { matches!( self, - Body::BirdMedium(_) | Body::Dragon(_) | Body::BirdSmall(_) + Body::BirdMedium(_) | Body::Dragon(_) | Body::BirdSmall(_) | Body::Ship(ship::Body::DefaultAirship) ) } diff --git a/common/src/util/find_dist.rs b/common/src/util/find_dist.rs index cb02c728fe..fc921bc758 100644 --- a/common/src/util/find_dist.rs +++ b/common/src/util/find_dist.rs @@ -39,7 +39,7 @@ impl Cylinder { char_state: Option<&crate::comp::CharacterState>, ) -> Self { let scale = scale.map_or(1.0, |s| s.0); - let radius = collider.map_or(0.5, |c| c.get_radius()) * scale; + let radius = collider.as_ref().map_or(0.5, |c| c.get_radius()) * scale; let z_limit_modifier = char_state .filter(|char_state| char_state.is_dodge()) .map_or(1.0, |_| 0.5) diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index cb0ee3cd51..8545888c7e 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -8,12 +8,16 @@ use common::{ resources::DeltaTime, terrain::{Block, TerrainGrid}, uid::Uid, - vol::ReadVol, + vol::{BaseVol, ReadVol}, }; use common_base::{prof_span, span}; use common_ecs::{Job, Origin, ParMode, Phase, PhysicsMetrics, System}; +use hashbrown::HashMap; use rayon::iter::ParallelIterator; -use specs::{Entities, Join, ParJoin, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; +use specs::{ + shred::{World, ResourceId}, + Entities, Entity, Join, ParJoin, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage, SystemData, +}; use std::ops::Range; use vek::*; @@ -62,112 +66,78 @@ fn calc_z_limit( #[derive(Default)] pub struct Sys; -impl<'a> System<'a> for Sys { - #[allow(clippy::type_complexity)] - type SystemData = ( - Entities<'a>, - ReadStorage<'a, Uid>, - ReadExpect<'a, TerrainGrid>, - Read<'a, DeltaTime>, - WriteExpect<'a, PhysicsMetrics>, - Read<'a, EventBus>, - ReadStorage<'a, Scale>, - ReadStorage<'a, Sticky>, - ReadStorage<'a, Mass>, - ReadStorage<'a, Collider>, - ReadStorage<'a, Gravity>, - WriteStorage<'a, PhysicsState>, - WriteStorage<'a, Pos>, - WriteStorage<'a, Vel>, - WriteStorage<'a, Ori>, - WriteStorage<'a, PreviousPhysCache>, - ReadStorage<'a, Mounting>, - ReadStorage<'a, Projectile>, - ReadStorage<'a, BeamSegment>, - ReadStorage<'a, Shockwave>, - ReadStorage<'a, CharacterState>, - ); +#[derive(SystemData)] +pub struct PhysicsSystemDataRead<'a> { + entities: Entities<'a>, + uids: ReadStorage<'a, Uid>, + terrain: ReadExpect<'a, TerrainGrid>, + dt: Read<'a, DeltaTime>, + event_bus: Read<'a, EventBus>, + scales: ReadStorage<'a, Scale>, + stickies: ReadStorage<'a, Sticky>, + masses: ReadStorage<'a, Mass>, + colliders: ReadStorage<'a, Collider>, + gravities: ReadStorage<'a, Gravity>, + mountings: ReadStorage<'a, Mounting>, + projectiles: ReadStorage<'a, Projectile>, + beams: ReadStorage<'a, BeamSegment>, + shockwaves: ReadStorage<'a, Shockwave>, + char_states: ReadStorage<'a, CharacterState>, +} - const NAME: &'static str = "phys"; - const ORIGIN: Origin = Origin::Common; - const PHASE: Phase = Phase::Create; +#[derive(SystemData)] +pub struct PhysicsSystemDataWrite<'a> { + physics_metrics: WriteExpect<'a, PhysicsMetrics>, + physics_states: WriteStorage<'a, PhysicsState>, + positions: WriteStorage<'a, Pos>, + velocities: WriteStorage<'a, Vel>, + orientations: WriteStorage<'a, Ori>, + previous_phys_cache: WriteStorage<'a, PreviousPhysCache>, +} - #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 - #[allow(clippy::blocks_in_if_conditions)] // TODO: Pending review in #587 - fn run( - job: &mut Job, - ( - entities, - uids, - terrain, - dt, - mut physics_metrics, - event_bus, - scales, - stickies, - masses, - colliders, - gravities, - mut physics_states, - mut positions, - mut velocities, - mut orientations, - mut previous_phys_cache, - mountings, - projectiles, - beams, - shockwaves, - char_states, - ): Self::SystemData, - ) { - let mut event_emitter = event_bus.emitter(); +#[derive(SystemData)] +pub struct PhysicsSystemData<'a> { + r: PhysicsSystemDataRead<'a>, + w: PhysicsSystemDataWrite<'a>, +} - // Add/reset physics state components +impl<'a> PhysicsSystemData<'a> { + /// Add/reset physics state components + fn reset(&mut self) { span!(guard, "Add/reset physics state components"); for (entity, _, _, _, _) in ( - &entities, - &colliders, - &positions, - &velocities, - &orientations, + &self.r.entities, + &self.r.colliders, + &self.w.positions, + &self.w.velocities, + &self.w.orientations, ) .join() { - let _ = physics_states + let _ = self.w.physics_states .entry(entity) .map(|e| e.or_insert_with(Default::default)); } drop(guard); + } - // 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. - // - // 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. + fn maintain_pushback_cache(&mut self) { span!(guard, "Maintain pushback cache"); //Add PreviousPhysCache for all relevant entities for entity in ( - &entities, - &velocities, - &positions, - !&previous_phys_cache, - !&mountings, - !&beams, - !&shockwaves, + &self.r.entities, + &self.w.velocities, + &self.w.positions, + !&self.w.previous_phys_cache, + !&self.r.mountings, + !&self.r.beams, + !&self.r.shockwaves, ) .join() .map(|(e, _, _, _, _, _, _)| e) .collect::>() { - let _ = previous_phys_cache.insert(entity, PreviousPhysCache { + let _ = self.w.previous_phys_cache.insert(entity, PreviousPhysCache { velocity_dt: Vec3::zero(), center: Vec3::zero(), collision_boundary: 0.0, @@ -178,16 +148,16 @@ impl<'a> System<'a> for Sys { //Update PreviousPhysCache for (_, vel, position, mut phys_cache, collider, scale, cs, _, _, _) in ( - &entities, - &velocities, - &positions, - &mut previous_phys_cache, - colliders.maybe(), - scales.maybe(), - char_states.maybe(), - !&mountings, - !&beams, - !&shockwaves, + &self.r.entities, + &self.w.velocities, + &self.w.positions, + &mut self.w.previous_phys_cache, + self.r.colliders.maybe(), + self.r.scales.maybe(), + self.r.char_states.maybe(), + !&self.r.mountings, + !&self.r.beams, + !&self.r.shockwaves, ) .join() { @@ -196,7 +166,7 @@ impl<'a> System<'a> for Sys { let z_limits = (z_limits.0 * scale, z_limits.1 * scale); let half_height = (z_limits.1 - z_limits.0) / 2.0; - phys_cache.velocity_dt = vel.0 * dt.0; + phys_cache.velocity_dt = vel.0 * self.r.dt.0; let entity_center = position.0 + Vec3::new(0.0, z_limits.0 + half_height, 0.0); let flat_radius = collider.map(|c| c.get_radius()).unwrap_or(0.5) * scale; let radius = (flat_radius.powi(2) + half_height.powi(2)).sqrt(); @@ -209,23 +179,26 @@ impl<'a> System<'a> for Sys { phys_cache.scaled_radius = flat_radius; } drop(guard); - + } + fn apply_pushback(&mut self, job: &mut Job) { span!(guard, "Apply pushback"); job.cpu_stats.measure(ParMode::Rayon); + let PhysicsSystemData { r: ref psdr, w: ref mut psdw } = self; + let (positions, previous_phys_cache) = (&psdw.positions, &psdw.previous_phys_cache); let metrics = ( - &entities, - &positions, - &mut velocities, - &previous_phys_cache, - masses.maybe(), - colliders.maybe(), - !&mountings, - stickies.maybe(), - &mut physics_states, + &psdr.entities, + positions, + &mut psdw.velocities, + previous_phys_cache, + psdr.masses.maybe(), + psdr.colliders.maybe(), + !&psdr.mountings, + psdr.stickies.maybe(), + &mut psdw.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(), - char_states.maybe(), + psdr.projectiles.maybe(), + psdr.char_states.maybe(), ) .par_join() .filter(|(_, _, _, _, _, _, _, sticky, physics, _, _)| { @@ -275,17 +248,17 @@ impl<'a> System<'a> for Sys { _, char_state_other_maybe, ) in ( - &entities, - &uids, - &positions, - &previous_phys_cache, - masses.maybe(), - colliders.maybe(), - !&projectiles, - !&mountings, - !&beams, - !&shockwaves, - char_states.maybe(), + &psdr.entities, + &psdr.uids, + positions, + previous_phys_cache, + psdr.masses.maybe(), + psdr.colliders.maybe(), + !&psdr.projectiles, + !&psdr.mountings, + !&psdr.beams, + !&psdr.shockwaves, + psdr.char_states.maybe(), ) .join() { @@ -358,7 +331,7 @@ impl<'a> System<'a> for Sys { } // Change velocity - vel.0 += vel_delta * dt.0; + vel.0 += vel_delta * psdr.dt.0; // Metrics PhysicsMetrics { @@ -373,435 +346,622 @@ impl<'a> System<'a> for Sys { entity_entity_collisions: old.entity_entity_collisions + new.entity_entity_collisions, }); - physics_metrics.entity_entity_collision_checks = metrics.entity_entity_collision_checks; - physics_metrics.entity_entity_collisions = metrics.entity_entity_collisions; + psdw.physics_metrics.entity_entity_collision_checks = metrics.entity_entity_collision_checks; + psdw.physics_metrics.entity_entity_collisions = metrics.entity_entity_collisions; drop(guard); + } + fn handle_movement_and_terrain(&mut self, job: &mut Job) { + let PhysicsSystemData { r: ref psdr, w: ref mut psdw } = self; // Apply movement inputs span!(guard, "Apply movement and terrain collision"); - let land_on_grounds = ( - &entities, - scales.maybe(), - stickies.maybe(), - &colliders, - &mut positions, - &mut velocities, - &mut orientations, - &mut physics_states, - !&mountings, - ) - .par_join() - .map_init( - || { - prof_span!(guard, "physics e<>t rayon job"); - guard - }, - |_guard, - (entity, _scale, sticky, collider, mut pos, mut vel, _ori, mut physics_state, _), - | { - let mut landed_on_ground = None; - - if sticky.is_some() && physics_state.on_surface().is_some() { - vel.0 = Vec3::zero(); - return landed_on_ground; - } - - // TODO: Use this - //let scale = scale.map(|s| s.0).unwrap_or(1.0); - - let old_vel = *vel; - // Integrate forces - // Friction is assumed to be a constant dependent on location - let friction = FRIC_AIR - .max(if physics_state.on_ground { - FRIC_GROUND - } else { - 0.0 - }) - .max(if physics_state.in_liquid.is_some() { - FRIC_FLUID - } else { - 0.0 - }); - let in_loaded_chunk = terrain - .get_key(terrain.pos_key(pos.0.map(|e| e.floor() as i32))) - .is_some(); - let downward_force = if !in_loaded_chunk { - 0.0 // No gravity in unloaded chunks - } else if physics_state - .in_liquid - .map(|depth| depth > 0.75) - .unwrap_or(false) - { - (1.0 - BOUYANCY) * GRAVITY - } else { - GRAVITY - } * gravities.get(entity).map(|g| g.0).unwrap_or_default(); - vel.0 = integrate_forces(dt.0, vel.0, downward_force, friction); - - // Don't move if we're not in a loaded chunk - let mut pos_delta = if in_loaded_chunk { - // this is an approximation that allows most framerates to - // behave in a similar manner. - let dt_lerp = 0.2; - (vel.0 * dt_lerp + old_vel.0 * (1.0 - dt_lerp)) * dt.0 - } else { - Vec3::zero() - }; - - match *collider { - Collider::Box { - radius, - z_min, - z_max, - } => { - // Scale collider - // TODO: Use scale & actual proportions when pathfinding is good enough to manage irregular entity - // sizes - let radius = radius.min(0.45); // * scale; - let z_min = z_min; // * scale; - let z_max = z_max.clamped(1.2, 1.95); // * scale; - - // Probe distances - let hdist = radius.ceil() as i32; - // Neighbouring blocks iterator - let near_iter = (-hdist..hdist + 1) - .map(move |i| { - (-hdist..hdist + 1).map(move |j| { - (1 - Block::MAX_HEIGHT.ceil() as i32 + z_min.floor() as i32 - ..z_max.ceil() as i32 + 1) - .map(move |k| (i, j, k)) - }) - }) - .flatten() - .flatten(); - - // Function for iterating over the blocks the player at a specific position - // collides with - fn collision_iter<'a>( - pos: Vec3, - terrain: &'a TerrainGrid, - hit: &'a impl Fn(&Block) -> bool, - height: &'a impl Fn(&Block) -> f32, - near_iter: impl Iterator + 'a, - radius: f32, - z_range: Range, - ) -> impl Iterator> + 'a { - near_iter.filter_map(move |(i, j, k)| { - let block_pos = pos.map(|e| e.floor() as i32) + Vec3::new(i, j, k); - - if let Some(block) = terrain.get(block_pos).ok().copied().filter(hit) { - let player_aabb = Aabb { - min: pos + Vec3::new(-radius, -radius, z_range.start), - max: pos + Vec3::new(radius, radius, z_range.end), - }; - let block_aabb = Aabb { - min: block_pos.map(|e| e as f32), - max: block_pos.map(|e| e as f32) - + Vec3::new(1.0, 1.0, height(&block)), - }; - - if player_aabb.collides_with_aabb(block_aabb) { - return Some(block_aabb); - } - } - - None - }) - } - - let z_range = z_min..z_max; - // Function for determining whether the player at a specific position collides - // with blocks with the given criteria - fn collision_with<'a>( - pos: Vec3, - terrain: &'a TerrainGrid, - hit: impl Fn(&Block) -> bool, - near_iter: impl Iterator + 'a, - radius: f32, - z_range: Range, - ) -> bool { - collision_iter(pos, terrain, &|block| block.is_solid() && hit(block), &Block::solid_height, near_iter, radius, z_range).count() - > 0 - } - - let was_on_ground = physics_state.on_ground; - physics_state.on_ground = false; - - let mut on_ground = false; - let mut on_ceiling = false; - let mut attempts = 0; // Don't loop infinitely here - - // Don't jump too far at once - let increments = (pos_delta.map(|e| e.abs()).reduce_partial_max() / 0.3) - .ceil() - .max(1.0); - let old_pos = pos.0; - fn block_true(_: &Block) -> bool { true } - for _ in 0..increments as usize { - pos.0 += pos_delta / increments; - - const MAX_ATTEMPTS: usize = 16; - - // While the player is colliding with the terrain... - while collision_with(pos.0, &terrain, block_true, near_iter.clone(), radius, z_range.clone()) - && attempts < MAX_ATTEMPTS - { - // Calculate the player's AABB - let player_aabb = Aabb { - min: pos.0 + Vec3::new(-radius, -radius, z_min), - max: pos.0 + Vec3::new(radius, radius, z_max), - }; - - // Determine the block that we are colliding with most (based on minimum - // collision axis) - let (_block_pos, block_aabb, block_height) = near_iter - .clone() - // Calculate the block's position in world space - .map(|(i, j, k)| pos.0.map(|e| e.floor() as i32) + Vec3::new(i, j, k)) - // Make sure the block is actually solid - .filter_map(|block_pos| { - if let Some(block) = terrain - .get(block_pos) - .ok() - .filter(|block| block.is_solid()) - { - // Calculate block AABB - Some(( - block_pos, - Aabb { - min: block_pos.map(|e| e as f32), - max: block_pos.map(|e| e as f32) + Vec3::new(1.0, 1.0, block.solid_height()), - }, - block.solid_height(), - )) - } else { - None - } - }) - // Determine whether the block's AABB collides with the player's AABB - .filter(|(_, block_aabb, _)| block_aabb.collides_with_aabb(player_aabb)) - // Find the maximum of the minimum collision axes (this bit is weird, trust me that it works) - .min_by_key(|(_, block_aabb, _)| { - ((block_aabb.center() - player_aabb.center() - Vec3::unit_z() * 0.5) - .map(|e| e.abs()) - .sum() - * 1_000_000.0) as i32 - }) - .expect("Collision detected, but no colliding blocks found!"); - - // Find the intrusion vector of the collision - let dir = player_aabb.collision_vector_with_aabb(block_aabb); - - // Determine an appropriate resolution vector (i.e: the minimum distance - // needed to push out of the block) - let max_axis = dir.map(|e| e.abs()).reduce_partial_min(); - let resolve_dir = -dir.map(|e| { - if e.abs().to_bits() == max_axis.to_bits() { - e - } else { - 0.0 - } - }); - - // When the resolution direction is pointing upwards, we must be on the - // ground - if resolve_dir.z > 0.0 && vel.0.z <= 0.0 { - on_ground = true; - - if !was_on_ground { - landed_on_ground = Some((entity, *vel)); - } - } else if resolve_dir.z < 0.0 && vel.0.z >= 0.0 { - on_ceiling = true; - } - - // When the resolution direction is non-vertical, we must be colliding - // with a wall If the space above is free... - if !collision_with(Vec3::new(pos.0.x, pos.0.y, (pos.0.z + 0.1).ceil()), &terrain, block_true, near_iter.clone(), radius, z_range.clone()) - // ...and we're being pushed out horizontally... - && resolve_dir.z == 0.0 - // ...and the vertical resolution direction is sufficiently great... - && -dir.z > 0.1 - // ...and we're falling/standing OR there is a block *directly* beneath our current origin (note: not hitbox)... - && (vel.0.z <= 0.0 || terrain - .get((pos.0 - Vec3::unit_z() * 0.1).map(|e| e.floor() as i32)) - .map(|block| block.is_solid()) - .unwrap_or(false)) - // ...and there is a collision with a block beneath our current hitbox... - && collision_with( - pos.0 + resolve_dir - Vec3::unit_z() * 1.05, - &terrain, - block_true, - near_iter.clone(), - radius, - z_range.clone(), - ) - { - // ...block-hop! - pos.0.z = (pos.0.z + 0.1).floor() + block_height; - vel.0.z = 0.0; - on_ground = true; - break; - } else { - // Correct the velocity - vel.0 = vel.0.map2(resolve_dir, |e, d| { - 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 }); - } - - // Resolve the collision normally - pos.0 += resolve_dir; - - attempts += 1; - } - - if attempts == MAX_ATTEMPTS { + let (positions, previous_phys_cache) = (&psdw.positions, &psdw.previous_phys_cache); + let (pos_writes, land_on_grounds) = + ( + &psdr.entities, + psdr.scales.maybe(), + psdr.stickies.maybe(), + &psdr.colliders, + positions, + &mut psdw.velocities, + &psdw.orientations, + &mut psdw.physics_states, + previous_phys_cache, + !&psdr.mountings, + ) + .par_join() + .fold( + || (Vec::new(), Vec::new()), + |(mut pos_writes, mut land_on_grounds), + ( + entity, + scale, + sticky, + collider, + pos, + mut vel, + _ori, + mut physics_state, + previous_cache, + _, + )| { + // defer the writes of positions to allow an inner loop over terrain-like + // entities + let old_pos = *pos; + let mut pos = *pos; + if sticky.is_some() && physics_state.on_surface().is_some() { vel.0 = Vec3::zero(); - pos.0 = old_pos; - break; + return (pos_writes, land_on_grounds); } - } - if on_ceiling { - physics_state.on_ceiling = true; - } - - if on_ground { - physics_state.on_ground = true; - // If the space below us is free, then "snap" to the ground - } else if collision_with( - pos.0 - Vec3::unit_z() * 1.05, - &terrain, - block_true, - near_iter.clone(), - radius, - z_range.clone(), - ) && vel.0.z < 0.0 - && vel.0.z > -1.5 - && was_on_ground - && !collision_with( - pos.0 - Vec3::unit_z() * 0.05, - &terrain, - |block| block.solid_height() >= (pos.0.z - 0.05).rem_euclid(1.0), - near_iter.clone(), - radius, - z_range.clone(), - ) - { - let snap_height = terrain - .get( - Vec3::new(pos.0.x, pos.0.y, pos.0.z - 0.05) - .map(|e| e.floor() as i32), - ) - .ok() - .filter(|block| block.is_solid()) - .map(|block| block.solid_height()) - .unwrap_or(0.0); - pos.0.z = (pos.0.z - 0.05).floor() + snap_height; - physics_state.on_ground = true; - } - - let dirs = [ - Vec3::unit_x(), - Vec3::unit_y(), - -Vec3::unit_x(), - -Vec3::unit_y(), - ]; - - if let (wall_dir, true) = - dirs.iter().fold((Vec3::zero(), false), |(a, hit), dir| { - if collision_with( - pos.0 + *dir * 0.01, - &terrain, - block_true, - near_iter.clone(), - radius, - z_range.clone(), - ) { - (a + dir, true) - } else { - (a, hit) - } - }) - { - physics_state.on_wall = Some(wall_dir); - } else { - physics_state.on_wall = None; - } - - // Figure out if we're in water - physics_state.in_liquid = collision_iter( - pos.0, - &terrain, - &|block| block.is_liquid(), - // The liquid part of a liquid block always extends 1 block high. - &|_block| 1.0, - near_iter.clone(), - radius, - z_min..z_max, - ) - .max_by_key(|block_aabb| (block_aabb.max.z * 100.0) as i32) - .map(|block_aabb| block_aabb.max.z - pos.0.z); - }, - Collider::Point => { - let (dist, block) = terrain.ray(pos.0, pos.0 + pos_delta) - .until(|block: &Block| block.is_filled()) - .ignore_error().cast(); - - pos.0 += pos_delta.try_normalized().unwrap_or(Vec3::zero()) * dist; - - // Can't fail since we do ignore_error above - if block.unwrap().is_some() { - let block_center = pos.0.map(|e| e.floor()) + 0.5; - let block_rpos = (pos.0 - block_center) - .try_normalized() - .unwrap_or(Vec3::zero()); - - // See whether we're on the top/bottom of a block, or the side - if block_rpos.z.abs() - > block_rpos.xy().map(|e| e.abs()).reduce_partial_max() - { - if block_rpos.z > 0.0 { - physics_state.on_ground = true; - } else { - physics_state.on_ceiling = true; - } - vel.0.z = 0.0; + let scale = if let Collider::Voxel { .. } = collider { + scale.map(|s| s.0).unwrap_or(1.0) } else { - physics_state.on_wall = - Some(if block_rpos.x.abs() > block_rpos.y.abs() { - vel.0.x = 0.0; - Vec3::unit_x() * -block_rpos.x.signum() - } else { - vel.0.y = 0.0; - Vec3::unit_y() * -block_rpos.y.signum() - }); + // TODO: Use scale & actual proportions when pathfinding is good + // enough to manage irregular entity sizes + 1.0 + }; + + let old_vel = *vel; + // Integrate forces + // Friction is assumed to be a constant dependent on location + let friction = FRIC_AIR + .max(if physics_state.on_ground { + FRIC_GROUND + } else { + 0.0 + }) + .max(if physics_state.in_liquid.is_some() { + FRIC_FLUID + } else { + 0.0 + }); + let in_loaded_chunk = psdr.terrain + .get_key(psdr.terrain.pos_key(pos.0.map(|e| e.floor() as i32))) + .is_some(); + let downward_force = + if !in_loaded_chunk { + 0.0 // No gravity in unloaded chunks + } else if physics_state + .in_liquid + .map(|depth| depth > 0.75) + .unwrap_or(false) + { + (1.0 - BOUYANCY) * GRAVITY + } else { + GRAVITY + } * psdr.gravities.get(entity).map(|g| g.0).unwrap_or_default(); + vel.0 = integrate_forces(psdr.dt.0, vel.0, downward_force, friction); + + // Don't move if we're not in a loaded chunk + let pos_delta = if in_loaded_chunk { + // this is an approximation that allows most framerates to + // behave in a similar manner. + let dt_lerp = 0.2; + (vel.0 * dt_lerp + old_vel.0 * (1.0 - dt_lerp)) * psdr.dt.0 + } else { + Vec3::zero() + }; + + match &*collider { + Collider::Voxel { .. } => { + // for now, treat entities with voxel colliders as their bounding + // cylinders for the purposes of colliding them with terrain + let radius = collider.get_radius() * scale; + let (z_min, z_max) = collider.get_z_limits(scale); + + let cylinder = (radius, z_min, z_max); + cylinder_voxel_collision( + cylinder, + &*psdr.terrain, + entity, + &mut pos, + pos_delta, + vel, + &mut physics_state, + &mut land_on_grounds, + ); + }, + Collider::Box { + radius, + z_min, + z_max, + } => { + // Scale collider + let radius = radius.min(0.45) * scale; + let z_min = *z_min * scale; + let z_max = z_max.clamped(1.2, 1.95) * scale; + + let cylinder = (radius, z_min, z_max); + cylinder_voxel_collision( + cylinder, + &*psdr.terrain, + entity, + &mut pos, + pos_delta, + vel, + &mut physics_state, + &mut land_on_grounds, + ); + }, + Collider::Point => { + let (dist, block) = psdr.terrain + .ray(pos.0, pos.0 + pos_delta) + .until(|block: &Block| block.is_filled()) + .ignore_error() + .cast(); + + pos.0 += pos_delta.try_normalized().unwrap_or(Vec3::zero()) * dist; + + // Can't fail since we do ignore_error above + if block.unwrap().is_some() { + let block_center = pos.0.map(|e| e.floor()) + 0.5; + let block_rpos = (pos.0 - block_center) + .try_normalized() + .unwrap_or(Vec3::zero()); + + // See whether we're on the top/bottom of a block, or the side + if block_rpos.z.abs() + > block_rpos.xy().map(|e| e.abs()).reduce_partial_max() + { + if block_rpos.z > 0.0 { + physics_state.on_ground = true; + } else { + physics_state.on_ceiling = true; + } + vel.0.z = 0.0; + } else { + physics_state.on_wall = + Some(if block_rpos.x.abs() > block_rpos.y.abs() { + vel.0.x = 0.0; + Vec3::unit_x() * -block_rpos.x.signum() + } else { + vel.0.y = 0.0; + Vec3::unit_y() * -block_rpos.y.signum() + }); + } + } + + physics_state.in_liquid = psdr.terrain + .get(pos.0.map(|e| e.floor() as i32)) + .ok() + .and_then(|vox| vox.is_liquid().then_some(1.0)); + }, } - } - physics_state.in_liquid = terrain.get(pos.0.map(|e| e.floor() as i32)) - .ok() - .and_then(|vox| vox.is_liquid().then_some(1.0)); - }, - } + // Collide with terrain-like entities + for ( + entity_other, + other, + pos_other, + previous_cache_other, + mass_other, + collider_other, + _, + _, + _, + _, + char_state_other_maybe, + ) in ( + &psdr.entities, + &psdr.uids, + positions, + previous_phys_cache, + psdr.masses.maybe(), + &psdr.colliders, + !&psdr.projectiles, + !&psdr.mountings, + !&psdr.beams, + !&psdr.shockwaves, + psdr.char_states.maybe(), + ) + .join() + { + let collision_boundary = previous_cache.collision_boundary + + previous_cache_other.collision_boundary; + if previous_cache + .center + .distance_squared(previous_cache_other.center) + > collision_boundary.powi(2) + || entity == entity_other + { + continue; + } - landed_on_ground - }).fold(Vec::new, |mut lands_on_grounds, landed_on_ground| { - if let Some(land_on_ground) = landed_on_ground { - lands_on_grounds.push(land_on_ground); - } - lands_on_grounds - }).reduce(Vec::new, |mut land_on_grounds_a, mut land_on_grounds_b| { - land_on_grounds_a.append(&mut land_on_grounds_b); - land_on_grounds_a - }); + if let Collider::Voxel { id } = collider_other { + // use bounding cylinder regardless of our collider + // TODO: extract point-terrain collision above to its own function + let radius = collider.get_radius() * scale; + let (z_min, z_max) = collider.get_z_limits(scale); + + let cylinder = (radius, z_min, z_max); + // TODO: load .vox into a Dyna, and use it (appropriately rotated) + // as the terrain + /*cylinder_voxel_collision( + cylinder, + &*psdr.terrain, + entity, + &mut pos, + pos_delta, + vel, + &mut physics_state, + &mut land_on_grounds, + );*/ + } + } + if pos != old_pos { + pos_writes.push((entity, pos)); + } + + (pos_writes, land_on_grounds) + }, + ) + .reduce( + || (Vec::new(), Vec::new()), + |(mut pos_writes_a, mut land_on_grounds_a), + (mut pos_writes_b, mut land_on_grounds_b)| { + pos_writes_a.append(&mut pos_writes_b); + land_on_grounds_a.append(&mut land_on_grounds_b); + (pos_writes_a, land_on_grounds_a) + }, + ); drop(guard); job.cpu_stats.measure(ParMode::Single); + let pos_writes: HashMap = pos_writes.into_iter().collect(); + for (entity, pos) in (&psdr.entities, &mut psdw.positions).join() { + if let Some(new_pos) = pos_writes.get(&entity) { + *pos = *new_pos; + } + } + + let mut event_emitter = psdr.event_bus.emitter(); land_on_grounds.into_iter().for_each(|(entity, vel)| { event_emitter.emit(ServerEvent::LandOnGround { entity, vel: vel.0 }); }); } } + +impl<'a> System<'a> for Sys { + type SystemData = PhysicsSystemData<'a>; + + const NAME: &'static str = "phys"; + const ORIGIN: Origin = Origin::Common; + const PHASE: Phase = Phase::Create; + + #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 + #[allow(clippy::blocks_in_if_conditions)] // TODO: Pending review in #587 + fn run( + job: &mut Job, + mut psd: Self::SystemData, + ) { + psd.reset(); + + // 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. + // + // 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. + psd.maintain_pushback_cache(); + psd.apply_pushback(job); + + + psd.handle_movement_and_terrain(job); + } +} + +fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( + cylinder: (f32, f32, f32), + terrain: &'a T, + entity: Entity, + pos: &mut Pos, + mut pos_delta: Vec3, + vel: &mut Vel, + physics_state: &mut PhysicsState, + land_on_grounds: &mut Vec<(Entity, Vel)>, +) { + let (radius, z_min, z_max) = cylinder; + + // Probe distances + let hdist = radius.ceil() as i32; + // Neighbouring blocks iterator + let near_iter = (-hdist..hdist + 1) + .map(move |i| { + (-hdist..hdist + 1).map(move |j| { + (1 - Block::MAX_HEIGHT.ceil() as i32 + z_min.floor() as i32 + ..z_max.ceil() as i32 + 1) + .map(move |k| (i, j, k)) + }) + }) + .flatten() + .flatten(); + + // Function for iterating over the blocks the player at a specific position + // collides with + fn collision_iter<'a, T: BaseVol + ReadVol>( + pos: Vec3, + terrain: &'a T, + hit: &'a impl Fn(&Block) -> bool, + height: &'a impl Fn(&Block) -> f32, + near_iter: impl Iterator + 'a, + radius: f32, + z_range: Range, + ) -> impl Iterator> + 'a { + near_iter.filter_map(move |(i, j, k)| { + let block_pos = pos.map(|e| e.floor() as i32) + Vec3::new(i, j, k); + + if let Some(block) = terrain.get(block_pos).ok().copied().filter(hit) { + let player_aabb = Aabb { + min: pos + Vec3::new(-radius, -radius, z_range.start), + max: pos + Vec3::new(radius, radius, z_range.end), + }; + let block_aabb = Aabb { + min: block_pos.map(|e| e as f32), + max: block_pos.map(|e| e as f32) + Vec3::new(1.0, 1.0, height(&block)), + }; + + if player_aabb.collides_with_aabb(block_aabb) { + return Some(block_aabb); + } + } + + None + }) + } + + let z_range = z_min..z_max; + // Function for determining whether the player at a specific position collides + // with blocks with the given criteria + fn collision_with<'a, T: BaseVol + ReadVol>( + pos: Vec3, + terrain: &'a T, + hit: impl Fn(&Block) -> bool, + near_iter: impl Iterator + 'a, + radius: f32, + z_range: Range, + ) -> bool { + collision_iter( + pos, + terrain, + &|block| block.is_solid() && hit(block), + &Block::solid_height, + near_iter, + radius, + z_range, + ) + .count() + > 0 + } + + let was_on_ground = physics_state.on_ground; + physics_state.on_ground = false; + + let mut on_ground = false; + let mut on_ceiling = false; + let mut attempts = 0; // Don't loop infinitely here + + // Don't jump too far at once + let increments = (pos_delta.map(|e| e.abs()).reduce_partial_max() / 0.3) + .ceil() + .max(1.0); + let old_pos = pos.0; + fn block_true(_: &Block) -> bool { true } + for _ in 0..increments as usize { + pos.0 += pos_delta / increments; + + const MAX_ATTEMPTS: usize = 16; + + // While the player is colliding with the terrain... + while collision_with( + pos.0, + &terrain, + block_true, + near_iter.clone(), + radius, + z_range.clone(), + ) && attempts < MAX_ATTEMPTS + { + // Calculate the player's AABB + let player_aabb = Aabb { + min: pos.0 + Vec3::new(-radius, -radius, z_min), + max: pos.0 + Vec3::new(radius, radius, z_max), + }; + + // Determine the block that we are colliding with most (based on minimum + // collision axis) + let (_block_pos, block_aabb, block_height) = near_iter + .clone() + // Calculate the block's position in world space + .map(|(i, j, k)| pos.0.map(|e| e.floor() as i32) + Vec3::new(i, j, k)) + // Make sure the block is actually solid + .filter_map(|block_pos| { + if let Some(block) = terrain + .get(block_pos) + .ok() + .filter(|block| block.is_solid()) + { + // Calculate block AABB + Some(( + block_pos, + Aabb { + min: block_pos.map(|e| e as f32), + max: block_pos.map(|e| e as f32) + Vec3::new(1.0, 1.0, block.solid_height()), + }, + block.solid_height(), + )) + } else { + None + } + }) + // Determine whether the block's AABB collides with the player's AABB + .filter(|(_, block_aabb, _)| block_aabb.collides_with_aabb(player_aabb)) + // Find the maximum of the minimum collision axes (this bit is weird, trust me that it works) + .min_by_key(|(_, block_aabb, _)| { + ((block_aabb.center() - player_aabb.center() - Vec3::unit_z() * 0.5) + .map(|e| e.abs()) + .sum() + * 1_000_000.0) as i32 + }) + .expect("Collision detected, but no colliding blocks found!"); + + // Find the intrusion vector of the collision + let dir = player_aabb.collision_vector_with_aabb(block_aabb); + + // Determine an appropriate resolution vector (i.e: the minimum distance + // needed to push out of the block) + let max_axis = dir.map(|e| e.abs()).reduce_partial_min(); + let resolve_dir = -dir.map(|e| { + if e.abs().to_bits() == max_axis.to_bits() { + e + } else { + 0.0 + } + }); + + // When the resolution direction is pointing upwards, we must be on the + // ground + if resolve_dir.z > 0.0 && vel.0.z <= 0.0 { + on_ground = true; + + if !was_on_ground { + land_on_grounds.push((entity, *vel)); + } + } else if resolve_dir.z < 0.0 && vel.0.z >= 0.0 { + on_ceiling = true; + } + + // When the resolution direction is non-vertical, we must be colliding + // with a wall If the space above is free... + if !collision_with(Vec3::new(pos.0.x, pos.0.y, (pos.0.z + 0.1).ceil()), &terrain, block_true, near_iter.clone(), radius, z_range.clone()) + // ...and we're being pushed out horizontally... + && resolve_dir.z == 0.0 + // ...and the vertical resolution direction is sufficiently great... + && -dir.z > 0.1 + // ...and we're falling/standing OR there is a block *directly* beneath our current origin (note: not hitbox)... + && (vel.0.z <= 0.0 || terrain + .get((pos.0 - Vec3::unit_z() * 0.1).map(|e| e.floor() as i32)) + .map(|block| block.is_solid()) + .unwrap_or(false)) + // ...and there is a collision with a block beneath our current hitbox... + && collision_with( + pos.0 + resolve_dir - Vec3::unit_z() * 1.05, + &terrain, + block_true, + near_iter.clone(), + radius, + z_range.clone(), + ) + { + // ...block-hop! + pos.0.z = (pos.0.z + 0.1).floor() + block_height; + vel.0.z = 0.0; + on_ground = true; + break; + } else { + // Correct the velocity + vel.0 = vel.0.map2( + resolve_dir, + |e, d| { + 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 }); + } + + // Resolve the collision normally + pos.0 += resolve_dir; + + attempts += 1; + } + + if attempts == MAX_ATTEMPTS { + vel.0 = Vec3::zero(); + pos.0 = old_pos; + break; + } + } + + if on_ceiling { + physics_state.on_ceiling = true; + } + + if on_ground { + physics_state.on_ground = true; + // If the space below us is free, then "snap" to the ground + } else if collision_with( + pos.0 - Vec3::unit_z() * 1.05, + &terrain, + block_true, + near_iter.clone(), + radius, + z_range.clone(), + ) && vel.0.z < 0.0 + && vel.0.z > -1.5 + && was_on_ground + && !collision_with( + pos.0 - Vec3::unit_z() * 0.05, + &terrain, + |block| block.solid_height() >= (pos.0.z - 0.05).rem_euclid(1.0), + near_iter.clone(), + radius, + z_range.clone(), + ) + { + let snap_height = terrain + .get(Vec3::new(pos.0.x, pos.0.y, pos.0.z - 0.05).map(|e| e.floor() as i32)) + .ok() + .filter(|block| block.is_solid()) + .map(|block| block.solid_height()) + .unwrap_or(0.0); + pos.0.z = (pos.0.z - 0.05).floor() + snap_height; + physics_state.on_ground = true; + } + + let dirs = [ + Vec3::unit_x(), + Vec3::unit_y(), + -Vec3::unit_x(), + -Vec3::unit_y(), + ]; + + if let (wall_dir, true) = dirs.iter().fold((Vec3::zero(), false), |(a, hit), dir| { + if collision_with( + pos.0 + *dir * 0.01, + &terrain, + block_true, + near_iter.clone(), + radius, + z_range.clone(), + ) { + (a + dir, true) + } else { + (a, hit) + } + }) { + physics_state.on_wall = Some(wall_dir); + } else { + physics_state.on_wall = None; + } + + // Figure out if we're in water + physics_state.in_liquid = collision_iter( + pos.0, + &*terrain, + &|block| block.is_liquid(), + // The liquid part of a liquid block always extends 1 block high. + &|_block| 1.0, + near_iter.clone(), + radius, + z_min..z_max, + ) + .max_by_key(|block_aabb| (block_aabb.max.z * 100.0) as i32) + .map(|block_aabb| block_aabb.max.z - pos.0.z); +} diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 9043ef5fe5..7e6dc4c454 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -77,6 +77,7 @@ type CommandHandler = fn(&mut Server, EcsEntity, EcsEntity, String, &ChatCommand fn get_handler(cmd: &ChatCommand) -> CommandHandler { match cmd { ChatCommand::Adminify => handle_adminify, + ChatCommand::Airship => handle_spawn_airship, ChatCommand::Alias => handle_alias, ChatCommand::Ban => handle_ban, ChatCommand::Build => handle_build, @@ -984,6 +985,39 @@ fn handle_spawn_training_dummy( } } +fn handle_spawn_airship( + server: &mut Server, + client: EcsEntity, + target: EcsEntity, + _args: String, + _action: &ChatCommand, +) { + match server.state.read_component_copied::(target) { + Some(pos) => { + server + .state + .create_ship(pos, comp::ship::Body::DefaultAirship) + .with(comp::Scale(50.0)) + .with(LightEmitter { + col: Rgb::new(1.0, 0.65, 0.2), + strength: 2.0, + flicker: 1.0, + animated: true, + }) + .build(); + + server.notify_client( + client, + ServerGeneral::server_msg(ChatType::CommandInfo, "Spawned an airship"), + ); + }, + None => server.notify_client( + client, + ServerGeneral::server_msg(ChatType::CommandError, "You have no position!"), + ), + } +} + fn handle_spawn_campfire( server: &mut Server, client: EcsEntity, diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index c1c9f8f450..4005c3131b 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -69,7 +69,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv find_dist::Cylinder::from_components( p.0, scales.get(entity).copied(), - colliders.get(entity).copied(), + colliders.get(entity).cloned(), char_states.get(entity), ) }) diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index a596b31cbb..770b6d28b0 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -41,6 +41,7 @@ pub trait StateExt { ) -> EcsEntityBuilder; /// Build a static object entity fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder; + fn create_ship(&mut self, pos: comp::Pos, object: comp::ship::Body) -> EcsEntityBuilder; /// Build a projectile fn create_projectile( &mut self, @@ -215,6 +216,18 @@ impl StateExt for State { .with(comp::Gravity(1.0)) } + fn create_ship(&mut self, pos: comp::Pos, object: comp::ship::Body) -> EcsEntityBuilder { + self.ecs_mut() + .create_entity_synced() + .with(pos) + .with(comp::Vel(Vec3::zero())) + .with(comp::Ori::default()) + .with(comp::Mass(50.0)) + .with(comp::Collider::Voxel { id: object.manifest_id().to_string() }) + .with(comp::Body::Ship(object)) + .with(comp::Gravity(1.0)) + } + fn create_projectile( &mut self, pos: comp::Pos, diff --git a/server/src/sys/sentinel.rs b/server/src/sys/sentinel.rs index 105b417a6c..778892724c 100644 --- a/server/src/sys/sentinel.rs +++ b/server/src/sys/sentinel.rs @@ -140,7 +140,7 @@ impl<'a> TrackedComps<'a> { self.mass.get(entity).copied().map(|c| comps.push(c.into())); self.collider .get(entity) - .copied() + .cloned() .map(|c| comps.push(c.into())); self.sticky .get(entity) diff --git a/voxygen/anim/src/lib.rs b/voxygen/anim/src/lib.rs index 4988954a6f..d903d6572f 100644 --- a/voxygen/anim/src/lib.rs +++ b/voxygen/anim/src/lib.rs @@ -51,6 +51,7 @@ pub mod fish_small; pub mod fixture; pub mod golem; pub mod object; +pub mod ship; pub mod quadruped_low; pub mod quadruped_medium; pub mod quadruped_small; diff --git a/voxygen/anim/src/ship/idle.rs b/voxygen/anim/src/ship/idle.rs new file mode 100644 index 0000000000..b96c9fd64e --- /dev/null +++ b/voxygen/anim/src/ship/idle.rs @@ -0,0 +1,34 @@ +use super::{ + super::{vek::*, Animation}, + ShipSkeleton, SkeletonAttr, +}; +use common::comp::item::ToolKind; + +pub struct IdleAnimation; + +impl Animation for IdleAnimation { + type Dependency = (Option, Option, f32); + type Skeleton = ShipSkeleton; + + #[cfg(feature = "use-dyn-lib")] + const UPDATE_FN: &'static [u8] = b"ship_idle\0"; + + #[cfg_attr(feature = "be-dyn-lib", export_name = "ship_idle")] + #[allow(clippy::approx_constant)] // TODO: Pending review in #587 + fn update_skeleton_inner( + skeleton: &Self::Skeleton, + (_active_tool_kind, _second_tool_kind, _global_time): Self::Dependency, + _anim_time: f32, + _rate: &mut f32, + s_a: &SkeletonAttr, + ) -> Self::Skeleton { + let mut next = (*skeleton).clone(); + + next.bone0.position = Vec3::new(s_a.bone0.0, s_a.bone0.1, s_a.bone0.2) / 11.0; + + next.bone1.position = Vec3::new(s_a.bone1.0, s_a.bone1.1, s_a.bone1.2) / 11.0; + + next + } +} + diff --git a/voxygen/anim/src/ship/mod.rs b/voxygen/anim/src/ship/mod.rs new file mode 100644 index 0000000000..2e2783add3 --- /dev/null +++ b/voxygen/anim/src/ship/mod.rs @@ -0,0 +1,71 @@ +pub mod idle; + +// Reexports +pub use self::idle::IdleAnimation; + +use super::{make_bone, vek::*, FigureBoneData, Skeleton}; +use common::comp::{self}; +use core::convert::TryFrom; + +pub type Body = comp::ship::Body; + +skeleton_impls!(struct ShipSkeleton { + + bone0, + + bone1, +}); + +impl Skeleton for ShipSkeleton { + type Attr = SkeletonAttr; + type Body = Body; + + const BONE_COUNT: usize = 2; + #[cfg(feature = "use-dyn-lib")] + const COMPUTE_FN: &'static [u8] = b"ship_compute_mats\0"; + + #[cfg_attr(feature = "be-dyn-lib", export_name = "ship_compute_mats")] + fn compute_matrices_inner( + &self, + base_mat: Mat4, + buf: &mut [FigureBoneData; super::MAX_BONE_COUNT], + ) -> Vec3 { + let bone0_mat = base_mat * Mat4::::from(self.bone0); + + *(<&mut [_; Self::BONE_COUNT]>::try_from(&mut buf[0..Self::BONE_COUNT]).unwrap()) = [ + make_bone(bone0_mat * Mat4::scaling_3d(1.0 / 11.0)), + make_bone(Mat4::::from(self.bone1) * Mat4::scaling_3d(1.0 / 11.0)), /* Decorellated from ori */ + ]; + Vec3::unit_z() * 0.5 + } +} + +pub struct SkeletonAttr { + bone0: (f32, f32, f32), + bone1: (f32, f32, f32), +} + +impl<'a> std::convert::TryFrom<&'a comp::Body> for SkeletonAttr { + type Error = (); + + fn try_from(body: &'a comp::Body) -> Result { + match body { + comp::Body::Ship(body) => Ok(SkeletonAttr::from(body)), + _ => Err(()), + } + } +} + +impl Default for SkeletonAttr { + fn default() -> Self { + Self { + bone0: (0.0, 0.0, 0.0), + bone1: (0.0, 0.0, 0.0), + } + } +} + +impl<'a> From<&'a Body> for SkeletonAttr { + fn from(_: &'a Body) -> Self { + Self::default() + } +} + diff --git a/voxygen/src/render/renderer.rs b/voxygen/src/render/renderer.rs index 6856dbbb4b..eec891e144 100644 --- a/voxygen/src/render/renderer.rs +++ b/voxygen/src/render/renderer.rs @@ -1972,7 +1972,7 @@ fn create_pipelines( &shaders.figure_vert.read().0, &shaders.figure_frag.read().0, &include_ctx, - gfx::state::CullFace::Back, + gfx::state::CullFace::Nothing, )?; // Construct a pipeline for rendering terrain diff --git a/voxygen/src/scene/figure/load.rs b/voxygen/src/scene/figure/load.rs index 6aca80bb7f..a584812462 100644 --- a/voxygen/src/scene/figure/load.rs +++ b/voxygen/src/scene/figure/load.rs @@ -13,6 +13,7 @@ use common::{ humanoid::{self, Body, BodyType, EyeColor, Skin, Species}, item::{ItemDef, ModularComponentKind}, object, + ship::{self, figuredata::{ShipSpec, ShipCentralSubSpec}}, quadruped_low::{self, BodyType as QLBodyType, Species as QLSpecies}, quadruped_medium::{self, BodyType as QMBodyType, Species as QMSpecies}, quadruped_small::{self, BodyType as QSBodyType, Species as QSSpecies}, @@ -22,7 +23,7 @@ use common::{ }; use hashbrown::HashMap; use serde::Deserialize; -use std::sync::Arc; +use std::{fmt, hash::Hash, sync::Arc}; use tracing::{error, warn}; use vek::*; @@ -4102,6 +4103,17 @@ impl QuadrupedLowLateralSpec { #[derive(Deserialize)] struct ObjectCentralSpec(HashMap); +/* +#[derive(Deserialize)] +struct ShipCentralSpec(HashMap); + +#[derive(Deserialize)] +struct SidedShipCentralVoxSpec { + bone0: ObjectCentralSubSpec, + bone1: ObjectCentralSubSpec, + bone2: ObjectCentralSubSpec, +}*/ + #[derive(Deserialize)] struct SidedObjectCentralVoxSpec { bone0: ObjectCentralSubSpec, @@ -4171,3 +4183,99 @@ impl ObjectCentralSpec { (central, Vec3::from(spec.bone1.offset)) } } + +/*make_vox_spec!( + ship::Body, + struct ShipSpec { + central: ShipCentralSpec = "server.manifests.ship_manifest", + }, + |FigureKey { body, .. }, spec| { + [ + Some(spec.central.read().0.mesh_bone( + body, |spec| &spec.bone0, + )), + Some(spec.central.read().0.mesh_bone( + body, |spec| &spec.bone1 + )), + Some(spec.central.read().0.mesh_bone( + body, |spec| &spec.bone2 + )), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ] + }, +); + +impl ShipCentralSpec { + fn mesh_bone &ObjectCentralSubSpec>(&self, obj: &ship::Body, f: F) -> BoneMeshes { + let spec = match self.0.get(&obj) { + Some(spec) => spec, + None => { + error!("No specification exists for {:?}", obj); + return load_mesh("not_found", Vec3::new(-5.0, -5.0, -2.5)); + }, + }; + let bone = f(spec); + let central = graceful_load_segment(&bone.central.0); + + (central, Vec3::from(bone.offset)) + } +}*/ +fn mesh_ship_bone &ShipCentralSubSpec>(map: &HashMap, obj: &K, f: F) -> BoneMeshes { + let spec = match map.get(&obj) { + Some(spec) => spec, + None => { + error!("No specification exists for {:?}", obj); + return load_mesh("not_found", Vec3::new(-5.0, -5.0, -2.5)); + }, + }; + let bone = f(spec); + let central = graceful_load_segment(&bone.central.0); + + (central, Vec3::from(bone.offset)) +} + +impl BodySpec for ship::Body { + type Spec = ShipSpec; + + #[allow(unused_variables)] + fn load_spec() -> Result, assets::Error> { + Self::Spec::load("") + } + + fn bone_meshes( + FigureKey { body, .. }: &FigureKey, + spec: &Self::Spec, + ) -> [Option; anim::MAX_BONE_COUNT] { + let map = &(spec.central.read().0).0; + [ + Some(mesh_ship_bone(map, body, |spec| &spec.bone0,)), + Some(mesh_ship_bone(map, body, |spec| &spec.bone1,)), + Some(mesh_ship_bone(map, body, |spec| &spec.bone2,)), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ] + } +} diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index db5146c923..12281f4edf 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -21,7 +21,7 @@ use anim::{ biped_large::BipedLargeSkeleton, biped_small::BipedSmallSkeleton, bird_medium::BirdMediumSkeleton, bird_small::BirdSmallSkeleton, character::CharacterSkeleton, dragon::DragonSkeleton, fish_medium::FishMediumSkeleton, fish_small::FishSmallSkeleton, - golem::GolemSkeleton, object::ObjectSkeleton, quadruped_low::QuadrupedLowSkeleton, + golem::GolemSkeleton, object::ObjectSkeleton, ship::ShipSkeleton, quadruped_low::QuadrupedLowSkeleton, quadruped_medium::QuadrupedMediumSkeleton, quadruped_small::QuadrupedSmallSkeleton, theropod::TheropodSkeleton, Animation, Skeleton, }; @@ -101,6 +101,7 @@ struct FigureMgrStates { biped_small_states: HashMap>, golem_states: HashMap>, object_states: HashMap>, + ship_states: HashMap>, } impl FigureMgrStates { @@ -120,6 +121,7 @@ impl FigureMgrStates { biped_small_states: HashMap::new(), golem_states: HashMap::new(), object_states: HashMap::new(), + ship_states: HashMap::new(), } } @@ -180,6 +182,7 @@ impl FigureMgrStates { .map(DerefMut::deref_mut), Body::Golem(_) => self.golem_states.get_mut(&entity).map(DerefMut::deref_mut), Body::Object(_) => self.object_states.get_mut(&entity).map(DerefMut::deref_mut), + Body::Ship(_) => self.ship_states.get_mut(&entity).map(DerefMut::deref_mut), } } @@ -205,6 +208,7 @@ impl FigureMgrStates { Body::BipedSmall(_) => self.biped_small_states.remove(&entity).map(|e| e.meta), Body::Golem(_) => self.golem_states.remove(&entity).map(|e| e.meta), Body::Object(_) => self.object_states.remove(&entity).map(|e| e.meta), + Body::Ship(_) => self.ship_states.remove(&entity).map(|e| e.meta), } } @@ -224,6 +228,7 @@ impl FigureMgrStates { self.biped_small_states.retain(|k, v| f(k, &mut *v)); self.golem_states.retain(|k, v| f(k, &mut *v)); self.object_states.retain(|k, v| f(k, &mut *v)); + self.ship_states.retain(|k, v| f(k, &mut *v)); } fn count(&self) -> usize { @@ -242,6 +247,7 @@ impl FigureMgrStates { + self.biped_small_states.len() + self.golem_states.len() + self.object_states.len() + + self.ship_states.len() } fn count_visible(&self) -> usize { @@ -314,6 +320,11 @@ impl FigureMgrStates { .iter() .filter(|(_, c)| c.visible()) .count() + + self + .ship_states + .iter() + .filter(|(_, c)| c.visible()) + .count() } } @@ -332,6 +343,7 @@ pub struct FigureMgr { biped_large_model_cache: FigureModelCache, biped_small_model_cache: FigureModelCache, object_model_cache: FigureModelCache, + ship_model_cache: FigureModelCache, golem_model_cache: FigureModelCache, states: FigureMgrStates, } @@ -353,6 +365,7 @@ impl FigureMgr { biped_large_model_cache: FigureModelCache::new(), biped_small_model_cache: FigureModelCache::new(), object_model_cache: FigureModelCache::new(), + ship_model_cache: FigureModelCache::new(), golem_model_cache: FigureModelCache::new(), states: FigureMgrStates::default(), } @@ -384,6 +397,7 @@ impl FigureMgr { self.biped_small_model_cache .clean(&mut self.col_lights, tick); self.object_model_cache.clean(&mut self.col_lights, tick); + self.ship_model_cache.clean(&mut self.col_lights, tick); self.golem_model_cache.clean(&mut self.col_lights, tick); } @@ -4088,6 +4102,79 @@ impl FigureMgr { _ => target_base, }; + state.skeleton = anim::vek::Lerp::lerp(&state.skeleton, &target_bones, dt_lerp); + state.update( + renderer, + pos.0, + ori, + scale, + col, + dt, + state_animation_rate, + model, + lpindex, + true, + is_player, + camera, + &mut update_buf, + terrain, + ); + }, + Body::Ship(body) => { + let (model, skeleton_attr) = self.ship_model_cache.get_or_create_model( + renderer, + &mut self.col_lights, + *body, + inventory, + tick, + player_camera_mode, + player_character_state, + scene_data.runtime, + ); + + let state = + self.states.ship_states.entry(entity).or_insert_with(|| { + FigureState::new(renderer, ShipSkeleton::default()) + }); + + let (character, last_character) = match (character, last_character) { + (Some(c), Some(l)) => (c, l), + _ => (&CharacterState::Idle, &Last { + 0: CharacterState::Idle, + }), + }; + + if !character.same_variant(&last_character.0) { + state.state_time = 0.0; + } + + let target_base = match ( + physics.on_ground, + vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving + physics.in_liquid.is_some(), // In water + ) { + // Standing + (true, false, false) => anim::ship::IdleAnimation::update_skeleton( + &ShipSkeleton::default(), + (active_tool_kind, second_tool_kind, time), + state.state_time, + &mut state_animation_rate, + skeleton_attr, + ), + _ => anim::ship::IdleAnimation::update_skeleton( + &ShipSkeleton::default(), + (active_tool_kind, second_tool_kind, time), + state.state_time, + &mut state_animation_rate, + skeleton_attr, + ), + }; + + let target_bones = match &character { + // TODO! + _ => target_base, + }; + state.skeleton = anim::vek::Lerp::lerp(&state.skeleton, &target_bones, dt_lerp); state.update( renderer, @@ -4313,6 +4400,7 @@ impl FigureMgr { biped_large_model_cache, biped_small_model_cache, object_model_cache, + ship_model_cache, golem_model_cache, states: FigureMgrStates { @@ -4330,6 +4418,7 @@ impl FigureMgr { biped_small_states, golem_states, object_states, + ship_states, }, } = self; let col_lights = &*col_lights_; @@ -4572,6 +4661,23 @@ impl FigureMgr { ), ) }), + Body::Ship(body) => ship_states + .get(&entity) + .filter(|state| filter_state(&*state)) + .map(move |state| { + ( + state.locals(), + state.bone_consts(), + ship_model_cache.get_model( + col_lights, + *body, + inventory, + tick, + player_camera_mode, + character_state, + ), + ) + }), } { let model_entry = model_entry?; diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 822b3d6156..8bcbffd4bf 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -1562,7 +1562,7 @@ fn under_cursor( let player_cylinder = Cylinder::from_components( player_pos, scales.get(player_entity).copied(), - colliders.get(player_entity).copied(), + colliders.get(player_entity).cloned(), char_states.get(player_entity), ); let terrain = client.state().terrain(); @@ -1643,7 +1643,7 @@ fn under_cursor( let target_cylinder = Cylinder::from_components( p, scales.get(*e).copied(), - colliders.get(*e).copied(), + colliders.get(*e).cloned(), char_states.get(*e), ); @@ -1706,7 +1706,7 @@ fn select_interactable( let player_cylinder = Cylinder::from_components( player_pos, scales.get(player_entity).copied(), - colliders.get(player_entity).copied(), + colliders.get(player_entity).cloned(), char_states.get(player_entity), ); @@ -1720,7 +1720,7 @@ fn select_interactable( .join() .filter(|(e, _, _, _, _)| *e != player_entity) .map(|(e, p, s, c, cs)| { - let cylinder = Cylinder::from_components(p.0, s.copied(), c.copied(), cs); + let cylinder = Cylinder::from_components(p.0, s.copied(), c.cloned(), cs); (e, cylinder) }) // Roughly filter out entities farther than interaction distance From 711509d0c110699f756481c0cd7254db7d32c9cd Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Thu, 11 Mar 2021 17:27:03 -0500 Subject: [PATCH 02/41] Airship progress: now possessable, and physics kind of works (desyncs from the visuals by a shift + scale, and rotation isn't done at all yet, but the contours are correct). --- assets/server/manifests/ship_manifest.ron | 11 +- assets/server/voxel/Human_Airship.vox | Bin 88100 -> 78024 bytes assets/voxygen/voxel/object/Human_Airship.vox | Bin 78024 -> 39 bytes common/src/comp/body/ship.rs | 57 +- common/src/comp/phys.rs | 4 +- common/src/volumes/dyna.rs | 10 + common/sys/src/phys.rs | 525 +++++++++--------- server/src/cmd.rs | 7 +- server/src/state_ext.rs | 16 +- voxygen/src/scene/figure/mod.rs | 8 +- 10 files changed, 356 insertions(+), 282 deletions(-) mode change 100644 => 120000 assets/voxygen/voxel/object/Human_Airship.vox diff --git a/assets/server/manifests/ship_manifest.ron b/assets/server/manifests/ship_manifest.ron index 42555fc051..4ccbe5fcc6 100644 --- a/assets/server/manifests/ship_manifest.ron +++ b/assets/server/manifests/ship_manifest.ron @@ -1,16 +1,17 @@ ({ DefaultAirship: ( bone0: ( - offset: (-20.0, -35.0, 1.0), + //offset: (-20.0, -35.0, 1.0), + offset: (3.0, 7.0, 1.0), central: ("object.Human_Airship"), ), bone1: ( - offset: (0.0, 0.0, 0.0), - central: ("propeller-l"), + offset: (0.0, 40.0, -8.0), + central: ("object.propeller-l"), ), bone2: ( - offset: (0.0, 0.0, 0.0), - central: ("propeller-r"), + offset: (0.0, 0.0, -4.0), + central: ("object.propeller-r"), ), ), }) diff --git a/assets/server/voxel/Human_Airship.vox b/assets/server/voxel/Human_Airship.vox index 687a8f45b3a6d2c4c65dabcfd9f2d4bb11449d16..e167e99d3d375b8322f7f13ed4381e454ec401bf 100644 GIT binary patch literal 78024 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 literal 88100 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 diff --git a/assets/voxygen/voxel/object/Human_Airship.vox b/assets/voxygen/voxel/object/Human_Airship.vox deleted file mode 100644 index 88e79f59a7a92962a6b023d576accc3a96ed2bcf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 78024 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 Y&;F$Em%~44c6t0yn|*KbpWZ+GA6s@|!vFvP diff --git a/assets/voxygen/voxel/object/Human_Airship.vox b/assets/voxygen/voxel/object/Human_Airship.vox new file mode 120000 index 0000000000..9995596018 --- /dev/null +++ b/assets/voxygen/voxel/object/Human_Airship.vox @@ -0,0 +1 @@ +../../../server/voxel/Human_Airship.vox \ No newline at end of file diff --git a/common/src/comp/body/ship.rs b/common/src/comp/body/ship.rs index ef2e93e113..f607cf2ae6 100644 --- a/common/src/comp/body/ship.rs +++ b/common/src/comp/body/ship.rs @@ -1,6 +1,4 @@ -use crate::{ - make_case_elim -}; +use crate::make_case_elim; use serde::{Deserialize, Serialize}; make_case_elim!( @@ -17,22 +15,29 @@ impl From for super::Body { } impl Body { - pub fn manifest_id(&self) -> &'static str { + pub fn manifest_entry(&self) -> &'static str { match self { - Body::DefaultAirship => "server.manifests.ship_manifest", + Body::DefaultAirship => "object.Human_Airship", } } } -/// Duplicate of some of the things defined in `voxygen::scene::figure::load` to avoid having to -/// refactor all of that to `common` for using voxels as collider geometry +/// Duplicate of some of the things defined in `voxygen::scene::figure::load` to +/// avoid having to refactor all of that to `common` for using voxels as +/// collider geometry pub mod figuredata { use crate::{ assets::{self, AssetExt, AssetHandle, DotVoxAsset, Ron}, - volumes::dyna::Dyna, + figure::cell::Cell, + terrain::{ + block::{Block, BlockKind}, + sprite::SpriteKind, + }, + volumes::dyna::{ColumnAccess, Dyna}, }; - use serde::Deserialize; use hashbrown::HashMap; + use lazy_static::lazy_static; + use serde::Deserialize; #[derive(Deserialize)] pub struct VoxSimple(pub String); @@ -57,13 +62,43 @@ pub mod figuredata { #[derive(Clone)] pub struct ShipSpec { pub central: AssetHandle>, + pub voxes: HashMap>, } impl assets::Compound for ShipSpec { - fn load(_: &assets::AssetCache, _: &str) -> Result { + fn load( + cache: &assets::AssetCache, + _: &str, + ) -> Result { + let manifest: AssetHandle> = AssetExt::load("server.manifests.ship_manifest")?; + let mut voxes = HashMap::new(); + for (_, spec) in (manifest.read().0).0.iter() { + for bone in [&spec.bone0, &spec.bone1, &spec.bone2].iter() { + // TODO: avoid the requirement for symlinks in "voxygen.voxel.object.", and load + // the models from "server.voxel." instead + let vox = + cache.load::(&["voxygen.voxel.", &bone.central.0].concat())?; + let dyna = Dyna::::from_vox(&vox.read().0, false); + voxes.insert(bone.central.0.clone(), dyna.map_into(|cell| { + if let Some(rgb) = cell.get_color() { + Block::new(BlockKind::Misc, rgb) + } else { + Block::air(SpriteKind::Empty) + } + })); + } + } Ok(ShipSpec { - central: AssetExt::load("server.manifests.ship_manifest")? + central: manifest, + voxes, }) } } + + lazy_static! { + // TODO: load this from the ECS as a resource, and maybe make it more general than ships + // (although figuring out how to keep the figure bones in sync with the terrain offsets seems + // like a hard problem if they're not the same manifest) + pub static ref VOXEL_COLLIDER_MANIFEST: ShipSpec = AssetExt::load_expect_cloned("server.manifests.ship_manifest"); + } } diff --git a/common/src/comp/phys.rs b/common/src/comp/phys.rs index 3e06418792..b10978cc58 100644 --- a/common/src/comp/phys.rs +++ b/common/src/comp/phys.rs @@ -68,7 +68,7 @@ pub enum Collider { impl Collider { pub fn get_radius(&self) -> f32 { match self { - Collider::Voxel { .. } => 0.0, + Collider::Voxel { .. } => 1.0, Collider::Box { radius, .. } => *radius, Collider::Point => 0.0, } @@ -76,7 +76,7 @@ impl Collider { pub fn get_z_limits(&self, modifier: f32) -> (f32, f32) { match self { - Collider::Voxel { .. } => (0.0, 0.0), + Collider::Voxel { .. } => (0.0, 1.0), Collider::Box { z_min, z_max, .. } => (*z_min * modifier, *z_max * modifier), Collider::Point => (0.0, 0.0), } diff --git a/common/src/volumes/dyna.rs b/common/src/volumes/dyna.rs index 3e736c5db1..d0648010a8 100644 --- a/common/src/volumes/dyna.rs +++ b/common/src/volumes/dyna.rs @@ -44,6 +44,16 @@ impl Dyna { None } } + + pub fn map_into W>(self, f: F) -> Dyna { + let Dyna { vox, meta, sz, _phantom } = self; + Dyna { + vox: vox.into_iter().map(f).collect(), + meta, + sz, + _phantom, + } + } } impl BaseVol for Dyna { diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index 8545888c7e..47c6abfd1e 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -1,7 +1,7 @@ use common::{ comp::{ BeamSegment, CharacterState, Collider, Gravity, Mass, Mounting, Ori, PhysicsState, Pos, - PreviousPhysCache, Projectile, Scale, Shockwave, Sticky, Vel, + PreviousPhysCache, Projectile, Scale, Shockwave, Sticky, Vel, body::ship::figuredata::VOXEL_COLLIDER_MANIFEST, }, consts::{FRIC_GROUND, GRAVITY}, event::{EventBus, ServerEvent}, @@ -15,8 +15,9 @@ use common_ecs::{Job, Origin, ParMode, Phase, PhysicsMetrics, System}; use hashbrown::HashMap; use rayon::iter::ParallelIterator; use specs::{ - shred::{World, ResourceId}, - Entities, Entity, Join, ParJoin, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage, SystemData, + shred::{ResourceId, World}, + Entities, Entity, Join, ParJoin, Read, ReadExpect, ReadStorage, SystemData, WriteExpect, + WriteStorage, }; use std::ops::Range; use vek::*; @@ -114,7 +115,9 @@ impl<'a> PhysicsSystemData<'a> { ) .join() { - let _ = self.w.physics_states + let _ = self + .w + .physics_states .entry(entity) .map(|e| e.or_insert_with(Default::default)); } @@ -137,13 +140,16 @@ impl<'a> PhysicsSystemData<'a> { .map(|(e, _, _, _, _, _, _)| e) .collect::>() { - let _ = self.w.previous_phys_cache.insert(entity, PreviousPhysCache { - velocity_dt: Vec3::zero(), - center: Vec3::zero(), - collision_boundary: 0.0, - scale: 0.0, - scaled_radius: 0.0, - }); + let _ = self + .w + .previous_phys_cache + .insert(entity, PreviousPhysCache { + velocity_dt: Vec3::zero(), + center: Vec3::zero(), + collision_boundary: 0.0, + scale: 0.0, + scaled_radius: 0.0, + }); } //Update PreviousPhysCache @@ -180,10 +186,14 @@ impl<'a> PhysicsSystemData<'a> { } drop(guard); } + fn apply_pushback(&mut self, job: &mut Job) { span!(guard, "Apply pushback"); job.cpu_stats.measure(ParMode::Rayon); - let PhysicsSystemData { r: ref psdr, w: ref mut psdw } = self; + let PhysicsSystemData { + r: ref psdr, + w: ref mut psdw, + } = self; let (positions, previous_phys_cache) = (&psdw.positions, &psdw.previous_phys_cache); let metrics = ( &psdr.entities, @@ -316,8 +326,13 @@ impl<'a> PhysicsSystemData<'a> { entity_entity_collisions += 1; } - // Don't apply repulsive force to projectiles - if diff.magnitude_squared() > 0.0 && !is_projectile { + // Don't apply repulsive force to projectiles or if we're colliding + // with a terrain-like entity, or if we are a terrain-like entity + if diff.magnitude_squared() > 0.0 + && !is_projectile + && !matches!(collider_other, Some(Collider::Voxel { .. })) + && !matches!(collider, Some(Collider::Voxel { .. })) + { let force = 400.0 * (collision_dist - diff.magnitude()) * mass_other / (mass + mass_other); @@ -346,265 +361,273 @@ impl<'a> PhysicsSystemData<'a> { entity_entity_collisions: old.entity_entity_collisions + new.entity_entity_collisions, }); - psdw.physics_metrics.entity_entity_collision_checks = metrics.entity_entity_collision_checks; + psdw.physics_metrics.entity_entity_collision_checks = + metrics.entity_entity_collision_checks; psdw.physics_metrics.entity_entity_collisions = metrics.entity_entity_collisions; drop(guard); } fn handle_movement_and_terrain(&mut self, job: &mut Job) { - let PhysicsSystemData { r: ref psdr, w: ref mut psdw } = self; + let PhysicsSystemData { + r: ref psdr, + w: ref mut psdw, + } = self; // Apply movement inputs span!(guard, "Apply movement and terrain collision"); let (positions, previous_phys_cache) = (&psdw.positions, &psdw.previous_phys_cache); - let (pos_writes, land_on_grounds) = - ( - &psdr.entities, - psdr.scales.maybe(), - psdr.stickies.maybe(), - &psdr.colliders, - positions, - &mut psdw.velocities, - &psdw.orientations, - &mut psdw.physics_states, - previous_phys_cache, - !&psdr.mountings, - ) - .par_join() - .fold( - || (Vec::new(), Vec::new()), - |(mut pos_writes, mut land_on_grounds), - ( - entity, - scale, - sticky, - collider, - pos, - mut vel, - _ori, - mut physics_state, - previous_cache, - _, - )| { - // defer the writes of positions to allow an inner loop over terrain-like - // entities - let old_pos = *pos; - let mut pos = *pos; - if sticky.is_some() && physics_state.on_surface().is_some() { - vel.0 = Vec3::zero(); - return (pos_writes, land_on_grounds); - } + let (pos_writes, land_on_grounds) = ( + &psdr.entities, + psdr.scales.maybe(), + psdr.stickies.maybe(), + &psdr.colliders, + positions, + &mut psdw.velocities, + &psdw.orientations, + &mut psdw.physics_states, + previous_phys_cache, + !&psdr.mountings, + ) + .par_join() + .fold( + || (Vec::new(), Vec::new()), + |(mut pos_writes, mut land_on_grounds), + ( + entity, + scale, + sticky, + collider, + pos, + mut vel, + _ori, + mut physics_state, + previous_cache, + _, + )| { + // defer the writes of positions to allow an inner loop over terrain-like + // entities + let old_pos = *pos; + let mut pos = *pos; + if sticky.is_some() && physics_state.on_surface().is_some() { + vel.0 = Vec3::zero(); + return (pos_writes, land_on_grounds); + } - let scale = if let Collider::Voxel { .. } = collider { - scale.map(|s| s.0).unwrap_or(1.0) + let scale = if let Collider::Voxel { .. } = collider { + scale.map(|s| s.0).unwrap_or(1.0) + } else { + // TODO: Use scale & actual proportions when pathfinding is good + // enough to manage irregular entity sizes + 1.0 + }; + + let old_vel = *vel; + // Integrate forces + // Friction is assumed to be a constant dependent on location + let friction = FRIC_AIR + .max(if physics_state.on_ground { + FRIC_GROUND } else { - // TODO: Use scale & actual proportions when pathfinding is good - // enough to manage irregular entity sizes - 1.0 - }; - - let old_vel = *vel; - // Integrate forces - // Friction is assumed to be a constant dependent on location - let friction = FRIC_AIR - .max(if physics_state.on_ground { - FRIC_GROUND - } else { - 0.0 - }) - .max(if physics_state.in_liquid.is_some() { - FRIC_FLUID - } else { - 0.0 - }); - let in_loaded_chunk = psdr.terrain - .get_key(psdr.terrain.pos_key(pos.0.map(|e| e.floor() as i32))) - .is_some(); - let downward_force = - if !in_loaded_chunk { - 0.0 // No gravity in unloaded chunks - } else if physics_state - .in_liquid - .map(|depth| depth > 0.75) - .unwrap_or(false) - { - (1.0 - BOUYANCY) * GRAVITY - } else { - GRAVITY - } * psdr.gravities.get(entity).map(|g| g.0).unwrap_or_default(); - vel.0 = integrate_forces(psdr.dt.0, vel.0, downward_force, friction); - - // Don't move if we're not in a loaded chunk - let pos_delta = if in_loaded_chunk { - // this is an approximation that allows most framerates to - // behave in a similar manner. - let dt_lerp = 0.2; - (vel.0 * dt_lerp + old_vel.0 * (1.0 - dt_lerp)) * psdr.dt.0 + 0.0 + }) + .max(if physics_state.in_liquid.is_some() { + FRIC_FLUID } else { - Vec3::zero() - }; - - match &*collider { - Collider::Voxel { .. } => { - // for now, treat entities with voxel colliders as their bounding - // cylinders for the purposes of colliding them with terrain - let radius = collider.get_radius() * scale; - let (z_min, z_max) = collider.get_z_limits(scale); - - let cylinder = (radius, z_min, z_max); - cylinder_voxel_collision( - cylinder, - &*psdr.terrain, - entity, - &mut pos, - pos_delta, - vel, - &mut physics_state, - &mut land_on_grounds, - ); - }, - Collider::Box { - radius, - z_min, - z_max, - } => { - // Scale collider - let radius = radius.min(0.45) * scale; - let z_min = *z_min * scale; - let z_max = z_max.clamped(1.2, 1.95) * scale; - - let cylinder = (radius, z_min, z_max); - cylinder_voxel_collision( - cylinder, - &*psdr.terrain, - entity, - &mut pos, - pos_delta, - vel, - &mut physics_state, - &mut land_on_grounds, - ); - }, - Collider::Point => { - let (dist, block) = psdr.terrain - .ray(pos.0, pos.0 + pos_delta) - .until(|block: &Block| block.is_filled()) - .ignore_error() - .cast(); - - pos.0 += pos_delta.try_normalized().unwrap_or(Vec3::zero()) * dist; - - // Can't fail since we do ignore_error above - if block.unwrap().is_some() { - let block_center = pos.0.map(|e| e.floor()) + 0.5; - let block_rpos = (pos.0 - block_center) - .try_normalized() - .unwrap_or(Vec3::zero()); - - // See whether we're on the top/bottom of a block, or the side - if block_rpos.z.abs() - > block_rpos.xy().map(|e| e.abs()).reduce_partial_max() - { - if block_rpos.z > 0.0 { - physics_state.on_ground = true; - } else { - physics_state.on_ceiling = true; - } - vel.0.z = 0.0; - } else { - physics_state.on_wall = - Some(if block_rpos.x.abs() > block_rpos.y.abs() { - vel.0.x = 0.0; - Vec3::unit_x() * -block_rpos.x.signum() - } else { - vel.0.y = 0.0; - Vec3::unit_y() * -block_rpos.y.signum() - }); - } - } - - physics_state.in_liquid = psdr.terrain - .get(pos.0.map(|e| e.floor() as i32)) - .ok() - .and_then(|vox| vox.is_liquid().then_some(1.0)); - }, - } - - // Collide with terrain-like entities - for ( - entity_other, - other, - pos_other, - previous_cache_other, - mass_other, - collider_other, - _, - _, - _, - _, - char_state_other_maybe, - ) in ( - &psdr.entities, - &psdr.uids, - positions, - previous_phys_cache, - psdr.masses.maybe(), - &psdr.colliders, - !&psdr.projectiles, - !&psdr.mountings, - !&psdr.beams, - !&psdr.shockwaves, - psdr.char_states.maybe(), - ) - .join() + 0.0 + }); + let in_loaded_chunk = psdr + .terrain + .get_key(psdr.terrain.pos_key(pos.0.map(|e| e.floor() as i32))) + .is_some(); + let downward_force = + if !in_loaded_chunk { + 0.0 // No gravity in unloaded chunks + } else if physics_state + .in_liquid + .map(|depth| depth > 0.75) + .unwrap_or(false) { - let collision_boundary = previous_cache.collision_boundary - + previous_cache_other.collision_boundary; - if previous_cache - .center - .distance_squared(previous_cache_other.center) - > collision_boundary.powi(2) - || entity == entity_other - { - continue; + (1.0 - BOUYANCY) * GRAVITY + } else { + GRAVITY + } * psdr.gravities.get(entity).map(|g| g.0).unwrap_or_default(); + vel.0 = integrate_forces(psdr.dt.0, vel.0, downward_force, friction); + + // Don't move if we're not in a loaded chunk + let pos_delta = if in_loaded_chunk { + // this is an approximation that allows most framerates to + // behave in a similar manner. + let dt_lerp = 0.2; + (vel.0 * dt_lerp + old_vel.0 * (1.0 - dt_lerp)) * psdr.dt.0 + } else { + Vec3::zero() + }; + + match &*collider { + Collider::Voxel { .. } => { + // for now, treat entities with voxel colliders as their bounding + // cylinders for the purposes of colliding them with terrain + let radius = collider.get_radius() * scale; + let (z_min, z_max) = collider.get_z_limits(scale); + + let cylinder = (radius, z_min, z_max); + cylinder_voxel_collision( + cylinder, + &*psdr.terrain, + entity, + &mut pos, + pos_delta, + vel, + &mut physics_state, + &mut land_on_grounds, + ); + }, + Collider::Box { + radius, + z_min, + z_max, + } => { + // Scale collider + let radius = radius.min(0.45) * scale; + let z_min = *z_min * scale; + let z_max = z_max.clamped(1.2, 1.95) * scale; + + let cylinder = (radius, z_min, z_max); + cylinder_voxel_collision( + cylinder, + &*psdr.terrain, + entity, + &mut pos, + pos_delta, + vel, + &mut physics_state, + &mut land_on_grounds, + ); + }, + Collider::Point => { + let (dist, block) = psdr + .terrain + .ray(pos.0, pos.0 + pos_delta) + .until(|block: &Block| block.is_filled()) + .ignore_error() + .cast(); + + pos.0 += pos_delta.try_normalized().unwrap_or(Vec3::zero()) * dist; + + // Can't fail since we do ignore_error above + if block.unwrap().is_some() { + let block_center = pos.0.map(|e| e.floor()) + 0.5; + let block_rpos = (pos.0 - block_center) + .try_normalized() + .unwrap_or(Vec3::zero()); + + // See whether we're on the top/bottom of a block, or the side + if block_rpos.z.abs() + > block_rpos.xy().map(|e| e.abs()).reduce_partial_max() + { + if block_rpos.z > 0.0 { + physics_state.on_ground = true; + } else { + physics_state.on_ceiling = true; + } + vel.0.z = 0.0; + } else { + physics_state.on_wall = + Some(if block_rpos.x.abs() > block_rpos.y.abs() { + vel.0.x = 0.0; + Vec3::unit_x() * -block_rpos.x.signum() + } else { + vel.0.y = 0.0; + Vec3::unit_y() * -block_rpos.y.signum() + }); + } } - if let Collider::Voxel { id } = collider_other { - // use bounding cylinder regardless of our collider - // TODO: extract point-terrain collision above to its own function - let radius = collider.get_radius() * scale; - let (z_min, z_max) = collider.get_z_limits(scale); + physics_state.in_liquid = psdr + .terrain + .get(pos.0.map(|e| e.floor() as i32)) + .ok() + .and_then(|vox| vox.is_liquid().then_some(1.0)); + }, + } - let cylinder = (radius, z_min, z_max); - // TODO: load .vox into a Dyna, and use it (appropriately rotated) - // as the terrain - /*cylinder_voxel_collision( + // Collide with terrain-like entities + for ( + entity_other, + other, + pos_other, + previous_cache_other, + mass_other, + collider_other, + _, + _, + _, + _, + char_state_other_maybe, + ) in ( + &psdr.entities, + &psdr.uids, + positions, + previous_phys_cache, + psdr.masses.maybe(), + &psdr.colliders, + !&psdr.projectiles, + !&psdr.mountings, + !&psdr.beams, + !&psdr.shockwaves, + psdr.char_states.maybe(), + ) + .join() + { + /*let collision_boundary = previous_cache.collision_boundary + + previous_cache_other.collision_boundary; + if previous_cache + .center + .distance_squared(previous_cache_other.center) + > collision_boundary.powi(2) + || entity == entity_other + { + continue; + }*/ + + if let Collider::Voxel { id } = collider_other { + // use bounding cylinder regardless of our collider + // TODO: extract point-terrain collision above to its own function + let radius = collider.get_radius() * scale; + let (z_min, z_max) = collider.get_z_limits(scale); + + pos.0 -= pos_other.0; + let cylinder = (radius, z_min, z_max); + if let Some(dyna) = VOXEL_COLLIDER_MANIFEST.voxes.get(id) { + cylinder_voxel_collision( cylinder, - &*psdr.terrain, + &*dyna, entity, &mut pos, pos_delta, vel, &mut physics_state, &mut land_on_grounds, - );*/ + ); } + pos.0 += pos_other.0; } - if pos != old_pos { - pos_writes.push((entity, pos)); - } + } + if pos != old_pos { + pos_writes.push((entity, pos)); + } - (pos_writes, land_on_grounds) - }, - ) - .reduce( - || (Vec::new(), Vec::new()), - |(mut pos_writes_a, mut land_on_grounds_a), - (mut pos_writes_b, mut land_on_grounds_b)| { - pos_writes_a.append(&mut pos_writes_b); - land_on_grounds_a.append(&mut land_on_grounds_b); - (pos_writes_a, land_on_grounds_a) - }, - ); + (pos_writes, land_on_grounds) + }, + ) + .reduce( + || (Vec::new(), Vec::new()), + |(mut pos_writes_a, mut land_on_grounds_a), + (mut pos_writes_b, mut land_on_grounds_b)| { + pos_writes_a.append(&mut pos_writes_b); + land_on_grounds_a.append(&mut land_on_grounds_b); + (pos_writes_a, land_on_grounds_a) + }, + ); drop(guard); job.cpu_stats.measure(ParMode::Single); @@ -631,10 +654,7 @@ impl<'a> System<'a> for Sys { #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 #[allow(clippy::blocks_in_if_conditions)] // TODO: Pending review in #587 - fn run( - job: &mut Job, - mut psd: Self::SystemData, - ) { + fn run(job: &mut Job, mut psd: Self::SystemData) { psd.reset(); // Apply pushback @@ -653,7 +673,6 @@ impl<'a> System<'a> for Sys { psd.maintain_pushback_cache(); psd.apply_pushback(job); - psd.handle_movement_and_terrain(job); } } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 7e6dc4c454..359bc42413 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -993,11 +993,12 @@ fn handle_spawn_airship( _action: &ChatCommand, ) { match server.state.read_component_copied::(target) { - Some(pos) => { + Some(mut pos) => { + pos.0.z += 50.0; server .state - .create_ship(pos, comp::ship::Body::DefaultAirship) - .with(comp::Scale(50.0)) + .create_ship(pos, comp::ship::Body::DefaultAirship, 1) + .with(comp::Scale(11.0)) .with(LightEmitter { col: Rgb::new(1.0, 0.65, 0.2), strength: 2.0, diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 770b6d28b0..0fdb8c124f 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -41,7 +41,7 @@ pub trait StateExt { ) -> EcsEntityBuilder; /// Build a static object entity fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder; - fn create_ship(&mut self, pos: comp::Pos, object: comp::ship::Body) -> EcsEntityBuilder; + fn create_ship(&mut self, pos: comp::Pos, object: comp::ship::Body, level: u16) -> EcsEntityBuilder; /// Build a projectile fn create_projectile( &mut self, @@ -216,16 +216,24 @@ impl StateExt for State { .with(comp::Gravity(1.0)) } - fn create_ship(&mut self, pos: comp::Pos, object: comp::ship::Body) -> EcsEntityBuilder { + fn create_ship(&mut self, pos: comp::Pos, ship: comp::ship::Body, level: u16) -> EcsEntityBuilder { self.ecs_mut() .create_entity_synced() .with(pos) .with(comp::Vel(Vec3::zero())) .with(comp::Ori::default()) .with(comp::Mass(50.0)) - .with(comp::Collider::Voxel { id: object.manifest_id().to_string() }) - .with(comp::Body::Ship(object)) + .with(comp::Collider::Voxel { id: ship.manifest_entry().to_string() }) + .with(comp::Body::Ship(ship)) .with(comp::Gravity(1.0)) + .with(comp::Controller::default()) + .with(comp::inventory::Inventory::new_empty()) + .with(comp::CharacterState::default()) + .with(comp::Energy::new(ship.into(), level)) + .with(comp::Health::new(ship.into(), level)) + .with(comp::Stats::new("Airship".to_string())) + .with(comp::Buffs::default()) + .with(comp::Combo::default()) } fn create_projectile( diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 12281f4edf..2bba21b833 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -4231,7 +4231,7 @@ impl FigureMgr { .join() // Don't render dead entities .filter(|(_, _, _, _, health, _, _)| health.map_or(true, |h| !h.is_dead)) - .for_each(|(entity, pos, _, body, _, inventory, _)| { + .for_each(|(entity, pos, _, body, _, inventory, scale)| { if let Some((locals, bone_consts, model, _)) = self.get_model_for_render( tick, camera, @@ -4241,7 +4241,7 @@ impl FigureMgr { inventory, false, pos.0, - figure_lod_render_distance, + figure_lod_render_distance * scale.map_or(1.0, |s| s.0), |state| state.can_shadow_sun(), ) { renderer.render_figure_shadow_directed( @@ -4273,7 +4273,7 @@ impl FigureMgr { let character_state_storage = state.read_storage::(); let character_state = character_state_storage.get(player_entity); - for (entity, pos, _, body, _, inventory, _) in ( + for (entity, pos, _, body, _, inventory, scale) in ( &ecs.entities(), &ecs.read_storage::(), ecs.read_storage::().maybe(), @@ -4298,7 +4298,7 @@ impl FigureMgr { inventory, false, pos.0, - figure_lod_render_distance, + figure_lod_render_distance * scale.map_or(1.0, |s| s.0), |state| state.visible(), ) { renderer.render_figure(model, &col_lights, global, locals, bone_consts, lod); From c5c9855ab856d3b0bb52fc1209672e2a0adb7e58 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Thu, 11 Mar 2021 19:01:16 -0500 Subject: [PATCH 03/41] Airship fixes: figure culling, player hitbox bounds w.r.t. airships, physics state unioning. --- assets/server/manifests/ship_manifest.ron | 3 ++- common/sys/src/phys.rs | 32 +++++++++++++++++++---- server/src/cmd.rs | 2 +- voxygen/src/scene/figure/mod.rs | 1 + 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/assets/server/manifests/ship_manifest.ron b/assets/server/manifests/ship_manifest.ron index 4ccbe5fcc6..e8e96539ca 100644 --- a/assets/server/manifests/ship_manifest.ron +++ b/assets/server/manifests/ship_manifest.ron @@ -2,7 +2,8 @@ DefaultAirship: ( bone0: ( //offset: (-20.0, -35.0, 1.0), - offset: (3.0, 7.0, 1.0), + //offset: (3.0, 7.0, 1.0), + offset: (0.25, 0.25, 0.25), central: ("object.Human_Airship"), ), bone1: ( diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index 47c6abfd1e..16d6af3a39 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -1,7 +1,8 @@ use common::{ comp::{ - BeamSegment, CharacterState, Collider, Gravity, Mass, Mounting, Ori, PhysicsState, Pos, - PreviousPhysCache, Projectile, Scale, Shockwave, Sticky, Vel, body::ship::figuredata::VOXEL_COLLIDER_MANIFEST, + body::ship::figuredata::VOXEL_COLLIDER_MANIFEST, BeamSegment, CharacterState, Collider, + Gravity, Mass, Mounting, Ori, PhysicsState, Pos, PreviousPhysCache, Projectile, Scale, + Shockwave, Sticky, Vel, }, consts::{FRIC_GROUND, GRAVITY}, event::{EventBus, ServerEvent}, @@ -592,9 +593,14 @@ impl<'a> PhysicsSystemData<'a> { if let Collider::Voxel { id } = collider_other { // use bounding cylinder regardless of our collider // TODO: extract point-terrain collision above to its own function - let radius = collider.get_radius() * scale; - let (z_min, z_max) = collider.get_z_limits(scale); + let radius = collider.get_radius(); + let (z_min, z_max) = collider.get_z_limits(1.0); + let radius = radius.min(0.45) * scale; + let z_min = z_min * scale; + let z_max = z_max.clamped(1.2, 1.95) * scale; + + let mut physics_state_delta = physics_state.clone(); pos.0 -= pos_other.0; let cylinder = (radius, z_min, z_max); if let Some(dyna) = VOXEL_COLLIDER_MANIFEST.voxes.get(id) { @@ -605,11 +611,27 @@ impl<'a> PhysicsSystemData<'a> { &mut pos, pos_delta, vel, - &mut physics_state, + &mut physics_state_delta, &mut land_on_grounds, ); } pos.0 += pos_other.0; + // union in the state updates, so that the state isn't just based on + // the most recent terrain that collision was attempted with + physics_state.on_ground |= physics_state_delta.on_ground; + physics_state.on_ceiling |= physics_state_delta.on_ceiling; + physics_state.on_wall = + physics_state.on_wall.or(physics_state_delta.on_wall); + physics_state + .touch_entities + .append(&mut physics_state_delta.touch_entities); + physics_state.in_liquid = + match (physics_state.in_liquid, physics_state_delta.in_liquid) { + // this match computes `x <|> y <|> liftA2 max x y` + (Some(x), Some(y)) => Some(x.max(y)), + (_, y @ Some(_)) => y, + _ => None, + }; } } if pos != old_pos { diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 359bc42413..4982592e3e 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -998,7 +998,7 @@ fn handle_spawn_airship( server .state .create_ship(pos, comp::ship::Body::DefaultAirship, 1) - .with(comp::Scale(11.0)) + .with(comp::Scale(11.0 / 0.8)) .with(LightEmitter { col: Rgb::new(1.0, 0.65, 0.2), strength: 2.0, diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 2bba21b833..f6ae55df31 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -687,6 +687,7 @@ impl FigureMgr { let (in_frustum, lpindex) = if let Some(mut meta) = state { let (in_frustum, lpindex) = BoundingSphere::new(pos.0.into_array(), radius) .coherent_test_against_frustum(frustum, meta.lpindex); + let in_frustum = in_frustum || matches!(body, Body::Ship(_)); meta.visible = in_frustum; meta.lpindex = lpindex; if in_frustum { From acf8b1b5cd5a3453b0f1acc18528bb339d246cef Mon Sep 17 00:00:00 2001 From: James Melkonian Date: Thu, 11 Mar 2021 18:58:57 -0800 Subject: [PATCH 04/41] Allow spawned airships to move --- assets/voxygen/voxel/object/Human_Airship.vox | 4 +- assets/voxygen/voxel/object/airship.vox | 4 +- assets/voxygen/voxel/object/propeller-l.vox | 4 +- assets/voxygen/voxel/object/propeller-r.vox | 4 +- common/src/cmd.rs | 2 +- common/src/comp/agent.rs | 9 +++ common/src/rtsim.rs | 6 ++ server/src/cmd.rs | 7 ++- server/src/rtsim/entity.rs | 5 ++ server/src/state_ext.rs | 59 +++++++++++++------ server/src/sys/agent.rs | 2 +- 11 files changed, 78 insertions(+), 28 deletions(-) diff --git a/assets/voxygen/voxel/object/Human_Airship.vox b/assets/voxygen/voxel/object/Human_Airship.vox index 9995596018..36e3758d03 120000 --- a/assets/voxygen/voxel/object/Human_Airship.vox +++ b/assets/voxygen/voxel/object/Human_Airship.vox @@ -1 +1,3 @@ -../../../server/voxel/Human_Airship.vox \ No newline at end of file +version https://git-lfs.github.com/spec/v1 +oid sha256:ba02746d73ebf853c0511b673510c09bd47e3ab0fff13d936feb181a8378ebd9 +size 78024 diff --git a/assets/voxygen/voxel/object/airship.vox b/assets/voxygen/voxel/object/airship.vox index 3479493953..06bebaa938 120000 --- a/assets/voxygen/voxel/object/airship.vox +++ b/assets/voxygen/voxel/object/airship.vox @@ -1 +1,3 @@ -../../../server/voxel/airship.vox \ No newline at end of file +version https://git-lfs.github.com/spec/v1 +oid sha256:86f317298900ea98f95c6a33192b25fbbcbd3ce5f105cad58ad3c595a7a7d9ee +size 70176 diff --git a/assets/voxygen/voxel/object/propeller-l.vox b/assets/voxygen/voxel/object/propeller-l.vox index a8105d8b1b..a193fa89ee 120000 --- a/assets/voxygen/voxel/object/propeller-l.vox +++ b/assets/voxygen/voxel/object/propeller-l.vox @@ -1 +1,3 @@ -../../../server/voxel/propeller-l.vox \ No newline at end of file +version https://git-lfs.github.com/spec/v1 +oid sha256:09ef4bad2557abcc5a2b938f21053babc7770ebe2333039aef9b98ba930b7ec7 +size 1584 diff --git a/assets/voxygen/voxel/object/propeller-r.vox b/assets/voxygen/voxel/object/propeller-r.vox index 647f3f66d0..5b940751e6 120000 --- a/assets/voxygen/voxel/object/propeller-r.vox +++ b/assets/voxygen/voxel/object/propeller-r.vox @@ -1 +1,3 @@ -../../../server/voxel/propeller-r.vox \ No newline at end of file +version https://git-lfs.github.com/spec/v1 +oid sha256:e4947977524b88bc5adfa934d9061a3499e94b960abb3bcf0a3e2aca482096dc +size 1584 diff --git a/common/src/cmd.rs b/common/src/cmd.rs index a74f0cab09..9aee158547 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -224,7 +224,7 @@ impl ChatCommand { "Temporarily gives a player admin permissions or removes them", Admin, ), - ChatCommand::Airship => cmd(vec![], "Spawns an airship", Admin), + ChatCommand::Airship => cmd(vec![Boolean("moving", "true".to_string(), Optional)], "Spawns an airship", Admin), ChatCommand::Alias => cmd(vec![Any("name", Required)], "Change your alias", NoAdmin), ChatCommand::Ban => cmd( vec![Any("username", Required), Message(Optional)], diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 9134e40091..eccf2ce9fa 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -222,6 +222,15 @@ impl Agent { self } + pub fn with_destination() -> Self { + Self { + can_speak: false, + psyche: Psyche { aggro: 1.0 }, + rtsim_controller: RtSimController::zero(), + ..Default::default() + } + } + pub fn new( patrol_origin: Option>, can_speak: bool, diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index e5c1268d2e..809a505b1f 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -46,4 +46,10 @@ impl Default for RtSimController { impl RtSimController { pub fn reset(&mut self) { *self = Self::default(); } + pub fn zero() -> Self { + Self { + travel_to: Some((Vec3::new(0.0, 0.0, 500.0), "".to_string())), + speed_factor: 0.05, + } + } } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 4982592e3e..0fbc8bcd0f 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -989,15 +989,16 @@ fn handle_spawn_airship( server: &mut Server, client: EcsEntity, target: EcsEntity, - _args: String, - _action: &ChatCommand, + args: String, + action: &ChatCommand, ) { + let moving = scan_fmt_some!(&args, &action.arg_fmt(), String).unwrap_or_else(|| "false".to_string()) == "true"; match server.state.read_component_copied::(target) { Some(mut pos) => { pos.0.z += 50.0; server .state - .create_ship(pos, comp::ship::Body::DefaultAirship, 1) + .create_ship(pos, comp::ship::Body::DefaultAirship, 1, moving) .with(comp::Scale(11.0 / 0.8)) .with(LightEmitter { col: Rgb::new(1.0, 0.65, 0.2), diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index a65a555890..2a8ef629d7 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -28,6 +28,9 @@ impl Entity { pub fn get_body(&self) -> comp::Body { match self.rng(PERM_GENUS).gen::() { //we want 50% birds, 50% humans for now + x if x < 0.05 => { + comp::Body::Ship(comp::ship::Body::DefaultAirship) + }, x if x < 0.50 => { let species = *(&comp::bird_medium::ALL_SPECIES) .choose(&mut self.rng(PERM_SPECIES)) @@ -53,6 +56,7 @@ impl Entity { comp::Body::BirdSmall(_) => "Warbler".to_string(), comp::Body::Dragon(b) => get_npc_name(&npc_names.dragon, b.species).to_string(), comp::Body::Humanoid(b) => get_npc_name(&npc_names.humanoid, b.species).to_string(), + comp::Body::Ship(_) => "Veloren Air".to_string(), //TODO: finish match as necessary _ => unimplemented!(), } @@ -131,6 +135,7 @@ impl Entity { .iter() .filter(|s| match self.get_body() { comp::Body::Humanoid(_) => s.1.is_settlement() | s.1.is_castle(), + comp::Body::Ship(_) => s.1.is_castle(), _ => s.1.is_dungeon(), }) .filter(|_| thread_rng().gen_range(0i32..4) == 0) diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 0fdb8c124f..787483df06 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -41,7 +41,7 @@ pub trait StateExt { ) -> EcsEntityBuilder; /// Build a static object entity fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder; - fn create_ship(&mut self, pos: comp::Pos, object: comp::ship::Body, level: u16) -> EcsEntityBuilder; + fn create_ship(&mut self, pos: comp::Pos, ship: comp::ship::Body, level: u16, moving: bool) -> EcsEntityBuilder; /// Build a projectile fn create_projectile( &mut self, @@ -216,24 +216,45 @@ impl StateExt for State { .with(comp::Gravity(1.0)) } - fn create_ship(&mut self, pos: comp::Pos, ship: comp::ship::Body, level: u16) -> EcsEntityBuilder { - self.ecs_mut() - .create_entity_synced() - .with(pos) - .with(comp::Vel(Vec3::zero())) - .with(comp::Ori::default()) - .with(comp::Mass(50.0)) - .with(comp::Collider::Voxel { id: ship.manifest_entry().to_string() }) - .with(comp::Body::Ship(ship)) - .with(comp::Gravity(1.0)) - .with(comp::Controller::default()) - .with(comp::inventory::Inventory::new_empty()) - .with(comp::CharacterState::default()) - .with(comp::Energy::new(ship.into(), level)) - .with(comp::Health::new(ship.into(), level)) - .with(comp::Stats::new("Airship".to_string())) - .with(comp::Buffs::default()) - .with(comp::Combo::default()) + fn create_ship(&mut self, pos: comp::Pos, ship: comp::ship::Body, level: u16, moving: bool) -> EcsEntityBuilder { + if moving { + self.ecs_mut() + .create_entity_synced() + .with(pos) + .with(comp::Vel(Vec3::zero())) + .with(comp::Ori::default()) + .with(comp::Mass(50.0)) + .with(comp::Collider::Voxel { id: ship.manifest_entry().to_string() }) + .with(comp::Body::Ship(ship)) + .with(comp::Gravity(1.0)) + .with(comp::Controller::default()) + .with(comp::inventory::Inventory::new_empty()) + .with(comp::CharacterState::default()) + .with(comp::Energy::new(ship.into(), level)) + .with(comp::Health::new(ship.into(), level)) + .with(comp::Stats::new("Airship".to_string())) + .with(comp::Buffs::default()) + .with(comp::Combo::default()) + .with(comp::Agent::with_destination()) + } else { + self.ecs_mut() + .create_entity_synced() + .with(pos) + .with(comp::Vel(Vec3::zero())) + .with(comp::Ori::default()) + .with(comp::Mass(50.0)) + .with(comp::Collider::Voxel { id: ship.manifest_entry().to_string() }) + .with(comp::Body::Ship(ship)) + .with(comp::Gravity(1.0)) + .with(comp::Controller::default()) + .with(comp::inventory::Inventory::new_empty()) + .with(comp::CharacterState::default()) + .with(comp::Energy::new(ship.into(), level)) + .with(comp::Health::new(ship.into(), level)) + .with(comp::Stats::new("Airship".to_string())) + .with(comp::Buffs::default()) + .with(comp::Combo::default()) + } } fn create_projectile( diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 74cc3aae94..fda3106b13 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -624,7 +624,7 @@ impl<'a> AgentData<'a> { .ray( self.pos.0 + Vec3::unit_z(), self.pos.0 - + bearing.try_normalized().unwrap_or_else(Vec3::unit_y) * 60.0 + + bearing.try_normalized().unwrap_or_else(Vec3::unit_y) * 80.0 + Vec3::unit_z(), ) .until(Block::is_solid) From c191b66371aa5d6f36c6849e8408056a67d5c60a Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Thu, 11 Mar 2021 23:42:53 -0500 Subject: [PATCH 05/41] Account for model translation in the physics. The voxel collider still needs to be north-aligned for now. --- assets/server/manifests/ship_manifest.ron | 15 +++-- common/src/comp/body/ship.rs | 29 ++++++--- common/src/comp/ori.rs | 2 +- common/sys/src/phys.rs | 75 +++++++++++++++-------- voxygen/src/render/renderer.rs | 2 +- voxygen/src/scene/figure/load.rs | 8 ++- 6 files changed, 90 insertions(+), 41 deletions(-) diff --git a/assets/server/manifests/ship_manifest.ron b/assets/server/manifests/ship_manifest.ron index e8e96539ca..6b361aa535 100644 --- a/assets/server/manifests/ship_manifest.ron +++ b/assets/server/manifests/ship_manifest.ron @@ -1,18 +1,23 @@ ({ DefaultAirship: ( bone0: ( - //offset: (-20.0, -35.0, 1.0), //offset: (3.0, 7.0, 1.0), - offset: (0.25, 0.25, 0.25), - central: ("object.Human_Airship"), + //offset: (-20.75, -34.75, 1.25), + //offset: (0.0, 0.0, 0.0), + offset: (-20.0, -35.0, 1.0), + //phys_offset: (0.25, 0.25, 0.25), + phys_offset: (0.0, 0.0, 0.0), + central: ("Human_Airship"), ), bone1: ( offset: (0.0, 40.0, -8.0), - central: ("object.propeller-l"), + phys_offset: (0.0, 0.0, 0.0), + central: ("propeller-l"), ), bone2: ( offset: (0.0, 0.0, -4.0), - central: ("object.propeller-r"), + phys_offset: (0.0, 0.0, 0.0), + central: ("propeller-r"), ), ), }) diff --git a/common/src/comp/body/ship.rs b/common/src/comp/body/ship.rs index f607cf2ae6..3cab7a0ecb 100644 --- a/common/src/comp/body/ship.rs +++ b/common/src/comp/body/ship.rs @@ -17,7 +17,7 @@ impl From for super::Body { impl Body { pub fn manifest_entry(&self) -> &'static str { match self { - Body::DefaultAirship => "object.Human_Airship", + Body::DefaultAirship => "Human_Airship", } } } @@ -38,6 +38,7 @@ pub mod figuredata { use hashbrown::HashMap; use lazy_static::lazy_static; use serde::Deserialize; + use vek::Vec3; #[derive(Deserialize)] pub struct VoxSimple(pub String); @@ -55,6 +56,7 @@ pub mod figuredata { #[derive(Deserialize)] pub struct ShipCentralSubSpec { pub offset: [f32; 3], + pub phys_offset: [f32; 3], pub central: VoxSimple, } @@ -62,7 +64,13 @@ pub mod figuredata { #[derive(Clone)] pub struct ShipSpec { pub central: AssetHandle>, - pub voxes: HashMap>, + pub colliders: HashMap, + } + + #[derive(Clone)] + pub struct VoxelCollider { + pub dyna: Dyna, + pub translation: Vec3, } impl assets::Compound for ShipSpec { @@ -71,26 +79,31 @@ pub mod figuredata { _: &str, ) -> Result { let manifest: AssetHandle> = AssetExt::load("server.manifests.ship_manifest")?; - let mut voxes = HashMap::new(); + let mut colliders = HashMap::new(); for (_, spec) in (manifest.read().0).0.iter() { for bone in [&spec.bone0, &spec.bone1, &spec.bone2].iter() { // TODO: avoid the requirement for symlinks in "voxygen.voxel.object.", and load // the models from "server.voxel." instead let vox = - cache.load::(&["voxygen.voxel.", &bone.central.0].concat())?; + cache.load::(&["server.voxel.", &bone.central.0].concat())?; let dyna = Dyna::::from_vox(&vox.read().0, false); - voxes.insert(bone.central.0.clone(), dyna.map_into(|cell| { + let dyna = dyna.map_into(|cell| { if let Some(rgb) = cell.get_color() { Block::new(BlockKind::Misc, rgb) } else { Block::air(SpriteKind::Empty) } - })); + }); + let collider = VoxelCollider { + dyna, + translation: Vec3::from(bone.offset) + Vec3::from(bone.phys_offset), + }; + colliders.insert(bone.central.0.clone(), collider); } } Ok(ShipSpec { central: manifest, - voxes, + colliders, }) } } @@ -99,6 +112,6 @@ pub mod figuredata { // TODO: load this from the ECS as a resource, and maybe make it more general than ships // (although figuring out how to keep the figure bones in sync with the terrain offsets seems // like a hard problem if they're not the same manifest) - pub static ref VOXEL_COLLIDER_MANIFEST: ShipSpec = AssetExt::load_expect_cloned("server.manifests.ship_manifest"); + pub static ref VOXEL_COLLIDER_MANIFEST: AssetHandle = AssetExt::load_expect("server.manifests.ship_manifest"); } } diff --git a/common/src/comp/ori.rs b/common/src/comp/ori.rs index 351db07066..de9e969bcc 100644 --- a/common/src/comp/ori.rs +++ b/common/src/comp/ori.rs @@ -9,7 +9,7 @@ use vek::{Quaternion, Vec2, Vec3}; #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(into = "SerdeOri")] #[serde(from = "SerdeOri")] -pub struct Ori(Quaternion); +pub struct Ori(pub Quaternion); impl Default for Ori { /// Returns the default orientation (no rotation; default Dir) diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index 16d6af3a39..f310b0ae7b 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -375,7 +375,7 @@ impl<'a> PhysicsSystemData<'a> { } = self; // Apply movement inputs span!(guard, "Apply movement and terrain collision"); - let (positions, previous_phys_cache) = (&psdw.positions, &psdw.previous_phys_cache); + let (positions, previous_phys_cache, orientations) = (&psdw.positions, &psdw.previous_phys_cache, &psdw.orientations); let (pos_writes, land_on_grounds) = ( &psdr.entities, psdr.scales.maybe(), @@ -383,7 +383,7 @@ impl<'a> PhysicsSystemData<'a> { &psdr.colliders, positions, &mut psdw.velocities, - &psdw.orientations, + orientations, &mut psdw.physics_states, previous_phys_cache, !&psdr.mountings, @@ -559,6 +559,7 @@ impl<'a> PhysicsSystemData<'a> { previous_cache_other, mass_other, collider_other, + ori_other, _, _, _, @@ -571,6 +572,7 @@ impl<'a> PhysicsSystemData<'a> { previous_phys_cache, psdr.masses.maybe(), &psdr.colliders, + orientations, !&psdr.projectiles, !&psdr.mountings, !&psdr.beams, @@ -585,10 +587,12 @@ impl<'a> PhysicsSystemData<'a> { .center .distance_squared(previous_cache_other.center) > collision_boundary.powi(2) - || entity == entity_other { continue; }*/ + if entity == entity_other { + continue; + } if let Collider::Voxel { id } = collider_other { // use bounding cylinder regardless of our collider @@ -600,13 +604,31 @@ impl<'a> PhysicsSystemData<'a> { let z_min = z_min * scale; let z_max = z_max.clamped(1.2, 1.95) * scale; - let mut physics_state_delta = physics_state.clone(); - pos.0 -= pos_other.0; - let cylinder = (radius, z_min, z_max); - if let Some(dyna) = VOXEL_COLLIDER_MANIFEST.voxes.get(id) { + if let Some(voxel_collider) = VOXEL_COLLIDER_MANIFEST.read().colliders.get(id) { + let mut physics_state_delta = physics_state.clone(); + //let ori_2d = ori_other.look_dir().xy(); + //let ori_2d_quat = Quaternion::rotation_z(ori_2d.y.atan2(ori_2d.x)); + //let ori_2d_quat = Quaternion::from_xyzw(ori_2d.x, ori_2d.y, 0.0, 1.0).normalized(); + // deliberately don't use scale yet here, because the 11.0/0.8 + // thing is in the comp::Scale for visual reasons + let t1 = Mat4::from(Transform { + position: pos_other.0 + voxel_collider.translation, + orientation: Quaternion::identity(), + scale: Vec3::broadcast(1.0), + }); + let t2 = Mat4::from(Transform { + position: Vec3::zero(), + orientation: ori_other.0.normalized(), + scale: Vec3::broadcast(1.0), + }); + //let transform = t2 * t1; + let transform = t1; + pos.0 = transform.inverted().mul_point(pos.0); + //vel.0 = t2.inverted().mul_point(pos.0); + let cylinder = (radius, z_min, z_max); cylinder_voxel_collision( cylinder, - &*dyna, + &voxel_collider.dyna, entity, &mut pos, pos_delta, @@ -614,24 +636,27 @@ impl<'a> PhysicsSystemData<'a> { &mut physics_state_delta, &mut land_on_grounds, ); + + pos.0 = transform.mul_point(pos.0); + //vel.0 = t2.mul_point(vel.0); + + // union in the state updates, so that the state isn't just based on + // the most recent terrain that collision was attempted with + physics_state.on_ground |= physics_state_delta.on_ground; + physics_state.on_ceiling |= physics_state_delta.on_ceiling; + physics_state.on_wall = + physics_state.on_wall.or(physics_state_delta.on_wall); + physics_state + .touch_entities + .append(&mut physics_state_delta.touch_entities); + physics_state.in_liquid = + match (physics_state.in_liquid, physics_state_delta.in_liquid) { + // this match computes `x <|> y <|> liftA2 max x y` + (Some(x), Some(y)) => Some(x.max(y)), + (_, y @ Some(_)) => y, + _ => None, + }; } - pos.0 += pos_other.0; - // union in the state updates, so that the state isn't just based on - // the most recent terrain that collision was attempted with - physics_state.on_ground |= physics_state_delta.on_ground; - physics_state.on_ceiling |= physics_state_delta.on_ceiling; - physics_state.on_wall = - physics_state.on_wall.or(physics_state_delta.on_wall); - physics_state - .touch_entities - .append(&mut physics_state_delta.touch_entities); - physics_state.in_liquid = - match (physics_state.in_liquid, physics_state_delta.in_liquid) { - // this match computes `x <|> y <|> liftA2 max x y` - (Some(x), Some(y)) => Some(x.max(y)), - (_, y @ Some(_)) => y, - _ => None, - }; } } if pos != old_pos { diff --git a/voxygen/src/render/renderer.rs b/voxygen/src/render/renderer.rs index eec891e144..6856dbbb4b 100644 --- a/voxygen/src/render/renderer.rs +++ b/voxygen/src/render/renderer.rs @@ -1972,7 +1972,7 @@ fn create_pipelines( &shaders.figure_vert.read().0, &shaders.figure_frag.read().0, &include_ctx, - gfx::state::CullFace::Nothing, + gfx::state::CullFace::Back, )?; // Construct a pipeline for rendering terrain diff --git a/voxygen/src/scene/figure/load.rs b/voxygen/src/scene/figure/load.rs index a584812462..ff334213e2 100644 --- a/voxygen/src/scene/figure/load.rs +++ b/voxygen/src/scene/figure/load.rs @@ -35,6 +35,9 @@ fn load_segment(mesh_name: &str) -> Segment { } fn graceful_load_vox(mesh_name: &str) -> AssetHandle { let full_specifier: String = ["voxygen.voxel.", mesh_name].concat(); + graceful_load_vox_fullspec(&full_specifier) +} +fn graceful_load_vox_fullspec(full_specifier: &str) -> AssetHandle { match DotVoxAsset::load(&full_specifier) { Ok(dot_vox) => dot_vox, Err(_) => { @@ -46,6 +49,9 @@ fn graceful_load_vox(mesh_name: &str) -> AssetHandle { fn graceful_load_segment(mesh_name: &str) -> Segment { Segment::from(&graceful_load_vox(mesh_name).read().0) } +fn graceful_load_segment_fullspec(full_specifier: &str) -> Segment { + Segment::from(&graceful_load_vox_fullspec(full_specifier).read().0) +} fn graceful_load_segment_flipped(mesh_name: &str, flipped: bool) -> Segment { Segment::from_vox(&graceful_load_vox(mesh_name).read().0, flipped) } @@ -4241,7 +4247,7 @@ fn mesh_ship_bone &ShipCentralSubSpec>(ma }, }; let bone = f(spec); - let central = graceful_load_segment(&bone.central.0); + let central = graceful_load_segment_fullspec(&["server.voxel.", &bone.central.0].concat()); (central, Vec3::from(bone.offset)) } From 707549626b962f0a1fd2504f632bd8bc1a37247f Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 12 Mar 2021 11:32:19 +0000 Subject: [PATCH 06/41] Fixed airship collisions under rotation, added position increment for velocity when on ground --- common/src/states/utils.rs | 2 +- common/sys/src/phys.rs | 89 ++++++++++++++++++++------------------ 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index ea780a1a38..05928fc940 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -169,7 +169,7 @@ impl Body { quadruped_low::Species::Lavadrake => 4.0, _ => 6.0, }, - Body::Ship(_) => 10.0, + Body::Ship(_) => 1.0, } } diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index f310b0ae7b..8e743b3b70 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -375,14 +375,14 @@ impl<'a> PhysicsSystemData<'a> { } = self; // Apply movement inputs span!(guard, "Apply movement and terrain collision"); - let (positions, previous_phys_cache, orientations) = (&psdw.positions, &psdw.previous_phys_cache, &psdw.orientations); - let (pos_writes, land_on_grounds) = ( + let (positions, velocities, previous_phys_cache, orientations) = (&psdw.positions, &psdw.velocities, &psdw.previous_phys_cache, &psdw.orientations); + let (pos_writes, vel_writes, land_on_grounds) = ( &psdr.entities, psdr.scales.maybe(), psdr.stickies.maybe(), &psdr.colliders, positions, - &mut psdw.velocities, + velocities, orientations, &mut psdw.physics_states, previous_phys_cache, @@ -390,15 +390,15 @@ impl<'a> PhysicsSystemData<'a> { ) .par_join() .fold( - || (Vec::new(), Vec::new()), - |(mut pos_writes, mut land_on_grounds), + || (Vec::new(), Vec::new(), Vec::new()), + |(mut pos_writes, mut vel_writes, mut land_on_grounds), ( entity, scale, sticky, collider, pos, - mut vel, + vel, _ori, mut physics_state, previous_cache, @@ -408,9 +408,10 @@ impl<'a> PhysicsSystemData<'a> { // entities let old_pos = *pos; let mut pos = *pos; + let mut vel = *vel; if sticky.is_some() && physics_state.on_surface().is_some() { vel.0 = Vec3::zero(); - return (pos_writes, land_on_grounds); + return (pos_writes, vel_writes, land_on_grounds); } let scale = if let Collider::Voxel { .. } = collider { @@ -421,7 +422,7 @@ impl<'a> PhysicsSystemData<'a> { 1.0 }; - let old_vel = *vel; + let old_vel = vel; // Integrate forces // Friction is assumed to be a constant dependent on location let friction = FRIC_AIR @@ -477,9 +478,9 @@ impl<'a> PhysicsSystemData<'a> { entity, &mut pos, pos_delta, - vel, + &mut vel, &mut physics_state, - &mut land_on_grounds, + |entity, vel| land_on_grounds.push((entity, vel)), ); }, Collider::Box { @@ -499,9 +500,9 @@ impl<'a> PhysicsSystemData<'a> { entity, &mut pos, pos_delta, - vel, + &mut vel, &mut physics_state, - &mut land_on_grounds, + |entity, vel| land_on_grounds.push((entity, vel)), ); }, Collider::Point => { @@ -556,6 +557,7 @@ impl<'a> PhysicsSystemData<'a> { entity_other, other, pos_other, + vel_other, previous_cache_other, mass_other, collider_other, @@ -569,6 +571,7 @@ impl<'a> PhysicsSystemData<'a> { &psdr.entities, &psdr.uids, positions, + velocities, previous_phys_cache, psdr.masses.maybe(), &psdr.colliders, @@ -606,39 +609,32 @@ impl<'a> PhysicsSystemData<'a> { if let Some(voxel_collider) = VOXEL_COLLIDER_MANIFEST.read().colliders.get(id) { let mut physics_state_delta = physics_state.clone(); - //let ori_2d = ori_other.look_dir().xy(); - //let ori_2d_quat = Quaternion::rotation_z(ori_2d.y.atan2(ori_2d.x)); - //let ori_2d_quat = Quaternion::from_xyzw(ori_2d.x, ori_2d.y, 0.0, 1.0).normalized(); // deliberately don't use scale yet here, because the 11.0/0.8 // thing is in the comp::Scale for visual reasons - let t1 = Mat4::from(Transform { - position: pos_other.0 + voxel_collider.translation, - orientation: Quaternion::identity(), - scale: Vec3::broadcast(1.0), - }); - let t2 = Mat4::from(Transform { - position: Vec3::zero(), - orientation: ori_other.0.normalized(), - scale: Vec3::broadcast(1.0), - }); - //let transform = t2 * t1; - let transform = t1; - pos.0 = transform.inverted().mul_point(pos.0); - //vel.0 = t2.inverted().mul_point(pos.0); + let transform_from = Mat4::::translation_3d(pos_other.0) + * Mat4::from(ori_other.0) + * Mat4::::translation_3d(voxel_collider.translation); + let transform_to = transform_from.inverted(); + pos.0 = transform_to.mul_point(pos.0); + vel.0 = transform_to.mul_direction(vel.0); let cylinder = (radius, z_min, z_max); cylinder_voxel_collision( cylinder, &voxel_collider.dyna, entity, &mut pos, - pos_delta, - vel, + transform_to.mul_direction(pos_delta), + &mut vel, &mut physics_state_delta, - &mut land_on_grounds, + |entity, vel| land_on_grounds.push((entity, Vel(transform_from.mul_direction(vel.0)))), ); - pos.0 = transform.mul_point(pos.0); - //vel.0 = t2.mul_point(vel.0); + pos.0 = transform_from.mul_point(pos.0); + vel.0 = transform_from.mul_direction(vel.0); + + if physics_state_delta.on_ground { + pos.0 += vel_other.0 * psdr.dt.0; + } // union in the state updates, so that the state isn't just based on // the most recent terrain that collision was attempted with @@ -662,27 +658,36 @@ impl<'a> PhysicsSystemData<'a> { if pos != old_pos { pos_writes.push((entity, pos)); } + if vel != old_vel { + vel_writes.push((entity, vel)); + } - (pos_writes, land_on_grounds) + (pos_writes, vel_writes, land_on_grounds) }, ) .reduce( - || (Vec::new(), Vec::new()), - |(mut pos_writes_a, mut land_on_grounds_a), - (mut pos_writes_b, mut land_on_grounds_b)| { + || (Vec::new(), Vec::new(), Vec::new()), + |(mut pos_writes_a, mut vel_writes_a, mut land_on_grounds_a), + (mut pos_writes_b, mut vel_writes_b, mut land_on_grounds_b)| { pos_writes_a.append(&mut pos_writes_b); + vel_writes_a.append(&mut vel_writes_b); land_on_grounds_a.append(&mut land_on_grounds_b); - (pos_writes_a, land_on_grounds_a) + (pos_writes_a, vel_writes_a, land_on_grounds_a) }, ); drop(guard); job.cpu_stats.measure(ParMode::Single); let pos_writes: HashMap = pos_writes.into_iter().collect(); - for (entity, pos) in (&psdr.entities, &mut psdw.positions).join() { + let vel_writes: HashMap = vel_writes.into_iter().collect(); + for (entity, pos, vel) in (&psdr.entities, &mut psdw.positions, &mut psdw.velocities).join() { if let Some(new_pos) = pos_writes.get(&entity) { *pos = *new_pos; } + + if let Some(new_vel) = vel_writes.get(&entity) { + *vel = *new_vel; + } } let mut event_emitter = psdr.event_bus.emitter(); @@ -732,7 +737,7 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( mut pos_delta: Vec3, vel: &mut Vel, physics_state: &mut PhysicsState, - land_on_grounds: &mut Vec<(Entity, Vel)>, + mut land_on_ground: impl FnMut(Entity, Vel), ) { let (radius, z_min, z_max) = cylinder; @@ -898,7 +903,7 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( on_ground = true; if !was_on_ground { - land_on_grounds.push((entity, *vel)); + land_on_ground(entity, *vel); } } else if resolve_dir.z < 0.0 && vel.0.z >= 0.0 { on_ceiling = true; From 59594feb1d564e2c0554d2f00396258911218bd4 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 12 Mar 2021 13:26:02 +0000 Subject: [PATCH 07/41] Made animations and friction ground-relative --- common/src/comp/phys.rs | 2 + common/sys/src/phys.rs | 34 +++- voxygen/src/scene/figure/mod.rs | 347 +++++++++++++++++++------------- voxygen/src/scene/simple.rs | 2 + 4 files changed, 234 insertions(+), 151 deletions(-) diff --git a/common/src/comp/phys.rs b/common/src/comp/phys.rs index b10978cc58..149f8ebfd4 100644 --- a/common/src/comp/phys.rs +++ b/common/src/comp/phys.rs @@ -109,6 +109,7 @@ pub struct PhysicsState { pub on_wall: Option>, pub touch_entities: Vec, pub in_liquid: Option, // Depth + pub ground_vel: Vec3, } impl PhysicsState { @@ -118,6 +119,7 @@ impl PhysicsState { touch_entities.clear(); *self = Self { touch_entities, + ground_vel: self.ground_vel, // Preserved, since it's the velocity of the last contact point ..Self::default() } } diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index 8e743b3b70..f4c1215c6e 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -426,11 +426,11 @@ impl<'a> PhysicsSystemData<'a> { // Integrate forces // Friction is assumed to be a constant dependent on location let friction = FRIC_AIR - .max(if physics_state.on_ground { - FRIC_GROUND - } else { - 0.0 - }) + // .max(if physics_state.on_ground { + // FRIC_GROUND + // } else { + // 0.0 + // }) .max(if physics_state.in_liquid.is_some() { FRIC_FLUID } else { @@ -480,6 +480,9 @@ impl<'a> PhysicsSystemData<'a> { pos_delta, &mut vel, &mut physics_state, + Vec3::zero(), + &psdr.dt, + true, |entity, vel| land_on_grounds.push((entity, vel)), ); }, @@ -502,6 +505,9 @@ impl<'a> PhysicsSystemData<'a> { pos_delta, &mut vel, &mut physics_state, + Vec3::zero(), + &psdr.dt, + true, |entity, vel| land_on_grounds.push((entity, vel)), ); }, @@ -626,18 +632,20 @@ impl<'a> PhysicsSystemData<'a> { transform_to.mul_direction(pos_delta), &mut vel, &mut physics_state_delta, + transform_to.mul_direction(vel_other.0), + &psdr.dt, + false, |entity, vel| land_on_grounds.push((entity, Vel(transform_from.mul_direction(vel.0)))), ); pos.0 = transform_from.mul_point(pos.0); vel.0 = transform_from.mul_direction(vel.0); - if physics_state_delta.on_ground { - pos.0 += vel_other.0 * psdr.dt.0; - } - // union in the state updates, so that the state isn't just based on // the most recent terrain that collision was attempted with + if physics_state_delta.on_ground { + physics_state.ground_vel = vel_other.0; + } physics_state.on_ground |= physics_state_delta.on_ground; physics_state.on_ceiling |= physics_state_delta.on_ceiling; physics_state.on_wall = @@ -737,6 +745,9 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( mut pos_delta: Vec3, vel: &mut Vel, physics_state: &mut PhysicsState, + ground_vel: Vec3, + dt: &DeltaTime, + apply_velocity_step: bool, // Stupid hack mut land_on_ground: impl FnMut(Entity, Vel), ) { let (radius, z_min, z_max) = cylinder; @@ -826,7 +837,9 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( let old_pos = pos.0; fn block_true(_: &Block) -> bool { true } for _ in 0..increments as usize { - pos.0 += pos_delta / increments; + if apply_velocity_step { + pos.0 += pos_delta / increments; + } const MAX_ATTEMPTS: usize = 16; @@ -966,6 +979,7 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( if on_ground { physics_state.on_ground = true; + vel.0 = ground_vel + (vel.0 - ground_vel) * (1.0 - FRIC_GROUND.min(1.0)).powf(dt.0 * 60.0); // If the space below us is free, then "snap" to the ground } else if collision_with( pos.0 - Vec3::unit_z() * 1.05, diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index f6ae55df31..926680fdfa 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -748,6 +748,9 @@ impl FigureMgr { let hands = (active_tool_hand, second_tool_hand); + // Velocity relative to the current ground + let rel_vel = vel.0 - physics.ground_vel; + match body { Body::Humanoid(body) => { let (model, skeleton_attr) = self.model_cache.get_or_create_model( @@ -768,6 +771,10 @@ impl FigureMgr { .or_insert_with(|| { FigureState::new(renderer, CharacterSkeleton::default()) }); + + // Average velocity relative to the current ground + let rel_avg_vel = state.avg_vel - physics.ground_vel; + let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), _ => continue, @@ -779,7 +786,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, - vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving + rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving physics.in_liquid.is_some(), // In water ) { // Standing @@ -790,7 +797,7 @@ impl FigureMgr { second_tool_kind, hands, time, - state.avg_vel, + rel_avg_vel, ), state.state_time, &mut state_animation_rate, @@ -803,12 +810,12 @@ impl FigureMgr { active_tool_kind, second_tool_kind, hands, - vel.0, + rel_vel, // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, state.acc_vel, ), state.state_time, @@ -822,7 +829,7 @@ impl FigureMgr { active_tool_kind, second_tool_kind, hands, - vel.0, + rel_vel, // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), @@ -839,12 +846,12 @@ impl FigureMgr { active_tool_kind, second_tool_kind, hands, - vel.0, + rel_vel, // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, ), state.state_time, &mut state_animation_rate, @@ -889,7 +896,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0.magnitude(), + rel_vel.magnitude(), time, None, ), @@ -917,7 +924,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0.magnitude(), + rel_vel.magnitude(), // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), @@ -949,7 +956,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0.magnitude(), + rel_vel.magnitude(), // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), @@ -983,7 +990,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0, + rel_vel, time, Some(s.stage_section), ), @@ -1016,7 +1023,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0, + rel_vel, time, Some(s.stage_section), ), @@ -1030,7 +1037,7 @@ impl FigureMgr { &target_base, ( active_tool_kind, - vel.0, + rel_vel, // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), @@ -1047,7 +1054,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0.magnitude(), + rel_vel.magnitude(), time, None, ), @@ -1106,7 +1113,7 @@ impl FigureMgr { active_tool_kind, second_tool_kind, time, - vel.0.magnitude(), + rel_vel.magnitude(), Some(s.stage_section), ), stage_progress, @@ -1173,7 +1180,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0, + rel_vel, time, Some(s.stage_section), ), @@ -1209,7 +1216,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0, + rel_vel, time, Some(s.stage_section), ), @@ -1239,7 +1246,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -1256,7 +1263,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -1287,7 +1294,7 @@ impl FigureMgr { active_tool_kind, second_tool_kind, time, - vel.0.magnitude(), + rel_vel.magnitude(), Some(s.stage_section), ), stage_progress, @@ -1351,7 +1358,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), ), @@ -1364,7 +1371,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0, + rel_vel, time, Some(s.stage_section), ), @@ -1377,7 +1384,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), ), @@ -1399,7 +1406,7 @@ impl FigureMgr { CharacterState::Equipping { .. } => { anim::character::EquipAnimation::update_skeleton( &target_base, - (active_tool_kind, second_tool_kind, vel.0.magnitude(), time), + (active_tool_kind, second_tool_kind, rel_vel.magnitude(), time), state.state_time, &mut state_animation_rate, skeleton_attr, @@ -1410,7 +1417,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0.magnitude(), + rel_vel.magnitude(), time, look_dir, ), @@ -1422,7 +1429,7 @@ impl FigureMgr { if physics.in_liquid.is_some() { anim::character::SwimWieldAnimation::update_skeleton( &target_base, - (active_tool_kind, second_tool_kind, vel.0.magnitude(), time), + (active_tool_kind, second_tool_kind, rel_vel.magnitude(), time), state.state_time, &mut state_animation_rate, skeleton_attr, @@ -1437,7 +1444,7 @@ impl FigureMgr { ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), look_dir, - vel.0, + rel_vel, time, ), state.state_time, @@ -1452,7 +1459,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0, + rel_vel, // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), @@ -1469,7 +1476,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0, + rel_vel, // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), time, @@ -1494,7 +1501,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0, + rel_vel, // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), @@ -1533,6 +1540,7 @@ impl FigureMgr { camera, &mut update_buf, terrain, + physics.ground_vel, ); }, Body::QuadrupedSmall(body) => { @@ -1556,6 +1564,9 @@ impl FigureMgr { FigureState::new(renderer, QuadrupedSmallSkeleton::default()) }); + // Average velocity relative to the current ground + let rel_avg_vel = state.avg_vel - physics.ground_vel; + let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), _ => continue, @@ -1567,7 +1578,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, - vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving + rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving physics.in_liquid.is_some(), // In water ) { // Standing @@ -1585,12 +1596,12 @@ impl FigureMgr { anim::quadruped_small::RunAnimation::update_skeleton( &QuadrupedSmallSkeleton::default(), ( - vel.0.magnitude(), + rel_vel.magnitude(), // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, state.acc_vel, ), state.state_time, @@ -1602,12 +1613,12 @@ impl FigureMgr { (false, _, true) => anim::quadruped_small::RunAnimation::update_skeleton( &QuadrupedSmallSkeleton::default(), ( - vel.0.magnitude(), + rel_vel.magnitude(), // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, state.acc_vel, ), state.state_time, @@ -1617,7 +1628,7 @@ impl FigureMgr { // In air (false, _, false) => anim::quadruped_small::JumpAnimation::update_skeleton( &QuadrupedSmallSkeleton::default(), - (vel.0.magnitude(), time), + (rel_vel.magnitude(), time), state.state_time, &mut state_animation_rate, skeleton_attr, @@ -1659,7 +1670,7 @@ impl FigureMgr { anim::quadruped_small::AlphaAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -1690,7 +1701,7 @@ impl FigureMgr { anim::quadruped_small::StunnedAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -1731,6 +1742,7 @@ impl FigureMgr { camera, &mut update_buf, terrain, + physics.ground_vel, ); }, Body::QuadrupedMedium(body) => { @@ -1754,6 +1766,9 @@ impl FigureMgr { FigureState::new(renderer, QuadrupedMediumSkeleton::default()) }); + // Average velocity relative to the current ground + let rel_avg_vel = state.avg_vel - physics.ground_vel; + let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), _ => continue, @@ -1765,7 +1780,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, - vel.0.magnitude_squared() > 0.25, // Moving + rel_vel.magnitude_squared() > 0.25, // Moving physics.in_liquid.is_some(), // In water ) { // Standing @@ -1783,12 +1798,12 @@ impl FigureMgr { anim::quadruped_medium::RunAnimation::update_skeleton( &QuadrupedMediumSkeleton::default(), ( - vel.0.magnitude(), + rel_vel.magnitude(), // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, state.acc_vel, ), state.state_time, @@ -1800,12 +1815,12 @@ impl FigureMgr { (false, _, true) => anim::quadruped_medium::RunAnimation::update_skeleton( &QuadrupedMediumSkeleton::default(), ( - vel.0.magnitude(), + rel_vel.magnitude(), // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, state.acc_vel, ), state.state_time, @@ -1850,7 +1865,7 @@ impl FigureMgr { anim::quadruped_medium::HoofAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -1878,7 +1893,7 @@ impl FigureMgr { anim::quadruped_medium::DashAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -1908,7 +1923,7 @@ impl FigureMgr { anim::quadruped_medium::LeapMeleeAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -1946,7 +1961,7 @@ impl FigureMgr { 1 => anim::quadruped_medium::AlphaAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -1958,7 +1973,7 @@ impl FigureMgr { 2 => anim::quadruped_medium::BetaAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -1970,7 +1985,7 @@ impl FigureMgr { _ => anim::quadruped_medium::AlphaAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -1999,7 +2014,7 @@ impl FigureMgr { anim::quadruped_medium::StunnedAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -2013,7 +2028,7 @@ impl FigureMgr { anim::quadruped_medium::StunnedAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -2054,6 +2069,7 @@ impl FigureMgr { camera, &mut update_buf, terrain, + physics.ground_vel, ); }, Body::QuadrupedLow(body) => { @@ -2077,6 +2093,9 @@ impl FigureMgr { FigureState::new(renderer, QuadrupedLowSkeleton::default()) }); + // Average velocity relative to the current ground + let rel_avg_vel = state.avg_vel - physics.ground_vel; + let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), _ => continue, @@ -2088,7 +2107,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, - vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving + rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving physics.in_liquid.is_some(), // In water ) { // Standing @@ -2105,12 +2124,12 @@ impl FigureMgr { (true, true, false) => anim::quadruped_low::RunAnimation::update_skeleton( &QuadrupedLowSkeleton::default(), ( - vel.0.magnitude(), + rel_vel.magnitude(), // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, state.acc_vel, ), state.state_time, @@ -2121,12 +2140,12 @@ impl FigureMgr { (false, _, true) => anim::quadruped_low::RunAnimation::update_skeleton( &QuadrupedLowSkeleton::default(), ( - vel.0.magnitude(), + rel_vel.magnitude(), // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, state.acc_vel, ), state.state_time, @@ -2136,7 +2155,7 @@ impl FigureMgr { // In air (false, _, false) => anim::quadruped_low::JumpAnimation::update_skeleton( &QuadrupedLowSkeleton::default(), - (vel.0.magnitude(), time), + (rel_vel.magnitude(), time), state.state_time, &mut state_animation_rate, skeleton_attr, @@ -2165,7 +2184,7 @@ impl FigureMgr { }; anim::quadruped_low::ShootAnimation::update_skeleton( &target_base, - (vel.0.magnitude(), time, Some(s.stage_section)), + (rel_vel.magnitude(), time, Some(s.stage_section)), stage_progress, &mut state_animation_rate, skeleton_attr, @@ -2190,7 +2209,7 @@ impl FigureMgr { anim::quadruped_low::BetaAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -2220,7 +2239,7 @@ impl FigureMgr { anim::quadruped_low::TailwhipAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -2248,7 +2267,7 @@ impl FigureMgr { anim::quadruped_low::StunnedAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -2262,7 +2281,7 @@ impl FigureMgr { anim::quadruped_low::StunnedAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -2302,7 +2321,7 @@ impl FigureMgr { 1 => anim::quadruped_low::AlphaAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -2314,7 +2333,7 @@ impl FigureMgr { 2 => anim::quadruped_low::BetaAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -2326,7 +2345,7 @@ impl FigureMgr { _ => anim::quadruped_low::AlphaAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -2352,7 +2371,7 @@ impl FigureMgr { anim::quadruped_low::BreatheAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -2380,7 +2399,7 @@ impl FigureMgr { anim::quadruped_low::DashAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -2410,6 +2429,7 @@ impl FigureMgr { camera, &mut update_buf, terrain, + physics.ground_vel, ); }, Body::BirdMedium(body) => { @@ -2432,6 +2452,9 @@ impl FigureMgr { FigureState::new(renderer, BirdMediumSkeleton::default()) }); + // Average velocity relative to the current ground + let rel_avg_vel = state.avg_vel - physics.ground_vel; + let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), _ => continue, @@ -2443,7 +2466,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, - vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving + rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving physics.in_liquid.is_some(), // In water ) { // Standing @@ -2457,7 +2480,7 @@ impl FigureMgr { // Running (true, true, false) => anim::bird_medium::RunAnimation::update_skeleton( &BirdMediumSkeleton::default(), - (vel.0.magnitude(), time), + (rel_vel.magnitude(), time), state.state_time, &mut state_animation_rate, skeleton_attr, @@ -2465,7 +2488,7 @@ impl FigureMgr { // Running (false, _, true) => anim::bird_medium::RunAnimation::update_skeleton( &BirdMediumSkeleton::default(), - (vel.0.magnitude(), time), + (rel_vel.magnitude(), time), state.state_time, &mut state_animation_rate, skeleton_attr, @@ -2473,7 +2496,7 @@ impl FigureMgr { // In air (false, _, false) => anim::bird_medium::FlyAnimation::update_skeleton( &BirdMediumSkeleton::default(), - (vel.0.magnitude(), time), + (rel_vel.magnitude(), time), state.state_time, &mut state_animation_rate, skeleton_attr, @@ -2516,6 +2539,7 @@ impl FigureMgr { camera, &mut update_buf, terrain, + physics.ground_vel, ); }, Body::FishMedium(body) => { @@ -2538,6 +2562,9 @@ impl FigureMgr { FigureState::new(renderer, FishMediumSkeleton::default()) }); + // Average velocity relative to the current ground + let rel_avg_vel = state.avg_vel - physics.ground_vel; + let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), _ => continue, @@ -2549,19 +2576,19 @@ impl FigureMgr { let target_base = match ( physics.on_ground, - vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving + rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving physics.in_liquid.is_some(), // In water ) { // Idle (_, false, _) => anim::fish_medium::IdleAnimation::update_skeleton( &FishMediumSkeleton::default(), ( - vel.0, + rel_vel, // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, ), state.state_time, &mut state_animation_rate, @@ -2571,12 +2598,12 @@ impl FigureMgr { (_, true, _) => anim::fish_medium::SwimAnimation::update_skeleton( &FishMediumSkeleton::default(), ( - vel.0, + rel_vel, // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, state.acc_vel, ), state.state_time, @@ -2601,6 +2628,7 @@ impl FigureMgr { camera, &mut update_buf, terrain, + physics.ground_vel, ); }, Body::BipedSmall(body) => { @@ -2623,6 +2651,9 @@ impl FigureMgr { FigureState::new(renderer, BipedSmallSkeleton::default()) }); + // Average velocity relative to the current ground + let rel_avg_vel = state.avg_vel - physics.ground_vel; + let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), _ => continue, @@ -2634,18 +2665,18 @@ impl FigureMgr { let target_base = match ( physics.on_ground, - vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving + rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving physics.in_liquid.is_some(), // In water ) { // Idle (true, false, false) => anim::biped_small::IdleAnimation::update_skeleton( &BipedSmallSkeleton::default(), ( - vel.0, + rel_vel, ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, ), state.state_time, &mut state_animation_rate, @@ -2655,11 +2686,11 @@ impl FigureMgr { (true, true, _) => anim::biped_small::RunAnimation::update_skeleton( &BipedSmallSkeleton::default(), ( - vel.0, + rel_vel, ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, state.acc_vel, ), state.state_time, @@ -2670,11 +2701,11 @@ impl FigureMgr { (false, _, false) => anim::biped_small::RunAnimation::update_skeleton( &BipedSmallSkeleton::default(), ( - vel.0, + rel_vel, ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, state.acc_vel, ), state.state_time, @@ -2685,11 +2716,11 @@ impl FigureMgr { (false, _, true) => anim::biped_small::RunAnimation::update_skeleton( &BipedSmallSkeleton::default(), ( - vel.0, + rel_vel, ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, state.acc_vel, ), state.state_time, @@ -2699,11 +2730,11 @@ impl FigureMgr { _ => anim::biped_small::IdleAnimation::update_skeleton( &BipedSmallSkeleton::default(), ( - vel.0, + rel_vel, ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, ), state.state_time, &mut state_animation_rate, @@ -2717,11 +2748,11 @@ impl FigureMgr { &target_base, ( active_tool_kind, - vel.0, + rel_vel, ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, state.acc_vel, ), state.state_time, @@ -2749,11 +2780,11 @@ impl FigureMgr { anim::biped_small::DashAnimation::update_skeleton( &target_base, ( - vel.0, + rel_vel, ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, state.acc_vel, Some(s.stage_section), state.state_time, @@ -2819,11 +2850,11 @@ impl FigureMgr { &target_base, ( active_tool_kind, - vel.0, + rel_vel, ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, state.acc_vel, Some(s.stage_section), state.state_time, @@ -2850,11 +2881,11 @@ impl FigureMgr { &target_base, ( active_tool_kind, - vel.0, + rel_vel, ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, state.acc_vel, Some(s.stage_section), state.state_time, @@ -2892,11 +2923,11 @@ impl FigureMgr { 1 => anim::biped_small::AlphaAnimation::update_skeleton( &target_base, ( - vel.0, + rel_vel, ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, state.acc_vel, Some(s.stage_section), state.state_time, @@ -2908,11 +2939,11 @@ impl FigureMgr { _ => anim::biped_small::AlphaAnimation::update_skeleton( &target_base, ( - vel.0, + rel_vel, ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, state.acc_vel, Some(s.stage_section), state.state_time, @@ -2943,6 +2974,7 @@ impl FigureMgr { camera, &mut update_buf, terrain, + physics.ground_vel, ); }, Body::Dragon(body) => { @@ -2962,6 +2994,9 @@ impl FigureMgr { FigureState::new(renderer, DragonSkeleton::default()) }); + // Average velocity relative to the current ground + let rel_avg_vel = state.avg_vel - physics.ground_vel; + let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), _ => continue, @@ -2973,7 +3008,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, - vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving + rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving physics.in_liquid.is_some(), // In water ) { // Standing @@ -2988,12 +3023,12 @@ impl FigureMgr { (true, true, false) => anim::dragon::RunAnimation::update_skeleton( &DragonSkeleton::default(), ( - vel.0.magnitude(), + rel_vel.magnitude(), // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, ), state.state_time, &mut state_animation_rate, @@ -3002,7 +3037,7 @@ impl FigureMgr { // In air (false, _, false) => anim::dragon::FlyAnimation::update_skeleton( &DragonSkeleton::default(), - (vel.0.magnitude(), time), + (rel_vel.magnitude(), time), state.state_time, &mut state_animation_rate, skeleton_attr, @@ -3033,6 +3068,7 @@ impl FigureMgr { camera, &mut update_buf, terrain, + physics.ground_vel, ); }, Body::Theropod(body) => { @@ -3053,6 +3089,9 @@ impl FigureMgr { .entry(entity) .or_insert_with(|| FigureState::new(renderer, TheropodSkeleton::default())); + // Average velocity relative to the current ground + let rel_avg_vel = state.avg_vel - physics.ground_vel; + let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), _ => continue, @@ -3064,7 +3103,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, - vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving + rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving physics.in_liquid.is_some(), // In water ) { // Standing @@ -3079,12 +3118,12 @@ impl FigureMgr { (true, true, false) => anim::theropod::RunAnimation::update_skeleton( &TheropodSkeleton::default(), ( - vel.0, + rel_vel, // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, state.acc_vel, ), state.state_time, @@ -3095,12 +3134,12 @@ impl FigureMgr { (false, _, false) => anim::theropod::JumpAnimation::update_skeleton( &TheropodSkeleton::default(), ( - vel.0.magnitude(), + rel_vel.magnitude(), // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, ), state.state_time, &mut state_animation_rate, @@ -3143,7 +3182,7 @@ impl FigureMgr { 1 => anim::theropod::AlphaAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -3155,7 +3194,7 @@ impl FigureMgr { _ => anim::theropod::BetaAnimation::update_skeleton( &target_base, ( - vel.0.magnitude(), + rel_vel.magnitude(), time, Some(s.stage_section), state.state_time, @@ -3216,6 +3255,7 @@ impl FigureMgr { camera, &mut update_buf, terrain, + physics.ground_vel, ); }, Body::BirdSmall(body) => { @@ -3238,6 +3278,9 @@ impl FigureMgr { FigureState::new(renderer, BirdSmallSkeleton::default()) }); + // Average velocity relative to the current ground + let rel_avg_vel = state.avg_vel - physics.ground_vel; + let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), _ => continue, @@ -3249,7 +3292,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, - vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving + rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving physics.in_liquid.is_some(), // In water ) { // Standing @@ -3263,7 +3306,7 @@ impl FigureMgr { // Running (true, true, false) => anim::bird_small::RunAnimation::update_skeleton( &BirdSmallSkeleton::default(), - (vel.0.magnitude(), time), + (rel_vel.magnitude(), time), state.state_time, &mut state_animation_rate, skeleton_attr, @@ -3271,7 +3314,7 @@ impl FigureMgr { // In air (false, _, false) => anim::bird_small::JumpAnimation::update_skeleton( &BirdSmallSkeleton::default(), - (vel.0.magnitude(), time), + (rel_vel.magnitude(), time), state.state_time, &mut state_animation_rate, skeleton_attr, @@ -3303,6 +3346,7 @@ impl FigureMgr { camera, &mut update_buf, terrain, + physics.ground_vel, ); }, Body::FishSmall(body) => { @@ -3325,6 +3369,9 @@ impl FigureMgr { FigureState::new(renderer, FishSmallSkeleton::default()) }); + // Average velocity relative to the current ground + let rel_avg_vel = state.avg_vel - physics.ground_vel; + let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), _ => continue, @@ -3336,19 +3383,19 @@ impl FigureMgr { let target_base = match ( physics.on_ground, - vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving + rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving physics.in_liquid.is_some(), // In water ) { // Idle (_, false, _) => anim::fish_small::IdleAnimation::update_skeleton( &FishSmallSkeleton::default(), ( - vel.0, + rel_vel, // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, ), state.state_time, &mut state_animation_rate, @@ -3358,12 +3405,12 @@ impl FigureMgr { (_, true, _) => anim::fish_small::SwimAnimation::update_skeleton( &FishSmallSkeleton::default(), ( - vel.0, + rel_vel, // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, state.acc_vel, ), state.state_time, @@ -3388,6 +3435,7 @@ impl FigureMgr { camera, &mut update_buf, terrain, + physics.ground_vel, ); }, Body::BipedLarge(body) => { @@ -3410,6 +3458,9 @@ impl FigureMgr { FigureState::new(renderer, BipedLargeSkeleton::default()) }); + // Average velocity relative to the current ground + let rel_avg_vel = state.avg_vel - physics.ground_vel; + let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), _ => continue, @@ -3421,7 +3472,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, - vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving + rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving physics.in_liquid.is_some(), // In water ) { // Running @@ -3430,12 +3481,12 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0, + rel_vel, // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, - state.avg_vel, + rel_avg_vel, state.acc_vel, ), state.state_time, @@ -3462,7 +3513,7 @@ impl FigureMgr { CharacterState::Equipping { .. } => { anim::biped_large::EquipAnimation::update_skeleton( &target_base, - (active_tool_kind, second_tool_kind, vel.0.magnitude(), time), + (active_tool_kind, second_tool_kind, rel_vel.magnitude(), time), state.state_time, &mut state_animation_rate, skeleton_attr, @@ -3474,7 +3525,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0, + rel_vel, time, state.acc_vel, ), @@ -3489,7 +3540,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0, + rel_vel, time, None, state.acc_vel, @@ -3518,7 +3569,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0, + rel_vel, // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), @@ -3550,7 +3601,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0, + rel_vel, // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), @@ -3585,7 +3636,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0, + rel_vel, time, Some(s.stage_section), state.acc_vel, @@ -3625,7 +3676,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0, + rel_vel, time, Some(s.stage_section), state.acc_vel, @@ -3639,7 +3690,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0, + rel_vel, time, Some(s.stage_section), state.acc_vel, @@ -3653,7 +3704,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0, + rel_vel, time, Some(s.stage_section), state.acc_vel, @@ -3691,7 +3742,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0, + rel_vel, time, Some(s.stage_section), ), @@ -3731,7 +3782,7 @@ impl FigureMgr { ( active_tool_kind, second_tool_kind, - vel.0, + rel_vel, time, Some(s.stage_section), ), @@ -3760,7 +3811,7 @@ impl FigureMgr { active_tool_kind, second_tool_kind, time, - vel.0.magnitude(), + rel_vel.magnitude(), Some(s.stage_section), ), stage_progress, @@ -3786,7 +3837,7 @@ impl FigureMgr { active_tool_kind, second_tool_kind, time, - vel.0, + rel_vel, Some(s.stage_section), state.acc_vel, ), @@ -3815,6 +3866,7 @@ impl FigureMgr { camera, &mut update_buf, terrain, + physics.ground_vel, ); }, Body::Golem(body) => { @@ -3834,6 +3886,9 @@ impl FigureMgr { FigureState::new(renderer, GolemSkeleton::default()) }); + // Average velocity relative to the current ground + let rel_avg_vel = state.avg_vel - physics.ground_vel; + let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), _ => continue, @@ -3845,7 +3900,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, - vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving + rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving physics.in_liquid.is_some(), // In water ) { // Standing @@ -3860,7 +3915,7 @@ impl FigureMgr { (true, true, false) => anim::golem::RunAnimation::update_skeleton( &GolemSkeleton::default(), ( - vel.0, + rel_vel, // TODO: Update to use the quaternion. ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), @@ -3875,7 +3930,7 @@ impl FigureMgr { (false, _, false) => anim::golem::RunAnimation::update_skeleton( &GolemSkeleton::default(), ( - vel.0, + rel_vel, ori * anim::vek::Vec3::::unit_y(), state.last_ori * anim::vek::Vec3::::unit_y(), time, @@ -3944,7 +3999,7 @@ impl FigureMgr { }; anim::golem::ShockwaveAnimation::update_skeleton( &target_base, - (Some(s.stage_section), vel.0.magnitude(), time), + (Some(s.stage_section), rel_vel.magnitude(), time), stage_progress, &mut state_animation_rate, skeleton_attr, @@ -3995,6 +4050,7 @@ impl FigureMgr { camera, &mut update_buf, terrain, + physics.ground_vel, ); }, Body::Object(body) => { @@ -4014,6 +4070,9 @@ impl FigureMgr { FigureState::new(renderer, ObjectSkeleton::default()) }); + // Average velocity relative to the current ground + let rel_avg_vel = state.avg_vel - physics.ground_vel; + let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), _ => (&CharacterState::Idle, &Last { @@ -4027,7 +4086,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, - vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving + rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving physics.in_liquid.is_some(), // In water ) { // Standing @@ -4119,6 +4178,7 @@ impl FigureMgr { camera, &mut update_buf, terrain, + physics.ground_vel, ); }, Body::Ship(body) => { @@ -4138,6 +4198,9 @@ impl FigureMgr { FigureState::new(renderer, ShipSkeleton::default()) }); + // Average velocity relative to the current ground + let rel_avg_vel = state.avg_vel - physics.ground_vel; + let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), _ => (&CharacterState::Idle, &Last { @@ -4151,7 +4214,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, - vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving + rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving physics.in_liquid.is_some(), // In water ) { // Standing @@ -4192,6 +4255,7 @@ impl FigureMgr { camera, &mut update_buf, terrain, + physics.ground_vel, ); }, } @@ -4891,6 +4955,7 @@ impl FigureState { _camera: &Camera, buf: &mut [anim::FigureBoneData; anim::MAX_BONE_COUNT], terrain: Option<&Terrain>, + ground_vel: Vec3, ) { // NOTE: As long as update() always gets called after get_or_create_model(), and // visibility is not set again until after the model is rendered, we @@ -5002,7 +5067,7 @@ impl FigureState { // Can potentially overflow if self.avg_vel.magnitude_squared() != 0.0 { - self.acc_vel += self.avg_vel.magnitude() * dt; + self.acc_vel += (self.avg_vel - ground_vel).magnitude() * dt; } else { self.acc_vel = 0.0; } diff --git a/voxygen/src/scene/simple.rs b/voxygen/src/scene/simple.rs index 2502c6c176..bc735af9e1 100644 --- a/voxygen/src/scene/simple.rs +++ b/voxygen/src/scene/simple.rs @@ -199,6 +199,7 @@ impl Scene { &camera, &mut buf, None, + Vec3::zero(), ); (model, state) }), @@ -378,6 +379,7 @@ impl Scene { &self.camera, &mut buf, None, + Vec3::zero(), ); } } From 1a850ae65f85af245b3b438b9819ee0e16de1eae Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 12 Mar 2021 14:56:47 +0000 Subject: [PATCH 08/41] Smooth airship movement --- common/src/states/utils.rs | 18 ++++++++++-------- common/sys/src/phys.rs | 4 +++- server/src/sys/agent.rs | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 05928fc940..688c9c0d7f 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -169,15 +169,16 @@ impl Body { quadruped_low::Species::Lavadrake => 4.0, _ => 6.0, }, - Body::Ship(_) => 1.0, + Body::Ship(_) => 0.1, } } - pub fn can_fly(&self) -> bool { - matches!( - self, - Body::BirdMedium(_) | Body::Dragon(_) | Body::BirdSmall(_) | Body::Ship(ship::Body::DefaultAirship) - ) + pub fn can_fly(&self) -> Option { + match self { + Body::BirdMedium(_) | Body::Dragon(_) | Body::BirdSmall(_) => Some(1.0), + Body::Ship(ship::Body::DefaultAirship) => Some(1.5), + _ => None, + } } pub fn can_climb(&self) -> bool { matches!(self, Body::Humanoid(_)) } @@ -189,9 +190,10 @@ pub fn handle_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) { swim_move(data, update, efficiency, depth); } else if input_is_pressed(data, InputKind::Fly) && !data.physics.on_ground - && data.body.can_fly() + && data.body.can_fly().is_some() { - fly_move(data, update, efficiency); + fly_move(data, update, efficiency * data.body.can_fly().expect("can_fly is_some right above this")); + } else if data.inputs.fly.is_pressed() && !data.physics.on_ground && data.body.can_fly().is_some() { } else { basic_move(data, update, efficiency); } diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index f4c1215c6e..23d93333ab 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -425,7 +425,7 @@ impl<'a> PhysicsSystemData<'a> { let old_vel = vel; // Integrate forces // Friction is assumed to be a constant dependent on location - let friction = FRIC_AIR + let friction = if physics_state.on_ground { 0.0 } else { FRIC_AIR } // .max(if physics_state.on_ground { // FRIC_GROUND // } else { @@ -979,7 +979,9 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( if on_ground { physics_state.on_ground = true; + vel.0 = ground_vel + (vel.0 - ground_vel) * (1.0 - FRIC_GROUND.min(1.0)).powf(dt.0 * 60.0); + physics_state.ground_vel = ground_vel; // If the space below us is free, then "snap" to the ground } else if collision_with( pos.0 - Vec3::unit_z() * 1.05, diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index fda3106b13..73046aeabd 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -213,7 +213,7 @@ impl<'a> System<'a> for Sys { in_liquid: physics_state.in_liquid.is_some(), min_tgt_dist: 1.0, can_climb: body.map(|b| b.can_climb()).unwrap_or(false), - can_fly: body.map(|b| b.can_fly()).unwrap_or(false), + can_fly: body.map(|b| b.can_fly().is_some()).unwrap_or(false), }; let flees = alignment From 3fe3e5f183ff3df1005b7ddbd0489a094d0d64fd Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 12 Mar 2021 15:26:12 +0000 Subject: [PATCH 09/41] Made airship terrain hitboxes smaller --- common/sys/src/phys.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index 23d93333ab..501714daf6 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -468,7 +468,8 @@ impl<'a> PhysicsSystemData<'a> { Collider::Voxel { .. } => { // for now, treat entities with voxel colliders as their bounding // cylinders for the purposes of colliding them with terrain - let radius = collider.get_radius() * scale; + // Actually no, make them smaller to avoid lag + let radius = collider.get_radius() * scale * 0.1; let (z_min, z_max) = collider.get_z_limits(scale); let cylinder = (radius, z_min, z_max); From db7d5a777159b7cfecf7ef7cca54a157a2408142 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 12 Mar 2021 15:38:48 +0000 Subject: [PATCH 10/41] Fixed water collision bug --- common/sys/src/phys.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index 501714daf6..99ed590750 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -658,6 +658,7 @@ impl<'a> PhysicsSystemData<'a> { match (physics_state.in_liquid, physics_state_delta.in_liquid) { // this match computes `x <|> y <|> liftA2 max x y` (Some(x), Some(y)) => Some(x.max(y)), + (x @ Some(_), _) => x, (_, y @ Some(_)) => y, _ => None, }; From 6dbf033c61a9b17b9c9a5b45630968438aa630ce Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 12 Mar 2021 17:08:40 +0000 Subject: [PATCH 11/41] Mountable airships --- common/src/states/utils.rs | 2 +- server/src/state_ext.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 688c9c0d7f..794e1c1c4b 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -176,7 +176,7 @@ impl Body { pub fn can_fly(&self) -> Option { match self { Body::BirdMedium(_) | Body::Dragon(_) | Body::BirdSmall(_) => Some(1.0), - Body::Ship(ship::Body::DefaultAirship) => Some(1.5), + Body::Ship(ship::Body::DefaultAirship) => Some(1.0), _ => None, } } diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 787483df06..f19b105b5d 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -233,6 +233,7 @@ impl StateExt for State { .with(comp::Energy::new(ship.into(), level)) .with(comp::Health::new(ship.into(), level)) .with(comp::Stats::new("Airship".to_string())) + .with(comp::MountState::Unmounted) .with(comp::Buffs::default()) .with(comp::Combo::default()) .with(comp::Agent::with_destination()) From 956b913a95fa9a4643f50e6b01153a8d893c13d6 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Fri, 12 Mar 2021 13:53:06 -0500 Subject: [PATCH 12/41] `/airship angle` command and RtSim airships. --- common/src/cmd.rs | 2 +- common/src/comp/agent.rs | 6 +-- common/src/comp/body.rs | 10 +++++ common/src/rtsim.rs | 6 +-- server/src/cmd.rs | 7 ++- server/src/rtsim/entity.rs | 4 +- server/src/rtsim/tick.rs | 5 ++- server/src/state_ext.rs | 91 ++++++++++++++++++++------------------ server/src/sys/agent.rs | 26 ++++++++++- 9 files changed, 100 insertions(+), 57 deletions(-) diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 9aee158547..3da0e9b52d 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -224,7 +224,7 @@ impl ChatCommand { "Temporarily gives a player admin permissions or removes them", Admin, ), - ChatCommand::Airship => cmd(vec![Boolean("moving", "true".to_string(), Optional)], "Spawns an airship", Admin), + ChatCommand::Airship => cmd(vec![Float("destination_degrees_ccw_of_east", 90.0, Optional)], "Spawns an airship", Admin), ChatCommand::Alias => cmd(vec![Any("name", Required)], "Change your alias", NoAdmin), ChatCommand::Ban => cmd( vec![Any("username", Required), Message(Optional)], diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index eccf2ce9fa..2df698c1c8 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -222,11 +222,11 @@ impl Agent { self } - pub fn with_destination() -> Self { + pub fn with_destination(pos: Vec3) -> Self { Self { - can_speak: false, + can_speak: true, psyche: Psyche { aggro: 1.0 }, - rtsim_controller: RtSimController::zero(), + rtsim_controller: RtSimController::with_destination(pos), ..Default::default() } } diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index 1733eec1e1..2abbe775c6 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -519,6 +519,16 @@ impl Body { } } + pub fn flying_height(&self) -> f32 { + match self { + Body::BirdSmall(_) => 30.0, + Body::BirdMedium(_) => 40.0, + Body::Dragon(_) => 60.0, + Body::Ship(ship::Body::DefaultAirship) => 60.0, + _ => 0.0, + } + } + pub fn immune_to(&self, buff: BuffKind) -> bool { match buff { BuffKind::Bleeding => matches!(self, Body::Object(_) | Body::Golem(_) | Body::Ship(_)), diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 809a505b1f..716bacf382 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -46,10 +46,10 @@ impl Default for RtSimController { impl RtSimController { pub fn reset(&mut self) { *self = Self::default(); } - pub fn zero() -> Self { + pub fn with_destination(pos: Vec3) -> Self { Self { - travel_to: Some((Vec3::new(0.0, 0.0, 500.0), "".to_string())), - speed_factor: 0.05, + travel_to: Some((pos, format!("{:0.1?}", pos))), + speed_factor: 0.25, } } } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 0fbc8bcd0f..7aae9f5339 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -992,13 +992,16 @@ fn handle_spawn_airship( args: String, action: &ChatCommand, ) { - let moving = scan_fmt_some!(&args, &action.arg_fmt(), String).unwrap_or_else(|| "false".to_string()) == "true"; + let angle = scan_fmt!(&args, &action.arg_fmt(), f32).ok(); match server.state.read_component_copied::(target) { Some(mut pos) => { pos.0.z += 50.0; + const DESTINATION_RADIUS: f32 = 2000.0; + let angle = angle.map(|a| a * std::f32::consts::PI / 180.0); + let destination = angle.map(|a| pos.0 + Vec3::new(DESTINATION_RADIUS*a.cos(), DESTINATION_RADIUS*a.sin(), 200.0)); server .state - .create_ship(pos, comp::ship::Body::DefaultAirship, 1, moving) + .create_ship(pos, comp::ship::Body::DefaultAirship, 1, destination) .with(comp::Scale(11.0 / 0.8)) .with(LightEmitter { col: Rgb::new(1.0, 0.65, 0.2), diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index 2a8ef629d7..18f500f695 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -27,7 +27,7 @@ impl Entity { pub fn get_body(&self) -> comp::Body { match self.rng(PERM_GENUS).gen::() { - //we want 50% birds, 50% humans for now + //we want 5% airships, 45% birds, 50% humans x if x < 0.05 => { comp::Body::Ship(comp::ship::Body::DefaultAirship) }, @@ -135,7 +135,7 @@ impl Entity { .iter() .filter(|s| match self.get_body() { comp::Body::Humanoid(_) => s.1.is_settlement() | s.1.is_castle(), - comp::Body::Ship(_) => s.1.is_castle(), + comp::Body::Ship(_) => s.1.is_settlement(), _ => s.1.is_dungeon(), }) .filter(|_| thread_rng().gen_range(0i32..4) == 0) diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index 6faddda0c1..30b16f5d1f 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -122,7 +122,10 @@ impl<'a> System<'a> for Sys { comp::Body::Humanoid(_) => comp::Alignment::Npc, _ => comp::Alignment::Wild, }, - scale: comp::Scale(1.0), + scale: match body { + comp::Body::Ship(_) => comp::Scale(11.0 / 0.8), + _ => comp::Scale(1.0), + }, drop_item: None, home_chunk: None, rtsim_entity: Some(RtSimEntity(id)), diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index f19b105b5d..155b07e286 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -41,7 +41,13 @@ pub trait StateExt { ) -> EcsEntityBuilder; /// Build a static object entity fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder; - fn create_ship(&mut self, pos: comp::Pos, ship: comp::ship::Body, level: u16, moving: bool) -> EcsEntityBuilder; + fn create_ship( + &mut self, + pos: comp::Pos, + ship: comp::ship::Body, + level: u16, + destination: Option>, + ) -> EcsEntityBuilder; /// Build a projectile fn create_projectile( &mut self, @@ -173,10 +179,15 @@ impl StateExt for State { )) .unwrap_or_default(), ) - .with(comp::Collider::Box { - radius: body.radius(), - z_min: 0.0, - z_max: body.height(), + .with(match body { + comp::Body::Ship(ship) => comp::Collider::Voxel { + id: ship.manifest_entry().to_string(), + }, + _ => comp::Collider::Box { + radius: body.radius(), + z_min: 0.0, + z_max: body.height(), + }, }) .with(comp::Controller::default()) .with(body) @@ -216,46 +227,38 @@ impl StateExt for State { .with(comp::Gravity(1.0)) } - fn create_ship(&mut self, pos: comp::Pos, ship: comp::ship::Body, level: u16, moving: bool) -> EcsEntityBuilder { - if moving { - self.ecs_mut() - .create_entity_synced() - .with(pos) - .with(comp::Vel(Vec3::zero())) - .with(comp::Ori::default()) - .with(comp::Mass(50.0)) - .with(comp::Collider::Voxel { id: ship.manifest_entry().to_string() }) - .with(comp::Body::Ship(ship)) - .with(comp::Gravity(1.0)) - .with(comp::Controller::default()) - .with(comp::inventory::Inventory::new_empty()) - .with(comp::CharacterState::default()) - .with(comp::Energy::new(ship.into(), level)) - .with(comp::Health::new(ship.into(), level)) - .with(comp::Stats::new("Airship".to_string())) - .with(comp::MountState::Unmounted) - .with(comp::Buffs::default()) - .with(comp::Combo::default()) - .with(comp::Agent::with_destination()) - } else { - self.ecs_mut() - .create_entity_synced() - .with(pos) - .with(comp::Vel(Vec3::zero())) - .with(comp::Ori::default()) - .with(comp::Mass(50.0)) - .with(comp::Collider::Voxel { id: ship.manifest_entry().to_string() }) - .with(comp::Body::Ship(ship)) - .with(comp::Gravity(1.0)) - .with(comp::Controller::default()) - .with(comp::inventory::Inventory::new_empty()) - .with(comp::CharacterState::default()) - .with(comp::Energy::new(ship.into(), level)) - .with(comp::Health::new(ship.into(), level)) - .with(comp::Stats::new("Airship".to_string())) - .with(comp::Buffs::default()) - .with(comp::Combo::default()) + fn create_ship( + &mut self, + pos: comp::Pos, + ship: comp::ship::Body, + level: u16, + destination: Option>, + ) -> EcsEntityBuilder { + let mut builder = self + .ecs_mut() + .create_entity_synced() + .with(pos) + .with(comp::Vel(Vec3::zero())) + .with(comp::Ori::default()) + .with(comp::Mass(50.0)) + .with(comp::Collider::Voxel { + id: ship.manifest_entry().to_string(), + }) + .with(comp::Body::Ship(ship)) + .with(comp::Gravity(1.0)) + .with(comp::Controller::default()) + .with(comp::inventory::Inventory::new_empty()) + .with(comp::CharacterState::default()) + .with(comp::Energy::new(ship.into(), level)) + .with(comp::Health::new(ship.into(), level)) + .with(comp::Stats::new("Airship".to_string())) + .with(comp::Buffs::default()) + .with(comp::MountState::Unmounted) + .with(comp::Combo::default()); + if let Some(pos) = destination { + builder = builder.with(comp::Agent::with_destination(pos)) } + builder } fn create_projectile( diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 73046aeabd..3d36f8f8c1 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -34,7 +34,7 @@ use specs::{ Entities, Entity as EcsEntity, Join, ParJoin, Read, ReadExpect, ReadStorage, SystemData, World, Write, WriteStorage, }; -use std::f32::consts::PI; +use std::{f32::consts::PI, sync::Arc}; use vek::*; struct AgentData<'a> { @@ -82,6 +82,7 @@ pub struct ReadData<'a> { mount_states: ReadStorage<'a, MountState>, time_of_day: Read<'a, TimeOfDay>, light_emitter: ReadStorage<'a, LightEmitter>, + world: ReadExpect<'a, Arc>, } // This is 3.1 to last longer than the last damage timer (3.0 seconds) @@ -631,6 +632,29 @@ impl<'a> AgentData<'a> { .cast() .1 .map_or(true, |b| b.is_some()) + || self + .body + .map(|body| { + let height_approx = self.pos.0.y + - read_data + .world + .sim() + .get_alt_approx(self.pos.0.xy().map(|x: f32| x as i32)) + .unwrap_or(0.0); + + height_approx < body.flying_height() + || read_data + .terrain + .ray( + self.pos.0, + self.pos.0 - body.flying_height() * Vec3::unit_z(), + ) + .until(|b: &Block| b.is_solid() || b.is_liquid()) + .cast() + .1 + .map_or(false, |b| b.is_some()) + }) + .unwrap_or(false) { 1.0 //fly up when approaching obstacles } else { From 1395462c2782f3acdbf5e250493565610074f7e0 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 12 Mar 2021 18:53:01 +0000 Subject: [PATCH 13/41] More relative motion changes --- common/src/states/utils.rs | 2 +- common/sys/src/phys.rs | 13 +++++++++---- common/sys/src/state.rs | 3 ++- server/src/events/entity_creation.rs | 2 +- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 794e1c1c4b..65a9849b97 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -169,7 +169,7 @@ impl Body { quadruped_low::Species::Lavadrake => 4.0, _ => 6.0, }, - Body::Ship(_) => 0.1, + Body::Ship(_) => 0.5, } } diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index 99ed590750..7144928bf6 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -464,6 +464,8 @@ impl<'a> PhysicsSystemData<'a> { Vec3::zero() }; + let was_on_ground = physics_state.on_ground; + match &*collider { Collider::Voxel { .. } => { // for now, treat entities with voxel colliders as their bounding @@ -484,6 +486,7 @@ impl<'a> PhysicsSystemData<'a> { Vec3::zero(), &psdr.dt, true, + was_on_ground, |entity, vel| land_on_grounds.push((entity, vel)), ); }, @@ -509,6 +512,7 @@ impl<'a> PhysicsSystemData<'a> { Vec3::zero(), &psdr.dt, true, + was_on_ground, |entity, vel| land_on_grounds.push((entity, vel)), ); }, @@ -636,6 +640,7 @@ impl<'a> PhysicsSystemData<'a> { transform_to.mul_direction(vel_other.0), &psdr.dt, false, + was_on_ground, |entity, vel| land_on_grounds.push((entity, Vel(transform_from.mul_direction(vel.0)))), ); @@ -750,6 +755,7 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( ground_vel: Vec3, dt: &DeltaTime, apply_velocity_step: bool, // Stupid hack + was_on_ground: bool, mut land_on_ground: impl FnMut(Entity, Vel), ) { let (radius, z_min, z_max) = cylinder; @@ -825,7 +831,6 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( > 0 } - let was_on_ground = physics_state.on_ground; physics_state.on_ground = false; let mut on_ground = false; @@ -948,7 +953,7 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( { // ...block-hop! pos.0.z = (pos.0.z + 0.1).floor() + block_height; - vel.0.z = 0.0; + vel.0.z = vel.0.z.max(0.0); on_ground = true; break; } else { @@ -956,7 +961,7 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( vel.0 = vel.0.map2( resolve_dir, |e, d| { - if d * e.signum() < 0.0 { 0.0 } else { e } + if d * e.signum() < 0.0 { if d < 0.0 { d.max(0.0) } else { d.min(0.0) } } else { e } }, ); pos_delta *= resolve_dir.map(|e| if e != 0.0 { 0.0 } else { 1.0 }); @@ -992,7 +997,7 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( near_iter.clone(), radius, z_range.clone(), - ) && vel.0.z < 0.0 + ) && vel.0.z < 0.01 && vel.0.z > -1.5 && was_on_ground && !collision_with( diff --git a/common/sys/src/state.rs b/common/sys/src/state.rs index 3d15d4e3ff..f0e9108f7c 100644 --- a/common/sys/src/state.rs +++ b/common/sys/src/state.rs @@ -455,10 +455,11 @@ impl State { for event in events { let mut velocities = self.ecs.write_storage::(); let mut controllers = self.ecs.write_storage::(); + let physics = self.ecs.read_storage::(); match event { LocalEvent::Jump(entity) => { if let Some(vel) = velocities.get_mut(entity) { - vel.0.z = HUMANOID_JUMP_ACCEL; + vel.0.z = HUMANOID_JUMP_ACCEL + physics.get(entity).map_or(0.0, |ps| ps.ground_vel.z); } }, LocalEvent::ApplyImpulse { entity, impulse } => { diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index a10b60c9d9..a3b3702975 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -129,7 +129,7 @@ pub fn handle_shoot( return; }; - let vel = *dir * speed; + let vel = *dir * speed + state.ecs().read_storage::().get(entity).map_or(Vec3::zero(), |v| v.0); // Add an outcome state From 65141673c05258652c4e7fc7dff673eadf591a0f Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 12 Mar 2021 19:49:20 +0000 Subject: [PATCH 14/41] Relative velocity collisions --- common/sys/src/phys.rs | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index 7144928bf6..9fbba02e14 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -627,7 +627,7 @@ impl<'a> PhysicsSystemData<'a> { * Mat4::::translation_3d(voxel_collider.translation); let transform_to = transform_from.inverted(); pos.0 = transform_to.mul_point(pos.0); - vel.0 = transform_to.mul_direction(vel.0); + vel.0 = transform_to.mul_direction(vel.0 - vel_other.0); let cylinder = (radius, z_min, z_max); cylinder_voxel_collision( cylinder, @@ -645,7 +645,7 @@ impl<'a> PhysicsSystemData<'a> { ); pos.0 = transform_from.mul_point(pos.0); - vel.0 = transform_from.mul_direction(vel.0); + vel.0 = transform_from.mul_direction(vel.0) + vel_other.0; // union in the state updates, so that the state isn't just based on // the most recent terrain that collision was attempted with @@ -919,7 +919,7 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( // When the resolution direction is pointing upwards, we must be on the // ground - if resolve_dir.z > 0.0 && vel.0.z <= 0.0 { + if resolve_dir.z > 0.0 /*&& vel.0.z <= 0.0*/ { on_ground = true; if !was_on_ground { @@ -986,39 +986,41 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( if on_ground { physics_state.on_ground = true; - - vel.0 = ground_vel + (vel.0 - ground_vel) * (1.0 - FRIC_GROUND.min(1.0)).powf(dt.0 * 60.0); - physics_state.ground_vel = ground_vel; // If the space below us is free, then "snap" to the ground } else if collision_with( - pos.0 - Vec3::unit_z() * 1.05, + pos.0 - Vec3::unit_z() * 1.1, &terrain, block_true, near_iter.clone(), radius, z_range.clone(), - ) && vel.0.z < 0.01 + ) && vel.0.z < 0.1 && vel.0.z > -1.5 - && was_on_ground - && !collision_with( - pos.0 - Vec3::unit_z() * 0.05, - &terrain, - |block| block.solid_height() >= (pos.0.z - 0.05).rem_euclid(1.0), - near_iter.clone(), - radius, - z_range.clone(), - ) + // && was_on_ground + // && !collision_with( + // pos.0 - Vec3::unit_z() * 0.0, + // &terrain, + // |block| block.solid_height() >= (pos.0.z - 0.1).rem_euclid(1.0), + // near_iter.clone(), + // radius, + // z_range.clone(), + // ) { let snap_height = terrain - .get(Vec3::new(pos.0.x, pos.0.y, pos.0.z - 0.05).map(|e| e.floor() as i32)) + .get(Vec3::new(pos.0.x, pos.0.y, pos.0.z - 0.1).map(|e| e.floor() as i32)) .ok() .filter(|block| block.is_solid()) .map(|block| block.solid_height()) .unwrap_or(0.0); - pos.0.z = (pos.0.z - 0.05).floor() + snap_height; + pos.0.z = (pos.0.z - 0.1).floor() + snap_height; physics_state.on_ground = true; } + if physics_state.on_ground { + vel.0 = ground_vel * 0.0 + (vel.0 - ground_vel * 0.0) * (1.0 - FRIC_GROUND.min(1.0)).powf(dt.0 * 60.0); + physics_state.ground_vel = ground_vel; + } + let dirs = [ Vec3::unit_x(), Vec3::unit_y(), From 036e0c1f021991583a4324a998defc99da11016c Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Fri, 12 Mar 2021 17:14:08 -0500 Subject: [PATCH 15/41] Fix fmt and clippy, and rename {psdr,psdw} to {read,write} in physics. --- common/src/cmd.rs | 6 +- common/src/comp/body.rs | 8 +- common/src/comp/body/ship.rs | 3 +- common/src/comp/mod.rs | 4 +- common/src/comp/phys.rs | 3 +- common/src/rtsim.rs | 1 + common/src/states/utils.rs | 3 +- common/src/volumes/dyna.rs | 7 +- common/sys/src/phys.rs | 276 ++++++++++++++------------- common/sys/src/state.rs | 3 +- server/src/cmd.rs | 9 +- server/src/events/entity_creation.rs | 7 +- server/src/rtsim/entity.rs | 4 +- server/src/sys/agent.rs | 51 ++--- voxygen/anim/src/lib.rs | 2 +- voxygen/anim/src/ship/idle.rs | 1 - voxygen/anim/src/ship/mod.rs | 5 +- voxygen/src/scene/figure/load.rs | 21 +- voxygen/src/scene/figure/mod.rs | 89 +++++---- 19 files changed, 280 insertions(+), 223 deletions(-) diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 3da0e9b52d..e53c82609e 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -224,7 +224,11 @@ impl ChatCommand { "Temporarily gives a player admin permissions or removes them", Admin, ), - ChatCommand::Airship => cmd(vec![Float("destination_degrees_ccw_of_east", 90.0, Optional)], "Spawns an airship", Admin), + ChatCommand::Airship => cmd( + vec![Float("destination_degrees_ccw_of_east", 90.0, Optional)], + "Spawns an airship", + Admin, + ), ChatCommand::Alias => cmd(vec![Any("name", Required)], "Change your alias", NoAdmin), ChatCommand::Ban => cmd( vec![Any("username", Required), Message(Optional)], diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index 2abbe775c6..9993e6a0c8 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -539,7 +539,13 @@ impl Body { /// Returns a multiplier representing increased difficulty not accounted for /// due to AI or not using an actual weapon // TODO: Match on species - pub fn combat_multiplier(&self) -> f32 { if let Body::Object(_) | Body::Ship(_) = self { 0.0 } else { 1.0 } } + pub fn combat_multiplier(&self) -> f32 { + if let Body::Object(_) | Body::Ship(_) = self { + 0.0 + } else { + 1.0 + } + } pub fn base_poise(&self) -> u32 { match self { diff --git a/common/src/comp/body/ship.rs b/common/src/comp/body/ship.rs index 3cab7a0ecb..4a49d9f2a1 100644 --- a/common/src/comp/body/ship.rs +++ b/common/src/comp/body/ship.rs @@ -78,7 +78,8 @@ pub mod figuredata { cache: &assets::AssetCache, _: &str, ) -> Result { - let manifest: AssetHandle> = AssetExt::load("server.manifests.ship_manifest")?; + let manifest: AssetHandle> = + AssetExt::load("server.manifests.ship_manifest")?; let mut colliders = HashMap::new(); for (_, spec) in (manifest.read().0).0.iter() { for bone in [&spec.bone0, &spec.bone1, &spec.bone2].iter() { diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index d97e265919..6f951b9704 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -48,8 +48,8 @@ pub use self::{ beam::{Beam, BeamSegment}, body::{ biped_large, biped_small, bird_medium, bird_small, dragon, fish_medium, fish_small, golem, - humanoid, object, quadruped_low, quadruped_medium, quadruped_small, theropod, ship, AllBodies, - Body, BodyData, + humanoid, object, quadruped_low, quadruped_medium, quadruped_small, ship, theropod, + AllBodies, Body, BodyData, }, buff::{ Buff, BuffCategory, BuffChange, BuffData, BuffEffect, BuffId, BuffKind, BuffSource, Buffs, diff --git a/common/src/comp/phys.rs b/common/src/comp/phys.rs index 149f8ebfd4..33729f28ad 100644 --- a/common/src/comp/phys.rs +++ b/common/src/comp/phys.rs @@ -119,7 +119,8 @@ impl PhysicsState { touch_entities.clear(); *self = Self { touch_entities, - ground_vel: self.ground_vel, // Preserved, since it's the velocity of the last contact point + ground_vel: self.ground_vel, /* Preserved, since it's the velocity of the last + * contact point */ ..Self::default() } } diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 716bacf382..49a831e12d 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -46,6 +46,7 @@ impl Default for RtSimController { impl RtSimController { pub fn reset(&mut self) { *self = Self::default(); } + pub fn with_destination(pos: Vec3) -> Self { Self { travel_to: Some((pos, format!("{:0.1?}", pos))), diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 65a9849b97..6ee2449950 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -3,7 +3,7 @@ use crate::{ biped_large, biped_small, inventory::slot::EquipSlot, item::{Hands, ItemKind, Tool, ToolKind}, - quadruped_low, quadruped_medium, quadruped_small, + quadruped_low, quadruped_medium, quadruped_small, ship, skills::Skill, theropod, ship, Body, CharacterAbility, CharacterState, InputKind, InventoryAction, StateUpdate, }, @@ -193,7 +193,6 @@ pub fn handle_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) { && data.body.can_fly().is_some() { fly_move(data, update, efficiency * data.body.can_fly().expect("can_fly is_some right above this")); - } else if data.inputs.fly.is_pressed() && !data.physics.on_ground && data.body.can_fly().is_some() { } else { basic_move(data, update, efficiency); } diff --git a/common/src/volumes/dyna.rs b/common/src/volumes/dyna.rs index d0648010a8..b6cb3e6bdc 100644 --- a/common/src/volumes/dyna.rs +++ b/common/src/volumes/dyna.rs @@ -46,7 +46,12 @@ impl Dyna { } pub fn map_into W>(self, f: F) -> Dyna { - let Dyna { vox, meta, sz, _phantom } = self; + let Dyna { + vox, + meta, + sz, + _phantom, + } = self; Dyna { vox: vox.into_iter().map(f).collect(), meta, diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index 9fbba02e14..e01bcd276f 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -69,7 +69,7 @@ fn calc_z_limit( pub struct Sys; #[derive(SystemData)] -pub struct PhysicsSystemDataRead<'a> { +pub struct PhysicsRead<'a> { entities: Entities<'a>, uids: ReadStorage<'a, Uid>, terrain: ReadExpect<'a, TerrainGrid>, @@ -88,7 +88,7 @@ pub struct PhysicsSystemDataRead<'a> { } #[derive(SystemData)] -pub struct PhysicsSystemDataWrite<'a> { +pub struct PhysicsWrite<'a> { physics_metrics: WriteExpect<'a, PhysicsMetrics>, physics_states: WriteStorage<'a, PhysicsState>, positions: WriteStorage<'a, Pos>, @@ -98,26 +98,26 @@ pub struct PhysicsSystemDataWrite<'a> { } #[derive(SystemData)] -pub struct PhysicsSystemData<'a> { - r: PhysicsSystemDataRead<'a>, - w: PhysicsSystemDataWrite<'a>, +pub struct PhysicsData<'a> { + read: PhysicsRead<'a>, + write: PhysicsWrite<'a>, } -impl<'a> PhysicsSystemData<'a> { +impl<'a> PhysicsData<'a> { /// Add/reset physics state components fn reset(&mut self) { span!(guard, "Add/reset physics state components"); for (entity, _, _, _, _) in ( - &self.r.entities, - &self.r.colliders, - &self.w.positions, - &self.w.velocities, - &self.w.orientations, + &self.read.entities, + &self.read.colliders, + &self.write.positions, + &self.write.velocities, + &self.write.orientations, ) .join() { let _ = self - .w + .write .physics_states .entry(entity) .map(|e| e.or_insert_with(Default::default)); @@ -129,20 +129,20 @@ impl<'a> PhysicsSystemData<'a> { span!(guard, "Maintain pushback cache"); //Add PreviousPhysCache for all relevant entities for entity in ( - &self.r.entities, - &self.w.velocities, - &self.w.positions, - !&self.w.previous_phys_cache, - !&self.r.mountings, - !&self.r.beams, - !&self.r.shockwaves, + &self.read.entities, + &self.write.velocities, + &self.write.positions, + !&self.write.previous_phys_cache, + !&self.read.mountings, + !&self.read.beams, + !&self.read.shockwaves, ) .join() .map(|(e, _, _, _, _, _, _)| e) .collect::>() { let _ = self - .w + .write .previous_phys_cache .insert(entity, PreviousPhysCache { velocity_dt: Vec3::zero(), @@ -155,16 +155,16 @@ impl<'a> PhysicsSystemData<'a> { //Update PreviousPhysCache for (_, vel, position, mut phys_cache, collider, scale, cs, _, _, _) in ( - &self.r.entities, - &self.w.velocities, - &self.w.positions, - &mut self.w.previous_phys_cache, - self.r.colliders.maybe(), - self.r.scales.maybe(), - self.r.char_states.maybe(), - !&self.r.mountings, - !&self.r.beams, - !&self.r.shockwaves, + &self.read.entities, + &self.write.velocities, + &self.write.positions, + &mut self.write.previous_phys_cache, + self.read.colliders.maybe(), + self.read.scales.maybe(), + self.read.char_states.maybe(), + !&self.read.mountings, + !&self.read.beams, + !&self.read.shockwaves, ) .join() { @@ -173,7 +173,7 @@ impl<'a> PhysicsSystemData<'a> { let z_limits = (z_limits.0 * scale, z_limits.1 * scale); let half_height = (z_limits.1 - z_limits.0) / 2.0; - phys_cache.velocity_dt = vel.0 * self.r.dt.0; + phys_cache.velocity_dt = vel.0 * self.read.dt.0; let entity_center = position.0 + Vec3::new(0.0, z_limits.0 + half_height, 0.0); let flat_radius = collider.map(|c| c.get_radius()).unwrap_or(0.5) * scale; let radius = (flat_radius.powi(2) + half_height.powi(2)).sqrt(); @@ -191,25 +191,25 @@ impl<'a> PhysicsSystemData<'a> { fn apply_pushback(&mut self, job: &mut Job) { span!(guard, "Apply pushback"); job.cpu_stats.measure(ParMode::Rayon); - let PhysicsSystemData { - r: ref psdr, - w: ref mut psdw, + let PhysicsData { + ref read, + ref mut write, } = self; - let (positions, previous_phys_cache) = (&psdw.positions, &psdw.previous_phys_cache); + let (positions, previous_phys_cache) = (&write.positions, &write.previous_phys_cache); let metrics = ( - &psdr.entities, + &read.entities, positions, - &mut psdw.velocities, + &mut write.velocities, previous_phys_cache, - psdr.masses.maybe(), - psdr.colliders.maybe(), - !&psdr.mountings, - psdr.stickies.maybe(), - &mut psdw.physics_states, + read.masses.maybe(), + read.colliders.maybe(), + !&read.mountings, + read.stickies.maybe(), + &mut write.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 - psdr.projectiles.maybe(), - psdr.char_states.maybe(), + read.projectiles.maybe(), + read.char_states.maybe(), ) .par_join() .filter(|(_, _, _, _, _, _, _, sticky, physics, _, _)| { @@ -259,17 +259,17 @@ impl<'a> PhysicsSystemData<'a> { _, char_state_other_maybe, ) in ( - &psdr.entities, - &psdr.uids, + &read.entities, + &read.uids, positions, previous_phys_cache, - psdr.masses.maybe(), - psdr.colliders.maybe(), - !&psdr.projectiles, - !&psdr.mountings, - !&psdr.beams, - !&psdr.shockwaves, - psdr.char_states.maybe(), + read.masses.maybe(), + read.colliders.maybe(), + !&read.projectiles, + !&read.mountings, + !&read.beams, + !&read.shockwaves, + read.char_states.maybe(), ) .join() { @@ -347,7 +347,7 @@ impl<'a> PhysicsSystemData<'a> { } // Change velocity - vel.0 += vel_delta * psdr.dt.0; + vel.0 += vel_delta * read.dt.0; // Metrics PhysicsMetrics { @@ -362,31 +362,36 @@ impl<'a> PhysicsSystemData<'a> { entity_entity_collisions: old.entity_entity_collisions + new.entity_entity_collisions, }); - psdw.physics_metrics.entity_entity_collision_checks = + write.physics_metrics.entity_entity_collision_checks = metrics.entity_entity_collision_checks; - psdw.physics_metrics.entity_entity_collisions = metrics.entity_entity_collisions; + write.physics_metrics.entity_entity_collisions = metrics.entity_entity_collisions; drop(guard); } fn handle_movement_and_terrain(&mut self, job: &mut Job) { - let PhysicsSystemData { - r: ref psdr, - w: ref mut psdw, + let PhysicsData { + ref read, + ref mut write, } = self; // Apply movement inputs span!(guard, "Apply movement and terrain collision"); - let (positions, velocities, previous_phys_cache, orientations) = (&psdw.positions, &psdw.velocities, &psdw.previous_phys_cache, &psdw.orientations); + let (positions, velocities, previous_phys_cache, orientations) = ( + &write.positions, + &write.velocities, + &write.previous_phys_cache, + &write.orientations, + ); let (pos_writes, vel_writes, land_on_grounds) = ( - &psdr.entities, - psdr.scales.maybe(), - psdr.stickies.maybe(), - &psdr.colliders, + &read.entities, + read.scales.maybe(), + read.stickies.maybe(), + &read.colliders, positions, velocities, orientations, - &mut psdw.physics_states, + &mut write.physics_states, previous_phys_cache, - !&psdr.mountings, + !&read.mountings, ) .par_join() .fold( @@ -401,7 +406,7 @@ impl<'a> PhysicsSystemData<'a> { vel, _ori, mut physics_state, - previous_cache, + _previous_cache, _, )| { // defer the writes of positions to allow an inner loop over terrain-like @@ -436,9 +441,9 @@ impl<'a> PhysicsSystemData<'a> { } else { 0.0 }); - let in_loaded_chunk = psdr + let in_loaded_chunk = read .terrain - .get_key(psdr.terrain.pos_key(pos.0.map(|e| e.floor() as i32))) + .get_key(read.terrain.pos_key(pos.0.map(|e| e.floor() as i32))) .is_some(); let downward_force = if !in_loaded_chunk { @@ -451,22 +456,22 @@ impl<'a> PhysicsSystemData<'a> { (1.0 - BOUYANCY) * GRAVITY } else { GRAVITY - } * psdr.gravities.get(entity).map(|g| g.0).unwrap_or_default(); - vel.0 = integrate_forces(psdr.dt.0, vel.0, downward_force, friction); + } * read.gravities.get(entity).map(|g| g.0).unwrap_or_default(); + vel.0 = integrate_forces(read.dt.0, vel.0, downward_force, friction); // Don't move if we're not in a loaded chunk let pos_delta = if in_loaded_chunk { // this is an approximation that allows most framerates to // behave in a similar manner. let dt_lerp = 0.2; - (vel.0 * dt_lerp + old_vel.0 * (1.0 - dt_lerp)) * psdr.dt.0 + (vel.0 * dt_lerp + old_vel.0 * (1.0 - dt_lerp)) * read.dt.0 } else { Vec3::zero() }; let was_on_ground = physics_state.on_ground; - match &*collider { + match &collider { Collider::Voxel { .. } => { // for now, treat entities with voxel colliders as their bounding // cylinders for the purposes of colliding them with terrain @@ -477,14 +482,14 @@ impl<'a> PhysicsSystemData<'a> { let cylinder = (radius, z_min, z_max); cylinder_voxel_collision( cylinder, - &*psdr.terrain, + &*read.terrain, entity, &mut pos, pos_delta, &mut vel, &mut physics_state, Vec3::zero(), - &psdr.dt, + &read.dt, true, was_on_ground, |entity, vel| land_on_grounds.push((entity, vel)), @@ -503,35 +508,35 @@ impl<'a> PhysicsSystemData<'a> { let cylinder = (radius, z_min, z_max); cylinder_voxel_collision( cylinder, - &*psdr.terrain, + &*read.terrain, entity, &mut pos, pos_delta, &mut vel, &mut physics_state, Vec3::zero(), - &psdr.dt, + &read.dt, true, was_on_ground, |entity, vel| land_on_grounds.push((entity, vel)), ); }, Collider::Point => { - let (dist, block) = psdr + let (dist, block) = read .terrain .ray(pos.0, pos.0 + pos_delta) .until(|block: &Block| block.is_filled()) .ignore_error() .cast(); - pos.0 += pos_delta.try_normalized().unwrap_or(Vec3::zero()) * dist; + 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() { let block_center = pos.0.map(|e| e.floor()) + 0.5; let block_rpos = (pos.0 - block_center) .try_normalized() - .unwrap_or(Vec3::zero()); + .unwrap_or_else(Vec3::zero); // See whether we're on the top/bottom of a block, or the side if block_rpos.z.abs() @@ -555,7 +560,7 @@ impl<'a> PhysicsSystemData<'a> { } } - physics_state.in_liquid = psdr + physics_state.in_liquid = read .terrain .get(pos.0.map(|e| e.floor() as i32)) .ok() @@ -566,35 +571,36 @@ impl<'a> PhysicsSystemData<'a> { // Collide with terrain-like entities for ( entity_other, - other, + _other, pos_other, vel_other, - previous_cache_other, - mass_other, + _previous_cache_other, + _mass_other, collider_other, ori_other, _, _, _, _, - char_state_other_maybe, + _char_state_other_maybe, ) in ( - &psdr.entities, - &psdr.uids, + &read.entities, + &read.uids, positions, velocities, previous_phys_cache, - psdr.masses.maybe(), - &psdr.colliders, + read.masses.maybe(), + &read.colliders, orientations, - !&psdr.projectiles, - !&psdr.mountings, - !&psdr.beams, - !&psdr.shockwaves, - psdr.char_states.maybe(), + !&read.projectiles, + !&read.mountings, + !&read.beams, + !&read.shockwaves, + read.char_states.maybe(), ) .join() { + // TODO: terrain-collider-size aware broadphase /*let collision_boundary = previous_cache.collision_boundary + previous_cache_other.collision_boundary; if previous_cache @@ -618,7 +624,9 @@ impl<'a> PhysicsSystemData<'a> { let z_min = z_min * scale; let z_max = z_max.clamped(1.2, 1.95) * scale; - if let Some(voxel_collider) = VOXEL_COLLIDER_MANIFEST.read().colliders.get(id) { + if let Some(voxel_collider) = + VOXEL_COLLIDER_MANIFEST.read().colliders.get(&*id) + { let mut physics_state_delta = physics_state.clone(); // deliberately don't use scale yet here, because the 11.0/0.8 // thing is in the comp::Scale for visual reasons @@ -638,10 +646,15 @@ impl<'a> PhysicsSystemData<'a> { &mut vel, &mut physics_state_delta, transform_to.mul_direction(vel_other.0), - &psdr.dt, + &read.dt, false, was_on_ground, - |entity, vel| land_on_grounds.push((entity, Vel(transform_from.mul_direction(vel.0)))), + |entity, vel| { + land_on_grounds.push(( + entity, + Vel(transform_from.mul_direction(vel.0)), + )) + }, ); pos.0 = transform_from.mul_point(pos.0); @@ -659,14 +672,16 @@ impl<'a> PhysicsSystemData<'a> { physics_state .touch_entities .append(&mut physics_state_delta.touch_entities); - physics_state.in_liquid = - match (physics_state.in_liquid, physics_state_delta.in_liquid) { - // this match computes `x <|> y <|> liftA2 max x y` - (Some(x), Some(y)) => Some(x.max(y)), - (x @ Some(_), _) => x, - (_, y @ Some(_)) => y, - _ => None, - }; + physics_state.in_liquid = match ( + physics_state.in_liquid, + physics_state_delta.in_liquid, + ) { + // this match computes `x <|> y <|> liftA2 max x y` + (Some(x), Some(y)) => Some(x.max(y)), + (x @ Some(_), _) => x, + (_, y @ Some(_)) => y, + _ => None, + }; } } } @@ -695,7 +710,9 @@ impl<'a> PhysicsSystemData<'a> { let pos_writes: HashMap = pos_writes.into_iter().collect(); let vel_writes: HashMap = vel_writes.into_iter().collect(); - for (entity, pos, vel) in (&psdr.entities, &mut psdw.positions, &mut psdw.velocities).join() { + for (entity, pos, vel) in + (&read.entities, &mut write.positions, &mut write.velocities).join() + { if let Some(new_pos) = pos_writes.get(&entity) { *pos = *new_pos; } @@ -705,7 +722,7 @@ impl<'a> PhysicsSystemData<'a> { } } - let mut event_emitter = psdr.event_bus.emitter(); + let mut event_emitter = read.event_bus.emitter(); land_on_grounds.into_iter().for_each(|(entity, vel)| { event_emitter.emit(ServerEvent::LandOnGround { entity, vel: vel.0 }); }); @@ -713,14 +730,12 @@ impl<'a> PhysicsSystemData<'a> { } impl<'a> System<'a> for Sys { - type SystemData = PhysicsSystemData<'a>; + type SystemData = PhysicsData<'a>; const NAME: &'static str = "phys"; const ORIGIN: Origin = Origin::Common; const PHASE: Phase = Phase::Create; - #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 - #[allow(clippy::blocks_in_if_conditions)] // TODO: Pending review in #587 fn run(job: &mut Job, mut psd: Self::SystemData) { psd.reset(); @@ -744,6 +759,7 @@ impl<'a> System<'a> for Sys { } } +#[allow(clippy::too_many_arguments)] fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( cylinder: (f32, f32, f32), terrain: &'a T, @@ -919,7 +935,9 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( // When the resolution direction is pointing upwards, we must be on the // ground - if resolve_dir.z > 0.0 /*&& vel.0.z <= 0.0*/ { + if resolve_dir.z > 0.0 + /* && vel.0.z <= 0.0 */ + { on_ground = true; if !was_on_ground { @@ -958,12 +976,13 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( break; } else { // Correct the velocity - vel.0 = vel.0.map2( - resolve_dir, - |e, d| { - if d * e.signum() < 0.0 { if d < 0.0 { d.max(0.0) } else { d.min(0.0) } } else { e } - }, - ); + vel.0 = vel.0.map2(resolve_dir, |e, d| { + if d * e.signum() < 0.0 { + if d < 0.0 { d.max(0.0) } else { d.min(0.0) } + } else { + e + } + }); pos_delta *= resolve_dir.map(|e| if e != 0.0 { 0.0 } else { 1.0 }); } @@ -996,15 +1015,15 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( z_range.clone(), ) && vel.0.z < 0.1 && vel.0.z > -1.5 - // && was_on_ground - // && !collision_with( - // pos.0 - Vec3::unit_z() * 0.0, - // &terrain, - // |block| block.solid_height() >= (pos.0.z - 0.1).rem_euclid(1.0), - // near_iter.clone(), - // radius, - // z_range.clone(), - // ) + // && was_on_ground + // && !collision_with( + // pos.0 - Vec3::unit_z() * 0.0, + // &terrain, + // |block| block.solid_height() >= (pos.0.z - 0.1).rem_euclid(1.0), + // near_iter.clone(), + // radius, + // z_range.clone(), + // ) { let snap_height = terrain .get(Vec3::new(pos.0.x, pos.0.y, pos.0.z - 0.1).map(|e| e.floor() as i32)) @@ -1017,7 +1036,8 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( } if physics_state.on_ground { - vel.0 = ground_vel * 0.0 + (vel.0 - ground_vel * 0.0) * (1.0 - FRIC_GROUND.min(1.0)).powf(dt.0 * 60.0); + vel.0 = ground_vel * 0.0 + + (vel.0 - ground_vel * 0.0) * (1.0 - FRIC_GROUND.min(1.0)).powf(dt.0 * 60.0); physics_state.ground_vel = ground_vel; } @@ -1054,7 +1074,7 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( &|block| block.is_liquid(), // The liquid part of a liquid block always extends 1 block high. &|_block| 1.0, - near_iter.clone(), + near_iter, radius, z_min..z_max, ) diff --git a/common/sys/src/state.rs b/common/sys/src/state.rs index f0e9108f7c..8aae465bfd 100644 --- a/common/sys/src/state.rs +++ b/common/sys/src/state.rs @@ -459,7 +459,8 @@ impl State { match event { LocalEvent::Jump(entity) => { if let Some(vel) = velocities.get_mut(entity) { - vel.0.z = HUMANOID_JUMP_ACCEL + physics.get(entity).map_or(0.0, |ps| ps.ground_vel.z); + vel.0.z = HUMANOID_JUMP_ACCEL + + physics.get(entity).map_or(0.0, |ps| ps.ground_vel.z); } }, LocalEvent::ApplyImpulse { entity, impulse } => { diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 7aae9f5339..851e8876c8 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -998,7 +998,14 @@ fn handle_spawn_airship( pos.0.z += 50.0; const DESTINATION_RADIUS: f32 = 2000.0; let angle = angle.map(|a| a * std::f32::consts::PI / 180.0); - let destination = angle.map(|a| pos.0 + Vec3::new(DESTINATION_RADIUS*a.cos(), DESTINATION_RADIUS*a.sin(), 200.0)); + let destination = angle.map(|a| { + pos.0 + + Vec3::new( + DESTINATION_RADIUS * a.cos(), + DESTINATION_RADIUS * a.sin(), + 200.0, + ) + }); server .state .create_ship(pos, comp::ship::Body::DefaultAirship, 1, destination) diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index a3b3702975..e9868eae25 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -129,7 +129,12 @@ pub fn handle_shoot( return; }; - let vel = *dir * speed + state.ecs().read_storage::().get(entity).map_or(Vec3::zero(), |v| v.0); + let vel = *dir * speed + + state + .ecs() + .read_storage::() + .get(entity) + .map_or(Vec3::zero(), |v| v.0); // Add an outcome state diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index 18f500f695..0803406c55 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -28,9 +28,7 @@ impl Entity { pub fn get_body(&self) -> comp::Body { match self.rng(PERM_GENUS).gen::() { //we want 5% airships, 45% birds, 50% humans - x if x < 0.05 => { - comp::Body::Ship(comp::ship::Body::DefaultAirship) - }, + //x if x < 0.05 => comp::Body::Ship(comp::ship::Body::DefaultAirship), x if x < 0.50 => { let species = *(&comp::bird_medium::ALL_SPECIES) .choose(&mut self.rng(PERM_SPECIES)) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 3d36f8f8c1..4abf971136 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -620,7 +620,7 @@ impl<'a> AgentData<'a> { controller.inputs.move_z = bearing.z + if self.traversal_config.can_fly { - if read_data + let obstacle_ahead = read_data .terrain .ray( self.pos.0 + Vec3::unit_z(), @@ -631,31 +631,32 @@ impl<'a> AgentData<'a> { .until(Block::is_solid) .cast() .1 - .map_or(true, |b| b.is_some()) - || self - .body - .map(|body| { - let height_approx = self.pos.0.y - - read_data - .world - .sim() - .get_alt_approx(self.pos.0.xy().map(|x: f32| x as i32)) - .unwrap_or(0.0); + .map_or(true, |b| b.is_some()); + let ground_too_close = self + .body + .map(|body| { + let height_approx = self.pos.0.y + - read_data + .world + .sim() + .get_alt_approx(self.pos.0.xy().map(|x: f32| x as i32)) + .unwrap_or(0.0); - height_approx < body.flying_height() - || read_data - .terrain - .ray( - self.pos.0, - self.pos.0 - body.flying_height() * Vec3::unit_z(), - ) - .until(|b: &Block| b.is_solid() || b.is_liquid()) - .cast() - .1 - .map_or(false, |b| b.is_some()) - }) - .unwrap_or(false) - { + height_approx < body.flying_height() + || read_data + .terrain + .ray( + self.pos.0, + self.pos.0 - body.flying_height() * Vec3::unit_z(), + ) + .until(|b: &Block| b.is_solid() || b.is_liquid()) + .cast() + .1 + .map_or(false, |b| b.is_some()) + }) + .unwrap_or(false); + + if obstacle_ahead || ground_too_close { 1.0 //fly up when approaching obstacles } else { -0.1 diff --git a/voxygen/anim/src/lib.rs b/voxygen/anim/src/lib.rs index d903d6572f..c424b4d869 100644 --- a/voxygen/anim/src/lib.rs +++ b/voxygen/anim/src/lib.rs @@ -51,10 +51,10 @@ pub mod fish_small; pub mod fixture; pub mod golem; pub mod object; -pub mod ship; pub mod quadruped_low; pub mod quadruped_medium; pub mod quadruped_small; +pub mod ship; pub mod theropod; pub mod vek; diff --git a/voxygen/anim/src/ship/idle.rs b/voxygen/anim/src/ship/idle.rs index b96c9fd64e..a8e59d5c56 100644 --- a/voxygen/anim/src/ship/idle.rs +++ b/voxygen/anim/src/ship/idle.rs @@ -31,4 +31,3 @@ impl Animation for IdleAnimation { next } } - diff --git a/voxygen/anim/src/ship/mod.rs b/voxygen/anim/src/ship/mod.rs index 2e2783add3..14316d2ab3 100644 --- a/voxygen/anim/src/ship/mod.rs +++ b/voxygen/anim/src/ship/mod.rs @@ -64,8 +64,5 @@ impl Default for SkeletonAttr { } impl<'a> From<&'a Body> for SkeletonAttr { - fn from(_: &'a Body) -> Self { - Self::default() - } + fn from(_: &'a Body) -> Self { Self::default() } } - diff --git a/voxygen/src/scene/figure/load.rs b/voxygen/src/scene/figure/load.rs index ff334213e2..233c77c01c 100644 --- a/voxygen/src/scene/figure/load.rs +++ b/voxygen/src/scene/figure/load.rs @@ -13,10 +13,13 @@ use common::{ humanoid::{self, Body, BodyType, EyeColor, Skin, Species}, item::{ItemDef, ModularComponentKind}, object, - ship::{self, figuredata::{ShipSpec, ShipCentralSubSpec}}, quadruped_low::{self, BodyType as QLBodyType, Species as QLSpecies}, quadruped_medium::{self, BodyType as QMBodyType, Species as QMSpecies}, quadruped_small::{self, BodyType as QSBodyType, Species as QSSpecies}, + ship::{ + self, + figuredata::{ShipCentralSubSpec, ShipSpec}, + }, theropod::{self, BodyType as TBodyType, Species as TSpecies}, }, figure::{DynaUnionizer, MatSegment, Material, Segment}, @@ -4238,7 +4241,11 @@ impl ShipCentralSpec { (central, Vec3::from(bone.offset)) } }*/ -fn mesh_ship_bone &ShipCentralSubSpec>(map: &HashMap, obj: &K, f: F) -> BoneMeshes { +fn mesh_ship_bone &ShipCentralSubSpec>( + map: &HashMap, + obj: &K, + f: F, +) -> BoneMeshes { let spec = match map.get(&obj) { Some(spec) => spec, None => { @@ -4256,9 +4263,7 @@ impl BodySpec for ship::Body { type Spec = ShipSpec; #[allow(unused_variables)] - fn load_spec() -> Result, assets::Error> { - Self::Spec::load("") - } + fn load_spec() -> Result, assets::Error> { Self::Spec::load("") } fn bone_meshes( FigureKey { body, .. }: &FigureKey, @@ -4266,9 +4271,9 @@ impl BodySpec for ship::Body { ) -> [Option; anim::MAX_BONE_COUNT] { let map = &(spec.central.read().0).0; [ - Some(mesh_ship_bone(map, body, |spec| &spec.bone0,)), - Some(mesh_ship_bone(map, body, |spec| &spec.bone1,)), - Some(mesh_ship_bone(map, body, |spec| &spec.bone2,)), + Some(mesh_ship_bone(map, body, |spec| &spec.bone0)), + Some(mesh_ship_bone(map, body, |spec| &spec.bone1)), + Some(mesh_ship_bone(map, body, |spec| &spec.bone2)), None, None, None, diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 926680fdfa..f2070d03cc 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -21,9 +21,9 @@ use anim::{ biped_large::BipedLargeSkeleton, biped_small::BipedSmallSkeleton, bird_medium::BirdMediumSkeleton, bird_small::BirdSmallSkeleton, character::CharacterSkeleton, dragon::DragonSkeleton, fish_medium::FishMediumSkeleton, fish_small::FishSmallSkeleton, - golem::GolemSkeleton, object::ObjectSkeleton, ship::ShipSkeleton, quadruped_low::QuadrupedLowSkeleton, + golem::GolemSkeleton, object::ObjectSkeleton, quadruped_low::QuadrupedLowSkeleton, quadruped_medium::QuadrupedMediumSkeleton, quadruped_small::QuadrupedSmallSkeleton, - theropod::TheropodSkeleton, Animation, Skeleton, + ship::ShipSkeleton, theropod::TheropodSkeleton, Animation, Skeleton, }; use common::{ comp::{ @@ -320,11 +320,7 @@ impl FigureMgrStates { .iter() .filter(|(_, c)| c.visible()) .count() - + self - .ship_states - .iter() - .filter(|(_, c)| c.visible()) - .count() + + self.ship_states.iter().filter(|(_, c)| c.visible()).count() } } @@ -787,18 +783,12 @@ impl FigureMgr { let target_base = match ( physics.on_ground, rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving - physics.in_liquid.is_some(), // In water + physics.in_liquid.is_some(), // In water ) { // Standing (true, false, false) => anim::character::StandAnimation::update_skeleton( &CharacterSkeleton::default(), - ( - active_tool_kind, - second_tool_kind, - hands, - time, - rel_avg_vel, - ), + (active_tool_kind, second_tool_kind, hands, time, rel_avg_vel), state.state_time, &mut state_animation_rate, skeleton_attr, @@ -1406,7 +1396,12 @@ impl FigureMgr { CharacterState::Equipping { .. } => { anim::character::EquipAnimation::update_skeleton( &target_base, - (active_tool_kind, second_tool_kind, rel_vel.magnitude(), time), + ( + active_tool_kind, + second_tool_kind, + rel_vel.magnitude(), + time, + ), state.state_time, &mut state_animation_rate, skeleton_attr, @@ -1429,7 +1424,12 @@ impl FigureMgr { if physics.in_liquid.is_some() { anim::character::SwimWieldAnimation::update_skeleton( &target_base, - (active_tool_kind, second_tool_kind, rel_vel.magnitude(), time), + ( + active_tool_kind, + second_tool_kind, + rel_vel.magnitude(), + time, + ), state.state_time, &mut state_animation_rate, skeleton_attr, @@ -1579,7 +1579,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving - physics.in_liquid.is_some(), // In water + physics.in_liquid.is_some(), // In water ) { // Standing (true, false, false) => { @@ -1781,7 +1781,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, rel_vel.magnitude_squared() > 0.25, // Moving - physics.in_liquid.is_some(), // In water + physics.in_liquid.is_some(), // In water ) { // Standing (true, false, false) => { @@ -2108,7 +2108,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving - physics.in_liquid.is_some(), // In water + physics.in_liquid.is_some(), // In water ) { // Standing (true, false, false) => { @@ -2453,7 +2453,7 @@ impl FigureMgr { }); // Average velocity relative to the current ground - let rel_avg_vel = state.avg_vel - physics.ground_vel; + let _rel_avg_vel = state.avg_vel - physics.ground_vel; let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), @@ -2467,7 +2467,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving - physics.in_liquid.is_some(), // In water + physics.in_liquid.is_some(), // In water ) { // Standing (true, false, false) => anim::bird_medium::IdleAnimation::update_skeleton( @@ -2577,7 +2577,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving - physics.in_liquid.is_some(), // In water + physics.in_liquid.is_some(), // In water ) { // Idle (_, false, _) => anim::fish_medium::IdleAnimation::update_skeleton( @@ -2666,7 +2666,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving - physics.in_liquid.is_some(), // In water + physics.in_liquid.is_some(), // In water ) { // Idle (true, false, false) => anim::biped_small::IdleAnimation::update_skeleton( @@ -3009,7 +3009,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving - physics.in_liquid.is_some(), // In water + physics.in_liquid.is_some(), // In water ) { // Standing (true, false, false) => anim::dragon::IdleAnimation::update_skeleton( @@ -3104,7 +3104,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving - physics.in_liquid.is_some(), // In water + physics.in_liquid.is_some(), // In water ) { // Standing (true, false, false) => anim::theropod::IdleAnimation::update_skeleton( @@ -3279,7 +3279,7 @@ impl FigureMgr { }); // Average velocity relative to the current ground - let rel_avg_vel = state.avg_vel - physics.ground_vel; + let _rel_avg_vel = state.avg_vel - physics.ground_vel; let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), @@ -3293,7 +3293,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving - physics.in_liquid.is_some(), // In water + physics.in_liquid.is_some(), // In water ) { // Standing (true, false, false) => anim::bird_small::IdleAnimation::update_skeleton( @@ -3384,7 +3384,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving - physics.in_liquid.is_some(), // In water + physics.in_liquid.is_some(), // In water ) { // Idle (_, false, _) => anim::fish_small::IdleAnimation::update_skeleton( @@ -3473,7 +3473,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving - physics.in_liquid.is_some(), // In water + physics.in_liquid.is_some(), // In water ) { // Running (true, true, false) => anim::biped_large::RunAnimation::update_skeleton( @@ -3513,7 +3513,12 @@ impl FigureMgr { CharacterState::Equipping { .. } => { anim::biped_large::EquipAnimation::update_skeleton( &target_base, - (active_tool_kind, second_tool_kind, rel_vel.magnitude(), time), + ( + active_tool_kind, + second_tool_kind, + rel_vel.magnitude(), + time, + ), state.state_time, &mut state_animation_rate, skeleton_attr, @@ -3887,7 +3892,7 @@ impl FigureMgr { }); // Average velocity relative to the current ground - let rel_avg_vel = state.avg_vel - physics.ground_vel; + let _rel_avg_vel = state.avg_vel - physics.ground_vel; let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), @@ -3901,7 +3906,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving - physics.in_liquid.is_some(), // In water + physics.in_liquid.is_some(), // In water ) { // Standing (true, false, false) => anim::golem::IdleAnimation::update_skeleton( @@ -4071,7 +4076,7 @@ impl FigureMgr { }); // Average velocity relative to the current ground - let rel_avg_vel = state.avg_vel - physics.ground_vel; + let _rel_avg_vel = state.avg_vel - physics.ground_vel; let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), @@ -4087,7 +4092,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving - physics.in_liquid.is_some(), // In water + physics.in_liquid.is_some(), // In water ) { // Standing (true, false, false) => anim::object::IdleAnimation::update_skeleton( @@ -4193,13 +4198,14 @@ impl FigureMgr { scene_data.runtime, ); - let state = - self.states.ship_states.entry(entity).or_insert_with(|| { - FigureState::new(renderer, ShipSkeleton::default()) - }); + let state = self + .states + .ship_states + .entry(entity) + .or_insert_with(|| FigureState::new(renderer, ShipSkeleton::default())); // Average velocity relative to the current ground - let rel_avg_vel = state.avg_vel - physics.ground_vel; + let _rel_avg_vel = state.avg_vel - physics.ground_vel; let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), @@ -4215,7 +4221,7 @@ impl FigureMgr { let target_base = match ( physics.on_ground, rel_vel.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving - physics.in_liquid.is_some(), // In water + physics.in_liquid.is_some(), // In water ) { // Standing (true, false, false) => anim::ship::IdleAnimation::update_skeleton( @@ -4234,6 +4240,7 @@ impl FigureMgr { ), }; + #[allow(clippy::match_single_binding)] let target_bones = match &character { // TODO! _ => target_base, From ee7f760e78c5f7502ec80780e2e29ea76678d866 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Fri, 12 Mar 2021 18:19:10 -0500 Subject: [PATCH 16/41] Address MR 1888 review comments. - Delete obsolete symbolic links. - Add suggested comments. - Remove dead code. --- assets/voxygen/voxel/object/Human_Airship.vox | 3 - assets/voxygen/voxel/object/airship.vox | 3 - assets/voxygen/voxel/object/propeller-l.vox | 3 - assets/voxygen/voxel/object/propeller-r.vox | 3 - common/src/comp/body/ship.rs | 7 +++ common/sys/src/phys.rs | 4 +- server/src/cmd.rs | 2 +- server/src/rtsim/entity.rs | 4 +- server/src/rtsim/tick.rs | 2 +- server/src/state_ext.rs | 2 + voxygen/src/scene/figure/load.rs | 59 ------------------- 11 files changed, 17 insertions(+), 75 deletions(-) delete mode 120000 assets/voxygen/voxel/object/Human_Airship.vox delete mode 120000 assets/voxygen/voxel/object/airship.vox delete mode 120000 assets/voxygen/voxel/object/propeller-l.vox delete mode 120000 assets/voxygen/voxel/object/propeller-r.vox diff --git a/assets/voxygen/voxel/object/Human_Airship.vox b/assets/voxygen/voxel/object/Human_Airship.vox deleted file mode 120000 index 36e3758d03..0000000000 --- a/assets/voxygen/voxel/object/Human_Airship.vox +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ba02746d73ebf853c0511b673510c09bd47e3ab0fff13d936feb181a8378ebd9 -size 78024 diff --git a/assets/voxygen/voxel/object/airship.vox b/assets/voxygen/voxel/object/airship.vox deleted file mode 120000 index 06bebaa938..0000000000 --- a/assets/voxygen/voxel/object/airship.vox +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:86f317298900ea98f95c6a33192b25fbbcbd3ce5f105cad58ad3c595a7a7d9ee -size 70176 diff --git a/assets/voxygen/voxel/object/propeller-l.vox b/assets/voxygen/voxel/object/propeller-l.vox deleted file mode 120000 index a193fa89ee..0000000000 --- a/assets/voxygen/voxel/object/propeller-l.vox +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:09ef4bad2557abcc5a2b938f21053babc7770ebe2333039aef9b98ba930b7ec7 -size 1584 diff --git a/assets/voxygen/voxel/object/propeller-r.vox b/assets/voxygen/voxel/object/propeller-r.vox deleted file mode 120000 index 5b940751e6..0000000000 --- a/assets/voxygen/voxel/object/propeller-r.vox +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e4947977524b88bc5adfa934d9061a3499e94b960abb3bcf0a3e2aca482096dc -size 1584 diff --git a/common/src/comp/body/ship.rs b/common/src/comp/body/ship.rs index 4a49d9f2a1..ceab9d86a1 100644 --- a/common/src/comp/body/ship.rs +++ b/common/src/comp/body/ship.rs @@ -22,6 +22,13 @@ impl Body { } } +/// Terrain is 11.0 scale relative to small-scale voxels, and all figures get +/// multiplied by 0.8 in rendering. For now, have a constant in `comp::Scale` +/// that compensates for both of these, but there might be a more elegant way +/// (e.g. using `Scale(0.8)` for everything else and not having a magic number +/// in figure rendering, and multiplying terrain models by 11.0 in animation). +pub const AIRSHIP_SCALE: f32 = 11.0 / 0.8; + /// Duplicate of some of the things defined in `voxygen::scene::figure::load` to /// avoid having to refactor all of that to `common` for using voxels as /// collider geometry diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index e01bcd276f..a73150ebaa 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -475,7 +475,9 @@ impl<'a> PhysicsData<'a> { Collider::Voxel { .. } => { // for now, treat entities with voxel colliders as their bounding // cylinders for the purposes of colliding them with terrain - // Actually no, make them smaller to avoid lag + + // Additionally, multiply radius by 0.1 to make the cylinder smaller to + // avoid lag let radius = collider.get_radius() * scale * 0.1; let (z_min, z_max) = collider.get_z_limits(scale); diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 851e8876c8..8ca53368fb 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1009,7 +1009,7 @@ fn handle_spawn_airship( server .state .create_ship(pos, comp::ship::Body::DefaultAirship, 1, destination) - .with(comp::Scale(11.0 / 0.8)) + .with(comp::Scale(comp::ship::AIRSHIP_SCALE)) .with(LightEmitter { col: Rgb::new(1.0, 0.65, 0.2), strength: 2.0, diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index 0803406c55..05b96fcbf2 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -27,7 +27,9 @@ impl Entity { pub fn get_body(&self) -> comp::Body { match self.rng(PERM_GENUS).gen::() { - //we want 5% airships, 45% birds, 50% humans + // we want 5% airships, 45% birds, 50% humans + // TODO: uncomment this to re-enable RtSim airships once physics is interpolated well + // in multiplayer. //x if x < 0.05 => comp::Body::Ship(comp::ship::Body::DefaultAirship), x if x < 0.50 => { let species = *(&comp::bird_medium::ALL_SPECIES) diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index 30b16f5d1f..13ab05faaf 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -123,7 +123,7 @@ impl<'a> System<'a> for Sys { _ => comp::Alignment::Wild, }, scale: match body { - comp::Body::Ship(_) => comp::Scale(11.0 / 0.8), + comp::Body::Ship(_) => comp::Scale(comp::ship::AIRSHIP_SCALE), _ => comp::Scale(1.0), }, drop_item: None, diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 155b07e286..4e9369a939 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -249,6 +249,8 @@ impl StateExt for State { .with(comp::Controller::default()) .with(comp::inventory::Inventory::new_empty()) .with(comp::CharacterState::default()) + // TODO: some of these are required in order for the character_behavior system to + // recognize a possesed airship; that system should be refactored to use `.maybe()` .with(comp::Energy::new(ship.into(), level)) .with(comp::Health::new(ship.into(), level)) .with(comp::Stats::new("Airship".to_string())) diff --git a/voxygen/src/scene/figure/load.rs b/voxygen/src/scene/figure/load.rs index 233c77c01c..a04e9c5d16 100644 --- a/voxygen/src/scene/figure/load.rs +++ b/voxygen/src/scene/figure/load.rs @@ -4112,17 +4112,6 @@ impl QuadrupedLowLateralSpec { #[derive(Deserialize)] struct ObjectCentralSpec(HashMap); -/* -#[derive(Deserialize)] -struct ShipCentralSpec(HashMap); - -#[derive(Deserialize)] -struct SidedShipCentralVoxSpec { - bone0: ObjectCentralSubSpec, - bone1: ObjectCentralSubSpec, - bone2: ObjectCentralSubSpec, -}*/ - #[derive(Deserialize)] struct SidedObjectCentralVoxSpec { bone0: ObjectCentralSubSpec, @@ -4193,54 +4182,6 @@ impl ObjectCentralSpec { } } -/*make_vox_spec!( - ship::Body, - struct ShipSpec { - central: ShipCentralSpec = "server.manifests.ship_manifest", - }, - |FigureKey { body, .. }, spec| { - [ - Some(spec.central.read().0.mesh_bone( - body, |spec| &spec.bone0, - )), - Some(spec.central.read().0.mesh_bone( - body, |spec| &spec.bone1 - )), - Some(spec.central.read().0.mesh_bone( - body, |spec| &spec.bone2 - )), - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - ] - }, -); - -impl ShipCentralSpec { - fn mesh_bone &ObjectCentralSubSpec>(&self, obj: &ship::Body, f: F) -> BoneMeshes { - let spec = match self.0.get(&obj) { - Some(spec) => spec, - None => { - error!("No specification exists for {:?}", obj); - return load_mesh("not_found", Vec3::new(-5.0, -5.0, -2.5)); - }, - }; - let bone = f(spec); - let central = graceful_load_segment(&bone.central.0); - - (central, Vec3::from(bone.offset)) - } -}*/ fn mesh_ship_bone &ShipCentralSubSpec>( map: &HashMap, obj: &K, From b24c89050a98e20a03e122f6ef4f2db7eb4c4b94 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 13 Mar 2021 02:24:36 +0000 Subject: [PATCH 17/41] Fewer precision issues by using player-relative coordinate space --- common/sys/src/phys.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index a73150ebaa..e2ff048790 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -632,12 +632,16 @@ impl<'a> PhysicsData<'a> { let mut physics_state_delta = physics_state.clone(); // deliberately don't use scale yet here, because the 11.0/0.8 // thing is in the comp::Scale for visual reasons - let transform_from = Mat4::::translation_3d(pos_other.0) - * Mat4::from(ori_other.0) - * Mat4::::translation_3d(voxel_collider.translation); + let wpos = pos.0; + let transform_from = + Mat4::::translation_3d(pos_other.0 - wpos) + * Mat4::from(ori_other.0) + * Mat4::::translation_3d(voxel_collider.translation); let transform_to = transform_from.inverted(); - pos.0 = transform_to.mul_point(pos.0); - vel.0 = transform_to.mul_direction(vel.0 - vel_other.0); + let ori_to = Mat4::from(ori_other.0); + let ori_from = ori_to.inverted(); + pos.0 = transform_to.mul_point(Vec3::zero()); + vel.0 = ori_to.mul_direction(vel.0 - vel_other.0); let cylinder = (radius, z_min, z_max); cylinder_voxel_collision( cylinder, @@ -647,20 +651,18 @@ impl<'a> PhysicsData<'a> { transform_to.mul_direction(pos_delta), &mut vel, &mut physics_state_delta, - transform_to.mul_direction(vel_other.0), + ori_to.mul_direction(vel_other.0), &read.dt, false, was_on_ground, |entity, vel| { - land_on_grounds.push(( - entity, - Vel(transform_from.mul_direction(vel.0)), - )) + land_on_grounds + .push((entity, Vel(ori_from.mul_direction(vel.0)))) }, ); - pos.0 = transform_from.mul_point(pos.0); - vel.0 = transform_from.mul_direction(vel.0) + vel_other.0; + pos.0 = transform_from.mul_point(pos.0) + wpos; + vel.0 = ori_from.mul_direction(vel.0) + vel_other.0; // union in the state updates, so that the state isn't just based on // the most recent terrain that collision was attempted with From a6638183244b18d8e1d15c4c3ec3551b55726e03 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 13 Mar 2021 10:52:45 +0000 Subject: [PATCH 18/41] Fixed incorrect wall climb direction on airships --- common/sys/src/phys.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index e2ff048790..80efbf3a74 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -672,7 +672,9 @@ impl<'a> PhysicsData<'a> { physics_state.on_ground |= physics_state_delta.on_ground; physics_state.on_ceiling |= physics_state_delta.on_ceiling; physics_state.on_wall = - physics_state.on_wall.or(physics_state_delta.on_wall); + physics_state.on_wall.or(physics_state_delta + .on_wall + .map(|dir| ori_to.mul_direction(dir))); physics_state .touch_entities .append(&mut physics_state_delta.touch_entities); From 50b3039cbcf329da0ea9875ab86ce887d5ceccab Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 13 Mar 2021 12:35:57 +0000 Subject: [PATCH 19/41] Fixed incorrectly reversed orientation matrix --- common/sys/src/phys.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index 80efbf3a74..212f3e119a 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -638,8 +638,8 @@ impl<'a> PhysicsData<'a> { * Mat4::from(ori_other.0) * Mat4::::translation_3d(voxel_collider.translation); let transform_to = transform_from.inverted(); - let ori_to = Mat4::from(ori_other.0); - let ori_from = ori_to.inverted(); + let ori_from = Mat4::from(ori_other.0); + let ori_to = ori_from.inverted(); pos.0 = transform_to.mul_point(Vec3::zero()); vel.0 = ori_to.mul_direction(vel.0 - vel_other.0); let cylinder = (radius, z_min, z_max); @@ -674,7 +674,7 @@ impl<'a> PhysicsData<'a> { physics_state.on_wall = physics_state.on_wall.or(physics_state_delta .on_wall - .map(|dir| ori_to.mul_direction(dir))); + .map(|dir| ori_from.mul_direction(dir))); physics_state .touch_entities .append(&mut physics_state_delta.touch_entities); @@ -1042,8 +1042,7 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( } if physics_state.on_ground { - vel.0 = ground_vel * 0.0 - + (vel.0 - ground_vel * 0.0) * (1.0 - FRIC_GROUND.min(1.0)).powf(dt.0 * 60.0); + vel.0 *= (1.0 - FRIC_GROUND.min(1.0)).powf(dt.0 * 60.0); physics_state.ground_vel = ground_vel; } From 26c9fd63ae5f071a1a24f21b35111d0a8a5fbf64 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 13 Mar 2021 14:25:56 +0000 Subject: [PATCH 20/41] Reduced air resistance for better-behaving airships --- common/src/states/glide.rs | 4 ++-- common/src/states/utils.rs | 9 ++++++--- common/sys/src/phys.rs | 19 ++++++++++--------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/common/src/states/glide.rs b/common/src/states/glide.rs index a54046e38e..8a6ac909b9 100644 --- a/common/src/states/glide.rs +++ b/common/src/states/glide.rs @@ -9,8 +9,8 @@ use vek::Vec2; // Gravity is 9.81 * 4, so this makes gravity equal to .15 const GLIDE_ANTIGRAV: f32 = crate::consts::GRAVITY * 0.90; -const GLIDE_ACCEL: f32 = 12.0; -const GLIDE_SPEED: f32 = 45.0; +const GLIDE_ACCEL: f32 = 8.0; +const GLIDE_SPEED: f32 = 16.0; #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq, Hash)] pub struct Data; diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 6ee2449950..f256559283 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -17,8 +17,8 @@ use std::time::Duration; use vek::*; pub const MOVEMENT_THRESHOLD_VEL: f32 = 3.0; -const BASE_HUMANOID_AIR_ACCEL: f32 = 8.0; -const BASE_FLIGHT_ACCEL: f32 = 16.0; +const BASE_HUMANOID_AIR_ACCEL: f32 = 0.5; +const BASE_FLIGHT_ACCEL: f32 = 3.5; const BASE_HUMANOID_WATER_ACCEL: f32 = 150.0; const BASE_HUMANOID_WATER_SPEED: f32 = 180.0; // const BASE_HUMANOID_CLIMB_ACCEL: f32 = 10.0; @@ -310,7 +310,10 @@ fn swim_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32, depth: fn fly_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) { // Update velocity (counteract gravity with lift) // TODO: Do this better - update.vel.0 += Vec3::unit_z() * data.dt.0 * GRAVITY + // A loss factor is needed to counteract the very slight deviation in gravity + // due to precision issues + const LOSS_FACTOR: f32 = 0.995; + update.vel.0 += Vec3::unit_z() * data.dt.0 * GRAVITY * LOSS_FACTOR + Vec3::new( data.inputs.move_dir.x, data.inputs.move_dir.y, diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index 212f3e119a..a2e3c6b916 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -30,7 +30,7 @@ pub const BOUYANCY: f32 = 1.0; // friction is 0.01, and the speed is 1.0, then after 1/60th of a second the // speed will be 0.99. after 1 second the speed will be 0.54, which is 0.99 ^ // 60. -pub const FRIC_AIR: f32 = 0.0125; +pub const FRIC_AIR: f32 = 0.0025; pub const FRIC_FLUID: f32 = 0.4; // Integrates forces, calculates the new velocity based off of the old velocity @@ -415,7 +415,7 @@ impl<'a> PhysicsData<'a> { let mut pos = *pos; let mut vel = *vel; if sticky.is_some() && physics_state.on_surface().is_some() { - vel.0 = Vec3::zero(); + vel.0 = physics_state.ground_vel; return (pos_writes, vel_writes, land_on_grounds); } @@ -977,7 +977,7 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( { // ...block-hop! pos.0.z = (pos.0.z + 0.1).floor() + block_height; - vel.0.z = vel.0.z.max(0.0); + vel.0.z = 0.0; on_ground = true; break; } else { @@ -1019,7 +1019,7 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( near_iter.clone(), radius, z_range.clone(), - ) && vel.0.z < 0.1 + ) && vel.0.z < 0.25 && vel.0.z > -1.5 // && was_on_ground // && !collision_with( @@ -1037,15 +1037,11 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( .filter(|block| block.is_solid()) .map(|block| block.solid_height()) .unwrap_or(0.0); + vel.0.z = 0.0; pos.0.z = (pos.0.z - 0.1).floor() + snap_height; physics_state.on_ground = true; } - if physics_state.on_ground { - vel.0 *= (1.0 - FRIC_GROUND.min(1.0)).powf(dt.0 * 60.0); - physics_state.ground_vel = ground_vel; - } - let dirs = [ Vec3::unit_x(), Vec3::unit_y(), @@ -1072,6 +1068,11 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( physics_state.on_wall = None; } + if physics_state.on_ground || physics_state.on_wall.is_some() { + vel.0 *= (1.0 - FRIC_GROUND.min(1.0)).powf(dt.0 * 60.0); + physics_state.ground_vel = ground_vel; + } + // Figure out if we're in water physics_state.in_liquid = collision_iter( pos.0, From f768c3f853261d51a871db618250ce2a7bb0bbed Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 13 Mar 2021 15:36:56 +0000 Subject: [PATCH 21/41] Properly propagate velocity steps during airship collision to avoid falling through airships --- common/src/states/glide.rs | 12 +++------ common/src/states/utils.rs | 4 +-- common/sys/src/phys.rs | 50 ++++++++++++++++++++++---------------- 3 files changed, 34 insertions(+), 32 deletions(-) diff --git a/common/src/states/glide.rs b/common/src/states/glide.rs index 8a6ac909b9..6655e5b440 100644 --- a/common/src/states/glide.rs +++ b/common/src/states/glide.rs @@ -9,7 +9,7 @@ use vek::Vec2; // Gravity is 9.81 * 4, so this makes gravity equal to .15 const GLIDE_ANTIGRAV: f32 = crate::consts::GRAVITY * 0.90; -const GLIDE_ACCEL: f32 = 8.0; +const GLIDE_ACCEL: f32 = 6.0; const GLIDE_SPEED: f32 = 16.0; #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq, Hash)] @@ -40,13 +40,7 @@ impl CharacterBehavior for Data { handle_climb(&data, &mut update); // Move player according to movement direction vector - update.vel.0 += Vec2::broadcast(data.dt.0) - * data.inputs.move_dir - * if data.vel.0.magnitude_squared() < GLIDE_SPEED.powi(2) { - GLIDE_ACCEL - } else { - 0.0 - }; + update.vel.0 += Vec2::broadcast(data.dt.0) * data.inputs.move_dir * GLIDE_ACCEL; // Determine orientation vector from movement direction vector let horiz_vel = Vec2::::from(update.vel.0); @@ -56,7 +50,7 @@ impl CharacterBehavior for Data { // Apply Glide antigrav lift let horiz_speed_sq = horiz_vel.magnitude_squared(); - if horiz_speed_sq < GLIDE_SPEED.powi(2) && update.vel.0.z < 0.0 { + if update.vel.0.z < 0.0 { let lift = (GLIDE_ANTIGRAV + update.vel.0.z.powi(2) * 0.15) * (horiz_speed_sq * f32::powf(0.075, 2.0)).clamp(0.2, 1.0); diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index f256559283..fda66fb758 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -17,8 +17,8 @@ use std::time::Duration; use vek::*; pub const MOVEMENT_THRESHOLD_VEL: f32 = 3.0; -const BASE_HUMANOID_AIR_ACCEL: f32 = 0.5; -const BASE_FLIGHT_ACCEL: f32 = 3.5; +const BASE_HUMANOID_AIR_ACCEL: f32 = 2.0; +const BASE_FLIGHT_ACCEL: f32 = 2.0; const BASE_HUMANOID_WATER_ACCEL: f32 = 150.0; const BASE_HUMANOID_WATER_SPEED: f32 = 180.0; // const BASE_HUMANOID_CLIMB_ACCEL: f32 = 10.0; diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index a2e3c6b916..53f9061e3f 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -411,7 +411,6 @@ impl<'a> PhysicsData<'a> { )| { // defer the writes of positions to allow an inner loop over terrain-like // entities - let old_pos = *pos; let mut pos = *pos; let mut vel = *vel; if sticky.is_some() && physics_state.on_surface().is_some() { @@ -469,6 +468,8 @@ impl<'a> PhysicsData<'a> { Vec3::zero() }; + let mut tgt_pos = pos.0 + pos_delta; + let was_on_ground = physics_state.on_ground; match &collider { @@ -481,21 +482,22 @@ impl<'a> PhysicsData<'a> { let radius = collider.get_radius() * scale * 0.1; let (z_min, z_max) = collider.get_z_limits(scale); + let mut cpos = pos; let cylinder = (radius, z_min, z_max); cylinder_voxel_collision( cylinder, &*read.terrain, entity, - &mut pos, - pos_delta, + &mut cpos, + tgt_pos, &mut vel, &mut physics_state, Vec3::zero(), &read.dt, - true, was_on_ground, |entity, vel| land_on_grounds.push((entity, vel)), ); + tgt_pos = cpos.0; }, Collider::Box { radius, @@ -508,20 +510,21 @@ impl<'a> PhysicsData<'a> { let z_max = z_max.clamped(1.2, 1.95) * scale; let cylinder = (radius, z_min, z_max); + let mut cpos = pos; cylinder_voxel_collision( cylinder, &*read.terrain, entity, - &mut pos, - pos_delta, + &mut cpos, + tgt_pos, &mut vel, &mut physics_state, Vec3::zero(), &read.dt, - true, was_on_ground, |entity, vel| land_on_grounds.push((entity, vel)), ); + tgt_pos = cpos.0; }, Collider::Point => { let (dist, block) = read @@ -567,6 +570,8 @@ impl<'a> PhysicsData<'a> { .get(pos.0.map(|e| e.floor() as i32)) .ok() .and_then(|vox| vox.is_liquid().then_some(1.0)); + + tgt_pos = pos.0; }, } @@ -632,7 +637,8 @@ impl<'a> PhysicsData<'a> { let mut physics_state_delta = physics_state.clone(); // deliberately don't use scale yet here, because the 11.0/0.8 // thing is in the comp::Scale for visual reasons - let wpos = pos.0; + let mut cpos = pos; + let wpos = cpos.0; let transform_from = Mat4::::translation_3d(pos_other.0 - wpos) * Mat4::from(ori_other.0) @@ -640,20 +646,19 @@ impl<'a> PhysicsData<'a> { let transform_to = transform_from.inverted(); let ori_from = Mat4::from(ori_other.0); let ori_to = ori_from.inverted(); - pos.0 = transform_to.mul_point(Vec3::zero()); + cpos.0 = transform_to.mul_point(Vec3::zero()); vel.0 = ori_to.mul_direction(vel.0 - vel_other.0); let cylinder = (radius, z_min, z_max); cylinder_voxel_collision( cylinder, &voxel_collider.dyna, entity, - &mut pos, - transform_to.mul_direction(pos_delta), + &mut cpos, + transform_to.mul_point(tgt_pos - wpos), &mut vel, &mut physics_state_delta, ori_to.mul_direction(vel_other.0), &read.dt, - false, was_on_ground, |entity, vel| { land_on_grounds @@ -661,8 +666,9 @@ impl<'a> PhysicsData<'a> { }, ); - pos.0 = transform_from.mul_point(pos.0) + wpos; + cpos.0 = transform_from.mul_point(cpos.0) + wpos; vel.0 = ori_from.mul_direction(vel.0) + vel_other.0; + tgt_pos = cpos.0; // union in the state updates, so that the state isn't just based on // the most recent terrain that collision was attempted with @@ -691,8 +697,9 @@ impl<'a> PhysicsData<'a> { } } } - if pos != old_pos { - pos_writes.push((entity, pos)); + + if tgt_pos != pos.0 { + pos_writes.push((entity, Pos(tgt_pos))); } if vel != old_vel { vel_writes.push((entity, vel)); @@ -771,12 +778,11 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( terrain: &'a T, entity: Entity, pos: &mut Pos, - mut pos_delta: Vec3, + tgt_pos: Vec3, vel: &mut Vel, physics_state: &mut PhysicsState, ground_vel: Vec3, dt: &DeltaTime, - apply_velocity_step: bool, // Stupid hack was_on_ground: bool, mut land_on_ground: impl FnMut(Entity, Vel), ) { @@ -859,6 +865,8 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( let mut on_ceiling = false; let mut attempts = 0; // Don't loop infinitely here + let mut pos_delta = tgt_pos - pos.0; + // Don't jump too far at once let increments = (pos_delta.map(|e| e.abs()).reduce_partial_max() / 0.3) .ceil() @@ -866,9 +874,7 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( let old_pos = pos.0; fn block_true(_: &Block) -> bool { true } for _ in 0..increments as usize { - if apply_velocity_step { - pos.0 += pos_delta / increments; - } + pos.0 += pos_delta / increments; const MAX_ATTEMPTS: usize = 16; @@ -1069,7 +1075,9 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( } if physics_state.on_ground || physics_state.on_wall.is_some() { - vel.0 *= (1.0 - FRIC_GROUND.min(1.0)).powf(dt.0 * 60.0); + if physics_state.on_ground { + vel.0 *= (1.0 - FRIC_GROUND.min(1.0)).powf(dt.0 * 60.0); + } physics_state.ground_vel = ground_vel; } From d1c15cbafe7dd90851e183ea13512c494526adcf Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 13 Mar 2021 16:14:13 +0000 Subject: [PATCH 22/41] Fixed arrows --- common/sys/src/phys.rs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index 53f9061e3f..041ad1b992 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -468,6 +468,18 @@ impl<'a> PhysicsData<'a> { Vec3::zero() }; + // What's going on here? Because collisions need to be resolved against multiple + // colliders, this code takes the current position and + // propagates it forward according to velocity to find a + // 'target' position. This is where we'd ideally end up at the end of the tick, + // assuming no collisions. Then, we refine this target by + // stepping from the original position to the target for + // every obstacle, refining the target position as we go. It's not perfect, but + // it works pretty well in practice. Oddities can occur on + // the intersection between multiple colliders, but it's not + // like any game physics system resolves these sort of things well anyway. At + // the very least, we don't do things that result in glitchy + // velocities or entirely broken position snapping. let mut tgt_pos = pos.0 + pos_delta; let was_on_ground = physics_state.on_ground; @@ -527,6 +539,8 @@ impl<'a> PhysicsData<'a> { tgt_pos = cpos.0; }, Collider::Point => { + let mut pos = pos; + let (dist, block) = read .terrain .ray(pos.0, pos.0 + pos_delta) @@ -966,11 +980,12 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( && resolve_dir.z == 0.0 // ...and the vertical resolution direction is sufficiently great... && -dir.z > 0.1 - // ...and we're falling/standing OR there is a block *directly* beneath our current origin (note: not hitbox)... - && (vel.0.z <= 0.0 || terrain - .get((pos.0 - Vec3::unit_z() * 0.1).map(|e| e.floor() as i32)) - .map(|block| block.is_solid()) - .unwrap_or(false)) + && was_on_ground + // // ...and we're falling/standing OR there is a block *directly* beneath our current origin (note: not hitbox)... + // && (vel.0.z <= 0.0 || terrain + // .get((pos.0 - Vec3::unit_z() * 0.1).map(|e| e.floor() as i32)) + // .map(|block| block.is_solid()) + // .unwrap_or(false)) // ...and there is a collision with a block beneath our current hitbox... && collision_with( pos.0 + resolve_dir - Vec3::unit_z() * 1.05, From 1050b6a87c438a58722d12936aa9dca19cd367de Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 13 Mar 2021 16:43:01 +0000 Subject: [PATCH 23/41] Removed unnecessary matrix mul --- common/src/states/glide.rs | 3 +-- common/sys/src/phys.rs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/common/src/states/glide.rs b/common/src/states/glide.rs index 6655e5b440..04bfbb8fe6 100644 --- a/common/src/states/glide.rs +++ b/common/src/states/glide.rs @@ -9,8 +9,7 @@ use vek::Vec2; // Gravity is 9.81 * 4, so this makes gravity equal to .15 const GLIDE_ANTIGRAV: f32 = crate::consts::GRAVITY * 0.90; -const GLIDE_ACCEL: f32 = 6.0; -const GLIDE_SPEED: f32 = 16.0; +const GLIDE_ACCEL: f32 = 5.0; #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq, Hash)] pub struct Data; diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index 041ad1b992..a7e10c92d1 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -411,7 +411,6 @@ impl<'a> PhysicsData<'a> { )| { // defer the writes of positions to allow an inner loop over terrain-like // entities - let mut pos = *pos; let mut vel = *vel; if sticky.is_some() && physics_state.on_surface().is_some() { vel.0 = physics_state.ground_vel; @@ -494,7 +493,7 @@ impl<'a> PhysicsData<'a> { let radius = collider.get_radius() * scale * 0.1; let (z_min, z_max) = collider.get_z_limits(scale); - let mut cpos = pos; + let mut cpos = *pos; let cylinder = (radius, z_min, z_max); cylinder_voxel_collision( cylinder, @@ -522,7 +521,7 @@ impl<'a> PhysicsData<'a> { let z_max = z_max.clamped(1.2, 1.95) * scale; let cylinder = (radius, z_min, z_max); - let mut cpos = pos; + let mut cpos = *pos; cylinder_voxel_collision( cylinder, &*read.terrain, @@ -539,7 +538,7 @@ impl<'a> PhysicsData<'a> { tgt_pos = cpos.0; }, Collider::Point => { - let mut pos = pos; + let mut pos = *pos; let (dist, block) = read .terrain @@ -651,7 +650,7 @@ impl<'a> PhysicsData<'a> { let mut physics_state_delta = physics_state.clone(); // deliberately don't use scale yet here, because the 11.0/0.8 // thing is in the comp::Scale for visual reasons - let mut cpos = pos; + let mut cpos = *pos; let wpos = cpos.0; let transform_from = Mat4::::translation_3d(pos_other.0 - wpos) @@ -691,10 +690,11 @@ impl<'a> PhysicsData<'a> { } physics_state.on_ground |= physics_state_delta.on_ground; physics_state.on_ceiling |= physics_state_delta.on_ceiling; - physics_state.on_wall = - physics_state.on_wall.or(physics_state_delta + physics_state.on_wall = physics_state.on_wall.or_else(|| { + physics_state_delta .on_wall - .map(|dir| ori_from.mul_direction(dir))); + .map(|dir| ori_from.mul_direction(dir)) + }); physics_state .touch_entities .append(&mut physics_state_delta.touch_entities); From d434ef35cc89c8c6c3f6f251ee770bf19ba4b736 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 13 Mar 2021 16:54:14 +0000 Subject: [PATCH 24/41] Slightly increase block-hop height for better airship block-hopping --- common/sys/src/phys.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index a7e10c92d1..1d3c008ade 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -988,7 +988,7 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( // .unwrap_or(false)) // ...and there is a collision with a block beneath our current hitbox... && collision_with( - pos.0 + resolve_dir - Vec3::unit_z() * 1.05, + pos.0 + resolve_dir - Vec3::unit_z() * 1.25, &terrain, block_true, near_iter.clone(), From 709444eedecf2ee00e4ee5ebb8d56d58c55b4dac Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 13 Mar 2021 19:40:00 +0000 Subject: [PATCH 25/41] Initial pass for gravity and air resistance for more stable physics --- common/sys/src/phys.rs | 88 ++++++++++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 34 deletions(-) diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index 1d3c008ade..d4d4614484 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -377,10 +377,59 @@ impl<'a> PhysicsData<'a> { span!(guard, "Apply movement and terrain collision"); let (positions, velocities, previous_phys_cache, orientations) = ( &write.positions, - &write.velocities, + &mut write.velocities, &write.previous_phys_cache, &write.orientations, ); + + // First pass: update velocity using air resistance and gravity for each entity. + // We do this in a first pass because it helps keep things more stable for + // entities that are anchored to other entities (such as airships). + ( + &read.entities, + positions, + velocities, + &write.physics_states, + !&read.mountings, + ) + .par_join() + .for_each(|(entity, pos, vel, physics_state, _)| { + let in_loaded_chunk = read + .terrain + .get_key(read.terrain.pos_key(pos.0.map(|e| e.floor() as i32))) + .is_some(); + // Integrate forces + // Friction is assumed to be a constant dependent on location + let friction = if physics_state.on_ground { 0.0 } else { FRIC_AIR } + // .max(if physics_state.on_ground { + // FRIC_GROUND + // } else { + // 0.0 + // }) + .max(if physics_state.in_liquid.is_some() { + FRIC_FLUID + } else { + 0.0 + }); + let downward_force = + if !in_loaded_chunk { + 0.0 // No gravity in unloaded chunks + } else if physics_state + .in_liquid + .map(|depth| depth > 0.75) + .unwrap_or(false) + { + (1.0 - BOUYANCY) * GRAVITY + } else { + GRAVITY + } * read.gravities.get(entity).map(|g| g.0).unwrap_or_default(); + + vel.0 = integrate_forces(read.dt.0, vel.0, downward_force, friction); + }); + + let velocities = &write.velocities; + + // Second pass: resolve collisions let (pos_writes, vel_writes, land_on_grounds) = ( &read.entities, read.scales.maybe(), @@ -409,9 +458,10 @@ impl<'a> PhysicsData<'a> { _previous_cache, _, )| { - // defer the writes of positions to allow an inner loop over terrain-like - // entities + // defer the writes of positions and velocities to allow an inner loop over + // terrain-like entities let mut vel = *vel; + let old_vel = vel; if sticky.is_some() && physics_state.on_surface().is_some() { vel.0 = physics_state.ground_vel; return (pos_writes, vel_writes, land_on_grounds); @@ -425,44 +475,14 @@ impl<'a> PhysicsData<'a> { 1.0 }; - let old_vel = vel; - // Integrate forces - // Friction is assumed to be a constant dependent on location - let friction = if physics_state.on_ground { 0.0 } else { FRIC_AIR } - // .max(if physics_state.on_ground { - // FRIC_GROUND - // } else { - // 0.0 - // }) - .max(if physics_state.in_liquid.is_some() { - FRIC_FLUID - } else { - 0.0 - }); let in_loaded_chunk = read .terrain .get_key(read.terrain.pos_key(pos.0.map(|e| e.floor() as i32))) .is_some(); - let downward_force = - if !in_loaded_chunk { - 0.0 // No gravity in unloaded chunks - } else if physics_state - .in_liquid - .map(|depth| depth > 0.75) - .unwrap_or(false) - { - (1.0 - BOUYANCY) * GRAVITY - } else { - GRAVITY - } * read.gravities.get(entity).map(|g| g.0).unwrap_or_default(); - vel.0 = integrate_forces(read.dt.0, vel.0, downward_force, friction); // Don't move if we're not in a loaded chunk let pos_delta = if in_loaded_chunk { - // this is an approximation that allows most framerates to - // behave in a similar manner. - let dt_lerp = 0.2; - (vel.0 * dt_lerp + old_vel.0 * (1.0 - dt_lerp)) * read.dt.0 + vel.0 * read.dt.0 } else { Vec3::zero() }; From 1637a5e8894292e40368d4f9e92a2111699be05d Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 13 Mar 2021 19:52:54 +0000 Subject: [PATCH 26/41] Fixed airship manifest offset --- assets/server/manifests/ship_manifest.ron | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/server/manifests/ship_manifest.ron b/assets/server/manifests/ship_manifest.ron index 6b361aa535..817d579120 100644 --- a/assets/server/manifests/ship_manifest.ron +++ b/assets/server/manifests/ship_manifest.ron @@ -4,7 +4,7 @@ //offset: (3.0, 7.0, 1.0), //offset: (-20.75, -34.75, 1.25), //offset: (0.0, 0.0, 0.0), - offset: (-20.0, -35.0, 1.0), + offset: (-17.5, -35.0, 1.0), //phys_offset: (0.25, 0.25, 0.25), phys_offset: (0.0, 0.0, 0.0), central: ("Human_Airship"), From 0c5ff2ea1141242f007c315c8a381ee141b29c54 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 13 Mar 2021 20:05:02 +0000 Subject: [PATCH 27/41] Prevented jumping airships --- common/src/states/utils.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index fda66fb758..3a82acb977 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -181,6 +181,13 @@ impl Body { } } + pub fn can_jump(&self) -> bool { + match self { + Body::Object(_) | Body::Ship(_) => false, + _ => true, + } + } + pub fn can_climb(&self) -> bool { matches!(self, Body::Humanoid(_)) } } @@ -435,6 +442,7 @@ pub fn handle_jump(data: &JoinData, update: &mut StateUpdate) { .in_liquid .map(|depth| depth > 1.0) .unwrap_or(false) + && data.body.can_jump() { update .local_events From 7b7b70b2e8e48cb846caa91728f35dd1f5e530d6 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 13 Mar 2021 20:51:32 +0000 Subject: [PATCH 28/41] Fixed jumping inconsistencies, no block-snapping for ships --- common/src/event.rs | 7 +------ common/src/states/climb.rs | 2 +- common/src/states/utils.rs | 16 +++++++++------- common/sys/src/phys.rs | 24 ++++++++++++------------ common/sys/src/state.rs | 22 ++-------------------- 5 files changed, 25 insertions(+), 46 deletions(-) diff --git a/common/src/event.rs b/common/src/event.rs index 21c8bc33af..c74ec46931 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -20,17 +20,12 @@ pub type SiteId = u64; pub enum LocalEvent { /// Applies upward force to entity's `Vel` - Jump(EcsEntity), + Jump(EcsEntity, f32), /// Applies the `impulse` to `entity`'s `Vel` ApplyImpulse { entity: EcsEntity, impulse: Vec3, }, - /// Applies leaping force to `entity`'s `Vel` away from `wall_dir` direction - WallLeap { - entity: EcsEntity, - wall_dir: Vec3, - }, /// Applies `vel` velocity to `entity` Boost { entity: EcsEntity, vel: Vec3 }, } diff --git a/common/src/states/climb.rs b/common/src/states/climb.rs index 51a5a176d5..cb97013ca9 100644 --- a/common/src/states/climb.rs +++ b/common/src/states/climb.rs @@ -36,7 +36,7 @@ impl CharacterBehavior for Data { // They've climbed atop something, give them a boost update .local_events - .push_front(LocalEvent::Jump(data.entity)); + .push_front(LocalEvent::Jump(data.entity, BASE_JUMP_IMPULSE * 0.5)); } update.character = CharacterState::Idle {}; return update; diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 3a82acb977..a9097aa08e 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -21,6 +21,7 @@ const BASE_HUMANOID_AIR_ACCEL: f32 = 2.0; const BASE_FLIGHT_ACCEL: f32 = 2.0; const BASE_HUMANOID_WATER_ACCEL: f32 = 150.0; const BASE_HUMANOID_WATER_SPEED: f32 = 180.0; +pub const BASE_JUMP_IMPULSE: f32 = 16.0; // const BASE_HUMANOID_CLIMB_ACCEL: f32 = 10.0; // const ROLL_SPEED: f32 = 17.0; // const CHARGE_SPEED: f32 = 20.0; @@ -181,10 +182,10 @@ impl Body { } } - pub fn can_jump(&self) -> bool { + pub fn jump_impulse(&self) -> Option { match self { - Body::Object(_) | Body::Ship(_) => false, - _ => true, + Body::Object(_) | Body::Ship(_) => None, + _ => Some(BASE_JUMP_IMPULSE), } } @@ -442,11 +443,12 @@ pub fn handle_jump(data: &JoinData, update: &mut StateUpdate) { .in_liquid .map(|depth| depth > 1.0) .unwrap_or(false) - && data.body.can_jump() + && data.body.jump_impulse().is_some() { - update - .local_events - .push_front(LocalEvent::Jump(data.entity)); + update.local_events.push_front(LocalEvent::Jump( + data.entity, + data.body.jump_impulse().unwrap(), + )); } } diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index d4d4614484..9c73962b31 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -1,8 +1,8 @@ use common::{ comp::{ - body::ship::figuredata::VOXEL_COLLIDER_MANIFEST, BeamSegment, CharacterState, Collider, - Gravity, Mass, Mounting, Ori, PhysicsState, Pos, PreviousPhysCache, Projectile, Scale, - Shockwave, Sticky, Vel, + body::ship::figuredata::VOXEL_COLLIDER_MANIFEST, BeamSegment, Body, CharacterState, + Collider, Gravity, Mass, Mounting, Ori, PhysicsState, Pos, PreviousPhysCache, Projectile, + Scale, Shockwave, Sticky, Vel, }, consts::{FRIC_GROUND, GRAVITY}, event::{EventBus, ServerEvent}, @@ -85,6 +85,7 @@ pub struct PhysicsRead<'a> { beams: ReadStorage<'a, BeamSegment>, shockwaves: ReadStorage<'a, Shockwave>, char_states: ReadStorage<'a, CharacterState>, + bodies: ReadStorage<'a, Body>, } #[derive(SystemData)] @@ -438,6 +439,7 @@ impl<'a> PhysicsData<'a> { positions, velocities, orientations, + read.bodies.maybe(), &mut write.physics_states, previous_phys_cache, !&read.mountings, @@ -454,6 +456,7 @@ impl<'a> PhysicsData<'a> { pos, vel, _ori, + body, mut physics_state, _previous_cache, _, @@ -502,6 +505,7 @@ impl<'a> PhysicsData<'a> { let mut tgt_pos = pos.0 + pos_delta; let was_on_ground = physics_state.on_ground; + let block_snap = body.map_or(false, |body| body.jump_impulse().is_some()); match &collider { Collider::Voxel { .. } => { @@ -526,6 +530,7 @@ impl<'a> PhysicsData<'a> { Vec3::zero(), &read.dt, was_on_ground, + block_snap, |entity, vel| land_on_grounds.push((entity, vel)), ); tgt_pos = cpos.0; @@ -553,6 +558,7 @@ impl<'a> PhysicsData<'a> { Vec3::zero(), &read.dt, was_on_ground, + block_snap, |entity, vel| land_on_grounds.push((entity, vel)), ); tgt_pos = cpos.0; @@ -693,6 +699,7 @@ impl<'a> PhysicsData<'a> { ori_to.mul_direction(vel_other.0), &read.dt, was_on_ground, + block_snap, |entity, vel| { land_on_grounds .push((entity, Vel(ori_from.mul_direction(vel.0)))) @@ -818,6 +825,7 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( ground_vel: Vec3, dt: &DeltaTime, was_on_ground: bool, + block_snap: bool, mut land_on_ground: impl FnMut(Entity, Vel), ) { let (radius, z_min, z_max) = cylinder; @@ -1062,15 +1070,7 @@ fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( z_range.clone(), ) && vel.0.z < 0.25 && vel.0.z > -1.5 - // && was_on_ground - // && !collision_with( - // pos.0 - Vec3::unit_z() * 0.0, - // &terrain, - // |block| block.solid_height() >= (pos.0.z - 0.1).rem_euclid(1.0), - // near_iter.clone(), - // radius, - // z_range.clone(), - // ) + && block_snap { let snap_height = terrain .get(Vec3::new(pos.0.x, pos.0.y, pos.0.z - 0.1).map(|e| e.floor() as i32)) diff --git a/common/sys/src/state.rs b/common/sys/src/state.rs index 8aae465bfd..a3d222ecdc 100644 --- a/common/sys/src/state.rs +++ b/common/sys/src/state.rs @@ -36,7 +36,6 @@ const DAY_CYCLE_FACTOR: f64 = 24.0 * 2.0; /// this value, the game's physics will begin to produce time lag. Ideally, we'd /// avoid such a situation. const MAX_DELTA_TIME: f32 = 1.0; -const HUMANOID_JUMP_ACCEL: f32 = 16.0; #[derive(Default)] pub struct BlockChange { @@ -454,13 +453,11 @@ impl State { let events = self.ecs.read_resource::>().recv_all(); for event in events { let mut velocities = self.ecs.write_storage::(); - let mut controllers = self.ecs.write_storage::(); let physics = self.ecs.read_storage::(); match event { - LocalEvent::Jump(entity) => { + LocalEvent::Jump(entity, impulse) => { if let Some(vel) = velocities.get_mut(entity) { - vel.0.z = HUMANOID_JUMP_ACCEL - + physics.get(entity).map_or(0.0, |ps| ps.ground_vel.z); + vel.0.z = impulse + physics.get(entity).map_or(0.0, |ps| ps.ground_vel.z); } }, LocalEvent::ApplyImpulse { entity, impulse } => { @@ -468,21 +465,6 @@ impl State { vel.0 = impulse; } }, - LocalEvent::WallLeap { entity, wall_dir } => { - if let (Some(vel), Some(_controller)) = - (velocities.get_mut(entity), controllers.get_mut(entity)) - { - let hspeed = Vec2::::from(vel.0).magnitude(); - if hspeed > 0.001 && hspeed < 0.5 { - vel.0 += vel.0.normalized() - * Vec3::new(1.0, 1.0, 0.0) - * HUMANOID_JUMP_ACCEL - * 1.5 - - wall_dir * 0.03; - vel.0.z = HUMANOID_JUMP_ACCEL * 0.5; - } - } - }, LocalEvent::Boost { entity, vel: extra_vel, From ca1e72cfa795cac35c412d34d2bcf6020a8d6507 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 13 Mar 2021 22:17:53 +0000 Subject: [PATCH 29/41] Made entities rotate smoothly with airships, fixed rotation speeds --- common/src/comp/phys.rs | 1 + common/src/states/utils.rs | 10 +++++++--- common/sys/src/phys.rs | 33 ++++++++++++++++++++++++++++----- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/common/src/comp/phys.rs b/common/src/comp/phys.rs index 33729f28ad..cddf4832f8 100644 --- a/common/src/comp/phys.rs +++ b/common/src/comp/phys.rs @@ -33,6 +33,7 @@ pub struct PreviousPhysCache { pub collision_boundary: f32, pub scale: f32, pub scaled_radius: f32, + pub ori: Quaternion, } impl Component for PreviousPhysCache { diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index a9097aa08e..1b74af1a55 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -170,7 +170,7 @@ impl Body { quadruped_low::Species::Lavadrake => 4.0, _ => 6.0, }, - Body::Ship(_) => 0.5, + Body::Ship(_) => 0.175, } } @@ -300,7 +300,11 @@ fn swim_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32, depth: } * efficiency; - handle_orientation(data, update, if data.physics.on_ground { 9.0 } else { 2.0 }); + handle_orientation( + data, + update, + data.body.base_ori_rate() * if data.physics.on_ground { 0.5 } else { 0.1 }, + ); // Swim update.vel.0.z = (update.vel.0.z @@ -330,7 +334,7 @@ fn fly_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) { * BASE_FLIGHT_ACCEL * efficiency; - handle_orientation(data, update, 1.0); + handle_orientation(data, update, data.body.base_ori_rate()); } /// Checks if an input related to an attack is held. If one is, moves entity diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index 9c73962b31..e5577546ec 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -151,6 +151,7 @@ impl<'a> PhysicsData<'a> { collision_boundary: 0.0, scale: 0.0, scaled_radius: 0.0, + ori: Quaternion::identity(), }); } @@ -620,7 +621,7 @@ impl<'a> PhysicsData<'a> { _other, pos_other, vel_other, - _previous_cache_other, + previous_cache_other, _mass_other, collider_other, ori_other, @@ -678,6 +679,9 @@ impl<'a> PhysicsData<'a> { // thing is in the comp::Scale for visual reasons let mut cpos = *pos; let wpos = cpos.0; + + // TODO: Cache the matrices here to avoid recomputing + let transform_from = Mat4::::translation_3d(pos_other.0 - wpos) * Mat4::from(ori_other.0) @@ -685,8 +689,21 @@ impl<'a> PhysicsData<'a> { let transform_to = transform_from.inverted(); let ori_from = Mat4::from(ori_other.0); let ori_to = ori_from.inverted(); + + // The velocity of the collider, taking into account orientation. + let wpos_rel = (Mat4::::translation_3d(pos_other.0) + * Mat4::from(ori_other.0) + * Mat4::::translation_3d(voxel_collider.translation)) + .inverted() + .mul_point(wpos); + let wpos_last = (Mat4::::translation_3d(pos_other.0) + * Mat4::from(previous_cache_other.ori) + * Mat4::::translation_3d(voxel_collider.translation)) + .mul_point(wpos_rel); + let vel_other = vel_other.0 + (wpos - wpos_last) / read.dt.0; + cpos.0 = transform_to.mul_point(Vec3::zero()); - vel.0 = ori_to.mul_direction(vel.0 - vel_other.0); + vel.0 = ori_to.mul_direction(vel.0 - vel_other); let cylinder = (radius, z_min, z_max); cylinder_voxel_collision( cylinder, @@ -696,7 +713,7 @@ impl<'a> PhysicsData<'a> { transform_to.mul_point(tgt_pos - wpos), &mut vel, &mut physics_state_delta, - ori_to.mul_direction(vel_other.0), + ori_to.mul_direction(vel_other), &read.dt, was_on_ground, block_snap, @@ -707,13 +724,13 @@ impl<'a> PhysicsData<'a> { ); cpos.0 = transform_from.mul_point(cpos.0) + wpos; - vel.0 = ori_from.mul_direction(vel.0) + vel_other.0; + vel.0 = ori_from.mul_direction(vel.0) + vel_other; tgt_pos = cpos.0; // union in the state updates, so that the state isn't just based on // the most recent terrain that collision was attempted with if physics_state_delta.on_ground { - physics_state.ground_vel = vel_other.0; + physics_state.ground_vel = vel_other; } physics_state.on_ground |= physics_state_delta.on_ground; physics_state.on_ceiling |= physics_state_delta.on_ceiling; @@ -776,6 +793,12 @@ impl<'a> PhysicsData<'a> { } } + for (ori, previous_phys_cache) in + (&write.orientations, &mut write.previous_phys_cache).join() + { + previous_phys_cache.ori = ori.0; + } + let mut event_emitter = read.event_bus.emitter(); land_on_grounds.into_iter().for_each(|(entity, vel)| { event_emitter.emit(ServerEvent::LandOnGround { entity, vel: vel.0 }); From ef1ebbcdce586839808d78fe60cea5b78cf0e04f Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Fri, 12 Mar 2021 21:53:19 -0500 Subject: [PATCH 30/41] Add machinery for storing component buffers for interpolation. --- Cargo.lock | 1 + common/net/Cargo.toml | 3 ++- common/net/src/msg/ecs_packet.rs | 6 ++--- common/net/src/sync/interpolation.rs | 26 +++++++++++++++++++++ common/net/src/sync/mod.rs | 6 +++-- common/net/src/sync/packet.rs | 34 ++++++++++++++++++++++++++++ common/sys/src/state.rs | 3 ++- 7 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 common/net/src/sync/interpolation.rs diff --git a/Cargo.lock b/Cargo.lock index 3e6de98ee3..c16404c034 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5619,6 +5619,7 @@ dependencies = [ "hashbrown", "serde", "specs", + "specs-idvs", "sum_type", "tracing", "vek 0.14.1", diff --git a/common/net/Cargo.toml b/common/net/Cargo.toml index 227ae093f9..b01fd9fc5e 100644 --- a/common/net/Cargo.toml +++ b/common/net/Cargo.toml @@ -25,6 +25,7 @@ authc = { git = "https://gitlab.com/veloren/auth.git", rev = "fb3dcbc4962b367253 # ECS specs = { git = "https://github.com/amethyst/specs.git", features = ["serde", "storage-event-control"], rev = "5a9b71035007be0e3574f35184acac1cd4530496" } +specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git", rev = "b65fb220e94f5d3c9bc30074a076149763795556" } # Serde -serde = { version = "1.0.110", features = ["derive"] } \ No newline at end of file +serde = { version = "1.0.110", features = ["derive"] } diff --git a/common/net/src/msg/ecs_packet.rs b/common/net/src/msg/ecs_packet.rs index 1ee77e127d..804d1ed1f1 100644 --- a/common/net/src/msg/ecs_packet.rs +++ b/common/net/src/msg/ecs_packet.rs @@ -100,7 +100,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Gravity(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Sticky(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::CharacterState(comp) => sync::handle_insert(comp, entity, world), - EcsCompPacket::Pos(comp) => sync::handle_insert(comp, entity, world), + EcsCompPacket::Pos(comp) => sync::handle_interp_insert(comp, entity, world), EcsCompPacket::Vel(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Ori(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Shockwave(comp) => sync::handle_insert(comp, entity, world), @@ -132,7 +132,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Gravity(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Sticky(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::CharacterState(comp) => sync::handle_modify(comp, entity, world), - EcsCompPacket::Pos(comp) => sync::handle_modify(comp, entity, world), + EcsCompPacket::Pos(comp) => sync::handle_interp_modify(comp, entity, world), EcsCompPacket::Vel(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Ori(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Shockwave(comp) => sync::handle_modify(comp, entity, world), @@ -168,7 +168,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPhantom::CharacterState(_) => { sync::handle_remove::(entity, world) }, - EcsCompPhantom::Pos(_) => sync::handle_remove::(entity, world), + EcsCompPhantom::Pos(_) => sync::handle_interp_remove::(entity, world), EcsCompPhantom::Vel(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Ori(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Shockwave(_) => sync::handle_remove::(entity, world), diff --git a/common/net/src/sync/interpolation.rs b/common/net/src/sync/interpolation.rs new file mode 100644 index 0000000000..6c321eb0a7 --- /dev/null +++ b/common/net/src/sync/interpolation.rs @@ -0,0 +1,26 @@ +// impls of `InterpolatableComponent` on things defined in `common`, since `common_net` is +// downstream of `common` +use common::comp::{Pos, Vel}; +use super::InterpolatableComponent; +use specs::{Component, Entity, World}; +use specs_idvs::IdvStorage; +use vek::Vec3; + +#[derive(Default)] +pub struct PosBuffer(pub [Vec3; 4]); + +impl Component for PosBuffer { + type Storage = IdvStorage; +} + +impl InterpolatableComponent for Pos { + type InterpData = PosBuffer; + + fn interpolate(self, data: &mut Self::InterpData, entity: Entity, world: &World) -> Self { + for i in 0..data.0.len()-1 { + data.0[i] = data.0[i+1]; + } + data.0[data.0.len()-1] = self.0; + self + } +} diff --git a/common/net/src/sync/mod.rs b/common/net/src/sync/mod.rs index 2b25c0db43..7997437db5 100644 --- a/common/net/src/sync/mod.rs +++ b/common/net/src/sync/mod.rs @@ -1,5 +1,6 @@ // Note: Currently only one-way sync is supported until a usecase for two-way // sync arises +pub mod interpolation; mod packet; mod sync_ext; mod track; @@ -7,8 +8,9 @@ mod track; // Reexports pub use common::uid::{Uid, UidAllocator}; pub use packet::{ - handle_insert, handle_modify, handle_remove, CompPacket, CompSyncPackage, EntityPackage, - EntitySyncPackage, StatePackage, + handle_insert, handle_interp_insert, handle_interp_modify, handle_interp_remove, handle_modify, + handle_remove, CompPacket, CompSyncPackage, EntityPackage, EntitySyncPackage, + InterpolatableComponent, StatePackage, }; pub use sync_ext::WorldSyncExt; pub use track::UpdateTracker; diff --git a/common/net/src/sync/packet.rs b/common/net/src/sync/packet.rs index e86dfb30fa..d9d3e67d0d 100644 --- a/common/net/src/sync/packet.rs +++ b/common/net/src/sync/packet.rs @@ -42,6 +42,40 @@ pub fn handle_remove(entity: Entity, world: &World) { world.write_storage::().remove(entity); } +pub trait InterpolatableComponent: Component { + type InterpData: Component + Default; + + fn interpolate(self, data: &mut Self::InterpData, entity: Entity, world: &World) -> Self; +} + +pub fn handle_interp_insert(comp: C, entity: Entity, world: &World) { + let mut interp_data = C::InterpData::default(); + let comp = comp.interpolate(&mut interp_data, entity, world); + handle_insert(comp, entity, world); + handle_insert(interp_data, entity, world); +} + +pub fn handle_interp_modify( + comp: C, + entity: Entity, + world: &World, +) { + if let Some(mut interp_data) = world.write_storage::().get_mut(entity) { + let comp = comp.interpolate(&mut interp_data, entity, world); + handle_modify(comp, entity, world); + } else { + error!( + ?comp, + "Error modifying interpolation data for synced component, it doesn't seem to exist" + ); + } +} + +pub fn handle_interp_remove(entity: Entity, world: &World) { + handle_remove::(entity, world); + handle_remove::(entity, world); +} + #[derive(Copy, Clone, Debug, Serialize, Deserialize)] pub enum CompUpdateKind { Inserted(P), diff --git a/common/sys/src/state.rs b/common/sys/src/state.rs index a3d222ecdc..b4575f0065 100644 --- a/common/sys/src/state.rs +++ b/common/sys/src/state.rs @@ -15,7 +15,7 @@ use common::{ }; use common_base::span; use common_ecs::{PhysicsMetrics, SysMetrics}; -use common_net::sync::WorldSyncExt; +use common_net::sync::{interpolation, WorldSyncExt}; use hashbrown::{HashMap, HashSet}; use rayon::{ThreadPool, ThreadPoolBuilder}; use specs::{ @@ -164,6 +164,7 @@ impl State { // Register client-local components // TODO: only register on the client ecs.register::(); + ecs.register::(); // Register server-local components // TODO: only register on the server From 649a54d188ffbc509993ec8a8e32fd8f03b78de8 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Sat, 13 Mar 2021 17:15:19 -0500 Subject: [PATCH 31/41] Get linear interpolation working for {Pos,Vel,Ori} with client-side timestamps. --- client/src/lib.rs | 2 + common/net/src/msg/ecs_packet.rs | 12 +-- common/net/src/sync/interpolation.rs | 127 ++++++++++++++++++++++++--- common/net/src/sync/packet.rs | 16 +++- common/src/resources.rs | 6 ++ common/sys/src/interpolation.rs | 63 +++++++++++++ common/sys/src/lib.rs | 1 + common/sys/src/state.rs | 12 ++- 8 files changed, 211 insertions(+), 28 deletions(-) create mode 100644 common/sys/src/interpolation.rs diff --git a/client/src/lib.rs b/client/src/lib.rs index 632f7205c4..2e4101f924 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -33,6 +33,7 @@ use common::{ grid::Grid, outcome::Outcome, recipe::RecipeBook, + resources::PlayerEntity, terrain::{block::Block, neighbors, BiomeKind, SitesKind, TerrainChunk, TerrainChunkSize}, trade::{PendingTrade, TradeAction, TradeId, TradeResult}, uid::{Uid, UidAllocator}, @@ -281,6 +282,7 @@ impl Client { let entity = state.ecs_mut().apply_entity_package(entity_package); *state.ecs_mut().write_resource() = time_of_day; + *state.ecs_mut().write_resource() = PlayerEntity(Some(entity)); state.ecs_mut().insert(material_stats); state.ecs_mut().insert(ability_map); diff --git a/common/net/src/msg/ecs_packet.rs b/common/net/src/msg/ecs_packet.rs index 804d1ed1f1..86049f7103 100644 --- a/common/net/src/msg/ecs_packet.rs +++ b/common/net/src/msg/ecs_packet.rs @@ -101,8 +101,8 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Sticky(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::CharacterState(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Pos(comp) => sync::handle_interp_insert(comp, entity, world), - EcsCompPacket::Vel(comp) => sync::handle_insert(comp, entity, world), - EcsCompPacket::Ori(comp) => sync::handle_insert(comp, entity, world), + EcsCompPacket::Vel(comp) => sync::handle_interp_insert(comp, entity, world), + EcsCompPacket::Ori(comp) => sync::handle_interp_insert(comp, entity, world), EcsCompPacket::Shockwave(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::BeamSegment(comp) => sync::handle_insert(comp, entity, world), } @@ -133,8 +133,8 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Sticky(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::CharacterState(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Pos(comp) => sync::handle_interp_modify(comp, entity, world), - EcsCompPacket::Vel(comp) => sync::handle_modify(comp, entity, world), - EcsCompPacket::Ori(comp) => sync::handle_modify(comp, entity, world), + EcsCompPacket::Vel(comp) => sync::handle_interp_modify(comp, entity, world), + EcsCompPacket::Ori(comp) => sync::handle_interp_modify(comp, entity, world), EcsCompPacket::Shockwave(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::BeamSegment(comp) => sync::handle_modify(comp, entity, world), } @@ -169,8 +169,8 @@ impl sync::CompPacket for EcsCompPacket { sync::handle_remove::(entity, world) }, EcsCompPhantom::Pos(_) => sync::handle_interp_remove::(entity, world), - EcsCompPhantom::Vel(_) => sync::handle_remove::(entity, world), - EcsCompPhantom::Ori(_) => sync::handle_remove::(entity, world), + EcsCompPhantom::Vel(_) => sync::handle_interp_remove::(entity, world), + EcsCompPhantom::Ori(_) => sync::handle_interp_remove::(entity, world), EcsCompPhantom::Shockwave(_) => sync::handle_remove::(entity, world), EcsCompPhantom::BeamSegment(_) => sync::handle_remove::(entity, world), } diff --git a/common/net/src/sync/interpolation.rs b/common/net/src/sync/interpolation.rs index 6c321eb0a7..17b9d4ef1c 100644 --- a/common/net/src/sync/interpolation.rs +++ b/common/net/src/sync/interpolation.rs @@ -1,26 +1,125 @@ -// impls of `InterpolatableComponent` on things defined in `common`, since `common_net` is -// downstream of `common` -use common::comp::{Pos, Vel}; +// impls of `InterpolatableComponent` on things defined in `common`, since +// `common_net` is downstream of `common`, and an `InterpolationSystem` that +// applies them use super::InterpolatableComponent; -use specs::{Component, Entity, World}; +use common::comp::{Ori, Pos, Vel}; +use specs::Component; use specs_idvs::IdvStorage; -use vek::Vec3; +use tracing::warn; +use vek::ops::{Lerp, Slerp}; -#[derive(Default)] -pub struct PosBuffer(pub [Vec3; 4]); +#[derive(Debug, Default)] +pub struct InterpBuffer { + pub buf: [(f64, T); 4], + pub i: usize, +} -impl Component for PosBuffer { +impl Component for InterpBuffer { type Storage = IdvStorage; } impl InterpolatableComponent for Pos { - type InterpData = PosBuffer; + type InterpData = InterpBuffer; + type ReadData = Vel; - fn interpolate(self, data: &mut Self::InterpData, entity: Entity, world: &World) -> Self { - for i in 0..data.0.len()-1 { - data.0[i] = data.0[i+1]; + fn update_component(&self, interp_data: &mut Self::InterpData, time: f64) { + let InterpBuffer { + ref mut buf, + ref mut i, + } = interp_data; + *i += 1; + *i %= buf.len(); + buf[*i] = (time, *self); + } + + fn interpolate(self, interp_data: &Self::InterpData, t2: f64, _vel: &Vel) -> Self { + // lerp to test interface, do hermite spline later + let InterpBuffer { ref buf, ref i } = interp_data; + let (t0, p0) = buf[(i + buf.len() - 1) % buf.len()]; + let (t1, p1) = buf[i % buf.len()]; + if (t1 - t0).abs() < f64::EPSILON { + return self; } - data.0[data.0.len()-1] = self.0; - self + let lerp_factor = 1.0 + ((t2 - t1) / (t1 - t0)) as f32; + let mut out = Lerp::lerp_unclamped(p0.0, p1.0, lerp_factor); + if out.map(|x| x.is_nan()).reduce_or() { + warn!( + "interpolation output is nan: {}, {}, {:?}", + t2, lerp_factor, buf + ); + out = p1.0; + } + + Pos(out) + } +} + +impl InterpolatableComponent for Vel { + type InterpData = InterpBuffer; + type ReadData = (); + + fn update_component(&self, interp_data: &mut Self::InterpData, time: f64) { + let InterpBuffer { + ref mut buf, + ref mut i, + } = interp_data; + *i += 1; + *i %= buf.len(); + buf[*i] = (time, *self); + } + + fn interpolate(self, interp_data: &Self::InterpData, t2: f64, _: &()) -> Self { + let InterpBuffer { ref buf, ref i } = interp_data; + let (t0, p0) = buf[(i + buf.len() - 1) % buf.len()]; + let (t1, p1) = buf[i % buf.len()]; + if (t1 - t0).abs() < f64::EPSILON { + return self; + } + let lerp_factor = 1.0 + ((t2 - t1) / (t1 - t0)) as f32; + let mut out = Lerp::lerp_unclamped(p0.0, p1.0, lerp_factor); + if out.map(|x| x.is_nan()).reduce_or() { + warn!( + "interpolation output is nan: {}, {}, {:?}", + t2, lerp_factor, buf + ); + out = p1.0; + } + + Vel(out) + } +} + +impl InterpolatableComponent for Ori { + type InterpData = InterpBuffer; + type ReadData = (); + + fn update_component(&self, interp_data: &mut Self::InterpData, time: f64) { + let InterpBuffer { + ref mut buf, + ref mut i, + } = interp_data; + *i += 1; + *i %= buf.len(); + buf[*i] = (time, *self); + } + + fn interpolate(self, interp_data: &Self::InterpData, t2: f64, _: &()) -> Self { + let InterpBuffer { ref buf, ref i } = interp_data; + let (t0, p0) = buf[(i + buf.len() - 1) % buf.len()]; + let (t1, p1) = buf[i % buf.len()]; + if (t1 - t0).abs() < f64::EPSILON { + return self; + } + let lerp_factor = 1.0 + ((t2 - t1) / (t1 - t0)) as f32; + let mut out = Slerp::slerp_unclamped(p0.0, p1.0, lerp_factor); + if out.into_vec4().map(|x| x.is_nan()).reduce_or() { + warn!( + "interpolation output is nan: {}, {}, {:?}", + t2, lerp_factor, buf + ); + out = p1.0; + } + + Ori(out.normalized()) } } diff --git a/common/net/src/sync/packet.rs b/common/net/src/sync/packet.rs index d9d3e67d0d..152b7946d6 100644 --- a/common/net/src/sync/packet.rs +++ b/common/net/src/sync/packet.rs @@ -1,5 +1,5 @@ use super::track::UpdateTracker; -use common::uid::Uid; +use common::{resources::Time, uid::Uid}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use specs::{Component, Entity, Join, ReadStorage, World, WorldExt}; use std::{ @@ -9,6 +9,10 @@ use std::{ }; use tracing::error; +// TODO: apply_{insert,modify,remove} all take the entity and call +// `write_storage` once per entity per component, instead of once per update +// batch(e.g. in a system-like memory access pattern); if sync ends up being a +// bottleneck, try optimizing this /// Implemented by type that carries component data for insertion and /// modification The assocatied `Phantom` type only carries information about /// which component type is of interest and is used to transmit deletion events @@ -44,13 +48,16 @@ pub fn handle_remove(entity: Entity, world: &World) { pub trait InterpolatableComponent: Component { type InterpData: Component + Default; + type ReadData; - fn interpolate(self, data: &mut Self::InterpData, entity: Entity, world: &World) -> Self; + fn update_component(&self, data: &mut Self::InterpData, time: f64); + fn interpolate(self, data: &Self::InterpData, time: f64, read_data: &Self::ReadData) -> Self; } pub fn handle_interp_insert(comp: C, entity: Entity, world: &World) { let mut interp_data = C::InterpData::default(); - let comp = comp.interpolate(&mut interp_data, entity, world); + let time = world.read_resource::