feat: implement contact listeners
This commit is contained in:
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user