1use crate::{
2 components::{RigidBody, Velocity},
3 integrator::Integrator,
4 solver::ConstraintSolver,
5};
6use gizmo_physics_core::broadphase::SpatialHash;
7use gizmo_physics_core::{CollisionEvent, ContactManifold, TriggerEvent};
8use gizmo_physics_core::raycast::{Ray, Raycast, RaycastHit};
9use gizmo_physics_core::components::{Collider, Transform};
10use gizmo_core::entity::Entity;
11
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
15pub enum ZoneShape {
16 Box {
17 min: gizmo_math::Vec3,
18 max: gizmo_math::Vec3,
19 },
20 Sphere {
21 center: gizmo_math::Vec3,
22 radius: f32,
23 },
24}
25
26impl ZoneShape {
27 pub fn contains(&self, p: gizmo_math::Vec3) -> bool {
28 match self {
29 ZoneShape::Box { min, max } => {
30 p.x >= min.x
31 && p.x <= max.x
32 && p.y >= min.y
33 && p.y <= max.y
34 && p.z >= min.z
35 && p.z <= max.z
36 }
37 ZoneShape::Sphere { center, radius } => {
38 (p - *center).length_squared() <= radius * radius
39 }
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
45pub struct GravityField {
46 pub shape: ZoneShape,
47 pub gravity: gizmo_math::Vec3,
48 pub falloff_radius: f32, pub priority: i32,
50}
51
52#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
53pub struct FluidZone {
54 pub shape: ZoneShape,
55 pub density: f32, pub viscosity: f32, pub linear_drag: f32, pub quadratic_drag: f32, }
60
61const PHYSICS_HZ: f32 = 240.0;
63const FIXED_DT: f32 = 1.0 / PHYSICS_HZ;
64const MAX_SUBSTEPS: u32 = 64; #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
68pub enum Weather {
69 Sunny,
70 Rain,
71 Snow,
72}
73
74impl Default for Weather {
75 fn default() -> Self {
76 Self::Sunny
77 }
78}
79
80#[derive(Clone)]
82pub struct PhysicsStateSnapshot {
83 pub transforms: Vec<Transform>,
84 pub velocities: Vec<Velocity>,
85}
86
87#[derive(serde::Serialize, serde::Deserialize)]
89pub struct PhysicsWorld {
90 pub weather: Weather,
91
92 #[serde(skip)]
93 pub integrator: Integrator,
94 #[serde(skip)]
95 pub solver: ConstraintSolver,
96 #[serde(skip)]
97 pub spatial_hash: SpatialHash,
98 #[serde(skip)]
99 pub collision_events: Vec<CollisionEvent>,
100 #[serde(skip)]
101 pub trigger_events: Vec<TriggerEvent>,
102 #[serde(skip)]
103 pub fracture_events: Vec<gizmo_physics_core::FractureEvent>,
104 #[serde(skip)]
105 pub fracture_cache: crate::fracture::PreFracturedCache,
106 #[serde(skip)]
107 pub joints: Vec<crate::joints::Joint>,
108 #[serde(skip)]
109 pub joint_solver: crate::joints::JointSolver,
110
111 pub gravity_fields: Vec<GravityField>,
112 pub fluid_zones: Vec<FluidZone>,
113
114 #[serde(skip)]
115 pub(crate) contact_cache: HashMap<(Entity, Entity), (bool, Option<ContactManifold>)>,
116
117 pub accumulator: f32,
118 pub render_alpha: f32,
119
120 #[serde(skip)]
121 pub metrics: crate::island::PhysicsMetrics,
122
123 pub entities: Vec<Entity>,
125 pub rigid_bodies: Vec<RigidBody>,
126 pub transforms: Vec<Transform>,
127 pub velocities: Vec<Velocity>,
128 pub colliders: Vec<Collider>,
129 pub entity_index_map: HashMap<u32, usize>,
130
131 #[serde(skip)]
133 pub is_paused: bool,
134 #[serde(skip)]
135 pub step_once: bool,
136 #[serde(skip)]
137 pub rewind_requested: bool,
138 #[serde(skip)]
139 pub history: std::collections::VecDeque<PhysicsStateSnapshot>,
140 pub max_history_frames: usize,
141
142 #[serde(skip)]
143 pub watchlist: std::collections::HashSet<Entity>,
144}
145
146impl Default for PhysicsWorld {
147 fn default() -> Self {
148 Self::new()
149 }
150}
151
152impl PhysicsWorld {
153 pub fn new() -> Self {
154 Self {
155 weather: Weather::Sunny,
156 integrator: Integrator::default(),
157 solver: ConstraintSolver::default(),
158 spatial_hash: SpatialHash::new(10.0),
159 collision_events: Vec::new(),
160 trigger_events: Vec::new(),
161 fracture_events: Vec::new(),
162 fracture_cache: crate::fracture::PreFracturedCache::new(),
163 joints: Vec::new(),
164 joint_solver: crate::joints::JointSolver::default(),
165 gravity_fields: Vec::new(),
166 fluid_zones: Vec::new(),
167
168
169 contact_cache: HashMap::new(),
170 accumulator: 0.0,
171 render_alpha: 1.0,
172 metrics: crate::island::PhysicsMetrics::default(),
173 entities: Vec::new(),
174 rigid_bodies: Vec::new(),
175 transforms: Vec::new(),
176 velocities: Vec::new(),
177 colliders: Vec::new(),
178 entity_index_map: HashMap::new(),
179 is_paused: false,
180 step_once: false,
181 rewind_requested: false,
182 history: std::collections::VecDeque::new(),
183 max_history_frames: 600, watchlist: std::collections::HashSet::new(),
185 }
186 }
187
188 pub fn with_gravity(mut self, gravity: gizmo_math::Vec3) -> Self {
189 self.integrator.gravity = gravity;
190 self
191 }
192
193
194 pub fn enable_gpu_compute(&mut self) {
195 }
196
197 pub fn with_cell_size(mut self, cell_size: f32) -> Self {
198 self.spatial_hash = SpatialHash::new(cell_size);
199 self
200 }
201
202 pub fn add_body(
205 &mut self,
206 entity: Entity,
207 rb: RigidBody,
208 t: Transform,
209 v: Velocity,
210 c: Collider,
211 ) {
212 let idx = self.entities.len();
213
214 let mut aabb = c.compute_aabb(t.position, t.rotation);
215 if rb.ccd_enabled {
216 let movement = v.linear * (1.0 / 60.0); let min_mov = aabb
218 .min
219 .min((gizmo_math::Vec3::from(aabb.min) + movement).into());
220 let max_mov = aabb
221 .max
222 .max((gizmo_math::Vec3::from(aabb.max) + movement).into());
223 aabb = gizmo_math::Aabb::new(min_mov, max_mov);
224 }
225 self.spatial_hash.insert(entity, aabb);
226
227 self.entities.push(entity);
228 self.rigid_bodies.push(rb);
229 self.transforms.push(t);
230 self.velocities.push(v);
231 self.colliders.push(c);
232 self.entity_index_map.insert(entity.id(), idx);
233 }
234
235 pub fn clear_bodies(&mut self) {
236 self.entities.clear();
237 self.rigid_bodies.clear();
238 self.transforms.clear();
239 self.velocities.clear();
240 self.colliders.clear();
241 self.entity_index_map.clear();
242 self.spatial_hash.clear();
243 }
244
245 pub fn sync_bodies<'a>(
246 &mut self,
247 incoming_bodies: impl Iterator<Item = &'a (Entity, RigidBody, Transform, Velocity, Collider)>,
248 ) {
249 let mut active_ids = std::collections::HashSet::new();
250
251 for (entity, rb, trans, vel, col) in incoming_bodies {
252 let e_id = entity.id();
253 active_ids.insert(e_id);
254
255 if let Some(&idx) = self.entity_index_map.get(&e_id) {
256 self.rigid_bodies[idx] = *rb;
258 self.transforms[idx] = *trans;
259 self.velocities[idx] = *vel;
260
261 self.colliders[idx] = col.clone();
263
264 let mut aabb = col.compute_aabb(trans.position, trans.rotation);
266 if rb.ccd_enabled {
267 let movement = vel.linear * (1.0 / 60.0);
268 let min_mov = aabb
269 .min
270 .min((gizmo_math::Vec3::from(aabb.min) + movement).into());
271 let max_mov = aabb
272 .max
273 .max((gizmo_math::Vec3::from(aabb.max) + movement).into());
274 aabb = gizmo_math::Aabb::new(min_mov, max_mov);
275 }
276 self.spatial_hash.update(*entity, aabb);
277 } else {
278 self.add_body(*entity, *rb, *trans, *vel, col.clone());
280 }
281 }
282
283 let mut i = 0;
285 while i < self.entities.len() {
286 if !active_ids.contains(&self.entities[i].id()) {
287 self.remove_body_at(i);
288 } else {
289 i += 1;
290 }
291 }
292 }
293
294 pub fn remove_body_at(&mut self, idx: usize) {
295 let last_idx = self.entities.len() - 1;
296 let entity = self.entities[idx];
297
298 self.spatial_hash.remove(entity);
299 self.entity_index_map.remove(&entity.id());
300
301 if idx != last_idx {
302 let last_entity = self.entities[last_idx];
303
304 self.entities.swap(idx, last_idx);
305 self.rigid_bodies.swap(idx, last_idx);
306 self.transforms.swap(idx, last_idx);
307 self.velocities.swap(idx, last_idx);
308 self.colliders.swap(idx, last_idx);
309
310 self.entity_index_map.insert(last_entity.id(), idx);
311 }
312
313 self.entities.pop();
314 self.rigid_bodies.pop();
315 self.transforms.pop();
316 self.velocities.pop();
317 self.colliders.pop();
318 }
319
320 pub fn step(
325 &mut self,
326
327
328 dt: f32,
329 ) -> Result<(), gizmo_physics_core::GizmoError> {
330 if self.rewind_requested {
331 self.rewind_requested = false;
332 if let Some(snapshot) = self.history.pop_back() {
333 if snapshot.transforms.len() == self.transforms.len() {
334 self.transforms = snapshot.transforms;
335 self.velocities = snapshot.velocities;
336 tracing::info!("Physics rewound by 1 frame!");
337 } else {
338 tracing::warn!("Cannot rewind: Entity count changed.");
339 }
340 }
341 return Ok(());
342 }
343
344 if self.is_paused && !self.step_once {
345 self.collision_events.clear();
347 self.trigger_events.clear();
348 self.fracture_events.clear();
349 return Ok(());
350 }
351
352 let frame_dt = if self.step_once {
354 self.step_once = false;
355 self.accumulator = 0.0; FIXED_DT
357 } else {
358 dt.min(0.25) };
360
361 self.collision_events.clear();
363 self.trigger_events.clear();
364 self.fracture_events.clear();
365
366 self.accumulator += frame_dt;
368
369 let mut steps = 0u32;
370 while self.accumulator >= FIXED_DT && steps < MAX_SUBSTEPS {
371 self.step_internal(FIXED_DT)?;
372 self.accumulator -= FIXED_DT;
373 steps += 1;
374 }
375
376 self.render_alpha = self.accumulator / FIXED_DT;
378
379 self.history.push_back(PhysicsStateSnapshot {
381 transforms: self.transforms.clone(),
382 velocities: self.velocities.clone(),
383 });
384 if self.history.len() > self.max_history_frames {
385 self.history.pop_front();
386 }
387
388 Ok(())
389 }
390
391 fn step_internal(
395 &mut self,
396
397
398 dt: f32,
399 ) -> Result<(), gizmo_physics_core::GizmoError> {
400 let _initial_energy = if cfg!(debug_assertions) {
402 self.calculate_total_energy()
403 } else {
404 0.0
405 };
406
407 self.velocity_integration_step(dt)?;
409
410 self.broadphase_step(dt);
414
415 let manifolds = self.narrowphase_and_collision_step(dt);
417
418 self.constraint_solve_step(manifolds, dt);
420
421 self.position_integration_step(dt)?;
423
424 if cfg!(debug_assertions) {
426 let _final_energy = self.calculate_total_energy();
427 }
428
429 Ok(())
430 }
431
432 pub fn collision_events(&self) -> &[CollisionEvent] {
434 &self.collision_events
435 }
436
437 pub fn trigger_events(&self) -> &[TriggerEvent] {
439 &self.trigger_events
440 }
441
442 pub fn apply_impulse(
444 &self,
445 rb: &RigidBody,
446 transform: &Transform,
447 vel: &mut Velocity,
448 impulse: gizmo_math::Vec3,
449 point: gizmo_math::Vec3,
450 ) {
451 Integrator::apply_impulse_at_point(rb, transform, vel, impulse, point);
452 }
453
454 pub fn apply_force(
456 &self,
457 rb: &RigidBody,
458 vel: &mut Velocity,
459 force: gizmo_math::Vec3,
460 dt: f32,
461 ) {
462 Integrator::apply_force(rb, vel, force, dt);
463 }
464
465 pub fn raycast(&self, ray: &Ray, max_distance: f32) -> Option<RaycastHit> {
467 let mut closest_hit: Option<RaycastHit> = None;
468 let mut closest_distance = max_distance;
469
470 let potential_hits = self
471 .spatial_hash
472 .query_ray(ray.origin, ray.direction, max_distance);
473
474 for (entity, _aabb_t) in potential_hits {
475 if let Some(&i) = self.entity_index_map.get(&entity.id()) {
476 let transform = &self.transforms[i];
477 let collider = &self.colliders[i];
478
479 if let Some((distance, normal)) =
481 Raycast::ray_shape(ray, &collider.shape, transform)
482 {
483 if distance < closest_distance {
484 closest_distance = distance;
485 closest_hit = Some(RaycastHit {
486 entity,
487 point: ray.point_at(distance),
488 normal,
489 distance,
490 });
491 }
492 }
493 }
494 }
495
496 closest_hit
497 }
498
499 pub fn raycast_all(&self, ray: &Ray, max_distance: f32) -> Vec<RaycastHit> {
501 let mut hits = Vec::new();
502
503 let potential_hits = self
504 .spatial_hash
505 .query_ray(ray.origin, ray.direction, max_distance);
506
507 for (entity, _aabb_t) in potential_hits {
508 if let Some(&i) = self.entity_index_map.get(&entity.id()) {
509 let transform = &self.transforms[i];
510 let collider = &self.colliders[i];
511
512 if let Some((distance, normal)) =
514 Raycast::ray_shape(ray, &collider.shape, transform)
515 {
516 hits.push(RaycastHit {
517 entity,
518 point: ray.point_at(distance),
519 normal,
520 distance,
521 });
522 }
523 }
524 }
525
526 hits.sort_by(|a, b| a.distance.total_cmp(&b.distance));
528 hits
529 }
530
531 pub fn trigger_snapshot(&self, reason: &str) {
533 tracing::error!("Creating physics snapshot due to: {}", reason);
534 let timestamp = std::time::SystemTime::now()
535 .duration_since(std::time::UNIX_EPOCH)
536 .unwrap()
537 .as_secs();
538 let filename = format!("physics_snapshot_{}.json", timestamp);
539
540 match std::fs::File::create(&filename) {
541 Ok(file) => {
542 if let Err(e) = serde_json::to_writer_pretty(file, self) {
543 tracing::error!("Failed to serialize physics snapshot: {:?}", e);
544 } else {
545 tracing::info!("Physics snapshot successfully saved to {}", filename);
546 }
547 }
548 Err(e) => {
549 tracing::error!("Failed to create snapshot file {}: {:?}", filename, e);
550 }
551 }
552 }
553
554 pub fn calculate_total_energy(&self) -> f32 {
556 let default_gravity = self.integrator.gravity;
557 let mut total_energy = 0.0;
558
559 for i in 0..self.entities.len() {
560 let rb = &self.rigid_bodies[i];
561 let vel = &self.velocities[i];
562 let trans = &self.transforms[i];
563
564 if rb.is_dynamic() && !rb.is_sleeping {
565 let ke_linear = 0.5 * rb.mass * vel.linear.length_squared();
567
568 let ke_angular = 0.5
571 * (rb.local_inertia.x * vel.angular.x * vel.angular.x
572 + rb.local_inertia.y * vel.angular.y * vel.angular.y
573 + rb.local_inertia.z * vel.angular.z * vel.angular.z);
574
575 let pe = if rb.use_gravity {
577 -rb.mass * default_gravity.dot(trans.position)
578 } else {
579 0.0
580 };
581
582 total_energy += ke_linear + ke_angular + pe;
583 }
584 }
585 total_energy
586 }
587}
588
589#[cfg(test)]
590mod tests {
591 use super::*;
592 use gizmo_core::entity::Entity;
593 use gizmo_math::Vec3;
594
595 #[test]
596 fn test_physics_world_creation() {
597 let world = PhysicsWorld::new();
598 assert_eq!(world.integrator.gravity, Vec3::new(0.0, -9.81, 0.0));
599 }
600
601 #[test]
602 fn test_physics_step() {
603 let mut world = PhysicsWorld::new();
604
605 let entity = Entity::new(1, 0);
606 let rb = RigidBody::default();
607 let transform = Transform::new(Vec3::new(0.0, 10.0, 0.0));
608 let vel = Velocity::default();
609 let collider = Collider::sphere(1.0);
610
611 world.add_body(entity, rb, transform, vel, collider);
612
613 for _ in 0..60 {
615 let _ = world.step(1.0 / 60.0);
616 }
617
618 assert!(world.transforms[0].position.y < 10.0);
620 }
621
622 #[test]
623 fn test_high_stack_stability() {
624 let mut world = PhysicsWorld::new();
625 world.solver.iterations = 30;
627
628 let mut ground_rb = RigidBody::default();
630 ground_rb.body_type = crate::components::rigid_body::BodyType::Static;
631 ground_rb.wake_up();
632 world.add_body(
633 Entity::new(0, 0),
634 ground_rb,
635 Transform::new(Vec3::new(0.0, -0.5, 0.0)),
636 Velocity::default(),
637 Collider::box_collider(Vec3::new(50.0, 0.5, 50.0)),
638 );
639
640 let box_count = 10;
642 let box_size = 1.0;
643 let half_size = box_size / 2.0;
644
645 for i in 1..=box_count {
646 let mut rb = RigidBody::new(1.0, 0.5, 0.5, true);
647 rb.wake_up(); let y_pos = half_size + (i - 1) as f32 * box_size;
650
651 world.add_body(
652 Entity::new(i, 0),
653 rb,
654 Transform::new(Vec3::new(0.0, y_pos, 0.0)),
655 Velocity::default(),
656 Collider::box_collider(Vec3::new(half_size, half_size, half_size)),
657 );
658 }
659
660 for i in 0..600 {
662 let _ = world.step(1.0 / 60.0);
663 if i % 60 == 0 {
664 println!("Frame {}: Y={}, X={}, Z={}", i, world.transforms[10].position.y, world.transforms[10].position.x, world.transforms[10].position.z);
665 }
666 }
667
668 let top_box_idx = box_count as usize; let top_box_pos = world.transforms[top_box_idx].position;
672
673 assert!(
675 top_box_pos.x.abs() < 0.1,
676 "Top box slid too much on X axis: {}",
677 top_box_pos.x
678 );
679 assert!(
680 top_box_pos.z.abs() < 0.1,
681 "Top box slid too much on Z axis: {}",
682 top_box_pos.z
683 );
684
685 let expected_y = half_size + (box_count - 1) as f32 * box_size;
687 let y_error = (top_box_pos.y - expected_y).abs();
688 assert!(
689 y_error < 0.1,
690 "Top box sunk or bounced too much. Expected Y: {}, Actual Y: {}",
691 expected_y,
692 top_box_pos.y
693 );
694 }
695
696 #[test]
697 fn test_ccd_tunneling_prevention() {
698 let mut world = PhysicsWorld::new();
699 world.integrator.gravity = Vec3::ZERO;
701
702 let mut wall_rb = RigidBody::new_static();
704 wall_rb.wake_up();
705 world.add_body(
706 Entity::new(0, 0),
707 wall_rb,
708 Transform::new(Vec3::new(0.0, 0.0, 0.0)),
709 Velocity::default(),
710 Collider::box_collider(Vec3::new(0.1, 5.0, 5.0)),
711 );
712
713 let mut bullet_rb = RigidBody::new(1.0, 0.0, 0.0, false);
715 bullet_rb.ccd_enabled = true;
716 bullet_rb.wake_up();
717
718 world.add_body(
721 Entity::new(1, 0),
722 bullet_rb,
723 Transform::new(Vec3::new(-5.0, 0.0, 0.0)),
724 Velocity::new(Vec3::new(1200.0, 0.0, 0.0)),
725 Collider::sphere(0.2),
726 );
727
728 let _ = world.step(1.0 / 60.0);
730
731 let bullet_pos = world.transforms[1].position;
732 let bullet_vel = world.velocities[1].linear;
733
734 assert!(
738 bullet_pos.x <= 0.0,
739 "TUNNELING FAILED! Bullet phased through the wall. Position: {}",
740 bullet_pos.x
741 );
742 assert!(
744 bullet_vel.x <= 0.01,
745 "Bullet did not lose velocity after hitting the wall. Vel: {}",
746 bullet_vel.x
747 );
748 }
749
750 #[test]
751 fn test_coulomb_friction_and_sleeping() {
752 let mut world = PhysicsWorld::new();
753 world.integrator.gravity = Vec3::new(0.0, -10.0, 0.0);
755
756 let mut ground_rb = RigidBody::new_static();
758 ground_rb.friction = 0.5;
759 ground_rb.wake_up();
760 world.add_body(
761 Entity::new(0, 0),
762 ground_rb,
763 Transform::new(Vec3::new(0.0, -0.5, 0.0)),
764 Velocity::default(),
765 Collider::box_collider(Vec3::new(100.0, 0.5, 100.0)),
766 );
767
768 let mut box_a = RigidBody::new(1.0, 0.0, 0.1, true);
770 box_a.wake_up();
771 world.add_body(
772 Entity::new(1, 0),
773 box_a,
774 Transform::new(Vec3::new(0.0, 0.5, -2.0)),
775 Velocity::new(Vec3::new(10.0, 0.0, 0.0)),
776 Collider::box_collider(Vec3::splat(0.5)),
777 );
778
779 let mut box_b = RigidBody::new(1.0, 0.0, 0.8, true);
781 box_b.wake_up();
782 world.add_body(
783 Entity::new(2, 0),
784 box_b,
785 Transform::new(Vec3::new(0.0, 0.5, 2.0)),
786 Velocity::new(Vec3::new(10.0, 0.0, 0.0)),
787 Collider::box_collider(Vec3::splat(0.5)),
788 );
789
790 for _ in 0..300 {
792 let _ = world.step(1.0 / 60.0);
793 }
794
795 let pos_a = world.transforms[1].position;
796 let pos_b = world.transforms[2].position;
797 let sleep_a = world.rigid_bodies[1].is_sleeping;
798 let sleep_b = world.rigid_bodies[2].is_sleeping;
799
800 assert!(
802 pos_b.x < pos_a.x,
803 "High friction box should travel less. Pos A: {}, Pos B: {}",
804 pos_a.x,
805 pos_b.x
806 );
807
808 assert!(
810 sleep_a,
811 "Low friction box did not enter sleeping mode!"
812 );
813 assert!(
814 sleep_b,
815 "High friction box did not enter sleeping mode!"
816 );
817 }
818
819 #[test]
820 fn test_car_simulation() {
821 use crate::joints::data::{Joint, JointData, HingeJointData};
822
823 let mut world = PhysicsWorld::new();
824 world.integrator.gravity = Vec3::new(0.0, -10.0, 0.0);
826
827 let mut ground_rb = RigidBody::new_static();
829 ground_rb.friction = 0.8;
830 ground_rb.wake_up();
831 world.add_body(
832 Entity::new(0, 0),
833 ground_rb,
834 Transform::new(Vec3::new(0.0, -0.5, 0.0)),
835 Velocity::default(),
836 Collider::box_collider(Vec3::new(100.0, 0.5, 100.0)),
837 );
838
839 let mut chassis_rb = RigidBody::new(1000.0, 0.1, 0.5, true);
842 chassis_rb.wake_up();
843 let chassis_entity = Entity::new(1, 0);
844 let chassis_pos = Vec3::new(0.0, 1.5, 0.0);
845 world.add_body(
846 chassis_entity,
847 chassis_rb,
848 Transform::new(chassis_pos),
849 Velocity::default(),
850 Collider::box_collider(Vec3::new(1.0, 0.5, 2.0)), );
852
853 let mut wheel_rb = RigidBody::new(50.0, 0.1, 0.9, true); wheel_rb.wake_up();
856
857 let wheel_radius = 0.5;
858 let wheel_offsets = vec![
859 Vec3::new(-1.2, -0.2, 1.5), Vec3::new(1.2, -0.2, 1.5), Vec3::new(-1.2, -0.2, -1.5), Vec3::new(1.2, -0.2, -1.5), ];
864
865 let mut wheel_entities = Vec::new();
866
867 for (i, offset) in wheel_offsets.iter().enumerate() {
868 let wheel_entity = Entity::new(2 + i as u32, 0);
869 wheel_entities.push(wheel_entity);
870
871 world.add_body(
872 wheel_entity,
873 wheel_rb.clone(),
874 Transform::new(chassis_pos + *offset),
875 Velocity::default(),
876 Collider::sphere(wheel_radius),
877 );
878
879 let is_rear = i >= 2;
881 let hinge_data = HingeJointData {
882 axis: Vec3::X, use_limits: false,
884 lower_limit: 0.0,
885 upper_limit: 0.0,
886 use_motor: is_rear, motor_target_velocity: if is_rear { 10.0 } else { 0.0 }, motor_max_force: if is_rear { 10000.0 } else { 0.0 }, current_angle: 0.0,
890 };
891
892 let joint = Joint {
893 entity_a: chassis_entity,
894 entity_b: wheel_entity,
895 local_anchor_a: *offset, local_anchor_b: Vec3::ZERO, break_force: f32::MAX, break_torque: f32::MAX,
899 is_broken: false,
900 collision_enabled: false, data: JointData::Hinge(hinge_data),
902 };
903
904 world.joints.push(joint);
905 }
906
907 for _ in 0..300 {
910 let _ = world.step(1.0 / 60.0);
911 }
912
913 let final_chassis_pos = world.transforms[1].position;
915
916 assert!(
918 final_chassis_pos.z > 3.0,
919 "Araba yeterince ileri gidemedi! Motor veya sürtünme çalışmıyor. Z pozisyonu: {}",
920 final_chassis_pos.z
921 );
922
923 assert!(
926 final_chassis_pos.y > 0.5 && final_chassis_pos.y < 2.0,
927 "Araba devrildi, uçtu veya yere battı! Y pozisyonu: {}",
928 final_chassis_pos.y
929 );
930
931 assert!(
933 final_chassis_pos.x.abs() < 1.0,
934 "Araba düz gidemedi, sağa sola savruldu! X pozisyonu: {}",
935 final_chassis_pos.x
936 );
937 }
938}