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