Something went wrong. Try again.
Homebrew 4D Game Engine In Rust, inspired by Engine4D
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652//! Scene serialization//!//! Provides Scene struct for loading/saving scenes from RON files.//! Scenes contain entity templates, physics settings, and player spawn info.
use serde::{Deserialize, Serialize};use std::fs;use std::io;use std::path::Path;
use crate::components::PhysicsBody;use crate::entity::EntityTemplate;use crate::shapes::ShapeTemplate;use crate::World;use rust4d_math::Vec4;use rust4d_physics::{BodyType, PhysicsConfig, PhysicsMaterial, RigidBody4D, StaticCollider};
/// A serializable scene containing entity templates////// Scenes are loaded from RON files and contain all the data needed/// to populate a game world: entities, physics settings, and spawn points.#[derive(Debug, Clone, Serialize, Deserialize)]pub struct Scene { /// Scene name (for display/debugging) pub name: String, /// Entity templates in this scene pub entities: Vec<EntityTemplate>, /// Gravity for physics (negative = downward) #[serde(default)] pub gravity: Option<f32>, /// Player spawn position [x, y, z, w] #[serde(default)] pub player_spawn: Option<[f32; 4]>,}
impl Scene { /// Create a new empty scene pub fn new(name: impl Into<String>) -> Self { Self { name: name.into(), entities: Vec::new(), gravity: None, player_spawn: None, } }
/// Load a scene from a RON file pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, SceneLoadError> { let contents = fs::read_to_string(path.as_ref())?; let scene: Scene = ron::from_str(&contents)?; log::debug!( "Loaded scene '{}' with gravity={:?}, player_spawn={:?}", scene.name, scene.gravity, scene.player_spawn ); Ok(scene) }
/// Save a scene to a RON file pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), SceneSaveError> { let pretty = ron::ser::PrettyConfig::new() .struct_names(true) .enumerate_arrays(false); let contents = ron::ser::to_string_pretty(self, pretty)?; fs::write(path, contents)?; Ok(()) }
/// Add an entity template to this scene pub fn add_entity(&mut self, entity: EntityTemplate) { self.entities.push(entity); }
/// Set the gravity for this scene pub fn with_gravity(mut self, gravity: f32) -> Self { self.gravity = Some(gravity); self }
/// Set the player spawn position pub fn with_player_spawn(mut self, x: f32, y: f32, z: f32, w: f32) -> Self { self.player_spawn = Some([x, y, z, w]); self }}
/// Error loading a scene#[derive(Debug)]pub enum SceneLoadError { /// IO error (file not found, permission denied, etc.) Io(io::Error), /// Parse error (invalid RON syntax) Parse(ron::error::SpannedError),}
impl From<io::Error> for SceneLoadError { fn from(e: io::Error) -> Self { SceneLoadError::Io(e) }}
impl From<ron::error::SpannedError> for SceneLoadError { fn from(e: ron::error::SpannedError) -> Self { SceneLoadError::Parse(e) }}
impl std::fmt::Display for SceneLoadError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { SceneLoadError::Io(e) => write!(f, "IO error: {}", e), SceneLoadError::Parse(e) => write!(f, "Parse error: {}", e), } }}
impl std::error::Error for SceneLoadError {}
/// Error saving a scene#[derive(Debug)]pub enum SceneSaveError { /// IO error (permission denied, disk full, etc.) Io(io::Error), /// Serialization error Serialize(ron::Error),}
impl From<io::Error> for SceneSaveError { fn from(e: io::Error) -> Self { SceneSaveError::Io(e) }}
impl From<ron::Error> for SceneSaveError { fn from(e: ron::Error) -> Self { SceneSaveError::Serialize(e) }}
impl std::fmt::Display for SceneSaveError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { SceneSaveError::Io(e) => write!(f, "IO error: {}", e), SceneSaveError::Serialize(e) => write!(f, "Serialize error: {}", e), } }}
impl std::error::Error for SceneSaveError {}
/// Unified error type for scene operations////// This is used by SceneManager for all scene-related errors, providing a single/// error type that covers loading, saving, and runtime scene management.#[derive(Debug)]pub enum SceneError { /// IO error (file not found, permission denied, etc.) Io(io::Error), /// Parse error (invalid RON syntax) Parse(ron::error::SpannedError), /// Serialization error Serialize(ron::Error), /// Scene not loaded (requested template doesn't exist) NotLoaded(String), /// No active scene on the stack NoActiveScene,}
impl From<io::Error> for SceneError { fn from(e: io::Error) -> Self { SceneError::Io(e) }}
impl From<ron::error::SpannedError> for SceneError { fn from(e: ron::error::SpannedError) -> Self { SceneError::Parse(e) }}
impl From<ron::Error> for SceneError { fn from(e: ron::Error) -> Self { SceneError::Serialize(e) }}
impl From<SceneLoadError> for SceneError { fn from(e: SceneLoadError) -> Self { match e { SceneLoadError::Io(io_err) => SceneError::Io(io_err), SceneLoadError::Parse(parse_err) => SceneError::Parse(parse_err), } }}
impl std::fmt::Display for SceneError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { SceneError::Io(e) => write!(f, "IO error: {}", e), SceneError::Parse(e) => write!(f, "Parse error: {}", e), SceneError::Serialize(e) => write!(f, "Serialize error: {}", e), SceneError::NotLoaded(name) => write!(f, "Scene not loaded: {}", name), SceneError::NoActiveScene => write!(f, "No active scene"), } }}
impl std::error::Error for SceneError {}
/// A runtime scene containing an instantiated World////// ActiveScene wraps a World instance that has been instantiated from a Scene template/// (or created programmatically). It tracks the scene name and player spawn position/// from the original template.pub struct ActiveScene { /// Scene name (from template or custom) pub name: String, /// Player spawn position (from template) pub player_spawn: Option<[f32; 4]>, /// The live world with entities and physics pub world: World, /// The player's physics body key (if spawned) pub player_body_key: Option<rust4d_physics::BodyKey>,}
impl ActiveScene { /// Create an active scene from a Scene template /// /// This instantiates all entities from the template into a new World, /// optionally enabling physics with the provided config. /// /// Player body creation is NOT handled here -- use /// `rust4d_game::scene_helpers::create_player_body()` at the application layer. pub fn from_template(template: &Scene, physics_config: Option<PhysicsConfig>) -> Self { log::debug!( "from_template: physics_config={:?}, template.gravity={:?}", physics_config, template.gravity );
// Create world with physics let mut world = if let Some(config) = physics_config { log::debug!( "Using provided physics_config with gravity={}", config.gravity ); World::new().with_physics(config) } else if let Some(gravity) = template.gravity { log::debug!("Using template gravity={}", gravity); World::new().with_physics(PhysicsConfig::new(gravity)) } else { log::debug!("No physics configured"); World::new() };
// Instantiate all entities from the template, setting up physics based on tags for entity_template in &template.entities { let is_static = entity_template.tags.contains(&"static".to_string()); let is_dynamic = entity_template.tags.contains(&"dynamic".to_string());
// Determine physics body key before spawning let mut body_key = None;
if let Some(physics) = world.physics_mut() { if is_static { // Create bounded static collider for floor/walls (objects can fall off edges) if let ShapeTemplate::Hyperplane { y, size, cell_size, thickness, .. } = &entity_template.shape { log::debug!("Adding bounded floor collider: y={}, size={}, cell_size={}, thickness={}", y, size, cell_size, thickness); physics.add_static_collider(StaticCollider::floor_bounded( *y, *size, // X/Z extent from hyperplane *cell_size, // W extent *thickness, // Y thickness PhysicsMaterial::CONCRETE, )); } } else if is_dynamic { // Create dynamic rigid body for movable objects let position = Vec4::new( entity_template.transform.position.x, entity_template.transform.position.y, entity_template.transform.position.z, entity_template.transform.position.w, );
// Choose a collider from the shape's hint: spheres for // round shapes, conservative AABBs for everything else. let body = match entity_template.shape.collider_hint() { crate::shapes::ColliderHint::Sphere { radius } => { RigidBody4D::new_sphere(position, radius) } crate::shapes::ColliderHint::Aabb { half_extent } => RigidBody4D::new_aabb( position, Vec4::new(half_extent, half_extent, half_extent, half_extent), ), } .with_body_type(BodyType::Dynamic) .with_mass(10.0) .with_material(PhysicsMaterial::WOOD);
body_key = Some(physics.add_body(body)); } }
let entity_handle = entity_template.spawn_in(&mut world); if let Some(bk) = body_key { let _ = world .ecs_mut_unchecked() .insert_one(entity_handle, PhysicsBody(bk)); } }
// Player body creation is handled at the application layer using // scene_helpers::create_player_body(). This avoids duplicating the player // body setup between rust4d_core and rust4d_game.
Self { name: template.name.clone(), player_spawn: template.player_spawn, world, player_body_key: None, } }
/// Create a new empty active scene with the given name pub fn new(name: impl Into<String>) -> Self { Self { name: name.into(), player_spawn: None, world: World::new(), player_body_key: None, } }
/// Create with physics enabled pub fn with_physics(mut self, config: PhysicsConfig) -> Self { self.world = self.world.with_physics(config); self }
/// Set the player spawn position pub fn with_player_spawn(mut self, spawn: [f32; 4]) -> Self { self.player_spawn = Some(spawn); self }
/// Update the scene (steps physics) pub fn update(&mut self, dt: f32) { self.world.update(dt); }}
#[cfg(test)]mod tests { use super::*; use crate::shapes::ShapeTemplate; use crate::{Material, Transform4D}; use rust4d_math::Vec4;
#[test] fn test_scene_new() { let scene = Scene::new("Test Scene"); assert_eq!(scene.name, "Test Scene"); assert!(scene.entities.is_empty()); assert!(scene.gravity.is_none()); assert!(scene.player_spawn.is_none()); }
#[test] fn test_scene_with_gravity() { let scene = Scene::new("Test").with_gravity(-20.0); assert_eq!(scene.gravity, Some(-20.0)); }
#[test] fn test_scene_with_player_spawn() { let scene = Scene::new("Test").with_player_spawn(1.0, 2.0, 3.0, 4.0); assert_eq!(scene.player_spawn, Some([1.0, 2.0, 3.0, 4.0])); }
#[test] fn test_scene_add_entity() { let mut scene = Scene::new("Test"); let entity = EntityTemplate::new( ShapeTemplate::tesseract(2.0), Transform4D::identity(), Material::WHITE, ); scene.add_entity(entity); assert_eq!(scene.entities.len(), 1); }
#[test] fn test_scene_serialization() { let mut scene = Scene::new("Test Scene") .with_gravity(-20.0) .with_player_spawn(0.0, 2.0, 5.0, 0.0);
let entity = EntityTemplate::new( ShapeTemplate::tesseract(2.0), Transform4D::from_position(Vec4::new(1.0, 0.0, 0.0, 0.0)), Material::RED, ) .with_name("test_cube") .with_tag("dynamic");
scene.add_entity(entity);
// Serialize to RON let pretty = ron::ser::PrettyConfig::new().struct_names(true); let serialized = ron::ser::to_string_pretty(&scene, pretty).unwrap();
// Verify it contains expected content assert!(serialized.contains("Test Scene")); assert!(serialized.contains("test_cube")); assert!(serialized.contains("Tesseract"));
// Deserialize back let deserialized: Scene = ron::from_str(&serialized).unwrap(); assert_eq!(deserialized.name, "Test Scene"); assert_eq!(deserialized.gravity, Some(-20.0)); assert_eq!(deserialized.entities.len(), 1); assert_eq!(deserialized.entities[0].name, Some("test_cube".to_string())); }
#[test] fn test_parse_scene_file_format() { // Test parsing a scene matching the actual serialization format let scene_ron = r#"Scene( name: "Test Scene", entities: [ EntityTemplate( name: Some("floor"), tags: ["static"], transform: Transform4D( position: Vec4(x: 0.0, y: -2.0, z: 0.0, w: 0.0), rotation: (s: 1.0, b_xy: 0.0, b_xz: 0.0, b_xw: 0.0, b_yz: 0.0, b_yw: 0.0, b_zw: 0.0, p: 0.0), scale: 1.0, ), shape: ShapeTemplate( type: "Hyperplane", y: -2.0, size: 10.0, subdivisions: 10, cell_size: 5.0, thickness: 0.001, ), material: Material(base_color: (0.5, 0.5, 0.5, 1.0)), ), EntityTemplate( name: Some("tesseract"), tags: ["dynamic"], transform: Transform4D( position: Vec4(x: 0.0, y: 0.0, z: 0.0, w: 0.0), rotation: (s: 1.0, b_xy: 0.0, b_xz: 0.0, b_xw: 0.0, b_yz: 0.0, b_yw: 0.0, b_zw: 0.0, p: 0.0), scale: 1.0, ), shape: ShapeTemplate( type: "Tesseract", size: 2.0, ), material: Material(base_color: (1.0, 1.0, 1.0, 1.0)), ), ], gravity: Some(-20.0), player_spawn: Some((0.0, 2.0, 5.0, 0.0)),)"#; let scene: Scene = ron::from_str(scene_ron).unwrap(); assert_eq!(scene.name, "Test Scene"); assert_eq!(scene.gravity, Some(-20.0)); assert_eq!(scene.player_spawn, Some([0.0, 2.0, 5.0, 0.0])); assert_eq!(scene.entities.len(), 2);
// Check floor entity assert_eq!(scene.entities[0].name, Some("floor".to_string())); assert_eq!(scene.entities[0].tags, vec!["static"]); match &scene.entities[0].shape { ShapeTemplate::Hyperplane { y, size, subdivisions, cell_size, thickness, } => { assert_eq!(*y, -2.0); assert_eq!(*size, 10.0); assert_eq!(*subdivisions, 10); assert_eq!(*cell_size, 5.0); assert_eq!(*thickness, 0.001); } _ => panic!("Expected Hyperplane shape"), }
// Check tesseract entity assert_eq!(scene.entities[1].name, Some("tesseract".to_string())); assert_eq!(scene.entities[1].tags, vec!["dynamic"]); match &scene.entities[1].shape { ShapeTemplate::Tesseract { size } => { assert_eq!(*size, 2.0); } _ => panic!("Expected Tesseract shape"), } }
#[test] fn test_entity_template_spawn_in() { let template = EntityTemplate::new( ShapeTemplate::tesseract(2.0), Transform4D::from_position(Vec4::new(1.0, 2.0, 3.0, 4.0)), Material::RED, ) .with_name("my_cube") .with_tag("dynamic");
let mut world = crate::World::new(); let entity = template.spawn_in(&mut world);
let name = world.ecs().get::<&crate::Name>(entity).unwrap(); assert_eq!(name.0, "my_cube");
let tags = world.ecs().get::<&crate::Tags>(entity).unwrap(); assert!(tags.has("dynamic"));
let transform = world.ecs().get::<&Transform4D>(entity).unwrap(); assert_eq!(transform.position.x, 1.0);
let material = world.ecs().get::<&Material>(entity).unwrap(); assert_eq!(material.base_color, [1.0, 0.0, 0.0, 1.0]);
let shape = world.ecs().get::<&crate::ShapeRef>(entity).unwrap(); assert_eq!(shape.as_shape().vertex_count(), 16); // Tesseract has 16 vertices }
// --- SceneError tests ---
#[test] fn test_scene_error_display() { let err = SceneError::NotLoaded("test_scene".to_string()); assert_eq!(format!("{}", err), "Scene not loaded: test_scene");
let err = SceneError::NoActiveScene; assert_eq!(format!("{}", err), "No active scene"); }
#[test] fn test_scene_error_from_io() { let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found"); let scene_err: SceneError = io_err.into(); match scene_err { SceneError::Io(_) => {} _ => panic!("Expected Io variant"), } }
#[test] fn test_scene_error_from_scene_load_error() { let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found"); let load_err = SceneLoadError::Io(io_err); let scene_err: SceneError = load_err.into(); match scene_err { SceneError::Io(_) => {} _ => panic!("Expected Io variant"), } }
// --- ActiveScene tests ---
#[test] fn test_active_scene_new() { let scene = ActiveScene::new("Test Scene"); assert_eq!(scene.name, "Test Scene"); assert!(scene.player_spawn.is_none()); assert!(scene.world.is_empty()); }
#[test] fn test_active_scene_with_physics() { let scene = ActiveScene::new("Physics Scene").with_physics(PhysicsConfig::new(-20.0)); assert!(scene.world.physics().is_some()); assert_eq!(scene.world.physics().unwrap().config.gravity, -20.0); }
#[test] fn test_active_scene_with_player_spawn() { let scene = ActiveScene::new("Spawn Scene").with_player_spawn([1.0, 2.0, 3.0, 4.0]); assert_eq!(scene.player_spawn, Some([1.0, 2.0, 3.0, 4.0])); }
#[test] fn test_active_scene_from_template() { // Create a scene template with entities let mut template = Scene::new("Template Scene") .with_gravity(-15.0) .with_player_spawn(0.0, 1.0, 5.0, 0.0);
template.add_entity( EntityTemplate::new( ShapeTemplate::tesseract(2.0), Transform4D::from_position(Vec4::new(1.0, 0.0, 0.0, 0.0)), Material::RED, ) .with_name("cube"), );
// Instantiate from template let active = ActiveScene::from_template(&template, None);
assert_eq!(active.name, "Template Scene"); assert_eq!(active.player_spawn, Some([0.0, 1.0, 5.0, 0.0])); assert_eq!(active.world.entity_count(), 1);
// Check physics was set from template gravity assert!(active.world.physics().is_some()); assert_eq!(active.world.physics().unwrap().config.gravity, -15.0);
// Check entity was instantiated let entity_handle = active.world.get_by_name("cube").unwrap(); let material = active.world.ecs().get::<&Material>(entity_handle).unwrap(); assert_eq!(material.base_color, [1.0, 0.0, 0.0, 1.0]); }
#[test] fn test_active_scene_from_template_override_physics() { let template = Scene::new("Template").with_gravity(-10.0);
// Override physics config let active = ActiveScene::from_template(&template, Some(PhysicsConfig::new(-30.0)));
// Should use overridden config, not template gravity assert_eq!(active.world.physics().unwrap().config.gravity, -30.0); }
#[test] fn test_active_scene_update() { let mut scene = ActiveScene::new("Update Test").with_physics(PhysicsConfig::new(-20.0));
// Just verify update doesn't panic scene.update(0.016); }}