Something went wrong. Try again.
Homebrew 4D Game Engine In Rust, inspired by Engine4D
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620//! Integration tests for physics pipeline//!//! These tests verify the full scene-physics-render pipeline works correctly://! 1. Scene loading creates correct physics bodies//! 2. Physics simulation applies gravity and collision//! 3. Entity transforms sync from physics bodies//! 4. Dirty flags trigger geometry rebuild
use rust4d_core::{ ActiveScene, DirtyFlags, EntityTemplate, Material, Name, PhysicsBody, Scene, ShapeRef, ShapeTemplate, Transform4D, World,};use rust4d_math::{Tesseract4D, Vec4};use rust4d_physics::{ BodyType, PhysicsConfig, PhysicsMaterial, PhysicsWorld, RigidBody4D, StaticCollider,};
// ==================== Scene Loading Tests ====================
/// Test that dynamic entities get physics bodies created#[test]fn test_scene_dynamic_entity_has_physics_body() { // Create a scene with a dynamic entity let mut scene = Scene::new("Test Scene").with_gravity(-20.0);
scene.add_entity( EntityTemplate::new( ShapeTemplate::tesseract(2.0), Transform4D::from_position(Vec4::new(0.0, 0.0, 0.0, 0.0)), Material::WHITE, ) .with_name("tesseract") .with_tag("dynamic"), );
// Instantiate the scene let active = ActiveScene::from_template(&scene, None);
// Get the entity let entity_handle = active .world .get_by_name("tesseract") .expect("Tesseract entity should exist");
// Verify physics body was created let body_comp = active.world.ecs().get::<&PhysicsBody>(entity_handle); assert!( body_comp.is_ok(), "Dynamic entity should have a physics body" );
// Verify the body exists in the physics world let physics = active.world.physics().expect("World should have physics"); let body_key = body_comp.unwrap().0; let body = physics .get_body(body_key) .expect("Physics body should exist");
// Verify body type is Dynamic assert!(!body.is_static(), "Body should not be static"); assert!( body.affected_by_gravity(), "Dynamic body should be affected by gravity" );}
/// Test that static floors get colliders created#[test]fn test_scene_static_floor_has_collider() { let mut scene = Scene::new("Test Scene").with_gravity(-20.0);
scene.add_entity( EntityTemplate::new( ShapeTemplate::hyperplane(-2.0, 10.0, 10, 5.0, 0.001), Transform4D::from_position(Vec4::new(0.0, -2.0, 0.0, 0.0)), Material::GRAY, ) .with_name("floor") .with_tag("static"), );
let active = ActiveScene::from_template(&scene, None);
// Verify static collider was created let physics = active.world.physics().expect("World should have physics"); assert!( !physics.static_colliders().is_empty(), "Static floor should create a collider" );}
// ==================== Physics Simulation Tests ====================
/// Test that a dynamic body falls under gravity#[test]fn test_dynamic_body_falls_under_gravity() { let mut physics = PhysicsWorld::with_config(PhysicsConfig::new(-20.0));
// Add a body at y=10 let body = RigidBody4D::new_sphere(Vec4::new(0.0, 10.0, 0.0, 0.0), 0.5) .with_body_type(BodyType::Dynamic); let key = physics.add_body(body);
// Step physics once physics.step(0.1);
// Body should have fallen let body = physics.get_body(key).unwrap(); assert!( body.position.y < 10.0, "Body should fall under gravity. Position: {:?}", body.position ); assert!( body.velocity.y < 0.0, "Body should have downward velocity. Velocity: {:?}", body.velocity );}
/// Test that a dynamic body lands on a floor and becomes grounded#[test]fn test_dynamic_body_lands_on_floor() { let mut physics = PhysicsWorld::with_config(PhysicsConfig::new(-20.0));
// Add a floor at y=0 physics.add_static_collider(StaticCollider::floor(0.0, PhysicsMaterial::CONCRETE));
// Add a body slightly above the floor let body = RigidBody4D::new_sphere(Vec4::new(0.0, 1.0, 0.0, 0.0), 0.5) .with_body_type(BodyType::Dynamic); let key = physics.add_body(body);
// Step physics multiple times until it settles for _ in 0..100 { physics.step(1.0 / 60.0); }
let body = physics.get_body(key).unwrap();
// Body should be near the floor (radius 0.5, floor at 0, so center at ~0.5) assert!( body.position.y < 1.0, "Body should have fallen. Y={}", body.position.y ); assert!( body.position.y > -1.0, "Body should be above floor. Y={}", body.position.y ); assert!(body.grounded, "Body should be grounded after settling");}
/// Test bounded floor collision (the specific bug scenario)#[test]fn test_aabb_body_lands_on_bounded_floor() { let mut physics = PhysicsWorld::with_config(PhysicsConfig::new(-20.0));
// Add a bounded floor at y=-2 (matching default.ron) physics.add_static_collider(StaticCollider::floor_bounded( -2.0, // y (surface level) 10.0, // half_size_xz 5.0, // half_size_w 5.0, // thickness (minimum) PhysicsMaterial::CONCRETE, ));
// Add an AABB body at y=0 with half_extent=1 (matching tesseract in default.ron) let body = RigidBody4D::new_aabb( Vec4::new(0.0, 0.0, 0.0, 0.0), // position Vec4::new(1.0, 1.0, 1.0, 1.0), // half_extents ) .with_body_type(BodyType::Dynamic) .with_mass(10.0) .with_material(PhysicsMaterial::WOOD);
let key = physics.add_body(body);
// Record initial position let initial_y = physics.get_body(key).unwrap().position.y;
// Step physics for 2 seconds for _ in 0..120 { physics.step(1.0 / 60.0); }
let body = physics.get_body(key).unwrap();
// Body should have fallen from y=0 assert!( body.position.y < initial_y, "Body should have fallen. Initial: {}, Final: {}", initial_y, body.position.y );
// Body center should be at approximately y=-1 (bottom at y=-2, floor surface at y=-2) // With half_extent.y=1, center at y=-1 means bottom is at y=-2 (floor surface) assert!( body.position.y > -2.0, "Body should be above floor. Y={}", body.position.y ); assert!( body.position.y < 0.0, "Body should be below starting position. Y={}", body.position.y );
// Body should be grounded assert!( body.grounded, "Body should be grounded after landing. Position: {:?}, Grounded: {}", body.position, body.grounded );}
// ==================== Entity-Physics Sync Tests ====================
/// Test that entity transform syncs from physics body#[test]fn test_entity_transform_syncs_from_physics() { let mut world = World::new().with_physics(PhysicsConfig::new(-20.0));
// Add a physics body with velocity let body = RigidBody4D::new_sphere(Vec4::new(0.0, 10.0, 0.0, 0.0), 0.5) .with_body_type(BodyType::Dynamic); let body_key = world.physics_mut().unwrap().add_body(body);
// Create an entity linked to the physics body let tesseract = Tesseract4D::new(2.0); let entity_handle = world.spawn(( ShapeRef::shared(tesseract), Transform4D::identity(), Material::default(), DirtyFlags::ALL, Name::new("test"), PhysicsBody(body_key), ));
// Clear dirty flags world.clear_all_dirty();
// Step physics world.update(0.1);
// Entity should have new position let transform = world.ecs().get::<&Transform4D>(entity_handle).unwrap(); assert!( transform.position.y < 10.0, "Entity should have moved. Y={}", transform.position.y );
// Entity should be marked dirty let dirty = world.ecs().get::<&DirtyFlags>(entity_handle).unwrap(); assert!( !dirty.is_empty(), "Entity should be marked dirty after position sync" );}
// ==================== Full Pipeline Test ====================
/// The critical test: full scene loading to physics settling/// This tests the exact scenario from default.ron#[test]fn test_scene_dynamic_entity_falls_to_floor() { // Create a scene matching default.ron structure let mut scene = Scene::new("Test Scene").with_gravity(-20.0);
// Add floor scene.add_entity( EntityTemplate::new( ShapeTemplate::hyperplane(-2.0, 10.0, 10, 5.0, 0.001), Transform4D::from_position(Vec4::new(0.0, -2.0, 0.0, 0.0)), Material::GRAY, ) .with_name("floor") .with_tag("static"), );
// Add tesseract at y=0 scene.add_entity( EntityTemplate::new( ShapeTemplate::tesseract(2.0), Transform4D::from_position(Vec4::new(0.0, 0.0, 0.0, 0.0)), Material::WHITE, ) .with_name("tesseract") .with_tag("dynamic"), );
// Instantiate scene let mut active = ActiveScene::from_template(&scene, None);
// Get initial tesseract position let entity_handle = active.world.get_by_name("tesseract").unwrap(); let initial_y = active .world .ecs() .get::<&Transform4D>(entity_handle) .unwrap() .position .y;
// Simulate 2 seconds (120 frames at 60fps) for _ in 0..120 { active.update(1.0 / 60.0); }
// Get final position let entity_handle = active.world.get_by_name("tesseract").unwrap(); let final_y = active .world .ecs() .get::<&Transform4D>(entity_handle) .unwrap() .position .y;
// Tesseract should have fallen assert!( final_y < initial_y, "Tesseract should have fallen. Initial: {}, Final: {}", initial_y, final_y );
// Tesseract should be near the floor (center at ~-1, bottom at -2) assert!( final_y > -2.0, "Tesseract should be above floor surface. Y={}", final_y ); assert!( final_y < 0.0, "Tesseract should be below starting position. Y={}", final_y );
// Verify physics body is grounded let physics = active.world.physics().unwrap(); let body_comp = active .world .ecs() .get::<&PhysicsBody>(entity_handle) .unwrap(); let body = physics.get_body(body_comp.0).unwrap(); assert!(body.grounded, "Tesseract physics body should be grounded");}
/// Test with actual scene file (requires scenes/default.ron to exist)////// Run with: cargo test --test physics_integration test_load_default_scene_file -- --ignored#[test]#[ignore = "Requires scenes/default.ron to exist"]fn test_load_default_scene_file() { // Try to load the actual default.ron scene let scene = Scene::load("../../../scenes/default.ron") .expect("scenes/default.ron should exist for this test");
// Instantiate scene let mut active = ActiveScene::from_template(&scene, None);
// Verify tesseract entity exists and has physics body let entity_handle = active .world .get_by_name("tesseract") .expect("Tesseract entity should exist in default scene");
assert!( active .world .ecs() .get::<&PhysicsBody>(entity_handle) .is_ok(), "Tesseract should have physics body" );
let initial_y = active .world .ecs() .get::<&Transform4D>(entity_handle) .unwrap() .position .y;
// Simulate 2 seconds for _ in 0..120 { active.update(1.0 / 60.0); }
// Get final position let entity_handle = active.world.get_by_name("tesseract").unwrap(); let final_y = active .world .ecs() .get::<&Transform4D>(entity_handle) .unwrap() .position .y;
// Tesseract should have fallen assert!( final_y < initial_y, "Tesseract should have fallen from {} to near floor. Final: {}", initial_y, final_y );}
// ==================== Diagnostic Tests ====================
/// Test that walking off the W edge causes falling (true 4D physics)#[test]fn test_kinematic_body_falls_off_w_edge() { let mut physics = PhysicsWorld::with_config(PhysicsConfig::new(-20.0));
// Add bounded floor: W extends from -5 to +5 physics.add_static_collider(StaticCollider::floor_bounded( -2.0, 10.0, 5.0, 5.0, PhysicsMaterial::CONCRETE, ));
// Add kinematic sphere at center, resting on floor, with gravity enabled let body = RigidBody4D::new_sphere(Vec4::new(0.0, -1.5, 0.0, 0.0), 0.5) .with_body_type(BodyType::Kinematic) .with_gravity(true); let key = physics.add_body(body);
// Step physics to settle for _ in 0..10 { physics.step(1.0 / 60.0); }
// Body should be grounded assert!( physics.body_is_grounded(key), "Body should be grounded at center" ); let start_y = physics.body_position(key).unwrap().y;
// Move body to W=6 (outside floor's W bounds of -5 to +5) for _ in 0..60 { physics.apply_body_movement(key, Vec4::new(0.0, 0.0, 0.0, 10.0)); physics.step(1.0 / 60.0); }
let pos = physics.body_position(key).unwrap(); assert!( pos.w > 5.0, "Body should have moved off W edge. W={}", pos.w );
assert!( !physics.body_is_grounded(key), "Body should NOT be grounded when off W edge. W={}", pos.w );
// Continue stepping - body should fall for _ in 0..60 { physics.apply_body_movement(key, Vec4::ZERO); physics.step(1.0 / 60.0); }
let final_pos = physics.body_position(key).unwrap(); assert!( final_pos.y < start_y, "Body should fall when off W edge. Start Y={}, Final Y={}", start_y, final_pos.y );}
/// Print detailed state for debugging#[test]fn test_physics_step_trace() { let mut physics = PhysicsWorld::with_config(PhysicsConfig::new(-20.0));
// Add bounded floor at y=-2 physics.add_static_collider(StaticCollider::floor_bounded( -2.0, 10.0, 5.0, 5.0, PhysicsMaterial::CONCRETE, ));
// Add AABB body at y=0 let body = RigidBody4D::new_aabb(Vec4::new(0.0, 0.0, 0.0, 0.0), Vec4::new(1.0, 1.0, 1.0, 1.0)) .with_body_type(BodyType::Dynamic) .with_mass(10.0);
let key = physics.add_body(body);
println!("=== Physics Step Trace ==="); println!("Gravity: {}", physics.config.gravity); println!("Static colliders: {}", physics.static_colliders().len());
for frame in 0..10 { let body = physics.get_body(key).unwrap(); println!( "Frame {}: pos.y={:.4}, vel.y={:.4}, grounded={}", frame, body.position.y, body.velocity.y, body.grounded ); physics.step(1.0 / 60.0); }
let body = physics.get_body(key).unwrap(); println!( "Final: pos.y={:.4}, vel.y={:.4}, grounded={}", body.position.y, body.velocity.y, body.grounded );
// Should be falling assert!(body.position.y < 0.0, "Body should have fallen");}
// ==================== Physics Cleanup Tests ====================
/// Test that removing an entity also removes its physics body#[test]fn test_remove_entity_cleans_up_physics_body() { let mut world = World::new().with_physics(PhysicsConfig::new(-20.0));
// Add a physics body let body = RigidBody4D::new_sphere(Vec4::new(0.0, 5.0, 0.0, 0.0), 0.5) .with_body_type(BodyType::Dynamic); let body_key = world.physics_mut().unwrap().add_body(body);
// Create an entity linked to the physics body let tesseract = Tesseract4D::new(2.0); let entity_key = world.spawn(( ShapeRef::shared(tesseract), Transform4D::identity(), Material::default(), DirtyFlags::ALL, Name::new("physics_test"), PhysicsBody(body_key), ));
// Verify body exists in physics world assert!( world.physics().unwrap().get_body(body_key).is_some(), "Physics body should exist before entity removal" ); assert_eq!( world.physics().unwrap().body_count(), 1, "Should have exactly 1 physics body" );
// Remove the entity let removed = world.despawn(entity_key); assert!(removed, "Entity should be removed");
// Verify physics body was also removed assert!( world.physics().unwrap().get_body(body_key).is_none(), "Physics body should be removed when entity is removed" ); assert_eq!( world.physics().unwrap().body_count(), 0, "Should have 0 physics bodies after entity removal" );}
/// Test that removing an entity without physics body works correctly#[test]fn test_remove_entity_without_physics_body() { let mut world = World::new().with_physics(PhysicsConfig::new(-20.0));
// Create an entity WITHOUT a physics body let tesseract = Tesseract4D::new(2.0); let entity_key = world.spawn(( ShapeRef::shared(tesseract), Transform4D::identity(), Material::default(), DirtyFlags::ALL, Name::new("no_physics"), ));
// Remove the entity - should not panic let removed = world.despawn(entity_key); assert!(removed, "Entity should be removed"); // Verify entity is gone assert!(!world.contains(entity_key));}
/// Test that removing an entity in a world without physics works#[test]fn test_remove_entity_world_without_physics() { let mut world = World::new(); // No physics enabled
// Create an entity let tesseract = Tesseract4D::new(2.0); let entity_key = world.spawn(( ShapeRef::shared(tesseract), Transform4D::identity(), Material::default(), DirtyFlags::ALL, Name::new("test"), ));
// Remove should work fine let removed = world.despawn(entity_key); assert!( removed, "Entity should be removed even without physics world" );}