diff --git a/engine/pill_engine/Cargo.toml b/engine/pill_engine/Cargo.toml index 1356fde9..91cc7480 100644 --- a/engine/pill_engine/Cargo.toml +++ b/engine/pill_engine/Cargo.toml @@ -24,12 +24,14 @@ lazy_static = "1.4.0" dyn-clone = "1.0.4" egui = { version = "0.32.1", optional = true } serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" bincode = "1.0" once_cell = "1.21.3" bitvec = "1.0.1" rand = "0.9.2" web-time = "1.1" rapier3d = { version = "0.32.0", optional = true } +uuid = { version = "1.23.4", features = ["v4"] } # Native-only dependencies (not available on WASM) [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/engine/pill_engine/src/ecs/components/mod.rs b/engine/pill_engine/src/ecs/components/mod.rs index 94206c23..14107d84 100644 --- a/engine/pill_engine/src/ecs/components/mod.rs +++ b/engine/pill_engine/src/ecs/components/mod.rs @@ -28,6 +28,7 @@ pub(crate) mod physics_world_component; pub(crate) mod rigid_body_component; pub(crate) mod time_component; pub(crate) mod transform_component; +pub(crate) mod uuid_component; // --- Use --- diff --git a/engine/pill_engine/src/ecs/components/uuid_component.rs b/engine/pill_engine/src/ecs/components/uuid_component.rs new file mode 100644 index 00000000..f0d90f08 --- /dev/null +++ b/engine/pill_engine/src/ecs/components/uuid_component.rs @@ -0,0 +1,30 @@ +use pill_core::PillTypeMapKey; +use uuid::Uuid; + +use crate::{Component, ComponentStorage}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct UuidComponent { + pub uuid: u128, +} + +impl Default for UuidComponent { + fn default() -> Self { + Self::new() + } +} + +impl UuidComponent { + pub fn new() -> Self { + Self { + uuid: Uuid::new_v4().as_u128(), + } + } +} + +impl PillTypeMapKey for UuidComponent { + type Storage = ComponentStorage; +} + +impl Component for UuidComponent {} diff --git a/engine/pill_engine/src/ecs/mod.rs b/engine/pill_engine/src/ecs/mod.rs index e1b62f07..db03de38 100644 --- a/engine/pill_engine/src/ecs/mod.rs +++ b/engine/pill_engine/src/ecs/mod.rs @@ -74,6 +74,8 @@ pub use components::network_manager_component::{ #[cfg(not(target_arch = "wasm32"))] pub use components::network_state_component::{NetworkEntityState, NetworkStateComponent}; +pub use components::uuid_component::UuidComponent; + // - Systems pub use systems::{SystemFunction, SystemManager, UpdatePhase}; diff --git a/engine/pill_engine/src/engine.rs b/engine/pill_engine/src/engine.rs index 5b5abd19..c495c58d 100644 --- a/engine/pill_engine/src/engine.rs +++ b/engine/pill_engine/src/engine.rs @@ -1,4 +1,5 @@ use crate::app_config::EngineProcessInfo; +use crate::serdeserialization::{JsonBackend, Serdeser, SerdeserBackend}; use crate::{app_config::EngineConfig, config::*, ecs::*, graphics::*, resources::*}; use pill_core::{ @@ -7,7 +8,7 @@ use pill_core::{ }; use pill_core::{ErrorContext, Result}; -use std::{any::TypeId, collections::VecDeque}; +use std::{any::TypeId, collections::VecDeque, path::Path}; use winit::{dpi::PhysicalPosition, event::KeyEvent}; // ------------------------------------------------------------------------------- @@ -38,6 +39,8 @@ pub struct Engine { pub(crate) window_size: winit::dpi::PhysicalSize, pub(crate) game_resources_directory_path: std::path::PathBuf, pub(crate) frame_delta_time: f32, // In milliseconds + pub(crate) serdeser: Serdeser, + pub(crate) serdeser_backend: Box, } // ---- INTERNAL ----------------------------------------------------------------- @@ -71,6 +74,8 @@ impl Engine { window_size: winit::dpi::PhysicalSize::::default(), game_resources_directory_path, frame_delta_time: 0.0, + serdeser: Serdeser::new(), + serdeser_backend: Box::new(JsonBackend), } } @@ -94,7 +99,9 @@ impl Engine { render_queue: Vec::::with_capacity(max_entity_count), window_size: winit::dpi::PhysicalSize::::default(), game_resources_directory_path: std::path::PathBuf::new(), - frame_delta_time: 0.0.into(), + frame_delta_time: 0.0, + serdeser: Serdeser::new(), + serdeser_backend: Box::new(JsonBackend), } } @@ -391,6 +398,11 @@ impl Engine { self.create_default_resources() .context("Failed to create default resources")?; + // TODO: register resources in serializer? + // register desired components in serializer's registry + self.serdeser.register_component::()?; + self.serdeser.register_component::()?; + // Start game self.start_game()?; @@ -569,6 +581,35 @@ impl Engine { pub fn get_input_queue(&self) -> &VecDeque { &self.input_queue } + + // TODO: temporary functions to trigger scene loading/saving + pub fn save_scene(&self) -> Result<()> { + let entities = self.iterate_two_components::()?; + // TODO: for now not dancing around this - later we get into trouble as we pass entities + // here that contain mut ref to the engine + let mut backend = JsonBackend; + let path = Path::new("test.scene"); + self.serdeser.serialize(entities, &mut backend, path)?; + println!("SCENE SAVED!"); + + Ok(()) + } + + pub fn load_scene(&mut self, scene_handle: SceneHandle) -> Result<()> { + // TODO: not loading the scene yet, just reading out what we have parsed from the file + // TODO: for now not dancing around this - later we get into trouble as we pass entities + // here that contain mut ref to the engine + let mut backend = JsonBackend; + let path = Path::new("test.scene"); + let decoded = self.serdeser.deserialize(&mut backend, path)?; + println!("Num of decoded entities: {}", decoded.len()); + for entity in decoded { + println!("Entity with uuid: {}", entity.uuid); + // TODO: spawn necessary objects + } + println!("SCENE LOADED !"); + Ok(()) + } } // --- API ------------------------------------------------------------------ @@ -679,12 +720,21 @@ impl Engine { .name_style() ); - self.scene_manager + let entity_handle = self + .scene_manager .create_entity(scene_handle) .context(format!( "Creating {} failed", "Entity".general_object_style() - )) + )); + + if let Ok(handle) = entity_handle { + // add UUID to all created entities + self.add_component_to_entity(scene_handle, handle, UuidComponent::new()) + .unwrap(); + } + + entity_handle } // Removes entity specified with entity handle from scene specified with scene handle @@ -1020,10 +1070,17 @@ impl Engine { // Creates scene pub fn create_scene(&mut self, name: &str) -> Result { info!(LogContext::ECS => "Creating scene: {}", name); - self.scene_manager.create_scene(name).context(format!( + let scene_handle = self.scene_manager.create_scene(name).context(format!( "Creating new {} failed", "Scene".general_object_style() - )) + )); + + // always register the uuid component with every scene + if let Ok(scene) = scene_handle { + self.register_component::(scene)?; + }; + + scene_handle } /// Returns handle to the scene specified by its name diff --git a/engine/pill_engine/src/lib.rs b/engine/pill_engine/src/lib.rs index a3b7b5af..06a8503b 100644 --- a/engine/pill_engine/src/lib.rs +++ b/engine/pill_engine/src/lib.rs @@ -8,6 +8,7 @@ mod ecs; mod engine; mod graphics; mod resources; +mod serdeserialization; // --- Macros --- diff --git a/engine/pill_engine/src/serdeserialization/mod.rs b/engine/pill_engine/src/serdeserialization/mod.rs new file mode 100644 index 00000000..0fe99df0 --- /dev/null +++ b/engine/pill_engine/src/serdeserialization/mod.rs @@ -0,0 +1,7 @@ +#![cfg_attr(debug_assertions, allow(dead_code, unused_imports, unused_variables))] + +mod serdeser; +mod serdeser_backend; + +pub use serdeser::Serdeser; +pub use serdeser_backend::{JsonBackend, SerdeserBackend}; diff --git a/engine/pill_engine/src/serdeserialization/serdeser.rs b/engine/pill_engine/src/serdeserialization/serdeser.rs new file mode 100644 index 00000000..cb5ab951 --- /dev/null +++ b/engine/pill_engine/src/serdeserialization/serdeser.rs @@ -0,0 +1,197 @@ +use std::{ + any::{type_name, Any, TypeId}, + collections::HashMap, + path::Path, +}; + +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +use crate::{ + ecs::{EntityHandle, TransformComponent, UuidComponent}, + serdeserialization::SerdeserBackend, + Component, +}; +use pill_core::{get_type_name, EngineError, ErrorContext, Result}; +use uuid::Uuid; +// Here we register everything that is worthy being serialized, then appropriate backend is chosen +// for the actual serialization + +pub type ComponentPayload = serde_json::Value; + +#[derive(Serialize, Deserialize)] +pub struct SerializedScene { + pub entities: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct SerializedEntity { + pub uuid: u128, + pub components: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct SerializedComponent { + pub key: String, + pub payload: ComponentPayload, +} + +pub struct DecodedComponent { + pub type_id: TypeId, + pub value: Box, +} + +pub struct DecodedEntity { + pub uuid: u128, // redundant unless be spawn it here + pub components: Vec, +} + +pub type ComponentSerializeFn = fn(component: &dyn Any) -> Result; +pub type ComponentDeserializeFn = fn(payload: ComponentPayload) -> Result>; + +pub struct ComponentSerdeDescriptor { + pub key: String, + pub serialize: ComponentSerializeFn, + pub deserialize: ComponentDeserializeFn, +} + +pub struct Serdeser { + pub by_type_id: HashMap, + pub type_id_by_key: HashMap, +} + +fn serialize_with_serde(component: &dyn Any) -> Result +where + C: Component + Serialize + 'static, +{ + let component = component.downcast_ref::().unwrap(); // TODO: error handling? + let value = serde_json::to_value(component)?; + Ok(value) +} + +fn deserialize_with_serde(payload: ComponentPayload) -> Result> +where + C: Component + DeserializeOwned + 'static, +{ + Ok(Box::new(serde_json::from_value::(payload)?)) +} + +impl Serdeser { + pub fn new() -> Serdeser { + Serdeser { + by_type_id: HashMap::new(), + type_id_by_key: HashMap::new(), + } + } + + pub fn register_component(&mut self) -> Result<()> + where + C: Component + Serialize + DeserializeOwned + 'static, + { + let type_id = TypeId::of::(); + + let descriptor = ComponentSerdeDescriptor { + key: get_type_name::() + .strip_suffix("Component") + .unwrap() + .to_string(), + serialize: serialize_with_serde::, + deserialize: deserialize_with_serde::, + }; + + self.type_id_by_key + .insert(descriptor.key.to_string(), type_id); // TODO: maybe use + // &'static str? + self.by_type_id.insert(type_id, descriptor); + Ok(()) + } + + // TODO: hardcoded entities - in the final implementation we need to have the handle to the world snapshot + pub fn serialize<'a>( + &self, + entities: impl Iterator, + backend: &mut dyn SerdeserBackend, + file: &Path, + ) -> Result<()> { + // dump engine/scene config options + // go through all entities in scene, dump them, mapping them on the fly into serdes types + // TODO: how to do that efficiently? + // in current ECS iterate all entities that have transform components for now + // TODO: we later want to serialize the knowledge that an entity contains some components + // and recreate them (this will be the archetype in the ECS - for now ignore) + + let mut scene = SerializedScene { + entities: Vec::new(), + }; + for (eh, uuid, transform) in entities { + // handle the componentId specially - write it as the entities ID + let mut entity = SerializedEntity { + uuid: uuid.uuid, + components: Vec::new(), + }; + + if let Some(transform_descriptor) = + self.by_type_id.get(&TypeId::of::()) + { + let serialized_transform = SerializedComponent { + key: transform_descriptor.key.clone(), + payload: (transform_descriptor.serialize)(transform)?, + }; + + entity.components.push(serialized_transform); + }; + + scene.entities.push(entity); + } + backend.write_scene(file, &scene)?; + Ok(()) + } + + // TODO: temporarily we have a spawn function injected that we will use to spawn a decoded + // entity + pub fn deserialize( + &self, + backend: &mut dyn SerdeserBackend, + file: &Path, + ) -> Result> { + let scene = backend.read_scene(file)?; + let mut decoded = Vec::with_capacity(scene.entities.len()); + + for entity in scene.entities { + let mut components = Vec::with_capacity(entity.components.len()); + for component in entity.components { + let type_id = *self.type_id_by_key.get(component.key.as_str()).ok_or_else( + || -> pill_core::PillError { + EngineError::Other(format!( + "Unknown serialized component key: {}", + component.key + )) + .into() + }, + )?; + let descriptor = + self.by_type_id + .get(&type_id) + .ok_or_else(|| -> pill_core::PillError { + EngineError::Other(format!( + "Deserializer registry is inconsistent for component key: {}", + component.key + )) + .into() + })?; + + let decoded_component = DecodedComponent { + type_id, + value: (descriptor.deserialize)(component.payload)?, + }; + components.push(decoded_component); + } + + decoded.push(DecodedEntity { + uuid: entity.uuid, + components, + }); + } + + Ok(decoded) + } +} diff --git a/engine/pill_engine/src/serdeserialization/serdeser_backend.rs b/engine/pill_engine/src/serdeserialization/serdeser_backend.rs new file mode 100644 index 00000000..b33c8e22 --- /dev/null +++ b/engine/pill_engine/src/serdeserialization/serdeser_backend.rs @@ -0,0 +1,26 @@ +use std::path::Path; + +use pill_core::{ErrorContext, Result}; + +use crate::serdeserialization::serdeser::SerializedScene; + +pub trait SerdeserBackend { + fn write_scene(&mut self, file: &Path, scene: &SerializedScene) -> Result<()>; + fn read_scene(&mut self, file: &Path) -> Result; +} + +pub struct JsonBackend; + +impl SerdeserBackend for JsonBackend { + fn write_scene(&mut self, file: &Path, scene: &SerializedScene) -> Result<()> { + let writer = std::fs::File::create(file)?; + serde_json::to_writer_pretty(writer, &scene)?; + Ok(()) + } + + fn read_scene(&mut self, file: &Path) -> Result { + let reader = std::fs::File::open(file)?; + let scene = serde_json::from_reader(reader)?; + Ok(scene) + } +} diff --git a/examples/physics/src/game.rs b/examples/physics/src/game.rs index c53284ba..85c8d7e1 100644 --- a/examples/physics/src/game.rs +++ b/examples/physics/src/game.rs @@ -902,6 +902,19 @@ fn demo_keyboard_control_system(engine: &mut Engine) -> Result<()> { let reset_pressed = input.get_key(KeyboardKey::KeyR); let spawn_pressed = input.get_key_pressed(KeyboardKey::Space); + let save_pressed = input.get_key_pressed(KeyboardKey::KeyS); + let load_pressed = input.get_key_pressed(KeyboardKey::KeyL); + let ctrl_pressed = input.get_key(KeyboardKey::ControlLeft); + // TODO: temporarily for demo + if save_pressed && ctrl_pressed { + println!("SAVING SCENE!"); + engine.save_scene()?; + } + + if load_pressed && ctrl_pressed { + println!("LOADING SCENE!"); + engine.load_scene(engine.get_active_scene_handle()?)?; + } if !reset_pressed && !spawn_pressed { return Ok(());