feat: add ObjectRef for scene loading to not rely on runtime id's
This commit is contained in:
@@ -96,6 +96,7 @@ set(SRC_FILES
|
|||||||
"src/ObjectModel/Component.cpp"
|
"src/ObjectModel/Component.cpp"
|
||||||
"src/ObjectModel/GameObject.cpp"
|
"src/ObjectModel/GameObject.cpp"
|
||||||
"src/ObjectModel/Object.cpp"
|
"src/ObjectModel/Object.cpp"
|
||||||
|
"src/ObjectModel/ObjectRef.cpp"
|
||||||
"src/ObjectModel/Transform.cpp"
|
"src/ObjectModel/Transform.cpp"
|
||||||
|
|
||||||
"src/Scene/Scene.cpp"
|
"src/Scene/Scene.cpp"
|
||||||
|
|||||||
@@ -44,9 +44,6 @@ public:
|
|||||||
|
|
||||||
[[nodiscard]] ObjectId GetId() const { return m_Id; }
|
[[nodiscard]] ObjectId GetId() const { return m_Id; }
|
||||||
|
|
||||||
// Use this only while loading a scene.
|
|
||||||
void SetIdForDeserialization(ObjectId id);
|
|
||||||
|
|
||||||
[[nodiscard]] Scene* GetScene() const { return m_Scene; }
|
[[nodiscard]] Scene* GetScene() const { return m_Scene; }
|
||||||
|
|
||||||
[[nodiscard]] std::vector<std::unique_ptr<Component>>& GetComponents() {
|
[[nodiscard]] std::vector<std::unique_ptr<Component>>& GetComponents() {
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#ifndef DESTRUM_OBJECTREF_H
|
||||||
|
#define DESTRUM_OBJECTREF_H
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <nlohmann/json_fwd.hpp>
|
||||||
|
|
||||||
|
#include <destrum/ObjectModel/ObjectId.h>
|
||||||
|
|
||||||
|
class GameObject;
|
||||||
|
class Scene;
|
||||||
|
|
||||||
|
class ObjectRef final {
|
||||||
|
public:
|
||||||
|
using ObjectMap = std::unordered_map<ObjectId, GameObject*>;
|
||||||
|
|
||||||
|
ObjectRef() = default;
|
||||||
|
explicit ObjectRef(ObjectId id) : m_Id(id) {}
|
||||||
|
explicit ObjectRef(const GameObject& object);
|
||||||
|
|
||||||
|
[[nodiscard]] ObjectId GetId() const { return m_Id; }
|
||||||
|
[[nodiscard]] bool IsSet() const { return m_Id != InvalidObjectId; }
|
||||||
|
|
||||||
|
[[nodiscard]] GameObject* Get(const Scene& scene) const;
|
||||||
|
|
||||||
|
bool Resolve(const ObjectMap& objects);
|
||||||
|
|
||||||
|
bool operator==(const ObjectRef&) const = default;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ObjectId m_Id{InvalidObjectId};
|
||||||
|
};
|
||||||
|
|
||||||
|
void to_json(nlohmann::json& json, const ObjectRef& ref);
|
||||||
|
void from_json(const nlohmann::json& json, ObjectRef& ref);
|
||||||
|
|
||||||
|
#endif //DESTRUM_OBJECTREF_H
|
||||||
@@ -39,19 +39,6 @@ GameObject::GameObject(const std::string& name)
|
|||||||
m_Id(AllocateId()) {
|
m_Id(AllocateId()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameObject::SetIdForDeserialization(ObjectId id) {
|
|
||||||
if (id == InvalidObjectId || id >= std::numeric_limits<ObjectId>::max() - 1) {
|
|
||||||
throw std::out_of_range("Object ID is reserved or cannot be incremented");
|
|
||||||
}
|
|
||||||
|
|
||||||
m_Id = id;
|
|
||||||
|
|
||||||
// Prevent newly created objects from reusing loaded IDs.
|
|
||||||
if (id != std::numeric_limits<ObjectId>::max() && id >= s_NextId) {
|
|
||||||
s_NextId = id + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ObjectId GameObject::AllocateId() {
|
ObjectId GameObject::AllocateId() {
|
||||||
if (s_NextId == InvalidObjectId ||
|
if (s_NextId == InvalidObjectId ||
|
||||||
s_NextId >= std::numeric_limits<ObjectId>::max() - 1) {
|
s_NextId >= std::numeric_limits<ObjectId>::max() - 1) {
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#include <destrum/ObjectModel/ObjectRef.h>
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
|
#include <destrum/Scene/Scene.h>
|
||||||
|
|
||||||
|
ObjectRef::ObjectRef(const GameObject& object)
|
||||||
|
: m_Id(object.GetId()) {
|
||||||
|
}
|
||||||
|
|
||||||
|
GameObject* ObjectRef::Get(const Scene& scene) const {
|
||||||
|
return scene.FindGameObject(m_Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ObjectRef::Resolve(const ObjectMap& objects) {
|
||||||
|
if (m_Id == InvalidObjectId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto it = objects.find(m_Id);
|
||||||
|
if (it == objects.end() || it->second == nullptr) {
|
||||||
|
// Keeping an unmatched file id would alias an unrelated runtime object.
|
||||||
|
m_Id = InvalidObjectId;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Id = it->second->GetId();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void to_json(nlohmann::json& json, const ObjectRef& ref) {
|
||||||
|
json = ref.GetId();
|
||||||
|
}
|
||||||
|
|
||||||
|
void from_json(const nlohmann::json& json, ObjectRef& ref) {
|
||||||
|
ref = json.is_number_unsigned() ? ObjectRef(json.get<ObjectId>()) : ObjectRef{};
|
||||||
|
}
|
||||||
@@ -563,8 +563,8 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
|||||||
const std::string name = objectJson.value("name", "GameObject");
|
const std::string name = objectJson.value("name", "GameObject");
|
||||||
const bool active = objectJson.value("active", true);
|
const bool active = objectJson.value("active", true);
|
||||||
|
|
||||||
|
// File ids are local to the file, every loaded object gets a fresh runtime id
|
||||||
GameObject* object = stagingScene.CreateGameObject(name);
|
GameObject* object = stagingScene.CreateGameObject(name);
|
||||||
object->SetIdForDeserialization(id);
|
|
||||||
object->SetActive(active);
|
object->SetActive(active);
|
||||||
|
|
||||||
idMap.emplace(id, object);
|
idMap.emplace(id, object);
|
||||||
@@ -687,11 +687,14 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
|||||||
scene.m_name = root.at("name").get<std::string>();
|
scene.m_name = root.at("name").get<std::string>();
|
||||||
}
|
}
|
||||||
scene.m_SelectedObjectId = InvalidObjectId;
|
scene.m_SelectedObjectId = InvalidObjectId;
|
||||||
scene.m_CurrentCameraId = root.value("currentCamera", InvalidObjectId);
|
scene.m_CurrentCameraId = InvalidObjectId;
|
||||||
if (scene.m_CurrentCameraId != InvalidObjectId && !idMap.contains(scene.m_CurrentCameraId)) {
|
if (const ObjectId cameraFileId = root.value("currentCamera", InvalidObjectId); cameraFileId != InvalidObjectId) {
|
||||||
spdlog::warn("Scene \"{}\" current camera id {} does not exist, ignoring it",
|
if (const auto it = idMap.find(cameraFileId); it != idMap.end()) {
|
||||||
scene.m_name, scene.m_CurrentCameraId);
|
scene.m_CurrentCameraId = it->second->GetId();
|
||||||
scene.m_CurrentCameraId = InvalidObjectId;
|
} else {
|
||||||
|
spdlog::warn("Scene \"{}\" current camera id {} does not exist, ignoring it",
|
||||||
|
scene.m_name, cameraFileId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
scene.m_Environment = SceneEnvironment{};
|
scene.m_Environment = SceneEnvironment{};
|
||||||
if (root.contains("environment") && root.at("environment").is_object()) {
|
if (root.contains("environment") && root.at("environment").is_object()) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <destrum/ObjectModel/ObjectId.h>
|
#include <destrum/ObjectModel/ObjectRef.h>
|
||||||
|
|
||||||
class InteractableButton final : public Interactable {
|
class InteractableButton final : public Interactable {
|
||||||
public:
|
public:
|
||||||
@@ -19,6 +19,7 @@ public:
|
|||||||
|
|
||||||
nlohmann::json Serialize() const override;
|
nlohmann::json Serialize() const override;
|
||||||
void Deserialize(const nlohmann::json& data) override;
|
void Deserialize(const nlohmann::json& data) override;
|
||||||
|
void ResolveReferences(const ObjectMap& objects) override;
|
||||||
void ImGuiInspector() override;
|
void ImGuiInspector() override;
|
||||||
|
|
||||||
[[nodiscard]] std::size_t GetColorIndex() const { return m_ColorIndex; }
|
[[nodiscard]] std::size_t GetColorIndex() const { return m_ColorIndex; }
|
||||||
@@ -29,7 +30,7 @@ public:
|
|||||||
// Every Activatable on a target object is activated when the button is pressed
|
// Every Activatable on a target object is activated when the button is pressed
|
||||||
void AddTarget(const GameObject& target);
|
void AddTarget(const GameObject& target);
|
||||||
void RemoveTarget(ObjectId target);
|
void RemoveTarget(ObjectId target);
|
||||||
[[nodiscard]] const std::vector<ObjectId>& GetTargets() const { return m_Targets; }
|
[[nodiscard]] const std::vector<ObjectRef>& GetTargets() const { return m_Targets; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void ApplyColor();
|
void ApplyColor();
|
||||||
@@ -39,7 +40,7 @@ private:
|
|||||||
std::size_t m_ColorIndex{0};
|
std::size_t m_ColorIndex{0};
|
||||||
int m_PressCount{0};
|
int m_PressCount{0};
|
||||||
bool m_CycleColors{true};
|
bool m_CycleColors{true};
|
||||||
std::vector<ObjectId> m_Targets;
|
std::vector<ObjectRef> m_Targets;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // LIGHTKEEPER_INTERACTABLEBUTTON_H
|
#endif // LIGHTKEEPER_INTERACTABLEBUTTON_H
|
||||||
|
|||||||
@@ -52,15 +52,16 @@ void InteractableButton::Interact(GameObject& interactor)
|
|||||||
|
|
||||||
void InteractableButton::AddTarget(const GameObject& target)
|
void InteractableButton::AddTarget(const GameObject& target)
|
||||||
{
|
{
|
||||||
if (target.GetId() == InvalidObjectId || std::ranges::find(m_Targets, target.GetId()) != m_Targets.end()) {
|
const ObjectRef ref(target);
|
||||||
|
if (!ref.IsSet() || std::ranges::find(m_Targets, ref) != m_Targets.end()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_Targets.push_back(target.GetId());
|
m_Targets.push_back(ref);
|
||||||
}
|
}
|
||||||
|
|
||||||
void InteractableButton::RemoveTarget(ObjectId target)
|
void InteractableButton::RemoveTarget(ObjectId target)
|
||||||
{
|
{
|
||||||
std::erase(m_Targets, target);
|
std::erase(m_Targets, ObjectRef(target));
|
||||||
}
|
}
|
||||||
|
|
||||||
void InteractableButton::ActivateTargets(GameObject& interactor)
|
void InteractableButton::ActivateTargets(GameObject& interactor)
|
||||||
@@ -70,10 +71,10 @@ void InteractableButton::ActivateTargets(GameObject& interactor)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const ObjectId targetId : m_Targets) {
|
for (const ObjectRef& targetRef : m_Targets) {
|
||||||
GameObject* target = scene->FindGameObject(targetId);
|
GameObject* target = targetRef.Get(*scene);
|
||||||
if (target == nullptr) {
|
if (target == nullptr) {
|
||||||
spdlog::warn("{} targets object {} which no longer exists", GetGameObject()->GetName(), targetId);
|
spdlog::warn("{} targets object {} which no longer exists", GetGameObject()->GetName(), targetRef.GetId());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +123,19 @@ void InteractableButton::Deserialize(const nlohmann::json& data)
|
|||||||
m_ColorIndex = data.value("colorIndex", m_ColorIndex) % ButtonColors.size();
|
m_ColorIndex = data.value("colorIndex", m_ColorIndex) % ButtonColors.size();
|
||||||
m_CycleColors = data.value("cycleColors", m_CycleColors);
|
m_CycleColors = data.value("cycleColors", m_CycleColors);
|
||||||
m_PressCount = data.value("pressCount", m_PressCount);
|
m_PressCount = data.value("pressCount", m_PressCount);
|
||||||
m_Targets = data.value("targets", std::vector<ObjectId>{});
|
m_Targets = data.value("targets", std::vector<ObjectRef>{});
|
||||||
|
}
|
||||||
|
|
||||||
|
void InteractableButton::ResolveReferences(const ObjectMap& objects)
|
||||||
|
{
|
||||||
|
std::erase_if(m_Targets, [&](ObjectRef& target) {
|
||||||
|
const ObjectId fileId = target.GetId();
|
||||||
|
if (!target.Resolve(objects)) {
|
||||||
|
spdlog::warn("{} targets object {} which is not in the scene file, dropping it", GetGameObject()->GetName(), fileId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !target.IsSet();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void InteractableButton::ImGuiInspector()
|
void InteractableButton::ImGuiInspector()
|
||||||
@@ -149,8 +162,9 @@ void InteractableButton::DrawTargetsInspector()
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::optional<ObjectId> targetToRemove;
|
std::optional<ObjectId> targetToRemove;
|
||||||
for (const ObjectId targetId : m_Targets) {
|
for (const ObjectRef& targetRef : m_Targets) {
|
||||||
const GameObject* target = scene->FindGameObject(targetId);
|
const ObjectId targetId = targetRef.GetId();
|
||||||
|
const GameObject* target = targetRef.Get(*scene);
|
||||||
ImGui::PushID(static_cast<int>(targetId));
|
ImGui::PushID(static_cast<int>(targetId));
|
||||||
if (ImGui::SmallButton("x")) {
|
if (ImGui::SmallButton("x")) {
|
||||||
targetToRemove = targetId;
|
targetToRemove = targetId;
|
||||||
@@ -170,7 +184,7 @@ void InteractableButton::DrawTargetsInspector()
|
|||||||
if (ImGui::BeginCombo("##AddTarget", "Add target...")) {
|
if (ImGui::BeginCombo("##AddTarget", "Add target...")) {
|
||||||
for (const auto& object : scene->GetObjects()) {
|
for (const auto& object : scene->GetObjects()) {
|
||||||
const bool alreadyTargeted = object != nullptr &&
|
const bool alreadyTargeted = object != nullptr &&
|
||||||
std::ranges::find(m_Targets, object->GetId()) != m_Targets.end();
|
std::ranges::find(m_Targets, ObjectRef(*object)) != m_Targets.end();
|
||||||
if (object == nullptr || object.get() == GetGameObject() || alreadyTargeted ||
|
if (object == nullptr || object.get() == GetGameObject() || alreadyTargeted ||
|
||||||
object->GetComponent<Activatable>() == nullptr) {
|
object->GetComponent<Activatable>() == nullptr) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
add_executable(destrum_tests
|
add_executable(destrum_tests
|
||||||
destrum_tests.cpp
|
destrum_tests.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/lightkeeper/src/components/Activatable.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/lightkeeper/src/components/Interactable.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/lightkeeper/src/components/InteractableButton.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set_target_properties(destrum_tests PROPERTIES
|
set_target_properties(destrum_tests PROPERTIES
|
||||||
@@ -12,6 +15,7 @@ target_compile_definitions(destrum_tests PRIVATE
|
|||||||
SDL_MAIN_HANDLED
|
SDL_MAIN_HANDLED
|
||||||
DESTRUM_ENGINE_ASSETS_SRC_DIR="${CMAKE_SOURCE_DIR}/destrum/assets_src"
|
DESTRUM_ENGINE_ASSETS_SRC_DIR="${CMAKE_SOURCE_DIR}/destrum/assets_src"
|
||||||
)
|
)
|
||||||
|
target_include_directories(destrum_tests PRIVATE "${CMAKE_SOURCE_DIR}/lightkeeper/include")
|
||||||
target_link_libraries(destrum_tests PRIVATE destrum::destrum)
|
target_link_libraries(destrum_tests PRIVATE destrum::destrum)
|
||||||
|
|
||||||
destrum_enable_warnings(destrum_tests)
|
destrum_enable_warnings(destrum_tests)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
#include <glm/gtc/constants.hpp>
|
#include <glm/gtc/constants.hpp>
|
||||||
#include <glm/gtc/packing.hpp>
|
#include <glm/gtc/packing.hpp>
|
||||||
@@ -44,6 +45,9 @@
|
|||||||
#include <destrum/Graphics/imageLoader.h>
|
#include <destrum/Graphics/imageLoader.h>
|
||||||
#include <destrum/Graphics/Managers/LineRenderingManager.h>
|
#include <destrum/Graphics/Managers/LineRenderingManager.h>
|
||||||
|
|
||||||
|
#include <components/Activatable.h>
|
||||||
|
#include <components/InteractableButton.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
class TestComponent final : public Component {
|
class TestComponent final : public Component {
|
||||||
public:
|
public:
|
||||||
@@ -53,6 +57,18 @@ namespace {
|
|||||||
std::string GetTypeName() const override { return "TestComponent"; }
|
std::string GetTypeName() const override { return "TestComponent"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class TestActivatable final : public Activatable {
|
||||||
|
public:
|
||||||
|
explicit TestActivatable(GameObject& owner) : Activatable(owner, "TestActivatable") {}
|
||||||
|
|
||||||
|
std::string GetTypeName() const override { return "TestActivatable"; }
|
||||||
|
|
||||||
|
int activations{0};
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void OnActivate(GameObject&) override { ++activations; }
|
||||||
|
};
|
||||||
|
|
||||||
class RemovingListener final : public EventListener {
|
class RemovingListener final : public EventListener {
|
||||||
public:
|
public:
|
||||||
explicit RemovingListener(std::unique_ptr<RemovingListener>* victim)
|
explicit RemovingListener(std::unique_ptr<RemovingListener>* victim)
|
||||||
@@ -1473,6 +1489,132 @@ namespace {
|
|||||||
!PrimitiveMeshes::MakeFromKey("Box").has_value(),
|
!PrimitiveMeshes::MakeFromKey("Box").has_value(),
|
||||||
"invalid primitive keys must not build a mesh");
|
"invalid primitive keys must not build a mesh");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void testSceneFileIdsAreLocal()
|
||||||
|
{
|
||||||
|
const auto path = std::filesystem::temp_directory_path() / "destrum_local_ids_scene.json";
|
||||||
|
std::ofstream{path}
|
||||||
|
<< R"({"version":2,"currentCamera":2,"objects":[)"
|
||||||
|
R"({"id":1,"name":"root"},)"
|
||||||
|
R"({"id":2,"name":"child","parent":1,"components":[{"type":"CameraComponent"}]}]})";
|
||||||
|
|
||||||
|
Scene& first = SceneManager::GetInstance().CreateScene("local ids a");
|
||||||
|
Scene& second = SceneManager::GetInstance().CreateScene("local ids b");
|
||||||
|
check(SceneSerializer::Load(first, path), "the first copy of the scene must load");
|
||||||
|
check(SceneSerializer::Load(second, path), "the second copy of the scene must load");
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
first.CommitPendingAdditions();
|
||||||
|
second.CommitPendingAdditions();
|
||||||
|
|
||||||
|
std::unordered_set<ObjectId> ids;
|
||||||
|
std::size_t count = 0;
|
||||||
|
for (const Scene* scene : {&first, &second}) {
|
||||||
|
for (const auto& object : scene->GetObjects()) {
|
||||||
|
ids.insert(object->GetId());
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check(count == 4 && ids.size() == 4, "loading one file into two scenes must not duplicate ids");
|
||||||
|
|
||||||
|
for (const Scene* scene : {&first, &second}) {
|
||||||
|
const CameraComponent* camera = scene->GetCurrentCamera();
|
||||||
|
check(camera != nullptr && camera->GetGameObject()->GetName() == "child" &&
|
||||||
|
camera->GetGameObject()->GetScene() == scene,
|
||||||
|
"the current camera must be remapped to the object in its own scene");
|
||||||
|
const Transform* parent = camera->GetGameObject()->GetTransform().GetParent();
|
||||||
|
check(parent != nullptr && parent->GetOwner()->GetName() == "root" &&
|
||||||
|
parent->GetOwner()->GetScene() == scene,
|
||||||
|
"parents must be remapped within each loaded scene");
|
||||||
|
}
|
||||||
|
|
||||||
|
const GameObject* fresh = first.CreateGameObject("fresh");
|
||||||
|
check(!ids.contains(fresh->GetId()), "objects created after a load must get unused ids");
|
||||||
|
|
||||||
|
SceneManager::GetInstance().Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
GameObject* findByName(const Scene& scene, const std::string& name)
|
||||||
|
{
|
||||||
|
for (const auto& object : scene.GetObjects()) {
|
||||||
|
if (object->GetName() == name) {
|
||||||
|
return object.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void testButtonTargetsRoundTrip()
|
||||||
|
{
|
||||||
|
ComponentFactory::Register<InteractableButton>("InteractableButton");
|
||||||
|
ComponentFactory::Register<TestActivatable>("TestActivatable");
|
||||||
|
|
||||||
|
Scene& scene = SceneManager::GetInstance().CreateScene("button targets");
|
||||||
|
GameObject* door = scene.CreateGameObject("door");
|
||||||
|
door->AddComponent<TestActivatable>();
|
||||||
|
GameObject* lamp = scene.CreateGameObject("lamp");
|
||||||
|
lamp->AddComponent<TestActivatable>();
|
||||||
|
GameObject* buttonObject = scene.CreateGameObject("button");
|
||||||
|
auto* button = buttonObject->AddComponent<InteractableButton>();
|
||||||
|
button->SetCycleColors(false);
|
||||||
|
button->AddTarget(*door);
|
||||||
|
button->AddTarget(*lamp);
|
||||||
|
button->AddTarget(*door);
|
||||||
|
scene.CommitPendingAdditions();
|
||||||
|
check(button->GetTargets().size() == 2, "a button must not target the same object twice");
|
||||||
|
|
||||||
|
const auto path = std::filesystem::temp_directory_path() / "destrum_button_targets_scene.json";
|
||||||
|
check(SceneSerializer::Save(scene, path), "the button scene must save");
|
||||||
|
check(SceneSerializer::Load(scene, path), "the button scene must load");
|
||||||
|
|
||||||
|
Scene& copy = SceneManager::GetInstance().CreateScene("button targets copy");
|
||||||
|
check(SceneSerializer::Load(copy, path), "the button scene must load into a second scene");
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
|
||||||
|
for (Scene* loaded : {&scene, ©}) {
|
||||||
|
loaded->CommitPendingAdditions();
|
||||||
|
GameObject* loadedButton = findByName(*loaded, "button");
|
||||||
|
auto* loadedComponent = loadedButton != nullptr ? loadedButton->GetComponent<InteractableButton>() : nullptr;
|
||||||
|
check(loadedComponent != nullptr, "the button must survive a round-trip");
|
||||||
|
|
||||||
|
const auto& targets = loadedComponent->GetTargets();
|
||||||
|
check(targets.size() == 2, "button targets must survive a round-trip");
|
||||||
|
GameObject* loadedDoor = targets[0].Get(*loaded);
|
||||||
|
GameObject* loadedLamp = targets[1].Get(*loaded);
|
||||||
|
check(loadedDoor == findByName(*loaded, "door") && loadedDoor != nullptr &&
|
||||||
|
loadedLamp == findByName(*loaded, "lamp") && loadedLamp != nullptr,
|
||||||
|
"button targets must point at the loaded objects in their own scene");
|
||||||
|
|
||||||
|
loadedComponent->Interact(*loadedButton);
|
||||||
|
check(loadedDoor->GetComponent<TestActivatable>()->activations == 1 &&
|
||||||
|
loadedLamp->GetComponent<TestActivatable>()->activations == 1,
|
||||||
|
"pressing a loaded button must activate its targets");
|
||||||
|
}
|
||||||
|
|
||||||
|
SceneManager::GetInstance().Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void testButtonDropsMissingTargets()
|
||||||
|
{
|
||||||
|
ComponentFactory::Register<InteractableButton>("InteractableButton");
|
||||||
|
|
||||||
|
Scene& scene = SceneManager::GetInstance().CreateScene("missing targets");
|
||||||
|
const auto path = std::filesystem::temp_directory_path() / "destrum_missing_targets_scene.json";
|
||||||
|
std::ofstream{path}
|
||||||
|
<< R"({"version":2,"objects":[{"id":5,"name":"button","components":)"
|
||||||
|
R"([{"type":"InteractableButton","data":{"cycleColors":false,"targets":[5,99]}}]}]})";
|
||||||
|
|
||||||
|
check(SceneSerializer::Load(scene, path), "a button with a dangling target must still load");
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
scene.CommitPendingAdditions();
|
||||||
|
|
||||||
|
const GameObject* object = scene.GetObjects().front().get();
|
||||||
|
const auto* button = object->GetComponent<InteractableButton>();
|
||||||
|
check(button != nullptr && button->GetTargets().size() == 1 &&
|
||||||
|
button->GetTargets().front().GetId() == object->GetId(),
|
||||||
|
"dangling targets must be dropped and valid ones remapped");
|
||||||
|
|
||||||
|
SceneManager::GetInstance().Destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int main()
|
int main()
|
||||||
@@ -1518,6 +1660,9 @@ int main()
|
|||||||
testSceneLoadDropsMissingCamera();
|
testSceneLoadDropsMissingCamera();
|
||||||
testGlmJson();
|
testGlmJson();
|
||||||
testPrimitiveMeshKeys();
|
testPrimitiveMeshKeys();
|
||||||
|
testSceneFileIdsAreLocal();
|
||||||
|
testButtonTargetsRoundTrip();
|
||||||
|
testButtonDropsMissingTargets();
|
||||||
std::cout << "destrum tests passed\n";
|
std::cout << "destrum tests passed\n";
|
||||||
return EXIT_SUCCESS;
|
return EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user