diff --git a/TheChef b/TheChef index 14f7dd9..38230f5 160000 --- a/TheChef +++ b/TheChef @@ -1 +1 @@ -Subproject commit 14f7dd9423de2bf36643837b190347be197147bf +Subproject commit 38230f509279ec3c2385ad9660dc0286868045b5 diff --git a/destrum/include/destrum/ObjectModel/Component.h b/destrum/include/destrum/ObjectModel/Component.h index 2db3990..75f4266 100644 --- a/destrum/include/destrum/ObjectModel/Component.h +++ b/destrum/include/destrum/ObjectModel/Component.h @@ -59,6 +59,16 @@ public: virtual void ResolveReferences(const ObjectMap&) { } + // Trigger callbacks called when this component's owner overlaps a sensor. + // Only called when the owner has a Collider set as a trigger (or overlaps one). + virtual void OnTriggerEnter(GameObject* other) { + (void)other; + } + + virtual void OnTriggerExit(GameObject* other) { + (void)other; + } + bool HasStarted{false}; protected: diff --git a/destrum/include/destrum/Physics/JoltPhysicsWorld.h b/destrum/include/destrum/Physics/JoltPhysicsWorld.h index f7b16a2..9feaf8d 100644 --- a/destrum/include/destrum/Physics/JoltPhysicsWorld.h +++ b/destrum/include/destrum/Physics/JoltPhysicsWorld.h @@ -55,6 +55,8 @@ public: float maxDistance, PhysicsRaycastHit& hit) const override; + std::vector ConsumeTriggerEvents() override; + private: class Impl; std::unique_ptr m_Impl; diff --git a/destrum/include/destrum/Physics/PhysicsSceneBridge.h b/destrum/include/destrum/Physics/PhysicsSceneBridge.h index f4aa49a..314a8d9 100644 --- a/destrum/include/destrum/Physics/PhysicsSceneBridge.h +++ b/destrum/include/destrum/Physics/PhysicsSceneBridge.h @@ -21,6 +21,7 @@ class PhysicsSceneBridge final { public: explicit PhysicsSceneBridge(std::unique_ptr world); + [[nodiscard]] bool IsValid() const { return m_World != nullptr; } [[nodiscard]] PhysicsWorld& GetWorld() { return *m_World; } [[nodiscard]] const PhysicsWorld& GetWorld() const { return *m_World; } diff --git a/destrum/include/destrum/Physics/PhysicsTypes.h b/destrum/include/destrum/Physics/PhysicsTypes.h index 88a6cc2..e147308 100644 --- a/destrum/include/destrum/Physics/PhysicsTypes.h +++ b/destrum/include/destrum/Physics/PhysicsTypes.h @@ -116,3 +116,9 @@ struct PhysicsBodyDesc { bool useGravity{true}; bool allowSleep{true}; }; + +struct TriggerEvent { + GameObject* owner{nullptr}; + GameObject* other{nullptr}; + bool entered{true}; +}; diff --git a/destrum/include/destrum/Physics/PhysicsWorld.h b/destrum/include/destrum/Physics/PhysicsWorld.h index ed48915..92ad1c3 100644 --- a/destrum/include/destrum/Physics/PhysicsWorld.h +++ b/destrum/include/destrum/Physics/PhysicsWorld.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -49,6 +51,9 @@ public: float maxDistance, PhysicsRaycastHit& hit) const = 0; + // Trigger events accumulated during the last Step(). + virtual std::vector ConsumeTriggerEvents() { return {}; } + private: std::unordered_set m_RegisteredRigidbodies; }; diff --git a/destrum/include/destrum/Scene/Scene.h b/destrum/include/destrum/Scene/Scene.h index 4efb887..756107f 100644 --- a/destrum/include/destrum/Scene/Scene.h +++ b/destrum/include/destrum/Scene/Scene.h @@ -3,6 +3,8 @@ #include +#include + #include #include #include @@ -12,6 +14,7 @@ class GameObject; class SceneSerializer; +class PhysicsWorld; class Scene final { friend Scene& SceneManager::CreateScene(const std::string& name); @@ -104,6 +107,7 @@ public: PhysicsSceneBridge& GetPhysics() { return m_Physics; } private: explicit Scene(const std::string& name); + explicit Scene(const std::string& name, std::unique_ptr physicsWorld); PhysicsSceneBridge m_Physics{std::make_unique()}; diff --git a/destrum/include/destrum/Scene/SceneManager.h b/destrum/include/destrum/Scene/SceneManager.h index 8c23758..7a0c8ba 100644 --- a/destrum/include/destrum/Scene/SceneManager.h +++ b/destrum/include/destrum/Scene/SceneManager.h @@ -16,6 +16,7 @@ public: Scene& CreateScene(const std::string& name); Scene& GetCurrentScene() const; + const std::vector>& GetActiveScenes() const { return m_activeScenes; } void Update(float dt); void FixedUpdate(float dt); @@ -34,7 +35,9 @@ public: void Destroy(); void SwitchScene(int index); - int GetActiveSceneId() const { return m_scenes.empty() ? -1 : m_ActiveSceneIndex; } + void AddActiveScene(int index); + void RemoveActiveScene(int index); + int GetActiveSceneId() const; [[nodiscard]] const std::vector>& GetScenes() const { return m_scenes; } @@ -47,8 +50,7 @@ private: SceneManager() = default; - int m_ActiveSceneIndex{0}; - + std::vector> m_activeScenes; std::vector> m_scenes; }; diff --git a/destrum/include/destrum/Serialization/SceneSerializer.h b/destrum/include/destrum/Serialization/SceneSerializer.h index 31b86a7..51f2e8a 100644 --- a/destrum/include/destrum/Serialization/SceneSerializer.h +++ b/destrum/include/destrum/Serialization/SceneSerializer.h @@ -2,13 +2,37 @@ #define DESTRUM_SCENESERIALIZER_H #include +#include +#include + +#include class Scene; class SceneSerializer final { public: + struct LoadResult { + bool success{false}; + std::string errorMessage; + nlohmann::json root; + }; + static bool Save(Scene& scene, const std::filesystem::path& path); - static bool Load(Scene& scene, const std::filesystem::path& path); + + static bool Load(Scene& scene, const std::filesystem::path& path, + std::function progressCallback = nullptr); + + // Thread-safe: parse and validate a scene file on any thread. + // No engine state is touched during parsing. + static LoadResult LoadSceneFile(const std::filesystem::path& path); + + // Main-thread-only: construct a scene from pre-validated JSON. + static bool ConstructScene(Scene& scene, LoadResult& result, + std::function progressCallback = nullptr); + + // Parse the scene file on a background thread. Call LoadFromJson on the + // main thread once the future is ready. + static std::future LoadSceneFileAsync(const std::filesystem::path& path); }; #endif //DESTRUM_SCENESERIALIZER_H diff --git a/destrum/src/Physics/JoltPhysicsWorld.cpp b/destrum/src/Physics/JoltPhysicsWorld.cpp index ac82aed..7852bdc 100644 --- a/destrum/src/Physics/JoltPhysicsWorld.cpp +++ b/destrum/src/Physics/JoltPhysicsWorld.cpp @@ -26,9 +26,12 @@ #include #include #include +#include #include #include #include +#include +#include #include @@ -276,6 +279,84 @@ namespace throw std::runtime_error("Cannot create Jolt body without a valid shape."); } } + + class SensorContactListener final : public ContactListener + { + public: + using BodyPair = std::pair; + + struct PairHash { + std::size_t operator()(const BodyPair& p) const { + return std::hash{}( + p.first.GetIndexAndSequenceNumber() ^ + (p.second.GetIndexAndSequenceNumber() << 7)); + } + }; + + struct PairData { + GameObject* objA{nullptr}; + GameObject* objB{nullptr}; + bool sensorA{false}; + bool sensorB{false}; + }; + + ValidateResult OnContactValidate(const Body&, const Body&, RVec3Arg, + const CollideShapeResult&) override + { + return ValidateResult::AcceptAllContactsForThisBodyPair; + } + + void OnContactAdded(const Body& body1, const Body& body2, + const ContactManifold&, ContactSettings& ioSettings) override + { + if (body1.IsSensor() || body2.IsSensor()) + { + PairData data; + data.objA = reinterpret_cast(body1.GetUserData()); + data.objB = reinterpret_cast(body2.GetUserData()); + data.sensorA = body1.IsSensor(); + data.sensorB = body2.IsSensor(); + std::lock_guard lock(m_Mutex); + m_Overlaps.insert_or_assign(MakePair(body1.GetID(), body2.GetID()), data); + } + } + + void OnContactPersisted(const Body& body1, const Body& body2, + const ContactManifold&, ContactSettings&) override + { + } + + void OnContactRemoved(const SubShapeIDPair& subShapePair) override + { + std::lock_guard lock(m_Mutex); + m_Overlaps.erase(MakePair(subShapePair.GetBody1ID(), subShapePair.GetBody2ID())); + } + + void Clear() + { + std::lock_guard lock(m_Mutex); + m_Overlaps.clear(); + } + + std::unordered_map SwapOverlaps() + { + std::lock_guard lock(m_Mutex); + return std::move(m_Overlaps); + } + + private: + static BodyPair MakePair(BodyID a, BodyID b) + { + if (a.GetIndexAndSequenceNumber() < b.GetIndexAndSequenceNumber()) + { + return {a, b}; + } + return {b, a}; + } + + std::mutex m_Mutex; + std::unordered_map m_Overlaps; + }; } // namespace class JoltPhysicsWorld::Impl @@ -307,6 +388,7 @@ public: m_ObjectVsBroadPhaseLayerFilter, m_ObjectLayerPairFilter); + m_PhysicsSystem.SetContactListener(&m_SensorListener); m_PhysicsSystem.SetGravity(ToJoltVec3(settings.gravity)); } @@ -426,6 +508,15 @@ public: collisionSteps, m_TempAllocator.get(), m_JobSystem.get()); + + ProcessSensorOverlaps(); + } + + std::vector ConsumeTriggerEvents() + { + std::vector events; + m_TriggerEvents.swap(events); + return events; } void SyncKinematicBodiesToPhysics() @@ -626,6 +717,43 @@ private: return {}; } + void ProcessSensorOverlaps() + { + const auto currentOverlaps = m_SensorListener.SwapOverlaps(); + + for (const auto& [pair, data] : currentOverlaps) + { + if (m_PreviousSensorOverlaps.find(pair) == m_PreviousSensorOverlaps.end()) + { + if (data.sensorA && data.objA) + { + m_TriggerEvents.push_back({data.objA, data.objB, true}); + } + if (data.sensorB && data.objB) + { + m_TriggerEvents.push_back({data.objB, data.objA, true}); + } + } + } + + for (const auto& [pair, data] : m_PreviousSensorOverlaps) + { + if (currentOverlaps.find(pair) == currentOverlaps.end()) + { + if (data.sensorA && data.objA) + { + m_TriggerEvents.push_back({data.objA, data.objB, false}); + } + if (data.sensorB && data.objB) + { + m_TriggerEvents.push_back({data.objB, data.objA, false}); + } + } + } + + m_PreviousSensorOverlaps = std::move(currentOverlaps); + } + Settings m_Settings{}; BPLayerInterfaceImpl m_BPLayerInterface{}; @@ -639,6 +767,10 @@ private: std::unordered_map m_Bodies; std::uint32_t m_NextHandle{0}; + + SensorContactListener m_SensorListener; + std::unordered_map m_PreviousSensorOverlaps; + std::vector m_TriggerEvents; }; JoltPhysicsWorld::JoltPhysicsWorld(const Settings& settings) @@ -710,3 +842,8 @@ bool JoltPhysicsWorld::Raycast(const glm::vec3& origin, { return m_Impl->Raycast(origin, direction, maxDistance, hit); } + +std::vector JoltPhysicsWorld::ConsumeTriggerEvents() +{ + return m_Impl->ConsumeTriggerEvents(); +} diff --git a/destrum/src/Physics/PhysicsSceneBridge.cpp b/destrum/src/Physics/PhysicsSceneBridge.cpp index e5eb7cf..0a7ebf1 100644 --- a/destrum/src/Physics/PhysicsSceneBridge.cpp +++ b/destrum/src/Physics/PhysicsSceneBridge.cpp @@ -1,6 +1,7 @@ #include #include +#include #include PhysicsSceneBridge::PhysicsSceneBridge(std::unique_ptr world) @@ -8,14 +9,16 @@ PhysicsSceneBridge::PhysicsSceneBridge(std::unique_ptr world) } void PhysicsSceneBridge::RegisterGameObject(GameObject& object) { + if (!m_World) return; if (auto* rb = object.GetComponent()) { - if (!rb->HasPhysicsBody() || rb->GetPhysicsWorld() != &GetWorld()) { + if (!rb->HasPhysicsBody() || rb->GetPhysicsWorld() != m_World.get()) { m_World->RegisterRigidbody(*rb); } } } void PhysicsSceneBridge::UnregisterGameObject(GameObject& object) { + if (!m_World) return; if (auto* rb = object.GetComponent()) { if (rb->GetPhysicsWorld() == m_World.get()) { m_World->UnregisterRigidbody(*rb); @@ -24,13 +27,29 @@ void PhysicsSceneBridge::UnregisterGameObject(GameObject& object) { } void PhysicsSceneBridge::RefreshGameObject(GameObject& object) { + if (!m_World) return; if (auto* rb = object.GetComponent()) { m_World->RefreshRigidbody(*rb); } } void PhysicsSceneBridge::FixedUpdate(float fixedDt) { + if (!m_World) return; m_World->SyncKinematicBodiesToPhysics(); m_World->Step(fixedDt); + + for (const auto& event : m_World->ConsumeTriggerEvents()) { + if (event.owner == nullptr) continue; + for (const auto& component : event.owner->GetComponents()) { + if (component && !component->IsBeingDestroyed() && component->isEnabled()) { + if (event.entered) { + component->OnTriggerEnter(event.other); + } else { + component->OnTriggerExit(event.other); + } + } + } + } + m_World->SyncDynamicBodiesToTransforms(); } diff --git a/destrum/src/Scene/Scene.cpp b/destrum/src/Scene/Scene.cpp index 60543ee..bea7b92 100644 --- a/destrum/src/Scene/Scene.cpp +++ b/destrum/src/Scene/Scene.cpp @@ -23,7 +23,12 @@ namespace { } Scene::Scene(const std::string& name) - : m_name(name), + : Scene(name, std::make_unique()) { +} + +Scene::Scene(const std::string& name, std::unique_ptr physicsWorld) + : m_Physics(std::move(physicsWorld)), + m_name(name), m_id(++m_idCounter) { } diff --git a/destrum/src/Scene/SceneManager.cpp b/destrum/src/Scene/SceneManager.cpp index 032b268..f7222c7 100644 --- a/destrum/src/Scene/SceneManager.cpp +++ b/destrum/src/Scene/SceneManager.cpp @@ -8,50 +8,40 @@ #include Scene& SceneManager::GetCurrentScene() const { - if (m_scenes.empty()) { - throw std::out_of_range("No scenes are available"); + if (m_activeScenes.empty()) { + throw std::out_of_range("No active scenes are available"); } - - if (m_ActiveSceneIndex < 0 || m_ActiveSceneIndex >= static_cast(m_scenes.size())) { - throw std::out_of_range("Active scene index is invalid"); - } - - return *m_scenes[static_cast(m_ActiveSceneIndex)]; + return *m_activeScenes.front(); } void SceneManager::Update(float dt) { - if (m_scenes.empty()) return; - (void)GetCurrentScene(); - const auto scene = m_scenes.at(static_cast(m_ActiveSceneIndex)); - scene->Update(dt); + for (const auto& scene : m_activeScenes) { + scene->Update(dt); + } } void SceneManager::FixedUpdate(float dt) { - if (m_scenes.empty()) return; - (void)GetCurrentScene(); - const auto scene = m_scenes.at(static_cast(m_ActiveSceneIndex)); - scene->FixedUpdate(dt); + for (const auto& scene : m_activeScenes) { + scene->FixedUpdate(dt); + } } void SceneManager::LateUpdate(float dt) { - if (m_scenes.empty()) return; - (void)GetCurrentScene(); - const auto scene = m_scenes.at(static_cast(m_ActiveSceneIndex)); - scene->LateUpdate(dt); + for (const auto& scene : m_activeScenes) { + scene->LateUpdate(dt); + } } void SceneManager::Render(const RenderContext& ctx) { - if (m_scenes.empty()) return; - (void)GetCurrentScene(); - const auto scene = m_scenes.at(static_cast(m_ActiveSceneIndex)); - scene->Render(ctx); + for (const auto& scene : m_activeScenes) { + scene->Render(ctx); + } } void SceneManager::RenderImgui() { - if (m_scenes.empty()) return; - (void)GetCurrentScene(); - const auto scene = m_scenes.at(static_cast(m_ActiveSceneIndex)); - scene->RenderImgui(); + for (const auto& scene : m_activeScenes) { + scene->RenderImgui(); + } } void SceneManager::HandleGameObjectDestroy() { @@ -61,7 +51,7 @@ void SceneManager::HandleGameObjectDestroy() { } void SceneManager::DestroyGameObjects() { - for (const auto& scene: m_scenes) { + for (const auto& scene : m_scenes) { scene->DestroyGameObjects(); } } @@ -73,37 +63,22 @@ void SceneManager::UnloadAllScenes() { } void SceneManager::HandleSceneDestroy() { - const std::shared_ptr activeScene = - m_ActiveSceneIndex >= 0 && - m_ActiveSceneIndex < static_cast(m_scenes.size()) - ? m_scenes[static_cast(m_ActiveSceneIndex)] - : nullptr; - for (auto it = m_scenes.begin(); it != m_scenes.end();) { if ((*it)->IsBeingUnloaded()) { + const auto activeIt = std::find(m_activeScenes.begin(), m_activeScenes.end(), *it); + if (activeIt != m_activeScenes.end()) { + m_activeScenes.erase(activeIt); + } it = m_scenes.erase(it); } else { ++it; } } - if (m_scenes.empty()) { - m_ActiveSceneIndex = 0; - return; - } - - if (activeScene != nullptr) { - const auto activeIt = std::find(m_scenes.begin(), m_scenes.end(), activeScene); - if (activeIt != m_scenes.end()) { - m_ActiveSceneIndex = static_cast(std::distance(m_scenes.begin(), activeIt)); - return; - } - } - - m_ActiveSceneIndex = std::clamp( - m_ActiveSceneIndex, - 0, - static_cast(m_scenes.size()) - 1); + // Remove any stale active scenes that are no longer in the scene list. + std::erase_if(m_activeScenes, [this](const auto& scene) { + return std::find(m_scenes.begin(), m_scenes.end(), scene) == m_scenes.end(); + }); } void SceneManager::HandleScene() { @@ -113,7 +88,6 @@ void SceneManager::HandleScene() { void SceneManager::Destroy() { if (m_scenes.empty()) { - m_ActiveSceneIndex = 0; return; } @@ -124,25 +98,60 @@ void SceneManager::Destroy() { } void SceneManager::SwitchScene(int index) { - // InputManager::GetInstance().RemoveAllBindings(); - if (index < 0 || index >= static_cast(m_scenes.size())) { throw std::out_of_range("Scene index out of range"); } - if (index == m_ActiveSceneIndex) { - return; - } - m_scenes[static_cast(m_ActiveSceneIndex)]->UnloadBindings(); - m_ActiveSceneIndex = index; - m_scenes[static_cast(m_ActiveSceneIndex)]->LoadBindings(); + for (const auto& scene : m_activeScenes) { + scene->UnloadBindings(); + } + m_activeScenes.clear(); + m_activeScenes.push_back(m_scenes[static_cast(index)]); + m_activeScenes.back()->LoadBindings(); } -Scene &SceneManager::CreateScene(const std::string &name) { +void SceneManager::AddActiveScene(int index) { + if (index < 0 || index >= static_cast(m_scenes.size())) { + throw std::out_of_range("Scene index out of range"); + } + + const auto& scene = m_scenes[static_cast(index)]; + if (std::find(m_activeScenes.begin(), m_activeScenes.end(), scene) == m_activeScenes.end()) { + scene->LoadBindings(); + m_activeScenes.push_back(scene); + } +} + +void SceneManager::RemoveActiveScene(int index) { + if (index < 0 || index >= static_cast(m_scenes.size())) { + throw std::out_of_range("Scene index out of range"); + } + + const auto& scene = m_scenes[static_cast(index)]; + const auto it = std::find(m_activeScenes.begin(), m_activeScenes.end(), scene); + if (it != m_activeScenes.end()) { + (*it)->UnloadBindings(); + m_activeScenes.erase(it); + } +} + +int SceneManager::GetActiveSceneId() const { + if (m_scenes.empty() || m_activeScenes.empty()) { + return -1; + } + + const auto it = std::find(m_scenes.begin(), m_scenes.end(), m_activeScenes.front()); + if (it == m_scenes.end()) { + return -1; + } + return static_cast(std::distance(m_scenes.begin(), it)); +} + +Scene& SceneManager::CreateScene(const std::string& name) { const auto scene = std::shared_ptr(new Scene(name)); m_scenes.push_back(scene); if (m_scenes.size() == 1) { - m_ActiveSceneIndex = 0; + m_activeScenes.push_back(scene); } return *scene; -} +} \ No newline at end of file diff --git a/destrum/src/Serialization/SceneSerializer.cpp b/destrum/src/Serialization/SceneSerializer.cpp index ccf8d5e..ad9c239 100644 --- a/destrum/src/Serialization/SceneSerializer.cpp +++ b/destrum/src/Serialization/SceneSerializer.cpp @@ -16,8 +16,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -201,6 +203,78 @@ namespace { return true; } + + [[nodiscard]] bool AssetFileExists(const std::string& path) { + if (path.empty()) { + return true; + } + + std::filesystem::path fullPath; + if (path.find("://") != std::string::npos) { + try { + fullPath = AssetFS::GetInstance().GetFullPath(path); + } catch (const std::exception&) { + return false; + } + } else { + fullPath = path; + } + + return std::filesystem::exists(fullPath); + } + + void ValidateAssetReferences(const json& root, const std::filesystem::path& scenePath) { + const auto report = [&](const std::string& objectName, const std::string& detail) { + std::cerr << "Scene asset warning (" << scenePath << "): " << objectName + << " references \"" << detail << "\" which could not be found\n"; + }; + + for (const auto& objectJson : root.at("objects")) { + const std::string objectName = objectJson.value("name", "GameObject"); + if (!objectJson.contains("components")) { + continue; + } + + for (const auto& componentJson : objectJson.at("components")) { + if (!componentJson.contains("data")) { + continue; + } + const auto& data = componentJson.at("data"); + + // MeshRendererComponent: meshKey / materialKey are asset cache keys. + for (const char* key : {"meshKey", "materialKey"}) { + if (data.contains(key) && data.at(key).is_string()) { + const std::string cacheKey = data.at(key).get(); + if (cacheKey.empty()) continue; + const auto asset = AssetReference::fromCacheKey(cacheKey); + if (asset && !AssetFileExists(asset->path)) { + report(objectName, cacheKey); + } + } + } + + // Generic: any nested object with a "path" field is an AssetReference. + std::function walk = [&](const json& node) { + if (node.is_object()) { + if (node.contains("path") && node.at("path").is_string()) { + const std::string path = node.at("path").get(); + if (!path.empty() && !AssetFileExists(path)) { + report(objectName, path); + } + } + for (auto it = node.begin(); it != node.end(); ++it) { + walk(it.value()); + } + } else if (node.is_array()) { + for (const auto& element : node) { + walk(element); + } + } + }; + walk(data); + } + } + } } bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) { @@ -290,30 +364,45 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) { return true; } -bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) { +SceneSerializer::LoadResult SceneSerializer::LoadSceneFile(const std::filesystem::path& path) { + LoadResult result; + + std::ifstream file(path); + if (!file.is_open()) { + result.errorMessage = "Failed to open scene file for reading: " + path.string(); + std::cerr << result.errorMessage << '\n'; + return result; + } + + try { + file >> result.root; + if (!ValidateSceneJson(result.root)) { + result.errorMessage = "Invalid scene data: " + path.string(); + std::cerr << result.errorMessage << '\n'; + return result; + } + } catch (const std::exception& exception) { + result.errorMessage = "Failed to parse scene file: " + std::string(exception.what()); + std::cerr << result.errorMessage << '\n'; + return result; + } + + ValidateAssetReferences(result.root, path); + result.success = true; + return result; +} + +bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result, + std::function progressCallback) { if (scene.IsIterating()) { std::cerr << "Cannot load a scene during an update or render phase: " << scene.GetName() << '\n'; return false; } - RegisterEngineComponents(); - - std::ifstream file(path); - if (!file.is_open()) { - std::cerr << "Failed to open scene file for reading: " << path << '\n'; - return false; - } - - json root; - try { - file >> root; - if (!ValidateSceneJson(root)) { - std::cerr << "Invalid scene data: " << path << '\n'; - return false; - } - } catch (const std::exception& exception) { - std::cerr << "Failed to parse scene file: " << exception.what() << '\n'; + if (!result.success) { + std::cerr << "Cannot construct scene from a failed load result: " + << result.errorMessage << '\n'; return false; } @@ -322,9 +411,16 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) { return false; } + RegisterEngineComponents(); + if (progressCallback) progressCallback(0.35f); + + const json& root = result.root; + // Build the replacement separately. The current scene is not touched // until all objects, transforms, and components have loaded successfully. - Scene stagingScene(scene.GetName()); + // No physics world is created for the staging scene - physics bodies are + // registered directly in the live scene's world once loading succeeds. + Scene stagingScene(scene.GetName(), nullptr); std::unordered_map idMap; std::vector objectJsonList; std::vector registeredObjects; @@ -343,7 +439,16 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) { objectJsonList.push_back(&objectJson); } - stagingScene.CommitPendingAdditions(); + if (progressCallback) progressCallback(0.4f); + + // Move objects from pending to objects list without physics registration + // (staging scene has no physics world). + for (auto& pending : stagingScene.m_pendingAdditions) { + if (pending) { + stagingScene.m_objects.emplace_back(std::move(pending)); + } + } + stagingScene.m_pendingAdditions.clear(); for (const json* objectJson : objectJsonList) { const ObjectId id = objectJson->at("id").get(); @@ -392,6 +497,8 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) { } } + if (progressCallback) progressCallback(0.5f); + for (const json* objectJson : objectJsonList) { GameObject* object = idMap.at(objectJson->at("id").get()); if (!objectJson->contains("components")) { @@ -412,6 +519,8 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) { } } + if (progressCallback) progressCallback(0.6f); + for (const auto& objectPtr : stagingScene.GetObjects()) { if (!objectPtr) { continue; @@ -424,6 +533,8 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) { } } + if (progressCallback) progressCallback(0.7f); + // Register the replacement bodies in the live physics world before // touching the current scene. If any body fails, the old scene and // its physics state can remain intact. @@ -438,6 +549,8 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) { registeredObjects.push_back(objectPtr.get()); } } + + if (progressCallback) progressCallback(0.8f); } catch (const std::exception& exception) { std::cerr << "Failed to load scene: " << exception.what() << '\n'; for (GameObject* object : registeredObjects) { @@ -449,6 +562,7 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) { return false; } + scene.UnloadBindings(); scene.RemoveAll(); scene.m_objects = std::move(stagingScene.m_objects); scene.m_pendingAdditions = std::move(stagingScene.m_pendingAdditions); @@ -456,6 +570,8 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) { scene.m_name = root.at("name").get(); } + if (progressCallback) progressCallback(0.9f); + for (const auto& object : scene.m_objects) { if (object) { object->SetScene(&scene); @@ -467,5 +583,27 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) { } } + if (progressCallback) progressCallback(1.0f); return true; } + +bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path, + std::function progressCallback) { + if (progressCallback) progressCallback(0.0f); + + auto result = LoadSceneFile(path); + if (!result.success) { + if (progressCallback) progressCallback(1.0f); + return false; + } + + if (progressCallback) progressCallback(0.3f); + return ConstructScene(scene, result, std::move(progressCallback)); +} + +std::future SceneSerializer::LoadSceneFileAsync( + const std::filesystem::path& path) { + return std::async(std::launch::async, [path]() { + return LoadSceneFile(path); + }); +} diff --git a/lightkeeper/CMakeLists.txt b/lightkeeper/CMakeLists.txt index 60c9621..83f02be 100644 --- a/lightkeeper/CMakeLists.txt +++ b/lightkeeper/CMakeLists.txt @@ -9,6 +9,7 @@ set(GAME_SRC src/components/GameComponentRegistry.cpp src/components/PrintComponent.cpp + src/components/TriggerLoggerComponent.cpp ) add_executable(lightkeeper ${GAME_SRC}) diff --git a/lightkeeper/include/components/GameComponentList.h b/lightkeeper/include/components/GameComponentList.h index 22303c1..ab6de98 100644 --- a/lightkeeper/include/components/GameComponentList.h +++ b/lightkeeper/include/components/GameComponentList.h @@ -2,8 +2,10 @@ #define LIGHTKEEPER_GAMECOMPONENTLIST_H #include +#include #define LIGHTKEEPER_GAME_COMPONENTS(X) \ - X(PrintComponent) + X(PrintComponent) \ + X(TriggerLoggerComponent) #endif // LIGHTKEEPER_GAMECOMPONENTLIST_H diff --git a/lightkeeper/include/components/TriggerLoggerComponent.h b/lightkeeper/include/components/TriggerLoggerComponent.h new file mode 100644 index 0000000..144829b --- /dev/null +++ b/lightkeeper/include/components/TriggerLoggerComponent.h @@ -0,0 +1,22 @@ +#ifndef DESTRUM_TRIGGERLOGGERCOMPONENT_H +#define DESTRUM_TRIGGERLOGGERCOMPONENT_H +#include "destrum/ObjectModel/Component.h" + +class TriggerLoggerComponent: public Component { +public: + TriggerLoggerComponent(GameObject& parent); + + std::string GetTypeName() const override { return "TriggerLoggerComponent"; } + + void OnTriggerEnter(GameObject* other) override; + void OnTriggerExit(GameObject* other) override; + + nlohmann::json Serialize() const override; + void Deserialize(const nlohmann::json&) override; + + int triggerEnterCount{0}; + int triggerExitCount{0}; + std::string lastOtherName; +}; + +#endif //DESTRUM_TRIGGERLOGGERCOMPONENT_H \ No newline at end of file diff --git a/lightkeeper/src/Lightkeeper.cpp b/lightkeeper/src/Lightkeeper.cpp index 94e8330..2bf9c29 100644 --- a/lightkeeper/src/Lightkeeper.cpp +++ b/lightkeeper/src/Lightkeeper.cpp @@ -18,6 +18,7 @@ #include "destrum/ObjectModel/GameObject.h" #include "destrum/Util/ModelDoc.h" #include "destrum/Components/Animator.h" +#include #include @@ -433,9 +434,15 @@ void LightKeeper::customInit() sphereMaterial = resources.materials().addSimpleColorMaterial({0.5f, 0.3f, 0.8f}, "Blue"); - auto obj = scene.CreateGameObject("GameObject"); - auto printComp = obj->AddComponent(); - printComp->SetMessage("Testing"); + // Trigger zone: a static box with a TriggerLoggerComponent. + // Spheres spawned via the "Spawn ball" button fall through it. + auto triggerObj = scene.CreateGameObject("TriggerBox"); + auto triggerBox = triggerObj->AddComponent(glm::vec3{3.0f, 3.0f, 3.0f}); + triggerBox->SetTrigger(true); + auto triggerRb = triggerObj->AddComponent(); + triggerRb->SetType(RigidbodyType::Static); + triggerObj->AddComponent(); + triggerObj->GetTransform().SetWorldPosition({0.0f, 0.0f, 0.0f}); } diff --git a/lightkeeper/src/components/TriggerLoggerComponent.cpp b/lightkeeper/src/components/TriggerLoggerComponent.cpp new file mode 100644 index 0000000..f0d2e8c --- /dev/null +++ b/lightkeeper/src/components/TriggerLoggerComponent.cpp @@ -0,0 +1,45 @@ +#include + +#include "spdlog/spdlog.h" +#include + +TriggerLoggerComponent::TriggerLoggerComponent(GameObject& parent) + : Component(parent, "TriggerLoggerComponent") {} + +void TriggerLoggerComponent::OnTriggerEnter(GameObject* other) { + ++triggerEnterCount; + lastOtherName = other ? other->GetName() : "null"; + spdlog::info( + "TriggerLogger: ENTER {} -> {} (enters: {}, exits: {})", + GetGameObject()->GetName(), + lastOtherName, + triggerEnterCount, + triggerExitCount); +} + +void TriggerLoggerComponent::OnTriggerExit(GameObject* other) { + ++triggerExitCount; + lastOtherName = other ? other->GetName() : "null"; + spdlog::info( + "TriggerLogger: EXIT {} -> {} (enters: {}, exits: {})", + GetGameObject()->GetName(), + lastOtherName, + triggerEnterCount, + triggerExitCount); +} + +nlohmann::json TriggerLoggerComponent::Serialize() const { + return { + {"triggerEnterCount", triggerEnterCount}, + {"triggerExitCount", triggerExitCount} + }; +} + +void TriggerLoggerComponent::Deserialize(const nlohmann::json& data) { + if (data.contains("triggerEnterCount")) { + triggerEnterCount = data.at("triggerEnterCount").get(); + } + if (data.contains("triggerExitCount")) { + triggerExitCount = data.at("triggerExitCount").get(); + } +} \ No newline at end of file diff --git a/tests/destrum_tests.cpp b/tests/destrum_tests.cpp index 36687df..f1bf47b 100644 --- a/tests/destrum_tests.cpp +++ b/tests/destrum_tests.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -483,6 +484,65 @@ namespace { check(!AssetReference::fromCacheKey("CharacterMedium.fbx").has_value(), "plain cache names must not be treated as file-backed assets"); } + + class TriggerTracker final : public Component { + public: + explicit TriggerTracker(GameObject& owner) + : Component(owner, "TriggerTracker") {} + + void Update(float) override {} + std::string GetTypeName() const override { return "TriggerTracker"; } + + void OnTriggerEnter(GameObject* other) override { + ++enterCount; + lastOther = other; + enterObjectName = other ? other->GetName() : "null"; + } + + void OnTriggerExit(GameObject* other) override { + ++exitCount; + } + + int enterCount{0}; + int exitCount{0}; + GameObject* lastOther{nullptr}; + std::string enterObjectName; + }; + + void testTriggerZone() + { + Scene& scene = SceneManager::GetInstance().CreateScene("trigger-test"); + + // Trigger box at origin (Static rigidbody + BoxCollider as trigger). + GameObject* triggerObj = scene.CreateGameObject("TriggerZone"); + triggerObj->AddComponent(glm::vec3{3.0f, 3.0f, 3.0f}); + triggerObj->GetComponent()->SetTrigger(true); + auto* triggerRb = triggerObj->AddComponent(); + triggerRb->SetType(RigidbodyType::Static); + auto* tracker = triggerObj->AddComponent(); + + // Falling sphere above the trigger. + GameObject* sphere = scene.CreateGameObject("FallingSphere"); + sphere->AddComponent(0.5f); + sphere->AddComponent(); + sphere->GetTransform().SetWorldPosition({0.0f, 8.0f, 0.0f}); + + scene.CommitPendingAdditions(); + + // Run physics for enough steps that the sphere falls into the trigger. + for (int step = 0; step < 120; ++step) { + scene.FixedUpdate(1.0f / 60.0f); + } + + check(tracker->enterCount >= 1, + "falling sphere must trigger OnTriggerEnter at least once"); + check(tracker->exitCount >= 1, + "falling sphere passing through trigger must exit"); + check(tracker->enterObjectName == "FallingSphere", + "OnTriggerEnter must report the correct entering object"); + + SceneManager::GetInstance().Destroy(); + } } int main() @@ -503,6 +563,7 @@ int main() testAnimatorAssetReferences(); testSimpleColorMaterial(); testAssetReferenceCacheKeys(); + testTriggerZone(); std::cout << "destrum tests passed\n"; return EXIT_SUCCESS; }