feat: add prefabs
This commit is contained in:
@@ -25,6 +25,8 @@
|
||||
#include <destrum/Serialization/ComponentFactory.h>
|
||||
#include <destrum/Serialization/ComponentRegistry.h>
|
||||
#include <destrum/Serialization/GlmJson.h>
|
||||
#include <destrum/Serialization/PrefabCache.h>
|
||||
#include <destrum/Serialization/PrefabSerializer.h>
|
||||
#include <destrum/Serialization/SceneSerializer.h>
|
||||
#include <destrum/Physics/SimplePhysicsWorld.h>
|
||||
#include <destrum/Physics/JoltPhysicsWorld.h>
|
||||
@@ -1615,6 +1617,134 @@ namespace {
|
||||
|
||||
SceneManager::GetInstance().Destroy();
|
||||
}
|
||||
|
||||
GameObject* buildTurret(Scene& scene, const GameObject& door)
|
||||
{
|
||||
GameObject* turret = scene.CreateGameObject("turret");
|
||||
turret->GetTransform().SetLocalPosition(glm::vec3{5.0f, 0.0f, 0.0f});
|
||||
GameObject* barrel = scene.CreateGameObject("barrel");
|
||||
barrel->GetTransform().SetParent(&turret->GetTransform(), false);
|
||||
barrel->GetTransform().SetLocalPosition(glm::vec3{0.0f, 1.0f, 0.0f});
|
||||
barrel->AddComponent<TestActivatable>();
|
||||
|
||||
auto* button = turret->AddComponent<InteractableButton>();
|
||||
button->SetCycleColors(false);
|
||||
button->AddTarget(*barrel);
|
||||
button->AddTarget(door);
|
||||
return turret;
|
||||
}
|
||||
|
||||
GameObject* onlyChild(GameObject& object)
|
||||
{
|
||||
const auto& children = object.GetTransform().GetChildren();
|
||||
return children.size() == 1 ? children.front()->GetOwner() : nullptr;
|
||||
}
|
||||
|
||||
void testPrefabRoundTrip()
|
||||
{
|
||||
ComponentFactory::Register<InteractableButton>("InteractableButton");
|
||||
ComponentFactory::Register<TestActivatable>("TestActivatable");
|
||||
|
||||
Scene& source = SceneManager::GetInstance().CreateScene("prefab source");
|
||||
GameObject* door = source.CreateGameObject("door");
|
||||
door->AddComponent<TestActivatable>();
|
||||
GameObject* turret = buildTurret(source, *door);
|
||||
source.CommitPendingAdditions();
|
||||
|
||||
const auto path = std::filesystem::temp_directory_path() / "destrum_turret.prefab";
|
||||
check(PrefabSerializer::Save(*turret, path), "a hierarchy must save as a prefab");
|
||||
|
||||
Scene& target = SceneManager::GetInstance().CreateScene("prefab target");
|
||||
GameObject* holder = target.CreateGameObject("holder");
|
||||
GameObject* first = PrefabSerializer::Instantiate(target, path.string());
|
||||
GameObject* second = PrefabSerializer::Instantiate(target, path.string(), &holder->GetTransform());
|
||||
target.CommitPendingAdditions();
|
||||
check(first != nullptr && second != nullptr, "a saved prefab must instantiate");
|
||||
check(target.GetObjects().size() == 5, "each instance must create the root and its child");
|
||||
check(first != second && first->GetId() != turret->GetId(), "instances must get fresh objects");
|
||||
check(first->GetName() == "turret" && first->GetTransform().GetParent() == nullptr,
|
||||
"an instance without a parent must be a scene root");
|
||||
check(second->GetTransform().GetParent() == &holder->GetTransform(),
|
||||
"an instance must be parented to the given transform");
|
||||
check(close(first->GetTransform().GetLocalPosition().x, 5.0f),
|
||||
"the root transform must come from the prefab");
|
||||
|
||||
for (GameObject* instance : {first, second}) {
|
||||
GameObject* barrel = onlyChild(*instance);
|
||||
check(barrel != nullptr && barrel->GetName() == "barrel" &&
|
||||
close(barrel->GetTransform().GetLocalPosition().y, 1.0f),
|
||||
"prefab children must keep their local transform");
|
||||
|
||||
const auto* button = instance->GetComponent<InteractableButton>();
|
||||
check(button != nullptr && button->GetTargets().size() == 1 &&
|
||||
button->GetTargets().front().Get(target) == barrel,
|
||||
"references inside the prefab must point at the instance's own objects, outside ones are dropped");
|
||||
}
|
||||
|
||||
check(PrefabCache::GetInstance().Get(path.string()) == PrefabCache::GetInstance().Get(path.string()),
|
||||
"a cached prefab must be parsed once");
|
||||
const auto before = PrefabCache::GetInstance().Get(path.string());
|
||||
turret->SetName("tower");
|
||||
check(PrefabSerializer::Save(*turret, path), "a prefab must save over an existing file");
|
||||
const auto after = PrefabCache::GetInstance().Get(path.string());
|
||||
check(after != before && after->at("name") == "tower", "saving a prefab must refresh the cache");
|
||||
|
||||
std::filesystem::remove(path);
|
||||
check(PrefabSerializer::Instantiate(target, path.string()) == nullptr,
|
||||
"instantiating a missing prefab must fail");
|
||||
|
||||
PrefabCache::GetInstance().Clear();
|
||||
SceneManager::GetInstance().Destroy();
|
||||
}
|
||||
|
||||
void testPrefabDuplicate()
|
||||
{
|
||||
ComponentFactory::Register<InteractableButton>("InteractableButton");
|
||||
ComponentFactory::Register<TestActivatable>("TestActivatable");
|
||||
|
||||
Scene& scene = SceneManager::GetInstance().CreateScene("duplicate");
|
||||
GameObject* door = scene.CreateGameObject("door");
|
||||
door->AddComponent<TestActivatable>();
|
||||
GameObject* holder = scene.CreateGameObject("holder");
|
||||
GameObject* turret = buildTurret(scene, *door);
|
||||
turret->GetTransform().SetParent(&holder->GetTransform(), false);
|
||||
|
||||
GameObject* copy = PrefabSerializer::Duplicate(*turret);
|
||||
scene.CommitPendingAdditions();
|
||||
check(copy != nullptr && copy != turret && copy->GetName() == "turret",
|
||||
"duplicate must create a new object");
|
||||
check(copy->GetTransform().GetParent() == &holder->GetTransform(),
|
||||
"a duplicate must keep the original's parent");
|
||||
|
||||
GameObject* barrel = onlyChild(*copy);
|
||||
check(barrel != nullptr && barrel != onlyChild(*turret), "a duplicate must copy children");
|
||||
|
||||
const auto* button = copy->GetComponent<InteractableButton>();
|
||||
check(button != nullptr && button->GetTargets().size() == 2 &&
|
||||
button->GetTargets()[0].Get(scene) == barrel &&
|
||||
button->GetTargets()[1].Get(scene) == door,
|
||||
"a duplicate must remap inner references and keep outer ones");
|
||||
|
||||
SceneManager::GetInstance().Destroy();
|
||||
}
|
||||
|
||||
void testPrefabValidation()
|
||||
{
|
||||
using nlohmann::json;
|
||||
check(PrefabSerializer::Validate(json::object()).has_value(), "an empty prefab must be rejected");
|
||||
|
||||
const json twoRoots = json::parse(R"({"version":1,"objects":[{"id":1},{"id":2}]})");
|
||||
check(PrefabSerializer::Validate(twoRoots).has_value(), "a prefab with two roots must be rejected");
|
||||
|
||||
const json childFirst = json::parse(R"({"version":1,"objects":[{"id":2,"parent":1},{"id":1}]})");
|
||||
check(PrefabSerializer::Validate(childFirst).has_value(), "the prefab root must come first");
|
||||
|
||||
const json valid = json::parse(R"({"version":1,"objects":[{"id":1},{"id":2,"parent":1}]})");
|
||||
check(!PrefabSerializer::Validate(valid).has_value(), "a single-root prefab must validate");
|
||||
|
||||
const json future = json::parse(R"({"version":99,"objects":[{"id":1}]})");
|
||||
check(PrefabSerializer::Validate(future).has_value(), "an unknown prefab version must be rejected");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
@@ -1663,6 +1793,9 @@ int main()
|
||||
testSceneFileIdsAreLocal();
|
||||
testButtonTargetsRoundTrip();
|
||||
testButtonDropsMissingTargets();
|
||||
testPrefabRoundTrip();
|
||||
testPrefabDuplicate();
|
||||
testPrefabValidation();
|
||||
std::cout << "destrum tests passed\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user