feat: implement contact listeners

This commit is contained in:
2026-09-02 21:42:49 +02:00
parent 287e5c6885
commit d515a36403
20 changed files with 595 additions and 95 deletions
+1 -1
Submodule TheChef updated: 14f7dd9423...38230f5092
@@ -59,6 +59,16 @@ public:
virtual void ResolveReferences(const ObjectMap&) { 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}; bool HasStarted{false};
protected: protected:
@@ -55,6 +55,8 @@ public:
float maxDistance, float maxDistance,
PhysicsRaycastHit& hit) const override; PhysicsRaycastHit& hit) const override;
std::vector<TriggerEvent> ConsumeTriggerEvents() override;
private: private:
class Impl; class Impl;
std::unique_ptr<Impl> m_Impl; std::unique_ptr<Impl> m_Impl;
@@ -21,6 +21,7 @@ class PhysicsSceneBridge final {
public: public:
explicit PhysicsSceneBridge(std::unique_ptr<PhysicsWorld> world); explicit PhysicsSceneBridge(std::unique_ptr<PhysicsWorld> world);
[[nodiscard]] bool IsValid() const { return m_World != nullptr; }
[[nodiscard]] PhysicsWorld& GetWorld() { return *m_World; } [[nodiscard]] PhysicsWorld& GetWorld() { return *m_World; }
[[nodiscard]] const PhysicsWorld& GetWorld() const { return *m_World; } [[nodiscard]] const PhysicsWorld& GetWorld() const { return *m_World; }
@@ -116,3 +116,9 @@ struct PhysicsBodyDesc {
bool useGravity{true}; bool useGravity{true};
bool allowSleep{true}; bool allowSleep{true};
}; };
struct TriggerEvent {
GameObject* owner{nullptr};
GameObject* other{nullptr};
bool entered{true};
};
@@ -1,5 +1,7 @@
#pragma once #pragma once
#include <vector>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <unordered_set> #include <unordered_set>
@@ -49,6 +51,9 @@ public:
float maxDistance, float maxDistance,
PhysicsRaycastHit& hit) const = 0; PhysicsRaycastHit& hit) const = 0;
// Trigger events accumulated during the last Step().
virtual std::vector<TriggerEvent> ConsumeTriggerEvents() { return {}; }
private: private:
std::unordered_set<Rigidbody*> m_RegisteredRigidbodies; std::unordered_set<Rigidbody*> m_RegisteredRigidbodies;
}; };
+4
View File
@@ -3,6 +3,8 @@
#include <functional> #include <functional>
#include <memory>
#include <destrum/Event.h> #include <destrum/Event.h>
#include <destrum/ObjectModel/ObjectId.h> #include <destrum/ObjectModel/ObjectId.h>
#include <destrum/Scene/SceneManager.h> #include <destrum/Scene/SceneManager.h>
@@ -12,6 +14,7 @@
class GameObject; class GameObject;
class SceneSerializer; class SceneSerializer;
class PhysicsWorld;
class Scene final { class Scene final {
friend Scene& SceneManager::CreateScene(const std::string& name); friend Scene& SceneManager::CreateScene(const std::string& name);
@@ -104,6 +107,7 @@ public:
PhysicsSceneBridge& GetPhysics() { return m_Physics; } PhysicsSceneBridge& GetPhysics() { return m_Physics; }
private: private:
explicit Scene(const std::string& name); explicit Scene(const std::string& name);
explicit Scene(const std::string& name, std::unique_ptr<PhysicsWorld> physicsWorld);
PhysicsSceneBridge m_Physics{std::make_unique<JoltPhysicsWorld>()}; PhysicsSceneBridge m_Physics{std::make_unique<JoltPhysicsWorld>()};
+5 -3
View File
@@ -16,6 +16,7 @@ public:
Scene& CreateScene(const std::string& name); Scene& CreateScene(const std::string& name);
Scene& GetCurrentScene() const; Scene& GetCurrentScene() const;
const std::vector<std::shared_ptr<Scene>>& GetActiveScenes() const { return m_activeScenes; }
void Update(float dt); void Update(float dt);
void FixedUpdate(float dt); void FixedUpdate(float dt);
@@ -34,7 +35,9 @@ public:
void Destroy(); void Destroy();
void SwitchScene(int index); 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<std::shared_ptr<Scene>>& GetScenes() const { return m_scenes; } [[nodiscard]] const std::vector<std::shared_ptr<Scene>>& GetScenes() const { return m_scenes; }
@@ -47,8 +50,7 @@ private:
SceneManager() = default; SceneManager() = default;
int m_ActiveSceneIndex{0}; std::vector<std::shared_ptr<Scene>> m_activeScenes;
std::vector<std::shared_ptr<Scene>> m_scenes; std::vector<std::shared_ptr<Scene>> m_scenes;
}; };
@@ -2,13 +2,37 @@
#define DESTRUM_SCENESERIALIZER_H #define DESTRUM_SCENESERIALIZER_H
#include <filesystem> #include <filesystem>
#include <functional>
#include <future>
#include <nlohmann/json.hpp>
class Scene; class Scene;
class SceneSerializer final { class SceneSerializer final {
public: public:
struct LoadResult {
bool success{false};
std::string errorMessage;
nlohmann::json root;
};
static bool Save(Scene& scene, const std::filesystem::path& path); 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<void(float)> 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<void(float)> progressCallback = nullptr);
// Parse the scene file on a background thread. Call LoadFromJson on the
// main thread once the future is ready.
static std::future<LoadResult> LoadSceneFileAsync(const std::filesystem::path& path);
}; };
#endif //DESTRUM_SCENESERIALIZER_H #endif //DESTRUM_SCENESERIALIZER_H
+137
View File
@@ -26,9 +26,12 @@
#include <cstdarg> #include <cstdarg>
#include <cstdio> #include <cstdio>
#include <memory> #include <memory>
#include <mutex>
#include <stdexcept> #include <stdexcept>
#include <thread> #include <thread>
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
#include <vector>
#include <glm/gtx/norm.hpp> #include <glm/gtx/norm.hpp>
@@ -276,6 +279,84 @@ namespace
throw std::runtime_error("Cannot create Jolt body without a valid shape."); throw std::runtime_error("Cannot create Jolt body without a valid shape.");
} }
} }
class SensorContactListener final : public ContactListener
{
public:
using BodyPair = std::pair<BodyID, BodyID>;
struct PairHash {
std::size_t operator()(const BodyPair& p) const {
return std::hash<uint32_t>{}(
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<GameObject*>(body1.GetUserData());
data.objB = reinterpret_cast<GameObject*>(body2.GetUserData());
data.sensorA = body1.IsSensor();
data.sensorB = body2.IsSensor();
std::lock_guard<std::mutex> 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<std::mutex> lock(m_Mutex);
m_Overlaps.erase(MakePair(subShapePair.GetBody1ID(), subShapePair.GetBody2ID()));
}
void Clear()
{
std::lock_guard<std::mutex> lock(m_Mutex);
m_Overlaps.clear();
}
std::unordered_map<BodyPair, PairData, PairHash> SwapOverlaps()
{
std::lock_guard<std::mutex> 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<BodyPair, PairData, PairHash> m_Overlaps;
};
} // namespace } // namespace
class JoltPhysicsWorld::Impl class JoltPhysicsWorld::Impl
@@ -307,6 +388,7 @@ public:
m_ObjectVsBroadPhaseLayerFilter, m_ObjectVsBroadPhaseLayerFilter,
m_ObjectLayerPairFilter); m_ObjectLayerPairFilter);
m_PhysicsSystem.SetContactListener(&m_SensorListener);
m_PhysicsSystem.SetGravity(ToJoltVec3(settings.gravity)); m_PhysicsSystem.SetGravity(ToJoltVec3(settings.gravity));
} }
@@ -426,6 +508,15 @@ public:
collisionSteps, collisionSteps,
m_TempAllocator.get(), m_TempAllocator.get(),
m_JobSystem.get()); m_JobSystem.get());
ProcessSensorOverlaps();
}
std::vector<TriggerEvent> ConsumeTriggerEvents()
{
std::vector<TriggerEvent> events;
m_TriggerEvents.swap(events);
return events;
} }
void SyncKinematicBodiesToPhysics() void SyncKinematicBodiesToPhysics()
@@ -626,6 +717,43 @@ private:
return {}; 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{}; Settings m_Settings{};
BPLayerInterfaceImpl m_BPLayerInterface{}; BPLayerInterfaceImpl m_BPLayerInterface{};
@@ -639,6 +767,10 @@ private:
std::unordered_map<std::uint32_t, BodyRecord> m_Bodies; std::unordered_map<std::uint32_t, BodyRecord> m_Bodies;
std::uint32_t m_NextHandle{0}; std::uint32_t m_NextHandle{0};
SensorContactListener m_SensorListener;
std::unordered_map<SensorContactListener::BodyPair, SensorContactListener::PairData, SensorContactListener::PairHash> m_PreviousSensorOverlaps;
std::vector<TriggerEvent> m_TriggerEvents;
}; };
JoltPhysicsWorld::JoltPhysicsWorld(const Settings& settings) JoltPhysicsWorld::JoltPhysicsWorld(const Settings& settings)
@@ -710,3 +842,8 @@ bool JoltPhysicsWorld::Raycast(const glm::vec3& origin,
{ {
return m_Impl->Raycast(origin, direction, maxDistance, hit); return m_Impl->Raycast(origin, direction, maxDistance, hit);
} }
std::vector<TriggerEvent> JoltPhysicsWorld::ConsumeTriggerEvents()
{
return m_Impl->ConsumeTriggerEvents();
}
+20 -1
View File
@@ -1,6 +1,7 @@
#include <destrum/Physics/PhysicsSceneBridge.h> #include <destrum/Physics/PhysicsSceneBridge.h>
#include <destrum/Components/Physics/Rigidbody.h> #include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/ObjectModel/Component.h>
#include <destrum/ObjectModel/GameObject.h> #include <destrum/ObjectModel/GameObject.h>
PhysicsSceneBridge::PhysicsSceneBridge(std::unique_ptr<PhysicsWorld> world) PhysicsSceneBridge::PhysicsSceneBridge(std::unique_ptr<PhysicsWorld> world)
@@ -8,14 +9,16 @@ PhysicsSceneBridge::PhysicsSceneBridge(std::unique_ptr<PhysicsWorld> world)
} }
void PhysicsSceneBridge::RegisterGameObject(GameObject& object) { void PhysicsSceneBridge::RegisterGameObject(GameObject& object) {
if (!m_World) return;
if (auto* rb = object.GetComponent<Rigidbody>()) { if (auto* rb = object.GetComponent<Rigidbody>()) {
if (!rb->HasPhysicsBody() || rb->GetPhysicsWorld() != &GetWorld()) { if (!rb->HasPhysicsBody() || rb->GetPhysicsWorld() != m_World.get()) {
m_World->RegisterRigidbody(*rb); m_World->RegisterRigidbody(*rb);
} }
} }
} }
void PhysicsSceneBridge::UnregisterGameObject(GameObject& object) { void PhysicsSceneBridge::UnregisterGameObject(GameObject& object) {
if (!m_World) return;
if (auto* rb = object.GetComponent<Rigidbody>()) { if (auto* rb = object.GetComponent<Rigidbody>()) {
if (rb->GetPhysicsWorld() == m_World.get()) { if (rb->GetPhysicsWorld() == m_World.get()) {
m_World->UnregisterRigidbody(*rb); m_World->UnregisterRigidbody(*rb);
@@ -24,13 +27,29 @@ void PhysicsSceneBridge::UnregisterGameObject(GameObject& object) {
} }
void PhysicsSceneBridge::RefreshGameObject(GameObject& object) { void PhysicsSceneBridge::RefreshGameObject(GameObject& object) {
if (!m_World) return;
if (auto* rb = object.GetComponent<Rigidbody>()) { if (auto* rb = object.GetComponent<Rigidbody>()) {
m_World->RefreshRigidbody(*rb); m_World->RefreshRigidbody(*rb);
} }
} }
void PhysicsSceneBridge::FixedUpdate(float fixedDt) { void PhysicsSceneBridge::FixedUpdate(float fixedDt) {
if (!m_World) return;
m_World->SyncKinematicBodiesToPhysics(); m_World->SyncKinematicBodiesToPhysics();
m_World->Step(fixedDt); 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(); m_World->SyncDynamicBodiesToTransforms();
} }
+6 -1
View File
@@ -23,7 +23,12 @@ namespace {
} }
Scene::Scene(const std::string& name) Scene::Scene(const std::string& name)
: m_name(name), : Scene(name, std::make_unique<JoltPhysicsWorld>()) {
}
Scene::Scene(const std::string& name, std::unique_ptr<PhysicsWorld> physicsWorld)
: m_Physics(std::move(physicsWorld)),
m_name(name),
m_id(++m_idCounter) { m_id(++m_idCounter) {
} }
+64 -55
View File
@@ -8,51 +8,41 @@
#include <destrum/Util/DeltaTime.h> #include <destrum/Util/DeltaTime.h>
Scene& SceneManager::GetCurrentScene() const { Scene& SceneManager::GetCurrentScene() const {
if (m_scenes.empty()) { if (m_activeScenes.empty()) {
throw std::out_of_range("No scenes are available"); throw std::out_of_range("No active scenes are available");
} }
return *m_activeScenes.front();
if (m_ActiveSceneIndex < 0 || m_ActiveSceneIndex >= static_cast<int>(m_scenes.size())) {
throw std::out_of_range("Active scene index is invalid");
}
return *m_scenes[static_cast<std::size_t>(m_ActiveSceneIndex)];
} }
void SceneManager::Update(float dt) { void SceneManager::Update(float dt) {
if (m_scenes.empty()) return; for (const auto& scene : m_activeScenes) {
(void)GetCurrentScene();
const auto scene = m_scenes.at(static_cast<std::size_t>(m_ActiveSceneIndex));
scene->Update(dt); scene->Update(dt);
} }
}
void SceneManager::FixedUpdate(float dt) { void SceneManager::FixedUpdate(float dt) {
if (m_scenes.empty()) return; for (const auto& scene : m_activeScenes) {
(void)GetCurrentScene();
const auto scene = m_scenes.at(static_cast<std::size_t>(m_ActiveSceneIndex));
scene->FixedUpdate(dt); scene->FixedUpdate(dt);
} }
}
void SceneManager::LateUpdate(float dt) { void SceneManager::LateUpdate(float dt) {
if (m_scenes.empty()) return; for (const auto& scene : m_activeScenes) {
(void)GetCurrentScene();
const auto scene = m_scenes.at(static_cast<std::size_t>(m_ActiveSceneIndex));
scene->LateUpdate(dt); scene->LateUpdate(dt);
} }
}
void SceneManager::Render(const RenderContext& ctx) { void SceneManager::Render(const RenderContext& ctx) {
if (m_scenes.empty()) return; for (const auto& scene : m_activeScenes) {
(void)GetCurrentScene();
const auto scene = m_scenes.at(static_cast<std::size_t>(m_ActiveSceneIndex));
scene->Render(ctx); scene->Render(ctx);
} }
}
void SceneManager::RenderImgui() { void SceneManager::RenderImgui() {
if (m_scenes.empty()) return; for (const auto& scene : m_activeScenes) {
(void)GetCurrentScene();
const auto scene = m_scenes.at(static_cast<std::size_t>(m_ActiveSceneIndex));
scene->RenderImgui(); scene->RenderImgui();
} }
}
void SceneManager::HandleGameObjectDestroy() { void SceneManager::HandleGameObjectDestroy() {
for (const auto& scene : m_scenes) { for (const auto& scene : m_scenes) {
@@ -73,37 +63,22 @@ void SceneManager::UnloadAllScenes() {
} }
void SceneManager::HandleSceneDestroy() { void SceneManager::HandleSceneDestroy() {
const std::shared_ptr<Scene> activeScene =
m_ActiveSceneIndex >= 0 &&
m_ActiveSceneIndex < static_cast<int>(m_scenes.size())
? m_scenes[static_cast<std::size_t>(m_ActiveSceneIndex)]
: nullptr;
for (auto it = m_scenes.begin(); it != m_scenes.end();) { for (auto it = m_scenes.begin(); it != m_scenes.end();) {
if ((*it)->IsBeingUnloaded()) { 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); it = m_scenes.erase(it);
} else { } else {
++it; ++it;
} }
} }
if (m_scenes.empty()) { // Remove any stale active scenes that are no longer in the scene list.
m_ActiveSceneIndex = 0; std::erase_if(m_activeScenes, [this](const auto& scene) {
return; return std::find(m_scenes.begin(), m_scenes.end(), scene) == m_scenes.end();
} });
if (activeScene != nullptr) {
const auto activeIt = std::find(m_scenes.begin(), m_scenes.end(), activeScene);
if (activeIt != m_scenes.end()) {
m_ActiveSceneIndex = static_cast<int>(std::distance(m_scenes.begin(), activeIt));
return;
}
}
m_ActiveSceneIndex = std::clamp(
m_ActiveSceneIndex,
0,
static_cast<int>(m_scenes.size()) - 1);
} }
void SceneManager::HandleScene() { void SceneManager::HandleScene() {
@@ -113,7 +88,6 @@ void SceneManager::HandleScene() {
void SceneManager::Destroy() { void SceneManager::Destroy() {
if (m_scenes.empty()) { if (m_scenes.empty()) {
m_ActiveSceneIndex = 0;
return; return;
} }
@@ -124,25 +98,60 @@ void SceneManager::Destroy() {
} }
void SceneManager::SwitchScene(int index) { void SceneManager::SwitchScene(int index) {
// InputManager::GetInstance().RemoveAllBindings();
if (index < 0 || index >= static_cast<int>(m_scenes.size())) { if (index < 0 || index >= static_cast<int>(m_scenes.size())) {
throw std::out_of_range("Scene index out of range"); throw std::out_of_range("Scene index out of range");
} }
if (index == m_ActiveSceneIndex) {
return; for (const auto& scene : m_activeScenes) {
scene->UnloadBindings();
}
m_activeScenes.clear();
m_activeScenes.push_back(m_scenes[static_cast<std::size_t>(index)]);
m_activeScenes.back()->LoadBindings();
} }
m_scenes[static_cast<std::size_t>(m_ActiveSceneIndex)]->UnloadBindings(); void SceneManager::AddActiveScene(int index) {
m_ActiveSceneIndex = index; if (index < 0 || index >= static_cast<int>(m_scenes.size())) {
m_scenes[static_cast<std::size_t>(m_ActiveSceneIndex)]->LoadBindings(); throw std::out_of_range("Scene index out of range");
}
const auto& scene = m_scenes[static_cast<std::size_t>(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<int>(m_scenes.size())) {
throw std::out_of_range("Scene index out of range");
}
const auto& scene = m_scenes[static_cast<std::size_t>(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<int>(std::distance(m_scenes.begin(), it));
} }
Scene& SceneManager::CreateScene(const std::string& name) { Scene& SceneManager::CreateScene(const std::string& name) {
const auto scene = std::shared_ptr<Scene>(new Scene(name)); const auto scene = std::shared_ptr<Scene>(new Scene(name));
m_scenes.push_back(scene); m_scenes.push_back(scene);
if (m_scenes.size() == 1) { if (m_scenes.size() == 1) {
m_ActiveSceneIndex = 0; m_activeScenes.push_back(scene);
} }
return *scene; return *scene;
} }
+158 -20
View File
@@ -16,8 +16,10 @@
#include <destrum/ObjectModel/GameObject.h> #include <destrum/ObjectModel/GameObject.h>
#include <destrum/ObjectModel/ObjectId.h> #include <destrum/ObjectModel/ObjectId.h>
#include <destrum/ObjectModel/Transform.h> #include <destrum/ObjectModel/Transform.h>
#include <destrum/Assets/AssetReference.h>
#include <destrum/Components/Physics/Collider.h> #include <destrum/Components/Physics/Collider.h>
#include <destrum/Components/Physics/Rigidbody.h> #include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/Serialization/ComponentFactory.h> #include <destrum/Serialization/ComponentFactory.h>
#include <destrum/Serialization/ComponentRegistry.h> #include <destrum/Serialization/ComponentRegistry.h>
@@ -201,6 +203,78 @@ namespace {
return true; 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<std::string>();
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<void(const json&)> 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<std::string>();
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) { 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; 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<void(float)> progressCallback) {
if (scene.IsIterating()) { if (scene.IsIterating()) {
std::cerr << "Cannot load a scene during an update or render phase: " std::cerr << "Cannot load a scene during an update or render phase: "
<< scene.GetName() << '\n'; << scene.GetName() << '\n';
return false; return false;
} }
RegisterEngineComponents(); if (!result.success) {
std::cerr << "Cannot construct scene from a failed load result: "
std::ifstream file(path); << result.errorMessage << '\n';
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';
return false; return false;
} }
@@ -322,9 +411,16 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
return false; return false;
} }
RegisterEngineComponents();
if (progressCallback) progressCallback(0.35f);
const json& root = result.root;
// Build the replacement separately. The current scene is not touched // Build the replacement separately. The current scene is not touched
// until all objects, transforms, and components have loaded successfully. // 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<ObjectId, GameObject*> idMap; std::unordered_map<ObjectId, GameObject*> idMap;
std::vector<const json*> objectJsonList; std::vector<const json*> objectJsonList;
std::vector<GameObject*> registeredObjects; std::vector<GameObject*> registeredObjects;
@@ -343,7 +439,16 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
objectJsonList.push_back(&objectJson); 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) { for (const json* objectJson : objectJsonList) {
const ObjectId id = objectJson->at("id").get<ObjectId>(); const ObjectId id = objectJson->at("id").get<ObjectId>();
@@ -392,6 +497,8 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
} }
} }
if (progressCallback) progressCallback(0.5f);
for (const json* objectJson : objectJsonList) { for (const json* objectJson : objectJsonList) {
GameObject* object = idMap.at(objectJson->at("id").get<ObjectId>()); GameObject* object = idMap.at(objectJson->at("id").get<ObjectId>());
if (!objectJson->contains("components")) { 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()) { for (const auto& objectPtr : stagingScene.GetObjects()) {
if (!objectPtr) { if (!objectPtr) {
continue; 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 // Register the replacement bodies in the live physics world before
// touching the current scene. If any body fails, the old scene and // touching the current scene. If any body fails, the old scene and
// its physics state can remain intact. // 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()); registeredObjects.push_back(objectPtr.get());
} }
} }
if (progressCallback) progressCallback(0.8f);
} catch (const std::exception& exception) { } catch (const std::exception& exception) {
std::cerr << "Failed to load scene: " << exception.what() << '\n'; std::cerr << "Failed to load scene: " << exception.what() << '\n';
for (GameObject* object : registeredObjects) { for (GameObject* object : registeredObjects) {
@@ -449,6 +562,7 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
return false; return false;
} }
scene.UnloadBindings();
scene.RemoveAll(); scene.RemoveAll();
scene.m_objects = std::move(stagingScene.m_objects); scene.m_objects = std::move(stagingScene.m_objects);
scene.m_pendingAdditions = std::move(stagingScene.m_pendingAdditions); 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<std::string>(); scene.m_name = root.at("name").get<std::string>();
} }
if (progressCallback) progressCallback(0.9f);
for (const auto& object : scene.m_objects) { for (const auto& object : scene.m_objects) {
if (object) { if (object) {
object->SetScene(&scene); object->SetScene(&scene);
@@ -467,5 +583,27 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path) {
} }
} }
if (progressCallback) progressCallback(1.0f);
return true; return true;
} }
bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path,
std::function<void(float)> 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::LoadResult> SceneSerializer::LoadSceneFileAsync(
const std::filesystem::path& path) {
return std::async(std::launch::async, [path]() {
return LoadSceneFile(path);
});
}
+1
View File
@@ -9,6 +9,7 @@ set(GAME_SRC
src/components/GameComponentRegistry.cpp src/components/GameComponentRegistry.cpp
src/components/PrintComponent.cpp src/components/PrintComponent.cpp
src/components/TriggerLoggerComponent.cpp
) )
add_executable(lightkeeper ${GAME_SRC}) add_executable(lightkeeper ${GAME_SRC})
@@ -2,8 +2,10 @@
#define LIGHTKEEPER_GAMECOMPONENTLIST_H #define LIGHTKEEPER_GAMECOMPONENTLIST_H
#include <components/PrintComponent.h> #include <components/PrintComponent.h>
#include <components/TriggerLoggerComponent.h>
#define LIGHTKEEPER_GAME_COMPONENTS(X) \ #define LIGHTKEEPER_GAME_COMPONENTS(X) \
X(PrintComponent) X(PrintComponent) \
X(TriggerLoggerComponent)
#endif // LIGHTKEEPER_GAMECOMPONENTLIST_H #endif // LIGHTKEEPER_GAMECOMPONENTLIST_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
+10 -3
View File
@@ -18,6 +18,7 @@
#include "destrum/ObjectModel/GameObject.h" #include "destrum/ObjectModel/GameObject.h"
#include "destrum/Util/ModelDoc.h" #include "destrum/Util/ModelDoc.h"
#include "destrum/Components/Animator.h" #include "destrum/Components/Animator.h"
#include <components/TriggerLoggerComponent.h>
#include <filesystem> #include <filesystem>
@@ -433,9 +434,15 @@ void LightKeeper::customInit()
sphereMaterial = resources.materials().addSimpleColorMaterial({0.5f, 0.3f, 0.8f}, "Blue"); sphereMaterial = resources.materials().addSimpleColorMaterial({0.5f, 0.3f, 0.8f}, "Blue");
auto obj = scene.CreateGameObject("GameObject"); // Trigger zone: a static box with a TriggerLoggerComponent.
auto printComp = obj->AddComponent<PrintComponent>(); // Spheres spawned via the "Spawn ball" button fall through it.
printComp->SetMessage("Testing"); auto triggerObj = scene.CreateGameObject("TriggerBox");
auto triggerBox = triggerObj->AddComponent<BoxCollider>(glm::vec3{3.0f, 3.0f, 3.0f});
triggerBox->SetTrigger(true);
auto triggerRb = triggerObj->AddComponent<Rigidbody>();
triggerRb->SetType(RigidbodyType::Static);
triggerObj->AddComponent<TriggerLoggerComponent>();
triggerObj->GetTransform().SetWorldPosition({0.0f, 0.0f, 0.0f});
} }
@@ -0,0 +1,45 @@
#include <components/TriggerLoggerComponent.h>
#include "spdlog/spdlog.h"
#include <destrum/ObjectModel/GameObject.h>
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<int>();
}
if (data.contains("triggerExitCount")) {
triggerExitCount = data.at("triggerExitCount").get<int>();
}
}
+61
View File
@@ -20,6 +20,7 @@
#include <destrum/Physics/JoltPhysicsWorld.h> #include <destrum/Physics/JoltPhysicsWorld.h>
#include <destrum/Components/Physics/SphereCollider.h> #include <destrum/Components/Physics/SphereCollider.h>
#include <destrum/Components/Physics/BoxCollider.h> #include <destrum/Components/Physics/BoxCollider.h>
#include <destrum/Components/Physics/Collider.h>
#include <destrum/Components/Animator.h> #include <destrum/Components/Animator.h>
#include <destrum/Components/Physics/Rigidbody.h> #include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/Graphics/Material.h> #include <destrum/Graphics/Material.h>
@@ -483,6 +484,65 @@ namespace {
check(!AssetReference::fromCacheKey("CharacterMedium.fbx").has_value(), check(!AssetReference::fromCacheKey("CharacterMedium.fbx").has_value(),
"plain cache names must not be treated as file-backed assets"); "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<BoxCollider>(glm::vec3{3.0f, 3.0f, 3.0f});
triggerObj->GetComponent<BoxCollider>()->SetTrigger(true);
auto* triggerRb = triggerObj->AddComponent<Rigidbody>();
triggerRb->SetType(RigidbodyType::Static);
auto* tracker = triggerObj->AddComponent<TriggerTracker>();
// Falling sphere above the trigger.
GameObject* sphere = scene.CreateGameObject("FallingSphere");
sphere->AddComponent<SphereCollider>(0.5f);
sphere->AddComponent<Rigidbody>();
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() int main()
@@ -503,6 +563,7 @@ int main()
testAnimatorAssetReferences(); testAnimatorAssetReferences();
testSimpleColorMaterial(); testSimpleColorMaterial();
testAssetReferenceCacheKeys(); testAssetReferenceCacheKeys();
testTriggerZone();
std::cout << "destrum tests passed\n"; std::cout << "destrum tests passed\n";
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }