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
+158 -20
View File
@@ -16,8 +16,10 @@
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/ObjectModel/ObjectId.h>
#include <destrum/ObjectModel/Transform.h>
#include <destrum/Assets/AssetReference.h>
#include <destrum/Components/Physics/Collider.h>
#include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/Serialization/ComponentFactory.h>
#include <destrum/Serialization/ComponentRegistry.h>
@@ -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<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) {
@@ -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<void(float)> 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<ObjectId, GameObject*> idMap;
std::vector<const json*> objectJsonList;
std::vector<GameObject*> 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<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) {
GameObject* object = idMap.at(objectJson->at("id").get<ObjectId>());
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<std::string>();
}
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<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);
});
}