veloren_common_systems/phys/
mod.rs

1use common::{
2    comp::{
3        Body, CharacterState, Collider, Density, Immovable, Mass, Ori, PhysicsState, Pos,
4        PosVelOriDefer, PreviousPhysCache, Projectile, Scale, Stats, Sticky, Vel,
5        body::ship::figuredata::VOXEL_COLLIDER_MANIFEST,
6        fluid_dynamics::{Fluid, Wings},
7        inventory::item::armor::Friction,
8    },
9    consts::{AIR_DENSITY, GRAVITY},
10    event::{EmitExt, EventBus, LandOnGroundEvent},
11    event_emitters,
12    link::Is,
13    mounting::{Rider, VolumeRider},
14    outcome::Outcome,
15    resources::{DeltaTime, GameMode, TimeOfDay},
16    states,
17    terrain::{CoordinateConversions, TerrainGrid},
18    uid::Uid,
19    util::{Projection, SpatialGrid},
20    weather::WeatherGrid,
21};
22use common_base::{prof_span, span};
23use common_ecs::{Job, Origin, ParMode, Phase, PhysicsMetrics, System};
24use rayon::iter::ParallelIterator;
25use specs::{
26    Entities, Join, LendJoin, ParJoin, Read, ReadExpect, ReadStorage, SystemData, Write,
27    WriteExpect, WriteStorage, shred,
28};
29use vek::*;
30
31mod collision;
32mod weather;
33use collision::ColliderData;
34
35/// The density of the fluid as a function of submersion ratio in given fluid
36/// where it is assumed that any unsubmersed part is is air.
37// TODO: Better suited partial submersion curve?
38fn fluid_density(height: f32, fluid: &Fluid) -> Density {
39    // If depth is less than our height (partial submersion), remove
40    // fluid density based on the ratio of displacement to full volume.
41    let immersion = fluid
42        .depth()
43        .map_or(1.0, |depth| (depth / height).clamp(0.0, 1.0));
44
45    Density(fluid.density().0 * immersion + AIR_DENSITY * (1.0 - immersion))
46}
47
48fn integrate_forces(
49    dt: &DeltaTime,
50    mut vel: Vel,
51    (body, wings): (&Body, Option<&Wings>),
52    density: &Density,
53    mass: &Mass,
54    fluid: &Fluid,
55    gravity: f32,
56    scale: Option<Scale>,
57) -> Vel {
58    let dim = body.dimensions();
59    let height = dim.z;
60    let rel_flow = fluid.relative_flow(&vel);
61    let fluid_density = fluid_density(height, fluid);
62    debug_assert!(mass.0 > 0.0);
63    debug_assert!(density.0 > 0.0);
64
65    // Aerodynamic/hydrodynamic forces
66    if !rel_flow.0.is_approx_zero() {
67        debug_assert!(!rel_flow.0.map(|a| a.is_nan()).reduce_or());
68        // HACK: We should really use the latter logic (i.e: `aerodynamic_forces`) for
69        // liquids, but it results in pretty serious dt-dependent problems that
70        // are extremely difficult to resolve. This is a compromise: for liquids
71        // only, we calculate drag using an incorrect (but still visually plausible)
72        // model that is much more resistant to differences in dt. Because it's
73        // physically incorrect anyway, there are magic coefficients that
74        // exist simply to get us closer to what water 'should' feel like.
75        if fluid.is_liquid() {
76            let fric = body
77                .drag_coefficient_liquid(fluid_density.0, scale.map_or(1.0, |s| s.0))
78                .powf(0.75)
79                * 0.02;
80
81            let fvel = fluid.flow_vel();
82
83            // Drag is relative to fluid velocity, so compensate before applying drag
84            vel.0 = (vel.0 - fvel.0) * (1.0 / (1.0 + fric)).powf(dt.0 * 10.0) + fvel.0;
85        } else {
86            let impulse = dt.0
87                * body.aerodynamic_forces(
88                    &rel_flow,
89                    fluid_density.0,
90                    wings,
91                    scale.map_or(1.0, |s| s.0),
92                );
93            debug_assert!(!impulse.map(|a| a.is_nan()).reduce_or());
94            if !impulse.is_approx_zero() {
95                let new_v = vel.0 + impulse / mass.0;
96                // If the new velocity is in the opposite direction, it's because the forces
97                // involved are too high for the current tick to handle. We deal with this by
98                // removing the component of our velocity vector along the direction of force.
99                // This way we can only ever lose velocity and will never experience a reverse
100                // in direction from events such as falling into water at high velocities.
101                if new_v.dot(vel.0) < 0.0 {
102                    // Multiply by a factor to prevent full stop,
103                    // as this can cause things to get stuck in high-density medium
104                    vel.0 -= vel.0.projected(&impulse) * 0.9;
105                } else {
106                    vel.0 = new_v;
107                }
108            }
109        }
110        debug_assert!(!vel.0.map(|a| a.is_nan()).reduce_or());
111    };
112
113    // Hydrostatic/aerostatic forces
114    // modify gravity to account for the effective density as a result of buoyancy
115    let down_force = dt.0 * gravity * (density.0 - fluid_density.0) / density.0;
116    vel.0.z -= down_force;
117
118    vel
119}
120
121fn calc_z_limit(char_state_maybe: Option<&CharacterState>, collider: &Collider) -> (f32, f32) {
122    let modifier = if char_state_maybe.is_some_and(|c_s| c_s.is_dodge() || c_s.is_glide()) {
123        0.5
124    } else {
125        1.0
126    };
127    collider.get_z_limits(modifier)
128}
129
130event_emitters! {
131    struct Events[Emitters] {
132        land_on_ground: LandOnGroundEvent,
133    }
134}
135
136/// This system applies forces and calculates new positions and velocities.
137#[derive(Default)]
138pub struct Sys;
139
140#[derive(SystemData)]
141pub struct PhysicsRead<'a> {
142    entities: Entities<'a>,
143    events: Events<'a>,
144    uids: ReadStorage<'a, Uid>,
145    terrain: ReadExpect<'a, TerrainGrid>,
146    dt: Read<'a, DeltaTime>,
147    game_mode: ReadExpect<'a, GameMode>,
148    scales: ReadStorage<'a, Scale>,
149    stickies: ReadStorage<'a, Sticky>,
150    immovables: ReadStorage<'a, Immovable>,
151    masses: ReadStorage<'a, Mass>,
152    colliders: ReadStorage<'a, Collider>,
153    is_riders: ReadStorage<'a, Is<Rider>>,
154    is_volume_riders: ReadStorage<'a, Is<VolumeRider>>,
155    projectiles: ReadStorage<'a, Projectile>,
156    character_states: ReadStorage<'a, CharacterState>,
157    bodies: ReadStorage<'a, Body>,
158    densities: ReadStorage<'a, Density>,
159    stats: ReadStorage<'a, Stats>,
160    weather: Option<Read<'a, WeatherGrid>>,
161    time_of_day: Read<'a, TimeOfDay>,
162}
163
164#[derive(SystemData)]
165pub struct PhysicsWrite<'a> {
166    physics_metrics: WriteExpect<'a, PhysicsMetrics>,
167    cached_spatial_grid: Write<'a, common::CachedSpatialGrid>,
168    physics_states: WriteStorage<'a, PhysicsState>,
169    positions: WriteStorage<'a, Pos>,
170    velocities: WriteStorage<'a, Vel>,
171    pos_vel_ori_defers: WriteStorage<'a, PosVelOriDefer>,
172    orientations: WriteStorage<'a, Ori>,
173    previous_phys_cache: WriteStorage<'a, PreviousPhysCache>,
174    outcomes: Read<'a, EventBus<Outcome>>,
175}
176
177#[derive(SystemData)]
178pub struct PhysicsData<'a> {
179    read: PhysicsRead<'a>,
180    write: PhysicsWrite<'a>,
181}
182
183impl PhysicsData<'_> {
184    /// Add/reset physics state components
185    fn reset(&mut self) {
186        span!(_guard, "Add/reset physics state components");
187        for (entity, _, _, _, _) in (
188            &self.read.entities,
189            &self.read.colliders,
190            &self.write.positions,
191            &self.write.velocities,
192            &self.write.orientations,
193        )
194            .join()
195        {
196            let _ = self
197                .write
198                .physics_states
199                .entry(entity)
200                .map(|e| e.or_insert_with(Default::default));
201        }
202    }
203
204    fn maintain_pushback_cache(&mut self) {
205        span!(_guard, "Maintain pushback cache");
206        // Add PreviousPhysCache for all relevant entities
207        for entity in (
208            &self.read.entities,
209            &self.read.colliders,
210            &self.write.velocities,
211            &self.write.positions,
212            !&self.write.previous_phys_cache,
213        )
214            .join()
215            .map(|(e, _, _, _, _)| e)
216            .collect::<Vec<_>>()
217        {
218            let _ = self
219                .write
220                .previous_phys_cache
221                .insert(entity, PreviousPhysCache {
222                    velocity: Vec3::zero(),
223                    velocity_dt: Vec3::zero(),
224                    in_fluid: None,
225                    center: Vec3::zero(),
226                    collision_boundary: 0.0,
227                    scale: 0.0,
228                    scaled_radius: 0.0,
229                    neighborhood_radius: 0.0,
230                    origins: None,
231                    pos: None,
232                    ori: Quaternion::identity(),
233                });
234        }
235
236        // Update PreviousPhysCache
237        for (_, vel, position, ori, phys_state, phys_cache, collider, scale, cs) in (
238            &self.read.entities,
239            &self.write.velocities,
240            &self.write.positions,
241            &self.write.orientations,
242            &self.write.physics_states,
243            &mut self.write.previous_phys_cache,
244            &self.read.colliders,
245            self.read.scales.maybe(),
246            self.read.character_states.maybe(),
247        )
248            .join()
249        {
250            let scale = scale.map(|s| s.0).unwrap_or(1.0);
251            let z_limits = calc_z_limit(cs, collider);
252            let (z_min, z_max) = z_limits;
253            let (z_min, z_max) = (z_min * scale, z_max * scale);
254            let half_height = (z_max - z_min) / 2.0;
255
256            phys_cache.velocity_dt = vel.0 * self.read.dt.0;
257            phys_cache.velocity = vel.0;
258            phys_cache.in_fluid = phys_state.in_fluid;
259            let entity_center = position.0 + Vec3::new(0.0, 0.0, z_min + half_height);
260            let flat_radius = collider.bounding_radius() * scale;
261            let radius = (flat_radius.powi(2) + half_height.powi(2)).sqrt();
262
263            // Move center to the middle between OLD and OLD+VEL_DT
264            // so that we can reduce the collision_boundary.
265            phys_cache.center = entity_center + phys_cache.velocity_dt / 2.0;
266            phys_cache.collision_boundary = radius + (phys_cache.velocity_dt / 2.0).magnitude();
267            phys_cache.scale = scale;
268            phys_cache.scaled_radius = flat_radius;
269
270            let neighborhood_radius = match collider {
271                Collider::CapsulePrism { radius, .. } => radius * scale,
272                Collider::Voxel { .. } | Collider::Volume(_) | Collider::Point => flat_radius,
273            };
274            phys_cache.neighborhood_radius = neighborhood_radius;
275
276            let ori = ori.to_quat();
277            let origins = match collider {
278                Collider::CapsulePrism { p0, p1, .. } => {
279                    let a = p1 - p0;
280                    let len = a.magnitude();
281                    // If origins are close enough, our capsule prism is cylinder
282                    // with one origin which we don't even need to rotate.
283                    //
284                    // Other advantage of early-return is that we don't
285                    // later divide by zero and return NaN
286                    if len < f32::EPSILON * 10.0 {
287                        Some((*p0, *p0))
288                    } else {
289                        // Apply orientation to origins of prism.
290                        //
291                        // We do this by building line between them,
292                        // rotate it and then split back to origins.
293                        // (Otherwise we will need to do the same with each
294                        // origin).
295                        //
296                        // Cast it to 3d and then convert it back to 2d
297                        // to apply quaternion.
298                        let a = a.with_z(0.0);
299                        let a = ori * a;
300                        let a = a.xy();
301                        // Previous operation could shrink x and y coordinates
302                        // if orientation had Z parameter.
303                        // Make sure we have the same length as before
304                        // (and scale it, while we on it).
305                        let a = a.normalized() * scale * len;
306                        let p0 = -a / 2.0;
307                        let p1 = a / 2.0;
308
309                        Some((p0, p1))
310                    }
311                },
312                Collider::Voxel { .. } | Collider::Volume(_) | Collider::Point => None,
313            };
314            phys_cache.origins = origins;
315        }
316    }
317
318    fn construct_spatial_grid(&mut self) -> SpatialGrid {
319        span!(_guard, "Construct spatial grid");
320        let &mut PhysicsData {
321            ref read,
322            ref write,
323        } = self;
324        // NOTE: i32 places certain constraints on how far out collision works
325        // NOTE: uses the radius of the entity and their current position rather than
326        // the radius of their bounding sphere for the current frame of movement
327        // because the nonmoving entity is what is collided against in the inner
328        // loop of the pushback collision code
329        // TODO: maintain frame to frame? (requires handling deletion)
330        // TODO: if not maintaining frame to frame consider counting entities to
331        // preallocate?
332        // TODO: assess parallelizing (overhead might dominate here? would need to merge
333        // the vecs in each hashmap)
334        let lg2_cell_size = 5;
335        let lg2_large_cell_size = 6;
336        let radius_cutoff = 8;
337        let mut spatial_grid = SpatialGrid::new(lg2_cell_size, lg2_large_cell_size, radius_cutoff);
338        for (entity, pos, phys_cache, _, _) in (
339            &read.entities,
340            &write.positions,
341            &write.previous_phys_cache,
342            write.velocities.mask(),
343            !&read.projectiles, // Not needed because they are skipped in the inner loop below
344        )
345            .join()
346        {
347            // Note: to not get too fine grained we use a 2D grid for now
348            let radius_2d = phys_cache.scaled_radius.ceil() as u32;
349            let pos_2d = pos.0.xy().map(|e| e as i32);
350            const POS_TRUNCATION_ERROR: u32 = 1;
351            spatial_grid.insert(pos_2d, radius_2d + POS_TRUNCATION_ERROR, entity);
352        }
353
354        spatial_grid
355    }
356
357    fn apply_pushback(&mut self, job: &mut Job<Sys>, spatial_grid: &SpatialGrid) {
358        span!(_guard, "Apply pushback");
359        job.cpu_stats.measure(ParMode::Rayon);
360        let &mut PhysicsData {
361            ref read,
362            ref mut write,
363        } = self;
364        let (positions, previous_phys_cache) = (&write.positions, &write.previous_phys_cache);
365        let metrics = (
366            &read.entities,
367            positions,
368            &mut write.velocities,
369            previous_phys_cache,
370            &read.masses,
371            &read.colliders,
372            read.is_riders.maybe(),
373            read.is_volume_riders.maybe(),
374            read.stickies.maybe(),
375            read.immovables.maybe(),
376            &mut write.physics_states,
377            // TODO: if we need to avoid collisions for other things consider
378            // moving whether it should interact into the collider component
379            // or into a separate component.
380            read.projectiles.maybe(),
381            read.character_states.maybe(),
382        )
383            .par_join()
384            .map_init(
385                || {
386                    prof_span!(guard, "physics e<>e rayon job");
387                    guard
388                },
389                |_guard,
390                 (
391                    entity,
392                    pos,
393                    vel,
394                    previous_cache,
395                    mass,
396                    collider,
397                    is_rider,
398                    is_volume_rider,
399                    sticky,
400                    immovable,
401                    physics,
402                    projectile,
403                    char_state_maybe,
404                )| {
405                    let is_sticky = sticky.is_some();
406                    let is_immovable = immovable.is_some();
407                    let is_mid_air = physics.on_surface().is_none();
408                    let mut entity_entity_collision_checks = 0;
409                    let mut entity_entity_collisions = 0;
410
411                    // TODO: quick fix for bad performance. At extrememly high
412                    // velocities use oriented rectangles at some threshold of
413                    // displacement/radius to query the spatial grid and limit
414                    // max displacement per tick somehow.
415                    if previous_cache.collision_boundary > 128.0 {
416                        return PhysicsMetrics {
417                            entity_entity_collision_checks,
418                            entity_entity_collisions,
419                        };
420                    }
421
422                    let z_limits = calc_z_limit(char_state_maybe, collider);
423
424                    // Resets touch_entities in physics
425                    physics.touch_entities.clear();
426
427                    let is_projectile = projectile.is_some();
428
429                    let mut vel_delta = Vec3::zero();
430
431                    let query_center = previous_cache.center.xy();
432                    let query_radius = previous_cache.collision_boundary;
433
434                    spatial_grid
435                        .in_circle_aabr(query_center, query_radius)
436                        .filter_map(|entity| {
437                            let uid = read.uids.get(entity)?;
438                            let pos = positions.get(entity)?;
439                            let previous_cache = previous_phys_cache.get(entity)?;
440                            let mass = read.masses.get(entity)?;
441                            let collider = read.colliders.get(entity)?;
442
443                            Some((
444                                entity,
445                                uid,
446                                pos,
447                                previous_cache,
448                                mass,
449                                collider,
450                                read.character_states.get(entity),
451                                read.is_riders.get(entity),
452                            ))
453                        })
454                        .for_each(
455                            |(
456                                entity_other,
457                                other,
458                                pos_other,
459                                previous_cache_other,
460                                mass_other,
461                                collider_other,
462                                char_state_other_maybe,
463                                other_is_rider_maybe,
464                            )| {
465                                let collision_boundary = previous_cache.collision_boundary
466                                    + previous_cache_other.collision_boundary;
467                                if previous_cache
468                                    .center
469                                    .distance_squared(previous_cache_other.center)
470                                    > collision_boundary.powi(2)
471                                    || entity == entity_other
472                                {
473                                    return;
474                                }
475
476                                let z_limits_other =
477                                    calc_z_limit(char_state_other_maybe, collider_other);
478
479                                entity_entity_collision_checks += 1;
480
481                                const MIN_COLLISION_DIST: f32 = 0.3;
482
483                                let increments = ((previous_cache.velocity_dt
484                                    - previous_cache_other.velocity_dt)
485                                    .magnitude()
486                                    / MIN_COLLISION_DIST)
487                                    .max(1.0)
488                                    .ceil()
489                                    as usize;
490                                let step_delta = 1.0 / increments as f32;
491
492                                let mut collision_registered = false;
493
494                                for i in 0..increments {
495                                    let factor = i as f32 * step_delta;
496                                    // We are not interested if collision succeed
497                                    // or no as of now.
498                                    // Collision reaction is done inside.
499                                    let _ = collision::resolve_e2e_collision(
500                                        // utility variables for our entity
501                                        &mut collision_registered,
502                                        &mut entity_entity_collisions,
503                                        factor,
504                                        physics,
505                                        char_state_maybe,
506                                        &mut vel_delta,
507                                        step_delta,
508                                        // physics flags
509                                        is_mid_air,
510                                        is_sticky,
511                                        is_immovable,
512                                        is_projectile,
513                                        // entity we colliding with
514                                        *other,
515                                        // symetrical collider context
516                                        ColliderData {
517                                            pos,
518                                            previous_cache,
519                                            z_limits,
520                                            collider,
521                                            mass: *mass,
522                                        },
523                                        ColliderData {
524                                            pos: pos_other,
525                                            previous_cache: previous_cache_other,
526                                            z_limits: z_limits_other,
527                                            collider: collider_other,
528                                            mass: *mass_other,
529                                        },
530                                        vel,
531                                        is_rider.is_some()
532                                            || is_volume_rider.is_some()
533                                            || other_is_rider_maybe.is_some(),
534                                    );
535                                }
536                            },
537                        );
538
539                    // Change velocity
540                    vel.0 += vel_delta * read.dt.0;
541
542                    // Metrics
543                    PhysicsMetrics {
544                        entity_entity_collision_checks,
545                        entity_entity_collisions,
546                    }
547                },
548            )
549            .reduce(PhysicsMetrics::default, |old, new| PhysicsMetrics {
550                entity_entity_collision_checks: old.entity_entity_collision_checks
551                    + new.entity_entity_collision_checks,
552                entity_entity_collisions: old.entity_entity_collisions
553                    + new.entity_entity_collisions,
554            });
555        write.physics_metrics.entity_entity_collision_checks =
556            metrics.entity_entity_collision_checks;
557        write.physics_metrics.entity_entity_collisions = metrics.entity_entity_collisions;
558    }
559
560    fn construct_voxel_collider_spatial_grid(&mut self) -> SpatialGrid {
561        span!(_guard, "Construct voxel collider spatial grid");
562        let &mut PhysicsData {
563            ref read,
564            ref write,
565        } = self;
566
567        let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read();
568
569        // NOTE: i32 places certain constraints on how far out collision works
570        // NOTE: uses the radius of the entity and their current position rather than
571        // the radius of their bounding sphere for the current frame of movement
572        // because the nonmoving entity is what is collided against in the inner
573        // loop of the pushback collision code
574        // TODO: optimize these parameters (especially radius cutoff)
575        let lg2_cell_size = 7; // 128
576        let lg2_large_cell_size = 8; // 256
577        let radius_cutoff = 64;
578        let mut spatial_grid = SpatialGrid::new(lg2_cell_size, lg2_large_cell_size, radius_cutoff);
579        // TODO: give voxel colliders their own component type
580        for (entity, pos, collider, scale, ori) in (
581            &read.entities,
582            &write.positions,
583            &read.colliders,
584            read.scales.maybe(),
585            &write.orientations,
586        )
587            .join()
588        {
589            let vol = collider.get_vol(&voxel_colliders_manifest);
590
591            if let Some(vol) = vol {
592                let sphere = collision::voxel_collider_bounding_sphere(vol, pos, ori, scale);
593                let radius = sphere.radius.ceil() as u32;
594                let pos_2d = sphere.center.xy().map(|e| e as i32);
595                const POS_TRUNCATION_ERROR: u32 = 1;
596                spatial_grid.insert(pos_2d, radius + POS_TRUNCATION_ERROR, entity);
597            }
598        }
599
600        spatial_grid
601    }
602
603    fn handle_movement_and_terrain(
604        &mut self,
605        job: &mut Job<Sys>,
606        voxel_collider_spatial_grid: &SpatialGrid,
607    ) {
608        let &mut PhysicsData {
609            ref read,
610            ref mut write,
611        } = self;
612
613        prof_span!(guard, "Apply Weather");
614        if let Some(weather) = &read.weather {
615            for (_, state, pos, phys) in (
616                &read.entities,
617                &read.character_states,
618                &write.positions,
619                &mut write.physics_states,
620            )
621                .join()
622            {
623                // Always reset air_vel to zero
624                let mut air_vel = Vec3::zero();
625
626                'simulation: {
627                    // Don't simulate for non-gliding, for now
628                    if !state.is_glide() {
629                        break 'simulation;
630                    }
631
632                    let pos_2d = pos.0.as_().xy();
633                    let chunk_pos: Vec2<i32> = pos_2d.wpos_to_cpos();
634                    let Some(current_chunk) = &read.terrain.get_key(chunk_pos) else {
635                        // oopsie
636                        break 'simulation;
637                    };
638
639                    let meta = current_chunk.meta();
640
641                    // Skip simulating for entites deeply under the ground
642                    if pos.0.z < meta.alt() - 25.0 {
643                        break 'simulation;
644                    }
645
646                    // If couldn't simulate wind for some reason, skip
647                    if let Ok(simulated_vel) =
648                        weather::simulated_wind_vel(pos, weather, &read.terrain, &read.time_of_day)
649                    {
650                        air_vel = simulated_vel
651                    };
652                }
653
654                phys.in_fluid = phys.in_fluid.map(|f| match f {
655                    Fluid::Air { elevation, .. } => Fluid::Air {
656                        vel: Vel(air_vel),
657                        elevation,
658                    },
659                    fluid => fluid,
660                });
661            }
662        }
663
664        drop(guard);
665
666        prof_span!(guard, "insert PosVelOriDefer");
667        // NOTE: keep in sync with join below
668        (
669            &read.entities,
670            read.colliders.mask(),
671            &write.positions,
672            &write.velocities,
673            &write.orientations,
674            write.orientations.mask(),
675            write.physics_states.mask(),
676            !&write.pos_vel_ori_defers, // This is the one we are adding
677            write.previous_phys_cache.mask(),
678            !&read.is_riders,
679            !&read.is_volume_riders,
680        )
681            .join()
682            .map(|t| (t.0, *t.2, *t.3, *t.4))
683            .collect::<Vec<_>>()
684            .into_iter()
685            .for_each(|(entity, pos, vel, ori)| {
686                let _ = write.pos_vel_ori_defers.insert(entity, PosVelOriDefer {
687                    pos: Some(pos),
688                    vel: Some(vel),
689                    ori: Some(ori),
690                });
691            });
692        drop(guard);
693
694        // Apply movement inputs
695        span!(guard, "Apply movement");
696        let (positions, velocities) = (&write.positions, &mut write.velocities);
697
698        // First pass: update velocity using air resistance and gravity for each entity.
699        // We do this in a first pass because it helps keep things more stable for
700        // entities that are anchored to other entities (such as airships).
701        (
702            positions,
703            velocities,
704            read.stickies.maybe(),
705            &read.bodies,
706            read.character_states.maybe(),
707            &write.physics_states,
708            &read.masses,
709            &read.densities,
710            read.scales.maybe(),
711            !&read.is_riders,
712            !&read.is_volume_riders,
713        )
714            .par_join()
715            .for_each_init(
716                || {
717                    prof_span!(guard, "velocity update rayon job");
718                    guard
719                },
720                |_guard,
721                 (
722                    pos,
723                    vel,
724                    sticky,
725                    body,
726                    character_state,
727                    physics_state,
728                    mass,
729                    density,
730                    scale,
731                    _,
732                    _,
733                )| {
734                    let in_loaded_chunk = read
735                        .terrain
736                        .contains_key(read.terrain.pos_key(pos.0.map(|e| e.floor() as i32)));
737
738                    // Apply physics only if in a loaded chunk
739                    if in_loaded_chunk
740                    // And not already stuck on a block (e.g., for arrows)
741                    && !(physics_state.on_surface().is_some() && sticky.is_some())
742                    // HACK: Special-case boats. Experimentally, clients are *bad* at making guesses about movement,
743                    // and this is a particular problem for volume entities since careful control of velocity is
744                    // required for nice movement of entities on top of the volume. Special-case volume entities here
745                    // to prevent weird drag/gravity guesses messing with movement, relying on the client's hermite
746                    // interpolation instead.
747                    && !(matches!(body, Body::Ship(_)) && matches!(&*read.game_mode, GameMode::Client))
748                    {
749                        // Clamp dt to an effective 10 TPS, to prevent gravity
750                        // from slamming the players into the floor when
751                        // stationary if other systems cause the server
752                        // to lag (as observed in the 0.9 release party).
753                        let dt = DeltaTime(read.dt.0.min(0.1));
754
755                        match physics_state.in_fluid {
756                            None => {
757                                vel.0.z -= dt.0 * GRAVITY;
758                            },
759                            Some(fluid) => {
760                                let wings = match character_state {
761                                    Some(&CharacterState::Glide(states::glide::Data {
762                                        aspect_ratio,
763                                        planform_area,
764                                        ori,
765                                        ..
766                                    })) => Some(Wings {
767                                        aspect_ratio,
768                                        planform_area,
769                                        ori,
770                                    }),
771
772                                    _ => None,
773                                };
774                                vel.0 = integrate_forces(
775                                    &dt,
776                                    *vel,
777                                    (body, wings.as_ref()),
778                                    density,
779                                    mass,
780                                    &fluid,
781                                    GRAVITY,
782                                    scale.copied(),
783                                )
784                                .0
785                            },
786                        }
787                    }
788                },
789            );
790        drop(guard);
791        job.cpu_stats.measure(ParMode::Single);
792
793        // Second pass: resolve collisions for terrain-like entities, this is required
794        // in order to update their positions before resolving collisions for
795        // non-terrain-like entities, since otherwise, collision is resolved
796        // based on where the terrain-like entity was in the previous frame.
797        Self::resolve_et_collision(job, read, write, voxel_collider_spatial_grid, true);
798
799        // Third pass: resolve collisions for non-terrain-like entities
800        Self::resolve_et_collision(job, read, write, voxel_collider_spatial_grid, false);
801
802        // Update cached 'old' physics values to the current values ready for the next
803        // tick
804        prof_span!(guard, "record ori into phys_cache");
805        for (pos, ori, previous_phys_cache) in (
806            &write.positions,
807            &write.orientations,
808            &mut write.previous_phys_cache,
809        )
810            .join()
811        {
812            // Note: updating ori with the rest of the cache values above was attempted but
813            // it did not work (investigate root cause?)
814            previous_phys_cache.pos = Some(*pos);
815            previous_phys_cache.ori = ori.to_quat();
816        }
817        drop(guard);
818    }
819
820    fn resolve_et_collision(
821        job: &mut Job<Sys>,
822        read: &PhysicsRead,
823        write: &mut PhysicsWrite,
824        voxel_collider_spatial_grid: &SpatialGrid,
825        terrain_like_entities: bool,
826    ) {
827        let (positions, velocities, previous_phys_cache, orientations) = (
828            &write.positions,
829            &write.velocities,
830            &write.previous_phys_cache,
831            &write.orientations,
832        );
833        span!(guard, "Apply terrain collision");
834        job.cpu_stats.measure(ParMode::Rayon);
835        let (land_on_grounds, outcomes) = (
836            &read.entities,
837            read.scales.maybe(),
838            read.stickies.maybe(),
839            &read.colliders,
840            positions,
841            velocities,
842            orientations,
843            read.bodies.maybe(),
844            read.character_states.maybe(),
845            &mut write.physics_states,
846            &mut write.pos_vel_ori_defers,
847            previous_phys_cache,
848            !&read.is_riders,
849            !&read.is_volume_riders,
850        )
851            .par_join()
852            .filter(|tuple| tuple.3.is_voxel() == terrain_like_entities)
853            .map_init(
854                || {
855                    prof_span!(guard, "physics e<>t rayon job");
856                    guard
857                },
858                |_guard,
859                 (
860                    entity,
861                    scale,
862                    sticky,
863                    collider,
864                    pos,
865                    vel,
866                    ori,
867                    body,
868                    character_state,
869                    physics_state,
870                    pos_vel_ori_defer,
871                    previous_cache,
872                    _,
873                    _,
874                )| {
875                    let mut land_on_ground = None;
876                    let mut outcomes = Vec::new();
877                    // Defer the writes of positions, velocities and orientations
878                    // to allow an inner loop over terrain-like entities.
879                    let old_vel = *vel;
880                    let mut vel = *vel;
881                    let old_ori = *ori;
882                    let mut ori = *ori;
883
884                    let scale = if collider.is_voxel() {
885                        scale.map(|s| s.0).unwrap_or(1.0)
886                    } else {
887                        // TODO: Use scale & actual proportions when pathfinding is good
888                        // enough to manage irregular entity sizes
889                        1.0
890                    };
891
892                    if let Some(state) = character_state {
893                        let footwear = state.footwear().unwrap_or(Friction::Normal);
894                        if footwear != physics_state.footwear {
895                            physics_state.footwear = footwear;
896                        }
897                    }
898
899                    let in_loaded_chunk = read
900                        .terrain
901                        .contains_key(read.terrain.pos_key(pos.0.map(|e| e.floor() as i32)));
902
903                    // Don't move if we're not in a loaded chunk
904                    let pos_delta = if in_loaded_chunk {
905                        vel.0 * read.dt.0
906                    } else {
907                        Vec3::zero()
908                    };
909
910                    // What's going on here?
911                    // Because collisions need to be resolved against multiple
912                    // colliders, this code takes the current position and
913                    // propagates it forward according to velocity to find a
914                    // 'target' position.
915                    //
916                    // This is where we'd ideally end up at the end of the tick,
917                    // assuming no collisions. Then, we refine this target by
918                    // stepping from the original position to the target for
919                    // every obstacle, refining the target position as we go.
920                    //
921                    // It's not perfect, but it works pretty well in practice.
922                    // Oddities can occur on the intersection between multiple
923                    // colliders, but it's not like any game physics system
924                    // resolves these sort of things well anyway.
925                    // At the very least, we don't do things that result in glitchy
926                    // velocities or entirely broken position snapping.
927                    let mut tgt_pos = pos.0 + pos_delta;
928
929                    let was_on_ground = physics_state.on_ground.is_some();
930                    let block_snap =
931                        body.is_some_and(|b| !matches!(b, Body::Object(_) | Body::Ship(_)));
932                    let climbing =
933                        character_state.is_some_and(|cs| matches!(cs, CharacterState::Climb(_)));
934
935                    let friction_factor = |vel: Vec3<f32>| {
936                        if let Some(Body::Ship(ship)) = body
937                            && ship.has_wheels()
938                        {
939                            vel.try_normalized()
940                                .and_then(|dir| {
941                                    Some(orientations.get(entity)?.right().dot(dir).abs())
942                                })
943                                .unwrap_or(1.0)
944                                .max(0.2)
945                        } else {
946                            1.0
947                        }
948                    };
949
950                    match &collider {
951                        Collider::Voxel { .. } | Collider::Volume(_) => {
952                            // For now, treat entities with voxel colliders
953                            // as their bounding cylinders for the purposes of
954                            // colliding them with terrain.
955                            //
956                            // Additionally, multiply radius by 0.1 to make
957                            // the cylinder smaller to avoid lag.
958                            let radius = collider.bounding_radius() * scale * 0.1;
959                            let (_, z_max) = collider.get_z_limits(scale);
960                            let z_min = 0.0;
961
962                            let mut cpos = *pos;
963                            let cylinder = (radius, z_min, z_max);
964                            collision::box_voxel_collision(
965                                cylinder,
966                                &*read.terrain,
967                                entity,
968                                &mut cpos,
969                                tgt_pos,
970                                &mut vel,
971                                physics_state,
972                                &read.dt,
973                                was_on_ground,
974                                block_snap,
975                                climbing,
976                                |entity, vel, surface_normal| {
977                                    land_on_ground = Some((entity, vel, surface_normal))
978                                },
979                                read,
980                                &ori,
981                                friction_factor,
982                            );
983                            tgt_pos = cpos.0;
984                        },
985                        Collider::CapsulePrism {
986                            z_min: _,
987                            z_max,
988                            p0: _,
989                            p1: _,
990                            radius: _,
991                        } => {
992                            // Scale collider
993                            let radius = collider.bounding_radius().min(0.45) * scale;
994                            let z_min = 0.0;
995                            let z_max = z_max.clamped(1.2, 1.95) * scale;
996
997                            let cylinder = (radius, z_min, z_max);
998                            let mut cpos = *pos;
999                            collision::box_voxel_collision(
1000                                cylinder,
1001                                &*read.terrain,
1002                                entity,
1003                                &mut cpos,
1004                                tgt_pos,
1005                                &mut vel,
1006                                physics_state,
1007                                &read.dt,
1008                                was_on_ground,
1009                                block_snap,
1010                                climbing,
1011                                |entity, vel, surface_normal| {
1012                                    land_on_ground = Some((entity, vel, surface_normal))
1013                                },
1014                                read,
1015                                &ori,
1016                                friction_factor,
1017                            );
1018
1019                            // Sticky things shouldn't move when on a surface
1020                            if physics_state.on_surface().is_some() && sticky.is_some() {
1021                                vel.0 = physics_state.ground_vel;
1022                            }
1023
1024                            tgt_pos = cpos.0;
1025                        },
1026                        Collider::Point => {
1027                            let mut pos = *pos;
1028
1029                            collision::point_voxel_collision(
1030                                entity,
1031                                &mut pos,
1032                                pos_delta,
1033                                &mut vel,
1034                                physics_state,
1035                                sticky.is_some(),
1036                                &mut outcomes,
1037                                read,
1038                            );
1039
1040                            tgt_pos = pos.0;
1041                        },
1042                    }
1043
1044                    // Compute center and radius of tick path bounding sphere
1045                    // for the entity for broad checks of whether it will
1046                    // collide with a voxel collider
1047                    let path_sphere = {
1048                        // TODO: duplicated with maintain_pushback_cache,
1049                        // make a common function to call to compute all this info?
1050                        let z_limits = calc_z_limit(character_state, collider);
1051                        let z_limits = (z_limits.0 * scale, z_limits.1 * scale);
1052                        let half_height = (z_limits.1 - z_limits.0) / 2.0;
1053
1054                        let entity_center = pos.0 + (z_limits.0 + half_height) * Vec3::unit_z();
1055                        let path_center = entity_center + pos_delta / 2.0;
1056
1057                        let flat_radius = collider.bounding_radius() * scale;
1058                        let radius = (flat_radius.powi(2) + half_height.powi(2)).sqrt();
1059                        let path_bounding_radius = radius + (pos_delta / 2.0).magnitude();
1060
1061                        Sphere {
1062                            center: path_center,
1063                            radius: path_bounding_radius,
1064                        }
1065                    };
1066                    // Collide with terrain-like entities
1067                    let query_center = path_sphere.center.xy();
1068                    let query_radius = path_sphere.radius;
1069
1070                    let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read();
1071
1072                    voxel_collider_spatial_grid
1073                        .in_circle_aabr(query_center, query_radius)
1074                        .filter_map(|entity| {
1075                            positions.get(entity).and_then(|pos| {
1076                                Some((
1077                                    entity,
1078                                    pos,
1079                                    velocities.get(entity)?,
1080                                    previous_phys_cache.get(entity)?,
1081                                    read.colliders.get(entity)?,
1082                                    read.scales.get(entity),
1083                                    orientations.get(entity)?,
1084                                ))
1085                            })
1086                        })
1087                        .for_each(
1088                            |(
1089                                entity_other,
1090                                pos_other,
1091                                vel_other,
1092                                previous_cache_other,
1093                                collider_other,
1094                                scale_other,
1095                                ori_other,
1096                            )| {
1097                                if entity == entity_other {
1098                                    return;
1099                                }
1100
1101                                let voxel_collider =
1102                                    collider_other.get_vol(&voxel_colliders_manifest);
1103
1104                                // use bounding cylinder regardless of our collider
1105                                // TODO: extract point-terrain collision above to its own
1106                                // function
1107                                let radius = collider.bounding_radius();
1108                                let (_, z_max) = collider.get_z_limits(1.0);
1109
1110                                let radius = radius.min(0.45) * scale;
1111                                let z_min = 0.0;
1112                                let z_max = z_max.clamped(1.2, 1.95) * scale;
1113
1114                                if let Some(voxel_collider) = voxel_collider {
1115                                    // TODO: cache/precompute sphere?
1116                                    let voxel_sphere = collision::voxel_collider_bounding_sphere(
1117                                        voxel_collider,
1118                                        pos_other,
1119                                        ori_other,
1120                                        scale_other,
1121                                    );
1122                                    // Early check
1123                                    if voxel_sphere.center.distance_squared(path_sphere.center)
1124                                        > (voxel_sphere.radius + path_sphere.radius).powi(2)
1125                                    {
1126                                        return;
1127                                    }
1128
1129                                    let mut physics_state_delta = physics_state.clone();
1130
1131                                    // Helper function for computing a transformation matrix and its
1132                                    // inverse. Should
1133                                    // be much cheaper than using `Mat4::inverted`.
1134                                    let from_to_matricies =
1135                                        |entity_rpos: Vec3<f32>, collider_ori: Quaternion<f32>| {
1136                                            (
1137                                                Mat4::<f32>::translation_3d(entity_rpos)
1138                                                    * Mat4::from(collider_ori)
1139                                                    * Mat4::scaling_3d(previous_cache_other.scale)
1140                                                    * Mat4::translation_3d(
1141                                                        voxel_collider.translation,
1142                                                    ),
1143                                                Mat4::<f32>::translation_3d(
1144                                                    -voxel_collider.translation,
1145                                                ) * Mat4::scaling_3d(
1146                                                    1.0 / previous_cache_other.scale,
1147                                                ) * Mat4::from(collider_ori.inverse())
1148                                                    * Mat4::translation_3d(-entity_rpos),
1149                                            )
1150                                        };
1151
1152                                    // Compute matrices that allow us to transform to and from the
1153                                    // coordinate space of
1154                                    // the collider. We have two variants of each, one for the
1155                                    // current state and one for
1156                                    // the previous state. This allows us to 'perfectly' track
1157                                    // change in position
1158                                    // between ticks, which prevents entities falling through voxel
1159                                    // colliders due to spurious
1160                                    // issues like differences in ping/variable dt.
1161                                    // TODO: Cache the matrices here to avoid recomputing for each
1162                                    // entity on them
1163                                    let (_transform_last_from, transform_last_to) =
1164                                        from_to_matricies(
1165                                            previous_cache_other.pos.unwrap_or(*pos_other).0
1166                                                - previous_cache.pos.unwrap_or(*pos).0,
1167                                            previous_cache_other.ori,
1168                                        );
1169                                    let (transform_from, transform_to) =
1170                                        from_to_matricies(pos_other.0 - pos.0, ori_other.to_quat());
1171
1172                                    // Compute the velocity of the collider, accounting for changes
1173                                    // in orientation
1174                                    // from the last tick. We then model this change as a change in
1175                                    // surface velocity
1176                                    // for the collider.
1177                                    let vel_other = {
1178                                        let pos_rel =
1179                                            (Mat4::<f32>::translation_3d(
1180                                                -voxel_collider.translation,
1181                                            ) * Mat4::from(ori_other.to_quat().inverse()))
1182                                            .mul_point(pos.0 - pos_other.0);
1183                                        let rpos_last =
1184                                            (Mat4::<f32>::from(previous_cache_other.ori)
1185                                                * Mat4::translation_3d(voxel_collider.translation))
1186                                            .mul_point(pos_rel);
1187                                        vel_other.0
1188                                            + (pos.0 - (pos_other.0 + rpos_last)) / read.dt.0
1189                                    };
1190
1191                                    {
1192                                        // Transform the entity attributes into the coordinate space
1193                                        // of the collider ready
1194                                        // for collision resolution
1195                                        let mut rpos =
1196                                            Pos(transform_last_to.mul_point(Vec3::zero()));
1197                                        vel.0 = previous_cache_other.ori.inverse()
1198                                            * (vel.0 - vel_other);
1199
1200                                        // Perform collision resolution
1201                                        collision::box_voxel_collision(
1202                                            (radius, z_min, z_max),
1203                                            &voxel_collider.volume(),
1204                                            entity,
1205                                            &mut rpos,
1206                                            transform_to.mul_point(tgt_pos - pos.0),
1207                                            &mut vel,
1208                                            &mut physics_state_delta,
1209                                            &read.dt,
1210                                            was_on_ground,
1211                                            block_snap,
1212                                            climbing,
1213                                            |entity, vel, surface_normal| {
1214                                                land_on_ground = Some((
1215                                                    entity,
1216                                                    Vel(previous_cache_other.ori * vel.0
1217                                                        + vel_other),
1218                                                    previous_cache_other.ori * surface_normal,
1219                                                ));
1220                                            },
1221                                            read,
1222                                            &ori,
1223                                            |vel| friction_factor(previous_cache_other.ori * vel),
1224                                        );
1225
1226                                        // Transform entity attributes back into world space now
1227                                        // that we've performed
1228                                        // collision resolution with them
1229                                        tgt_pos = transform_from.mul_point(rpos.0) + pos.0;
1230                                        vel.0 = previous_cache_other.ori * vel.0 + vel_other;
1231                                    }
1232
1233                                    // Collision resolution may also change the physics state. Since
1234                                    // we may be interacting
1235                                    // with multiple colliders at once (along with the regular
1236                                    // terrain!) we keep track
1237                                    // of a physics state 'delta' and try to sensibly resolve them
1238                                    // against one-another at each step.
1239                                    if physics_state_delta.on_ground.is_some() {
1240                                        // TODO: Do we need to do this? Perhaps just take the
1241                                        // ground_vel regardless?
1242                                        physics_state.ground_vel = previous_cache_other.ori
1243                                            * physics_state_delta.ground_vel
1244                                            + vel_other;
1245                                    }
1246                                    if physics_state_delta.on_surface().is_some() {
1247                                        // If the collision resulted in us being on a surface,
1248                                        // rotate us with the
1249                                        // collider. Really this should be modelled via friction or
1250                                        // something, but
1251                                        // our physics model doesn't really take orientation into
1252                                        // consideration.
1253                                        ori = ori.rotated(
1254                                            ori_other.to_quat()
1255                                                * previous_cache_other.ori.inverse(),
1256                                        );
1257                                    }
1258                                    physics_state.on_ground =
1259                                        physics_state.on_ground.or(physics_state_delta.on_ground);
1260                                    physics_state.on_ceiling |= physics_state_delta.on_ceiling;
1261                                    physics_state.on_wall = physics_state.on_wall.or_else(|| {
1262                                        physics_state_delta
1263                                            .on_wall
1264                                            .map(|dir| previous_cache_other.ori * dir)
1265                                    });
1266                                    physics_state.in_fluid = match (
1267                                        physics_state.in_fluid,
1268                                        physics_state_delta.in_fluid,
1269                                    ) {
1270                                        (Some(x), Some(y)) => x
1271                                            .depth()
1272                                            .and_then(|xh| {
1273                                                y.depth()
1274                                                    .map(|yh| xh > yh)
1275                                                    .unwrap_or(true)
1276                                                    .then_some(x)
1277                                            })
1278                                            .or(Some(y)),
1279                                        (x @ Some(_), _) => x,
1280                                        (_, y @ Some(_)) => y,
1281                                        _ => None,
1282                                    };
1283                                }
1284                            },
1285                        );
1286
1287                    if tgt_pos != pos.0 {
1288                        pos_vel_ori_defer.pos = Some(Pos(tgt_pos));
1289                    } else {
1290                        pos_vel_ori_defer.pos = None;
1291                    }
1292                    if vel != old_vel {
1293                        pos_vel_ori_defer.vel = Some(vel);
1294                    } else {
1295                        pos_vel_ori_defer.vel = None;
1296                    }
1297                    if ori != old_ori {
1298                        pos_vel_ori_defer.ori = Some(ori);
1299                    } else {
1300                        pos_vel_ori_defer.ori = None;
1301                    }
1302
1303                    (land_on_ground, outcomes)
1304                },
1305            )
1306            .fold(
1307                || (Vec::new(), Vec::new()),
1308                |(mut land_on_grounds, mut all_outcomes), (land_on_ground, mut outcomes)| {
1309                    land_on_ground.map(|log| land_on_grounds.push(log));
1310                    all_outcomes.append(&mut outcomes);
1311                    (land_on_grounds, all_outcomes)
1312                },
1313            )
1314            .reduce(
1315                || (Vec::new(), Vec::new()),
1316                |(mut land_on_grounds_a, mut outcomes_a),
1317                 (mut land_on_grounds_b, mut outcomes_b)| {
1318                    land_on_grounds_a.append(&mut land_on_grounds_b);
1319                    outcomes_a.append(&mut outcomes_b);
1320                    (land_on_grounds_a, outcomes_a)
1321                },
1322            );
1323        drop(guard);
1324        job.cpu_stats.measure(ParMode::Single);
1325
1326        write.outcomes.emitter().emit_many(outcomes);
1327
1328        prof_span!(guard, "write deferred pos and vel");
1329        for (_, pos, vel, ori, pos_vel_ori_defer, _) in (
1330            &read.entities,
1331            &mut write.positions,
1332            &mut write.velocities,
1333            &mut write.orientations,
1334            &mut write.pos_vel_ori_defers,
1335            &read.colliders,
1336        )
1337            .join()
1338            .filter(|tuple| tuple.5.is_voxel() == terrain_like_entities)
1339        {
1340            if let Some(new_pos) = pos_vel_ori_defer.pos.take() {
1341                *pos = new_pos;
1342            }
1343            if let Some(new_vel) = pos_vel_ori_defer.vel.take() {
1344                *vel = new_vel;
1345            }
1346            if let Some(new_ori) = pos_vel_ori_defer.ori.take() {
1347                *ori = new_ori;
1348            }
1349        }
1350        drop(guard);
1351
1352        let mut emitters = read.events.get_emitters();
1353        emitters.emit_many(
1354            land_on_grounds
1355                .into_iter()
1356                .map(|(entity, vel, surface_normal)| LandOnGroundEvent {
1357                    entity,
1358                    vel: vel.0,
1359                    surface_normal,
1360                }),
1361        );
1362    }
1363
1364    fn update_cached_spatial_grid(&mut self) {
1365        span!(_guard, "Update cached spatial grid");
1366        let &mut PhysicsData {
1367            ref read,
1368            ref mut write,
1369        } = self;
1370
1371        let spatial_grid = &mut write.cached_spatial_grid.0;
1372        spatial_grid.clear();
1373        (
1374            &read.entities,
1375            &write.positions,
1376            read.scales.maybe(),
1377            read.colliders.maybe(),
1378        )
1379            .join()
1380            .for_each(|(entity, pos, scale, collider)| {
1381                let scale = scale.map(|s| s.0).unwrap_or(1.0);
1382                let radius_2d =
1383                    (collider.map(|c| c.bounding_radius()).unwrap_or(0.5) * scale).ceil() as u32;
1384                let pos_2d = pos.0.xy().map(|e| e as i32);
1385                const POS_TRUNCATION_ERROR: u32 = 1;
1386                spatial_grid.insert(pos_2d, radius_2d + POS_TRUNCATION_ERROR, entity);
1387            });
1388    }
1389}
1390
1391impl<'a> System<'a> for Sys {
1392    type SystemData = PhysicsData<'a>;
1393
1394    const NAME: &'static str = "phys";
1395    const ORIGIN: Origin = Origin::Common;
1396    const PHASE: Phase = Phase::Create;
1397
1398    fn run(job: &mut Job<Self>, mut physics_data: Self::SystemData) {
1399        physics_data.reset();
1400
1401        // Apply pushback
1402        //
1403        // Note: We now do this first because we project velocity ahead. This is slighty
1404        // imperfect and implies that we might get edge-cases where entities
1405        // standing right next to the edge of a wall may get hit by projectiles
1406        // fired into the wall very close to them. However, this sort of thing is
1407        // already possible with poorly-defined hitboxes anyway so it's not too
1408        // much of a concern.
1409        //
1410        // If this situation becomes a problem, this code should be integrated with the
1411        // terrain collision code below, although that's not trivial to do since
1412        // it means the step needs to take into account the speeds of both
1413        // entities.
1414        physics_data.maintain_pushback_cache();
1415
1416        let spatial_grid = physics_data.construct_spatial_grid();
1417        physics_data.apply_pushback(job, &spatial_grid);
1418
1419        let voxel_collider_spatial_grid = physics_data.construct_voxel_collider_spatial_grid();
1420        physics_data.handle_movement_and_terrain(job, &voxel_collider_spatial_grid);
1421
1422        // Spatial grid used by other systems
1423        physics_data.update_cached_spatial_grid();
1424    }
1425}