From c262e26da5986cd8eaffb469f2e26a28aa45260a Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Sun, 4 Oct 2026 21:53:48 +0300 Subject: [PATCH] feat: save and load scene hierarchies with SceneSerializer Adds SceneSerializer and a ComponentRegistry for saving GameObject trees to JSON, YAML or binary. Meshes, textures, shaders and models now remember where they came from so they can be stored as references. --- CMakeLists.txt | 2 + include/private/Assets/InternalGeometry.hpp | 10 + include/public/Core/GameObject.hpp | 5 + include/public/Core/SceneSerializer.hpp | 62 +++ include/public/ECS/Component.hpp | 6 + include/public/ECS/ComponentRegistry.hpp | 95 ++++ .../public/ECS/Components/MeshComponent.hpp | 6 + .../ECS/Components/ModelSourceComponent.hpp | 32 ++ .../ECS/Components/TransformComponent.hpp | 2 +- include/public/Runtime/Material.hpp | 3 + include/public/Runtime/MeshData.hpp | 11 + include/public/Runtime/Texture.hpp | 7 + src/Assets/ModelLoader.cpp | 2 + src/Graphics/Common/Material.cpp | 2 + src/Graphics/Common/ResourceManager.cpp | 1 + src/Scene/ComponentRegistry.cpp | 391 ++++++++++++++ src/Scene/MeshComponent.cpp | 1 + src/Scene/SceneSerializer.cpp | 507 ++++++++++++++++++ 18 files changed, 1144 insertions(+), 1 deletion(-) create mode 100644 include/public/Core/SceneSerializer.hpp create mode 100644 include/public/ECS/ComponentRegistry.hpp create mode 100644 include/public/ECS/Components/ModelSourceComponent.hpp create mode 100644 src/Scene/ComponentRegistry.cpp create mode 100644 src/Scene/SceneSerializer.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2212728b..920b9a55 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -202,12 +202,14 @@ set(PHYSICS_SOURCES set(SCENE_SOURCES src/Scene/Camera.cpp + src/Scene/ComponentRegistry.cpp src/Scene/FirstPersonController.cpp src/Scene/FreeLookCameraControllerComponent.cpp src/Scene/GameObject.cpp src/Scene/MaterialComponent.cpp src/Scene/MeshComponent.cpp src/Scene/SceneBase.cpp + src/Scene/SceneSerializer.cpp src/Scene/Scene.cpp src/Scene/TransformComponent.cpp ) diff --git a/include/private/Assets/InternalGeometry.hpp b/include/private/Assets/InternalGeometry.hpp index 5b1b0325..a2d6e632 100644 --- a/include/private/Assets/InternalGeometry.hpp +++ b/include/private/Assets/InternalGeometry.hpp @@ -50,6 +50,8 @@ inline Sleak::MeshData GetPlaneMesh(float width, float height, int subdivisionsX } } + mesh.source = {"Plane", {width, height, float(subdivisionsX), + float(subdivisionsY)}}; return mesh; } @@ -109,6 +111,7 @@ inline Sleak::MeshData GetCubeMesh() { 16, 17, 18, 16, 18, 19, // Bottom face 20, 21, 22, 20, 22, 23}; + mesh.source = {"Cube", {}}; return mesh; } @@ -143,6 +146,7 @@ inline Sleak::MeshData GetSphereMesh(int stacks = 16, int slices = 16) { mesh.indices.add(first + 1); } } + mesh.source = {"Sphere", {float(stacks), float(slices)}}; return mesh; } @@ -207,6 +211,7 @@ inline Sleak::MeshData GetCylinderMesh(int segments, float height, float radius) mesh.indices.add(base + 3); } + mesh.source = {"Cylinder", {float(segments), height, radius}}; return mesh; } @@ -263,6 +268,8 @@ inline Sleak::MeshData GetCapsuleMesh(int segments, int rings, float height, flo } } + mesh.source = {"Capsule", + {float(segments), float(rings), height, radius}}; return mesh; } @@ -309,6 +316,8 @@ inline Sleak::MeshData GetTorusMesh(int segments, int rings, float innerRadius, } } + mesh.source = {"Torus", {float(segments), float(rings), innerRadius, + outerRadius}}; return mesh; } @@ -349,5 +358,6 @@ inline Sleak::MeshData GetPyramidMesh() { mesh.indices.add(4); mesh.indices.add(1); + mesh.source = {"Pyramid", {}}; return mesh; } diff --git a/include/public/Core/GameObject.hpp b/include/public/Core/GameObject.hpp index 19085363..67d57167 100644 --- a/include/public/Core/GameObject.hpp +++ b/include/public/Core/GameObject.hpp @@ -117,6 +117,11 @@ namespace Sleak { return GetComponent() != nullptr; } + /// Every attached component, in the order they were added. + const List>& GetComponents() const { + return Components; + } + /// Initializes the object and its components; called once before the first Update. virtual void Initialize(); virtual void Update(float deltaTime); diff --git a/include/public/Core/SceneSerializer.hpp b/include/public/Core/SceneSerializer.hpp new file mode 100644 index 00000000..cac684f5 --- /dev/null +++ b/include/public/Core/SceneSerializer.hpp @@ -0,0 +1,62 @@ +#ifndef _SCENE_SERIALIZER_HPP_ +#define _SCENE_SERIALIZER_HPP_ + +#include +#include +#include + +namespace Sleak { + +class SceneBase; +class GameObject; + +/// Saves a scene's GameObject hierarchy to JSON, YAML or binary and loads it +/// back into another scene. +/// +/// Each object stores its name, tag, active flag, Camera or Light settings, +/// and the components ComponentRegistry knows about. Meshes are stored as +/// the primitive that built them, and objects created by ModelLoader as the +/// model path, so a model's generated children are rebuilt by reloading it. +/// Anything that cannot be stored (a hand-built mesh, an unregistered +/// component) is skipped with a warning naming the object. +/// +/// @code{.cpp} +/// Sleak::SceneSerializer(*this).Save("assets/scenes/level1.json"); +/// +/// Sleak::SceneSerializer loader(*otherScene); +/// if (!loader.Load("assets/scenes/level1.json")) { /* logged */ } +/// @endcode +/// @see ComponentRegistry, Serializable +/// @ingroup scene +class ENGINE_API SceneSerializer { + public: + /// Version written to every scene file. + static constexpr int kVersion = 1; + + explicit SceneSerializer(SceneBase& scene) : m_scene(scene) {} + + /// Writes the scene to path, format picked from the extension. Logs and + /// returns false on failure. + bool Save(const std::string& path) const; + /// Adds the objects stored in path to the scene, keeping existing ones. + /// Logs and returns false on failure, leaving the scene untouched. + bool Load(const std::string& path); + + /// Builds the scene document in memory. + SerialValue SaveToValue() const; + /// Loads from an in-memory scene document. + bool LoadFromValue(const SerialValue& document); + + /// Serializes one object and its children. + static SerialValue SerializeObject(const GameObject& object); + /// Builds an object tree from SerializeObject output. The caller owns it + /// until it is added to a scene. Returns nullptr on malformed data. + static GameObject* DeserializeObject(const SerialValue& data); + + private: + SceneBase& m_scene; +}; + +} // namespace Sleak + +#endif // _SCENE_SERIALIZER_HPP_ diff --git a/include/public/ECS/Component.hpp b/include/public/ECS/Component.hpp index a10a52ea..aaefedcd 100644 --- a/include/public/ECS/Component.hpp +++ b/include/public/ECS/Component.hpp @@ -7,6 +7,7 @@ namespace Sleak { class GameObject; + class ISerializationContext; /// Base for behavior attached to a GameObject. Owner deletes it via /// GameObject::RemoveComponent(), never directly. /// @@ -73,6 +74,11 @@ namespace Sleak { virtual void OnEnable() {} virtual void OnDisable() {} + /// Writes state for SceneSerializer. Saves nothing by default. + virtual void Serialize(ISerializationContext& context) const {} + /// Restores what Serialize wrote. Does nothing by default. + virtual void Deserialize(const ISerializationContext& context) {} + GameObject* GetOwner() { return owner; } diff --git a/include/public/ECS/ComponentRegistry.hpp b/include/public/ECS/ComponentRegistry.hpp new file mode 100644 index 00000000..19c11101 --- /dev/null +++ b/include/public/ECS/ComponentRegistry.hpp @@ -0,0 +1,95 @@ +#ifndef _COMPONENT_REGISTRY_HPP_ +#define _COMPONENT_REGISTRY_HPP_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Sleak { + +/// Maps component type names to factories so SceneSerializer can save and +/// rebuild components it knows nothing about at compile time. +/// +/// Engine components are registered on first use. Game components opt in by +/// overriding Component::Serialize/Deserialize and calling Register() once +/// at startup, or by passing their own functions when the constructor needs +/// more than the owner. +/// +/// @code{.cpp} +/// Sleak::ComponentRegistry::Register("Spin"); +/// @endcode +/// @ingroup scene +class ENGINE_API ComponentRegistry { + public: + /// Adds the component to owner from saved data and returns it. + using CreateFn = std::function; + /// Finds the existing component of this type on owner, or nullptr. + using FindFn = std::function; + /// Writes the component's data. Returning false skips the component. + using SaveFn = + std::function; + /// Applies saved data to a component that already exists. + using LoadFn = + std::function; + + /// One registered component type. + struct Entry { + std::string name; + std::type_index type; + CreateFn create; + FindFn find; + SaveFn save; + LoadFn load; + }; + + /// Registers a type with explicit functions. A second registration of the + /// same name or type replaces the first. + static void Register(const std::string& name, std::type_index type, + CreateFn create, FindFn find, SaveFn save, + LoadFn load); + + /// Registers T using its (GameObject*) constructor and its + /// Serialize/Deserialize overrides. + template + static void Register(const std::string& name) { + static_assert(std::is_base_of_v, + "T must derive from Component!"); + Register( + name, typeid(T), + [](GameObject& owner, const ISerializationContext& data) { + owner.AddComponent(); + Component* c = owner.GetComponent(); + if (c) c->Deserialize(data); + return c; + }, + [](GameObject& owner) -> Component* { + return owner.GetComponent(); + }, + [](const Component& c, ISerializationContext& data) { + c.Serialize(data); + return true; + }, + [](Component& c, const ISerializationContext& data) { + c.Deserialize(data); + }); + } + + /// Entry for a saved type name, nullptr when unknown. + static const Entry* Find(const std::string& name); + /// Entry for the dynamic type of component, nullptr when unregistered. + static const Entry* Find(const Component& component); + + private: + static std::vector& Entries(); +}; + +} // namespace Sleak + +#endif // _COMPONENT_REGISTRY_HPP_ diff --git a/include/public/ECS/Components/MeshComponent.hpp b/include/public/ECS/Components/MeshComponent.hpp index 1da11242..6284a942 100644 --- a/include/public/ECS/Components/MeshComponent.hpp +++ b/include/public/ECS/Components/MeshComponent.hpp @@ -6,6 +6,7 @@ #include #include #include +#include namespace Sleak { class MeshData; @@ -49,6 +50,10 @@ namespace Sleak { // Local-space bounds, transformed by the owner each Update. void SetCullBoundsLocal(const Math::AABB& bounds); + /// Primitive this mesh was generated from, empty when it was not. + const MeshSource& GetSource() const { return m_source; } + void SetSource(const MeshSource& source) { m_source = source; } + private: RefPtr VertexBuffer{}; RefPtr IndexBuffer{}; @@ -61,6 +66,7 @@ namespace Sleak { bool m_hasCullBounds = false; Math::AABB m_localBounds{}; bool m_hasLocalBounds = false; + MeshSource m_source; }; } diff --git a/include/public/ECS/Components/ModelSourceComponent.hpp b/include/public/ECS/Components/ModelSourceComponent.hpp new file mode 100644 index 00000000..437384e1 --- /dev/null +++ b/include/public/ECS/Components/ModelSourceComponent.hpp @@ -0,0 +1,32 @@ +#ifndef _MODEL_SOURCE_COMPONENT_HPP_ +#define _MODEL_SOURCE_COMPONENT_HPP_ + +#include +#include +#include + +namespace Sleak { + +/// Marks the root object ModelLoader built, remembering the file and options +/// so SceneSerializer can store a reference instead of the generated meshes. +/// @ingroup scene +class ModelSourceComponent : public Component { + public: + ModelSourceComponent(GameObject* owner, const std::string& path, + const ModelLoadOptions& options = {}) + : Component(owner), m_path(path), m_options(options) {} + + bool Initialize() override { return true; } + void Update(float) override {} + + const std::string& GetPath() const { return m_path; } + const ModelLoadOptions& GetOptions() const { return m_options; } + + private: + std::string m_path; + ModelLoadOptions m_options; +}; + +} // namespace Sleak + +#endif // _MODEL_SOURCE_COMPONENT_HPP_ diff --git a/include/public/ECS/Components/TransformComponent.hpp b/include/public/ECS/Components/TransformComponent.hpp index 37307a81..04a2ce44 100644 --- a/include/public/ECS/Components/TransformComponent.hpp +++ b/include/public/ECS/Components/TransformComponent.hpp @@ -94,7 +94,7 @@ namespace Sleak { Vector3D position = Vector3D(0.0f, 0.0f, 0.0f); Quaternion rotation = Quaternion(); Vector3D scale = Vector3D(1.0f, 1.0f, 1.0f); - TransformMatrix* Transform; + TransformMatrix* Transform = nullptr; private: RefPtr ConstantBuffer; diff --git a/include/public/Runtime/Material.hpp b/include/public/Runtime/Material.hpp index 4ef3d822..e1407075 100644 --- a/include/public/Runtime/Material.hpp +++ b/include/public/Runtime/Material.hpp @@ -96,6 +96,8 @@ namespace Sleak { void SetShader(RenderEngine::Shader* shader); void SetShader(const std::string& shaderPath); RenderEngine::Shader* GetShader() const; + /// Path given to SetShader(path), empty when set from a Shader pointer. + const std::string& GetShaderPath() const { return m_shaderPath; } // Texture slots: each pair of Set overloads takes either an owned Texture* or loads by path void SetDiffuseTexture(Texture* texture); @@ -203,6 +205,7 @@ namespace Sleak { // Shader ObjectPtr m_shader; + std::string m_shaderPath; // GPU constant buffer (slot 1) ObjectPtr m_materialBuffer; diff --git a/include/public/Runtime/MeshData.hpp b/include/public/Runtime/MeshData.hpp index c0035a4f..0b3b029d 100644 --- a/include/public/Runtime/MeshData.hpp +++ b/include/public/Runtime/MeshData.hpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include namespace Sleak { @@ -98,11 +100,20 @@ namespace Sleak { Sleak::List vertices; }; + /// Names the built-in primitive a mesh was generated from, so it can be + /// rebuilt on load. Empty for imported and hand-built meshes. + /// @ingroup rendering + struct MeshSource { + std::string primitive; + std::vector params; + }; + /// CPU-side vertex and index buffers for one mesh, ready for upload. /// @ingroup rendering struct MeshData { VertexGroup vertices; IndexGroup indices; + MeshSource source; }; } diff --git a/include/public/Runtime/Texture.hpp b/include/public/Runtime/Texture.hpp index 6e5f1c29..fdcbed06 100644 --- a/include/public/Runtime/Texture.hpp +++ b/include/public/Runtime/Texture.hpp @@ -105,6 +105,13 @@ class Texture { virtual float GetLodBias() const { return 0.0f; } virtual uint64_t GetImGuiTextureID() const { return 0; } + + /// File the texture was loaded from, empty for textures built in memory. + const std::string& GetSourcePath() const { return m_sourcePath; } + void SetSourcePath(const std::string& path) { m_sourcePath = path; } + +private: + std::string m_sourcePath; }; } // namespace Sleak diff --git a/src/Assets/ModelLoader.cpp b/src/Assets/ModelLoader.cpp index b3d1d4b6..279e0454 100644 --- a/src/Assets/ModelLoader.cpp +++ b/src/Assets/ModelLoader.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -107,6 +108,7 @@ GameObject* ModelLoader::Load(const std::string& filePath, options, textureCache, skeleton, clips); } + root->AddComponent(filePath, options); InitializeRecursive(root); return root; } diff --git a/src/Graphics/Common/Material.cpp b/src/Graphics/Common/Material.cpp index 57943951..cbeebf0a 100644 --- a/src/Graphics/Common/Material.cpp +++ b/src/Graphics/Common/Material.cpp @@ -124,9 +124,11 @@ namespace Sleak { void Material::SetShader(RenderEngine::Shader* shader) { m_shader = ObjectPtr(shader); + m_shaderPath.clear(); } void Material::SetShader(const std::string& shaderPath) { + m_shaderPath = shaderPath; auto* shader = RenderEngine::ResourceManager::CreateShader(shaderPath); if (shader) diff --git a/src/Graphics/Common/ResourceManager.cpp b/src/Graphics/Common/ResourceManager.cpp index 5f1e34ce..ff6fc1d6 100644 --- a/src/Graphics/Common/ResourceManager.cpp +++ b/src/Graphics/Common/ResourceManager.cpp @@ -49,6 +49,7 @@ namespace Sleak { try { if (result.has_value()) { Texture* texture = std::any_cast(result); + if (texture) texture->SetSourcePath(TexturePath); return texture; } } catch (const std::exception& e) { diff --git a/src/Scene/ComponentRegistry.cpp b/src/Scene/ComponentRegistry.cpp new file mode 100644 index 00000000..a63a5442 --- /dev/null +++ b/src/Scene/ComponentRegistry.cpp @@ -0,0 +1,391 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Sleak { + +namespace { + +float Param(const MeshSource& s, size_t i, float fallback) { + return i < s.params.size() ? s.params[i] : fallback; +} + +bool BuildPrimitive(const MeshSource& s, MeshData& out) { + const std::string& p = s.primitive; + if (p == "Plane") + out = GetPlaneMesh(Param(s, 0, 1), Param(s, 1, 1), + static_cast(Param(s, 2, 1)), + static_cast(Param(s, 3, 1))); + else if (p == "Cube") + out = GetCubeMesh(); + else if (p == "Sphere") + out = GetSphereMesh(static_cast(Param(s, 0, 16)), + static_cast(Param(s, 1, 16))); + else if (p == "Cylinder") + out = GetCylinderMesh(static_cast(Param(s, 0, 16)), Param(s, 1, 1), + Param(s, 2, 0.5f)); + else if (p == "Capsule") + out = GetCapsuleMesh(static_cast(Param(s, 0, 16)), + static_cast(Param(s, 1, 8)), Param(s, 2, 1), + Param(s, 3, 0.5f)); + else if (p == "Torus") + out = GetTorusMesh(static_cast(Param(s, 0, 16)), + static_cast(Param(s, 1, 8)), Param(s, 2, 8), + Param(s, 3, 9)); + else if (p == "Pyramid") + out = GetPyramidMesh(); + else + return false; + return true; +} + +void SaveSource(const MeshSource& s, ISerializationContext& out) { + out.Set("primitive", s.primitive); + out.Set("params", s.params); +} + +MeshSource LoadSource(const ISerializationContext& in) { + MeshSource s; + s.primitive = in.Get("primitive", ""); + s.params = in.Get("params", std::vector{}); + return s; +} + +// Transform + +bool SaveTransform(const Component& c, ISerializationContext& out) { + auto& t = static_cast(c); + out.Set("position", t.GetWorldPosition()); + out.Set("rotation", t.GetWorldRotation()); + out.Set("scale", t.GetWorldScale()); + return true; +} + +void LoadTransform(Component& c, const ISerializationContext& in) { + auto& t = static_cast(c); + t.SetPosition(in.Get("position", t.GetWorldPosition())); + t.SetRotation(in.Get("rotation", t.GetWorldRotation())); + t.SetScale(in.Get("scale", t.GetWorldScale())); +} + +Component* CreateTransform(GameObject& owner, const ISerializationContext& in) { + owner.AddComponent( + in.Get("position", Math::Vector3D(0, 0, 0)), + in.Get("rotation", Math::Quaternion()), + in.Get("scale", Math::Vector3D(1, 1, 1))); + return owner.GetComponent(); +} + +// Material + +std::string TexturePath(const Texture* texture) { + return texture ? texture->GetSourcePath() : std::string(); +} + +void SaveTexture(const Material& m, const Texture* texture, const char* key, + ISerializationContext& out) { + if (!texture) return; + std::string path = TexturePath(texture); + if (path.empty()) { + SLEAK_WARN( + "SceneSerializer: material '{}' has a {} that was not " + "loaded from a file, skipped", + m.GetName(), key); + return; + } + out.Set(key, path); +} + +bool SaveMaterial(const Component& c, ISerializationContext& out) { + auto& mc = static_cast(c); + const Material* m = mc.GetMaterialRaw(); + if (!m) return true; + if (!m->GetShaderPath().empty()) out.Set("shader", m->GetShaderPath()); + SaveTexture(*m, m->GetDiffuseTexture(), "diffuseTexture", out); + SaveTexture(*m, m->GetNormalTexture(), "normalTexture", out); + SaveTexture(*m, m->GetSpecularTexture(), "specularTexture", out); + SaveTexture(*m, m->GetRoughnessTexture(), "roughnessTexture", out); + SaveTexture(*m, m->GetMetallicTexture(), "metallicTexture", out); + SaveTexture(*m, m->GetAOTexture(), "aoTexture", out); + SaveTexture(*m, m->GetEmissiveTexture(), "emissiveTexture", out); + out.Set("diffuseColor", m->GetDiffuseColor()); + out.Set("specularColor", m->GetSpecularColor()); + out.Set("emissiveColor", m->GetEmissiveColor()); + out.Set("shininess", m->GetShininess()); + out.Set("metallic", m->GetMetallic()); + out.Set("roughness", m->GetRoughness()); + out.Set("ao", m->GetAO()); + out.Set("normalIntensity", m->GetNormalIntensity()); + out.Set("emissiveIntensity", m->GetEmissiveIntensity()); + out.Set("opacity", m->GetOpacity()); + out.Set("alphaCutoff", m->GetAlphaCutoff()); + out.Set("tiling", m->GetTiling()); + out.Set("offset", m->GetOffset()); + out.Set("renderMode", m->GetRenderMode()); + out.Set("twoSided", m->IsTwoSided()); + return true; +} + +void LoadMaterial(Component& c, const ISerializationContext& in) { + auto& mc = static_cast(c); + Material* m = mc.GetMaterialRaw(); + if (!m) return; + std::string path; + if (in.TryGet("shader", path)) m->SetShader(path); + if (in.TryGet("diffuseTexture", path)) m->SetDiffuseTexture(path); + if (in.TryGet("normalTexture", path)) m->SetNormalTexture(path); + if (in.TryGet("specularTexture", path)) m->SetSpecularTexture(path); + if (in.TryGet("roughnessTexture", path)) m->SetRoughnessTexture(path); + if (in.TryGet("metallicTexture", path)) m->SetMetallicTexture(path); + if (in.TryGet("aoTexture", path)) m->SetAOTexture(path); + if (in.TryGet("emissiveTexture", path)) m->SetEmissiveTexture(path); + m->SetDiffuseColor(in.Get("diffuseColor", m->GetDiffuseColor())); + m->SetSpecularColor(in.Get("specularColor", m->GetSpecularColor())); + m->SetEmissiveColor(in.Get("emissiveColor", m->GetEmissiveColor())); + m->SetShininess(in.Get("shininess", m->GetShininess())); + m->SetMetallic(in.Get("metallic", m->GetMetallic())); + m->SetRoughness(in.Get("roughness", m->GetRoughness())); + m->SetAO(in.Get("ao", m->GetAO())); + m->SetNormalIntensity(in.Get("normalIntensity", m->GetNormalIntensity())); + m->SetEmissiveIntensity( + in.Get("emissiveIntensity", m->GetEmissiveIntensity())); + m->SetOpacity(in.Get("opacity", m->GetOpacity())); + m->SetAlphaCutoff(in.Get("alphaCutoff", m->GetAlphaCutoff())); + m->SetTiling(in.Get("tiling", m->GetTiling())); + m->SetOffset(in.Get("offset", m->GetOffset())); + m->SetRenderMode(in.Get("renderMode", m->GetRenderMode())); + m->SetTwoSided(in.Get("twoSided", m->IsTwoSided())); +} + +Component* CreateMaterial(GameObject& owner, const ISerializationContext& in) { + owner.AddComponent(RefPtr(new Material())); + Component* c = owner.GetComponent(); + if (c) LoadMaterial(*c, in); + return c; +} + +// Mesh + +bool SaveMesh(const Component& c, ISerializationContext& out) { + auto& mesh = static_cast(c); + const MeshSource& source = mesh.GetSource(); + if (source.primitive.empty()) { + SLEAK_WARN( + "SceneSerializer: skipped mesh on '{}', it was not built " + "from a primitive or a model", + const_cast(mesh).GetOwner()->GetName()); + return false; + } + SaveSource(source, out); + return true; +} + +void LoadMesh(Component&, const ISerializationContext&) {} + +Component* CreateMesh(GameObject& owner, const ISerializationContext& in) { + MeshSource source = LoadSource(in); + MeshData data; + if (!BuildPrimitive(source, data)) { + SLEAK_WARN("SceneSerializer: unknown primitive '{}' on '{}'", + source.primitive, owner.GetName()); + return nullptr; + } + owner.AddComponent(std::move(data)); + return owner.GetComponent(); +} + +// Rigidbody + +bool SaveRigidbody(const Component& c, ISerializationContext& out) { + auto& rb = static_cast(c); + out.Set("bodyType", rb.GetBodyType()); + out.Set("mass", rb.GetMass()); + out.Set("useGravity", rb.GetUseGravity()); + out.Set("gravity", rb.GetGravity()); + out.Set("terminalVelocity", rb.GetTerminalVelocity()); + out.Set("velocity", rb.GetVelocity()); + return true; +} + +void LoadRigidbody(Component& c, const ISerializationContext& in) { + auto& rb = static_cast(c); + rb.SetBodyType(in.Get("bodyType", rb.GetBodyType())); + rb.SetMass(in.Get("mass", rb.GetMass())); + rb.SetUseGravity(in.Get("useGravity", rb.GetUseGravity())); + rb.SetGravity(in.Get("gravity", rb.GetGravity())); + rb.SetTerminalVelocity( + in.Get("terminalVelocity", rb.GetTerminalVelocity())); + rb.SetVelocity(in.Get("velocity", rb.GetVelocity())); +} + +Component* CreateRigidbody(GameObject& owner, const ISerializationContext& in) { + owner.AddComponent( + in.Get("bodyType", BodyType::Kinematic)); + Component* c = owner.GetComponent(); + if (c) LoadRigidbody(*c, in); + return c; +} + +// Collider + +bool SaveCollider(const Component& c, ISerializationContext& out) { + auto& col = static_cast(c); + const Physics::ColliderShape& shape = col.GetShape(); + if (auto* box = std::get_if(&shape)) { + out.Set("shape", std::string("AABB")); + out.Set("min", box->min); + out.Set("max", box->max); + } else if (auto* s = std::get_if(&shape)) { + out.Set("shape", std::string("Sphere")); + out.Set("center", s->center); + out.Set("radius", s->radius); + } else if (auto* cap = std::get_if(&shape)) { + out.Set("shape", std::string("Capsule")); + out.Set("center", cap->center); + out.Set("radius", cap->radius); + out.Set("halfHeight", cap->halfHeight); + out.Set("axis", cap->axis); + } else { + GameObject* owner = const_cast(col).GetOwner(); + auto* mesh = owner ? owner->GetComponent() : nullptr; + if (!mesh || mesh->GetSource().primitive.empty()) { + SLEAK_WARN( + "SceneSerializer: skipped triangle mesh collider on " + "'{}', its mesh has no primitive to rebuild from", + owner ? owner->GetName() : std::string("?")); + return false; + } + out.Set("shape", std::string("Mesh")); + SaveSource(mesh->GetSource(), out); + } + out.Set("offset", col.GetOffset()); + out.Set("layer", col.GetLayer()); + out.Set("mask", col.GetMask()); + out.Set("trigger", col.IsTrigger()); + return true; +} + +void LoadColliderSettings(Component& c, const ISerializationContext& in) { + auto& col = static_cast(c); + col.SetOffset(in.Get("offset", col.GetOffset())); + col.SetLayer(in.Get("layer", col.GetLayer())); + col.SetMask(in.Get("mask", col.GetMask())); + col.SetTrigger(in.Get("trigger", col.IsTrigger())); +} + +Component* CreateCollider(GameObject& owner, const ISerializationContext& in) { + std::string shape = in.Get("shape", ""); + if (shape == "AABB") { + Physics::AABB box; + box.min = in.Get("min", Math::Vector3D(-0.5f, -0.5f, -0.5f)); + box.max = in.Get("max", Math::Vector3D(0.5f, 0.5f, 0.5f)); + owner.AddComponent(box); + } else if (shape == "Sphere") { + owner.AddComponent(Physics::BoundingSphere( + in.Get("center", Math::Vector3D(0, 0, 0)), in.Get("radius", 0.5f))); + } else if (shape == "Capsule") { + owner.AddComponent(Physics::BoundingCapsule( + in.Get("center", Math::Vector3D(0, 0, 0)), in.Get("radius", 0.5f), + in.Get("halfHeight", 0.5f), in.Get("axis", 1))); + } else if (shape == "Mesh") { + MeshData data; + if (!BuildPrimitive(LoadSource(in), data)) return nullptr; + owner.AddComponent(data, true); + } else { + SLEAK_WARN("SceneSerializer: unknown collider shape '{}' on '{}'", + shape, owner.GetName()); + return nullptr; + } + Component* c = owner.GetComponent(); + if (c) LoadColliderSettings(*c, in); + return c; +} + +template +ComponentRegistry::FindFn FindOf() { + return + [](GameObject& owner) -> Component* { return owner.GetComponent(); }; +} + +std::recursive_mutex& RegistryMutex() { + static std::recursive_mutex mutex; + return mutex; +} + +void RegisterBuiltins() { + ComponentRegistry::Register("Transform", typeid(TransformComponent), + CreateTransform, FindOf(), + SaveTransform, LoadTransform); + ComponentRegistry::Register("Material", typeid(MaterialComponent), + CreateMaterial, FindOf(), + SaveMaterial, LoadMaterial); + ComponentRegistry::Register("Mesh", typeid(MeshComponent), CreateMesh, + FindOf(), SaveMesh, LoadMesh); + ComponentRegistry::Register("Rigidbody", typeid(RigidbodyComponent), + CreateRigidbody, FindOf(), + SaveRigidbody, LoadRigidbody); + ComponentRegistry::Register("Collider", typeid(ColliderComponent), + CreateCollider, FindOf(), + SaveCollider, LoadColliderSettings); +} + +} // namespace + +std::vector& ComponentRegistry::Entries() { + static std::vector entries; + static bool builtins = false; + if (!builtins) { + builtins = true; + RegisterBuiltins(); + } + return entries; +} + +void ComponentRegistry::Register(const std::string& name, std::type_index type, + CreateFn create, FindFn find, SaveFn save, + LoadFn load) { + std::lock_guard lock(RegistryMutex()); + auto& entries = Entries(); + Entry entry{name, + type, + std::move(create), + std::move(find), + std::move(save), + std::move(load)}; + for (auto& existing : entries) { + if (existing.name == name || existing.type == type) { + existing = std::move(entry); + return; + } + } + entries.push_back(std::move(entry)); +} + +const ComponentRegistry::Entry* ComponentRegistry::Find( + const std::string& name) { + std::lock_guard lock(RegistryMutex()); + for (const auto& entry : Entries()) + if (entry.name == name) return &entry; + return nullptr; +} + +const ComponentRegistry::Entry* ComponentRegistry::Find( + const Component& component) { + std::lock_guard lock(RegistryMutex()); + std::type_index type(typeid(component)); + for (const auto& entry : Entries()) + if (entry.type == type) return &entry; + return nullptr; +} + +} // namespace Sleak diff --git a/src/Scene/MeshComponent.cpp b/src/Scene/MeshComponent.cpp index 186e2d37..36f81dab 100644 --- a/src/Scene/MeshComponent.cpp +++ b/src/Scene/MeshComponent.cpp @@ -37,6 +37,7 @@ MeshComponent::MeshComponent(GameObject* object, MeshData data) : Component(obje VertexCount = data.vertices.GetSize(); IndexCount = data.indices.GetSize(); + m_source = data.source; } bool MeshComponent::Initialize() { diff --git a/src/Scene/SceneSerializer.cpp b/src/Scene/SceneSerializer.cpp new file mode 100644 index 00000000..9332d834 --- /dev/null +++ b/src/Scene/SceneSerializer.cpp @@ -0,0 +1,507 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Sleak { + +namespace { + +constexpr const char* kFormatTag = "SleakScene"; + +struct SaveState { + std::unordered_map ids; + const std::unordered_set* registered = nullptr; + int64_t nextId = 1; +}; + +struct LoadState { + std::unordered_map byId; + std::vector registered; +}; + +const char* ObjectTypeName(const GameObject& object) { + if (dynamic_cast(&object)) return "Camera"; + if (dynamic_cast(&object)) + return "DirectionalLight"; + if (dynamic_cast(&object)) return "SpotLight"; + if (dynamic_cast(&object)) return "PointLight"; + if (dynamic_cast(&object)) return "AreaLight"; + return "GameObject"; +} + +void SaveCamera(const Camera& cam, SerializationContext& out) { + out.Set("fieldOfView", cam.GetFieldOfView()); + out.Set("nearPlane", cam.GetNearPlane()); + out.Set("farPlane", cam.GetFarPlane()); + out.Set("position", cam.GetPosition()); + out.Set("lookTarget", cam.GetLookTarget()); + out.Set("up", cam.GetUp()); + out.Set("projection", cam.GetProjectionType()); +} + +void LoadCamera(Camera& cam, const SerializationContext& in) { + cam.SetFieldOfView(in.Get("fieldOfView", cam.GetFieldOfView())); + cam.SetNearPlane(in.Get("nearPlane", cam.GetNearPlane())); + cam.SetFarPlane(in.Get("farPlane", cam.GetFarPlane())); + cam.SetPosition(in.Get("position", cam.GetPosition())); + cam.SetLookTarget(in.Get("lookTarget", cam.GetLookTarget())); + cam.SetUp(in.Get("up", cam.GetUp())); + cam.SetProjectionType(in.Get("projection", cam.GetProjectionType())); +} + +void SaveLight(const Light& light, SerializationContext& out) { + Math::Vector3D color = light.GetColor(); + out.Set("color", color); + out.Set("intensity", light.GetIntensity()); + out.Set("enabled", light.IsEnabled()); + out.Set("castShadows", light.GetCastShadows()); + out.Set("shadowBias", light.GetShadowBias()); + out.Set("shadowStrength", light.GetShadowStrength()); + out.Set("shadowNormalBias", light.GetShadowNormalBias()); + out.Set("lightSize", light.GetLightSize()); + + if (auto* d = dynamic_cast(&light)) { + out.Set("direction", d->GetDirection()); + out.Set("shadowFrustumSize", d->GetShadowFrustumSize()); + out.Set("shadowDistance", d->GetShadowDistance()); + out.Set("shadowNearPlane", d->GetShadowNearPlane()); + out.Set("shadowFarPlane", d->GetShadowFarPlane()); + } else if (auto* s = dynamic_cast(&light)) { + out.Set("position", s->GetPosition()); + out.Set("direction", s->GetDirection()); + out.Set("range", s->GetRange()); + out.Set("innerConeAngle", s->GetInnerConeAngle()); + out.Set("outerConeAngle", s->GetOuterConeAngle()); + } else if (auto* p = dynamic_cast(&light)) { + out.Set("position", p->GetPosition()); + out.Set("range", p->GetRange()); + } else if (auto* a = dynamic_cast(&light)) { + out.Set("position", a->GetPosition()); + out.Set("direction", a->GetDirection()); + out.Set("range", a->GetRange()); + out.Set("width", a->GetWidth()); + out.Set("height", a->GetHeight()); + out.Set("shape", a->GetShape()); + out.Set("twoSided", a->IsTwoSided()); + } +} + +void LoadLight(Light& light, const SerializationContext& in) { + Math::Vector3D color = in.Get("color", light.GetColor()); + light.SetColor(color.GetX(), color.GetY(), color.GetZ()); + light.SetIntensity(in.Get("intensity", light.GetIntensity())); + light.SetEnabled(in.Get("enabled", light.IsEnabled())); + light.SetCastShadows(in.Get("castShadows", light.GetCastShadows())); + light.SetShadowBias(in.Get("shadowBias", light.GetShadowBias())); + light.SetShadowStrength( + in.Get("shadowStrength", light.GetShadowStrength())); + light.SetShadowNormalBias( + in.Get("shadowNormalBias", light.GetShadowNormalBias())); + light.SetLightSize(in.Get("lightSize", light.GetLightSize())); + + if (auto* d = dynamic_cast(&light)) { + d->SetDirection(in.Get("direction", d->GetDirection())); + d->SetShadowFrustumSize( + in.Get("shadowFrustumSize", d->GetShadowFrustumSize())); + d->SetShadowDistance(in.Get("shadowDistance", d->GetShadowDistance())); + d->SetShadowNearPlane( + in.Get("shadowNearPlane", d->GetShadowNearPlane())); + d->SetShadowFarPlane(in.Get("shadowFarPlane", d->GetShadowFarPlane())); + } else if (auto* s = dynamic_cast(&light)) { + s->SetPosition(in.Get("position", s->GetPosition())); + s->SetDirection(in.Get("direction", s->GetDirection())); + s->SetRange(in.Get("range", s->GetRange())); + s->SetInnerConeAngle(in.Get("innerConeAngle", s->GetInnerConeAngle())); + s->SetOuterConeAngle(in.Get("outerConeAngle", s->GetOuterConeAngle())); + } else if (auto* p = dynamic_cast(&light)) { + p->SetPosition(in.Get("position", p->GetPosition())); + p->SetRange(in.Get("range", p->GetRange())); + } else if (auto* a = dynamic_cast(&light)) { + a->SetPosition(in.Get("position", a->GetPosition())); + a->SetDirection(in.Get("direction", a->GetDirection())); + a->SetRange(in.Get("range", a->GetRange())); + a->SetWidth(in.Get("width", a->GetWidth())); + a->SetHeight(in.Get("height", a->GetHeight())); + a->SetShape(in.Get("shape", a->GetShape())); + a->SetTwoSided(in.Get("twoSided", a->IsTwoSided())); + } +} + +SerialValue SaveModelSource(const ModelSourceComponent& model) { + const ModelLoadOptions& o = model.GetOptions(); + SerializationContext out; + out.Set("path", model.GetPath()); + out.Set("scaleFactor", o.scaleFactor); + out.Set("flipUVs", o.flipUVs); + out.Set("flipNormals", o.flipNormals); + out.Set("flipWinding", o.flipWinding); + out.Set("position", o.position); + out.Set("rotation", o.rotation); + return std::move(out.GetRoot()); +} + +SerialValue SaveObject(const GameObject& constObject, SaveState& state) { + GameObject& object = const_cast(constObject); + SerializationContext out; + + int64_t id = state.nextId++; + state.ids[&constObject] = id; + + const char* type = ObjectTypeName(object); + if (std::string(type) == "GameObject" && + typeid(object) != typeid(GameObject)) { + SLEAK_WARN( + "SceneSerializer: '{}' is a {} which has no serializer, " + "saving it as a plain GameObject", + object.GetName(), typeid(object).name()); + } + + out.Set("id", id); + out.Set("type", std::string(type)); + out.Set("name", object.GetName()); + out.Set("tag", object.GetTag()); + out.Set("active", object.IsActive()); + if (state.registered && object.HasParent()) + out.Set("registered", state.registered->count(&constObject) > 0); + + if (auto* cam = dynamic_cast(&object)) { + SerializationContext props; + SaveCamera(*cam, props); + out.Set("camera", props.GetRoot()); + } else if (auto* light = dynamic_cast(&object)) { + SerializationContext props; + SaveLight(*light, props); + out.Set("light", props.GetRoot()); + } + + const ModelSourceComponent* model = nullptr; + SerialValue components = SerialValue::MakeArray(); + const auto& list = object.GetComponents(); + for (size_t i = 0; i < list.GetSize(); ++i) { + const Component* component = list[i].get(); + if (!component) continue; + if (auto* m = dynamic_cast(component)) { + model = m; + continue; + } + const ComponentRegistry::Entry* entry = + ComponentRegistry::Find(*component); + if (!entry) { + SLEAK_WARN( + "SceneSerializer: skipped unregistered component {} " + "on '{}'", + typeid(*component).name(), object.GetName()); + continue; + } + SerializationContext data; + data.Set("type", entry->name); + if (!entry->save(*component, data)) continue; + components.Append(std::move(data.GetRoot())); + } + + if (model) { + out.Set("model", SaveModelSource(*model)); + size_t generated = object.GetChildren().GetSize(); + if (generated > 0) { + SLEAK_INFO( + "SceneSerializer: '{}' is stored as model '{}', its {} " + "child objects are rebuilt from the file on load", + object.GetName(), model->GetPath(), generated); + } + } + out.Set("components", components); + + if (!model) { + SerialValue children = SerialValue::MakeArray(); + const auto& kids = object.GetChildren(); + for (size_t i = 0; i < kids.GetSize(); ++i) { + if (kids[i]) children.Append(SaveObject(*kids[i], state)); + } + out.Set("children", children); + } + + return std::move(out.GetRoot()); +} + +GameObject* CreateObject(const SerializationContext& in) { + std::string type = in.Get("type", "GameObject"); + std::string name = in.Get("name", "GameObject"); + + if (const SerialValue* model = in.FindValue("model")) { + SerializationContext m( + SerializationFormat::JSON, + model->IsObject() ? *model : SerialValue::MakeObject()); + std::string path = m.Get("path", ""); + ModelLoadOptions o; + o.scaleFactor = m.Get("scaleFactor", o.scaleFactor); + o.flipUVs = m.Get("flipUVs", o.flipUVs); + o.flipNormals = m.Get("flipNormals", o.flipNormals); + o.flipWinding = m.Get("flipWinding", o.flipWinding); + o.position = m.Get("position", o.position); + o.rotation = m.Get("rotation", o.rotation); + GameObject* loaded = + path.empty() ? nullptr : ModelLoader::Load(path, o); + if (loaded) return loaded; + SLEAK_ERROR( + "SceneSerializer: could not load model '{}' for '{}', " + "creating an empty object instead", + path, name); + return new GameObject(name); + } + + if (type == "Camera") return new Camera(name); + if (type == "DirectionalLight") return new DirectionalLight(name); + if (type == "PointLight") return new PointLight(name); + if (type == "SpotLight") return new SpotLight(name); + if (type == "AreaLight") return new AreaLight(name); + if (type != "GameObject") { + SLEAK_WARN( + "SceneSerializer: unknown object type '{}' for '{}', " + "loading it as a GameObject", + type, name); + } + return new GameObject(name); +} + +void LoadComponents(GameObject& object, const SerialValue& components) { + for (size_t i = 0; i < components.Size(); ++i) { + const SerialValue& data = components.At(i); + if (!data.IsObject()) continue; + SerializationContext in(SerializationFormat::JSON, data); + std::string type = in.Get("type", ""); + const ComponentRegistry::Entry* entry = ComponentRegistry::Find(type); + if (!entry) { + SLEAK_WARN( + "SceneSerializer: skipped unknown component '{}' on " + "'{}'", + type, object.GetName()); + continue; + } + if (Component* existing = entry->find(object)) { + entry->load(*existing, in); + } else if (!entry->create(object, in)) { + SLEAK_WARN( + "SceneSerializer: could not rebuild component '{}' on " + "'{}'", + type, object.GetName()); + } + } +} + +GameObject* LoadObject(const SerialValue& data, LoadState& state, bool isRoot) { + if (!data.IsObject()) return nullptr; + SerializationContext in(SerializationFormat::JSON, data); + + GameObject* object = CreateObject(in); + object->SetName(in.Get("name", object->GetName())); + object->SetTag(in.Get("tag", object->GetTag())); + + if (const SerialValue* props = in.FindValue("camera")) { + if (auto* cam = dynamic_cast(object); cam && props->IsObject()) + LoadCamera(*cam, + SerializationContext(SerializationFormat::JSON, *props)); + } + if (const SerialValue* props = in.FindValue("light")) { + if (auto* light = dynamic_cast(object); + light && props->IsObject()) + LoadLight(*light, + SerializationContext(SerializationFormat::JSON, *props)); + } + + if (const SerialValue* components = in.FindValue("components")) + LoadComponents(*object, *components); + + int64_t id = 0; + if (in.TryGet("id", id)) state.byId[id] = object; + if (!isRoot && in.Get("registered", false)) + state.registered.push_back(object); + + if (const SerialValue* children = in.FindValue("children")) { + for (size_t i = 0; i < children->Size(); ++i) { + GameObject* child = LoadObject(children->At(i), state, false); + if (child) child->SetParent(object); + } + } + return object; +} + +void ApplyActive(GameObject& object, const SerialValue& data) { + if (!data.IsObject()) return; + SerializationContext in(SerializationFormat::JSON, data); + object.SetActive(in.Get("active", true)); + + const SerialValue* children = in.FindValue("children"); + if (!children) return; + const auto& kids = object.GetChildren(); + for (size_t i = 0; i < children->Size() && i < kids.GetSize(); ++i) + if (kids[i]) ApplyActive(*kids[i], children->At(i)); +} + +void DeleteTree(GameObject* object) { + if (!object) return; + std::vector kids; + for (size_t i = 0; i < object->GetChildren().GetSize(); ++i) + kids.push_back(object->GetChildren()[i]); + for (GameObject* kid : kids) DeleteTree(kid); + delete object; +} + +} // namespace + +SerialValue SceneSerializer::SerializeObject(const GameObject& object) { + SaveState state; + return SaveObject(object, state); +} + +GameObject* SceneSerializer::DeserializeObject(const SerialValue& data) { + LoadState state; + try { + GameObject* object = LoadObject(data, state, true); + if (object) ApplyActive(*object, data); + return object; + } catch (const std::exception& e) { + SLEAK_ERROR("SceneSerializer: failed to load object: {}", e.what()); + return nullptr; + } +} + +SerialValue SceneSerializer::SaveToValue() const { + const auto& objects = m_scene.GetObjects(); + + std::unordered_set registered; + std::vector roots; + std::unordered_set seenRoots; + for (size_t i = 0; i < objects.GetSize(); ++i) { + const GameObject* object = objects[i]; + if (!object || object->IsPendingDestroy()) continue; + registered.insert(object); + const GameObject* root = object; + while (root->GetParent()) root = root->GetParent(); + if (seenRoots.insert(root).second) roots.push_back(root); + } + + SaveState state; + state.registered = ®istered; + + SerializationContext doc; + doc.Set("format", std::string(kFormatTag)); + doc.Set("version", kVersion); + doc.Set("name", m_scene.GetName()); + + SerialValue list = SerialValue::MakeArray(); + for (const GameObject* root : roots) list.Append(SaveObject(*root, state)); + + if (const Camera* cam = m_scene.GetActiveCamera()) { + auto it = state.ids.find(cam); + if (it != state.ids.end()) + doc.Set("activeCamera", it->second); + else + SLEAK_WARN( + "SceneSerializer: active camera '{}' is not part of " + "the scene hierarchy, not saved", + cam->GetName()); + } + doc.Set("objects", list); + return std::move(doc.GetRoot()); +} + +bool SceneSerializer::Save(const std::string& path) const { + try { + SerializationContext::SaveToFile(SaveToValue(), path); + } catch (const std::exception& e) { + SLEAK_ERROR("SceneSerializer: failed to save '{}': {}", path, e.what()); + return false; + } + SLEAK_INFO("SceneSerializer: saved scene '{}' to {}", m_scene.GetName(), + path); + return true; +} + +bool SceneSerializer::LoadFromValue(const SerialValue& document) { + if (!document.IsObject()) { + SLEAK_ERROR("SceneSerializer: scene document is not an object"); + return false; + } + SerializationContext doc(SerializationFormat::JSON, document); + if (doc.Get("format", "") != kFormatTag) { + SLEAK_ERROR("SceneSerializer: not a scene file (missing format tag)"); + return false; + } + int version = doc.Get("version", 0); + if (version < 1 || version > kVersion) { + SLEAK_ERROR( + "SceneSerializer: unsupported scene version {} (this " + "build reads up to {})", + version, kVersion); + return false; + } + const SerialValue* list = doc.FindValue("objects"); + if (!list || !list->IsArray()) { + SLEAK_ERROR("SceneSerializer: scene has no object list"); + return false; + } + + LoadState state; + std::vector roots; + try { + for (size_t i = 0; i < list->Size(); ++i) { + GameObject* root = LoadObject(list->At(i), state, true); + if (root) { + roots.push_back(root); + ApplyActive(*root, list->At(i)); + } + } + } catch (const std::exception& e) { + SLEAK_ERROR("SceneSerializer: failed to load scene: {}", e.what()); + for (GameObject* root : roots) DeleteTree(root); + return false; + } + + for (GameObject* root : roots) m_scene.AddObject(root); + for (GameObject* object : state.registered) m_scene.AddObject(object); + + int64_t cameraId = 0; + if (doc.TryGet("activeCamera", cameraId)) { + auto it = state.byId.find(cameraId); + Camera* cam = it != state.byId.end() ? dynamic_cast(it->second) + : nullptr; + if (cam) + m_scene.SetActiveCamera(cam); + else + SLEAK_WARN("SceneSerializer: active camera id {} not found", + cameraId); + } + return true; +} + +bool SceneSerializer::Load(const std::string& path) { + SerialValue document; + try { + document = SerializationContext::LoadFromFile(path); + } catch (const std::exception& e) { + SLEAK_ERROR("SceneSerializer: failed to load '{}': {}", path, e.what()); + return false; + } + if (!LoadFromValue(document)) { + SLEAK_ERROR("SceneSerializer: '{}' was not loaded", path); + return false; + } + SLEAK_INFO("SceneSerializer: loaded {} into scene '{}'", path, + m_scene.GetName()); + return true; +} + +} // namespace Sleak