284 lines
11 KiB
C++
284 lines
11 KiB
C++
#include <filesystem>
|
|
#include <memory>
|
|
#include <string>
|
|
|
|
#include <destrum/Event.h>
|
|
#include <destrum/ObjectModel/Component.h>
|
|
#include <destrum/ObjectModel/GameObject.h>
|
|
#include <destrum/Scene/Scene.h>
|
|
#include <destrum/Scene/SceneManager.h>
|
|
#include <destrum/Serialization/ComponentFactory.h>
|
|
#include <destrum/Serialization/ComponentRegistry.h>
|
|
#include <destrum/Serialization/GlmJson.h>
|
|
#include <destrum/Serialization/SceneSerializer.h>
|
|
#include <destrum/Components/Physics/SphereCollider.h>
|
|
#include <destrum/Components/Physics/BoxCollider.h>
|
|
#include <destrum/Components/Physics/CapsuleCollider.h>
|
|
#include <destrum/Components/Physics/Collider.h>
|
|
#include <destrum/Components/Animator.h>
|
|
#include <destrum/Components/CameraComponent.h>
|
|
#include <destrum/Components/UI/Canvas.h>
|
|
#include <destrum/Components/UI/RectTransform.h>
|
|
#include <destrum/Components/UI/UIImage.h>
|
|
#include <destrum/Components/UI/UIText.h>
|
|
#include <destrum/Components/LightComponent.h>
|
|
#include <destrum/Components/Physics/Rigidbody.h>
|
|
|
|
#include "TestUtils.h"
|
|
|
|
using namespace test;
|
|
|
|
namespace {
|
|
class TestComponent final : public Component {
|
|
public:
|
|
explicit TestComponent(GameObject& owner) : Component(owner, "TestComponent") {}
|
|
|
|
void Update(float) override {}
|
|
std::string GetTypeName() const override { return "TestComponent"; }
|
|
};
|
|
|
|
class RemovingListener final : public EventListener {
|
|
public:
|
|
explicit RemovingListener(std::unique_ptr<RemovingListener>* victim)
|
|
: victim(victim) {}
|
|
|
|
void OnEvent(int) {
|
|
if (victim != nullptr) {
|
|
victim->reset();
|
|
victim = nullptr;
|
|
}
|
|
++calls;
|
|
}
|
|
|
|
int calls{0};
|
|
|
|
private:
|
|
std::unique_ptr<RemovingListener>* victim;
|
|
};
|
|
|
|
void testTransforms()
|
|
{
|
|
GameObject parent{"parent"};
|
|
GameObject child{"child"};
|
|
|
|
parent.GetTransform().SetLocalPosition({10.f, 2.f, -4.f});
|
|
parent.GetTransform().SetLocalRotation({0.f, 0.f, 90.f});
|
|
parent.GetTransform().SetLocalScale({2.f, 2.f, 2.f});
|
|
child.GetTransform().SetLocalPosition({1.f, 0.f, 0.f});
|
|
|
|
child.GetTransform().SetParent(&parent.GetTransform(), true);
|
|
const auto worldPosition = child.GetTransform().GetWorldPosition();
|
|
check(close(worldPosition.x, 1.f) && close(worldPosition.y, 0.f),
|
|
"reparenting must preserve world position");
|
|
|
|
child.GetTransform().SetWorldPosition({20.f, 10.f, 0.f});
|
|
const auto movedPosition = child.GetTransform().GetWorldPosition();
|
|
check(close(movedPosition.x, 20.f) && close(movedPosition.y, 10.f),
|
|
"world position must account for parent rotation and scale");
|
|
|
|
parent.Destroy();
|
|
check(parent.IsBeingDestroyed() && child.IsBeingDestroyed(),
|
|
"destroying a parent must mark descendants");
|
|
}
|
|
|
|
void testSceneFindGameObject()
|
|
{
|
|
Scene& scene = SceneManager::GetInstance().CreateScene("find-object");
|
|
GameObject* pending = scene.CreateGameObject("pending");
|
|
check(scene.FindGameObject(pending->GetId()) == pending, "objects must be found while still pending");
|
|
|
|
scene.CommitPendingAdditions();
|
|
GameObject* committed = scene.CreateGameObject("committed");
|
|
scene.CommitPendingAdditions();
|
|
check(scene.FindGameObject(committed->GetId()) == committed, "committed objects must be found by id");
|
|
check(scene.FindGameObject(InvalidObjectId) == nullptr, "the invalid id must never match");
|
|
|
|
const ObjectId destroyedId = committed->GetId();
|
|
committed->Destroy();
|
|
check(scene.FindGameObject(destroyedId) == nullptr, "objects being destroyed must not be found");
|
|
|
|
SceneManager::GetInstance().Destroy();
|
|
}
|
|
|
|
void testEventMutation()
|
|
{
|
|
Event<int> event;
|
|
int calls = 0;
|
|
event.AddListener([&](int) { ++calls; });
|
|
event.AddListener([&](int) {
|
|
++calls;
|
|
event.AddListener([&](int) { ++calls; });
|
|
});
|
|
|
|
event.Invoke(1);
|
|
check(calls == 2, "event mutation must not invalidate the current invocation");
|
|
event.Invoke(1);
|
|
check(calls == 5, "new event listeners must run on subsequent invocations");
|
|
}
|
|
|
|
void testEventRemoval()
|
|
{
|
|
Event<int> event;
|
|
std::unique_ptr<RemovingListener> victim;
|
|
RemovingListener remover{&victim};
|
|
victim = std::make_unique<RemovingListener>(nullptr);
|
|
|
|
event.AddListener(&remover, &RemovingListener::OnEvent);
|
|
event.AddListener(victim.get(), &RemovingListener::OnEvent);
|
|
event.Invoke(1);
|
|
|
|
check(remover.calls == 1, "event remover must run once");
|
|
check(victim == nullptr, "event listener must be removable during invocation");
|
|
}
|
|
|
|
void testComponentRemoval()
|
|
{
|
|
Scene& scene = SceneManager::GetInstance().CreateScene("component-removal");
|
|
GameObject* object = scene.CreateGameObject("object");
|
|
object->AddComponent<TestComponent>();
|
|
scene.CommitPendingAdditions();
|
|
|
|
check(object->GetComponent<TestComponent>() != nullptr,
|
|
"test component must be discoverable before removal");
|
|
check(object->DestroyComponent<TestComponent>() != nullptr,
|
|
"destroying a component must find it");
|
|
scene.CleanupDestroyedGameObjects();
|
|
check(object->GetComponent<TestComponent>() == nullptr,
|
|
"destroyed components must be removed at the scene boundary");
|
|
|
|
SceneManager::GetInstance().Destroy();
|
|
}
|
|
|
|
void testEngineComponentRegistration()
|
|
{
|
|
RegisterEngineComponents();
|
|
|
|
const char* canonicalNames[] = {
|
|
"MeshRendererComponent",
|
|
"LightComponent",
|
|
"Rotator",
|
|
"Spinner",
|
|
"OrbitAndSpin",
|
|
"Animator",
|
|
"Rigidbody",
|
|
"BoxCollider",
|
|
"SphereCollider",
|
|
"CapsuleCollider",
|
|
"Canvas",
|
|
"RectTransform",
|
|
"UIImage",
|
|
"UIText"
|
|
};
|
|
for (const char* name : canonicalNames) {
|
|
check(ComponentFactory::IsRegistered(name),
|
|
"all canonical engine component names must be registered");
|
|
}
|
|
|
|
check(!ComponentFactory::IsRegistered("MeshRenderer"),
|
|
"MeshRenderer legacy alias must not be registered");
|
|
check(!ComponentFactory::IsRegistered("RigidBody"),
|
|
"RigidBody legacy alias must not be registered");
|
|
}
|
|
|
|
void testSceneCurrentCamera()
|
|
{
|
|
Scene& scene = SceneManager::GetInstance().CreateScene("cameras");
|
|
check(scene.GetCurrentCamera() == nullptr, "a scene without cameras must have no current camera");
|
|
|
|
GameObject* first = scene.CreateGameObject("first camera");
|
|
auto* firstCamera = first->AddComponent<CameraComponent>();
|
|
scene.CommitPendingAdditions();
|
|
check(scene.GetCurrentCamera() == firstCamera, "the only camera in a scene must be the current camera");
|
|
|
|
GameObject* second = scene.CreateGameObject("second camera");
|
|
auto* secondCamera = second->AddComponent<CameraComponent>();
|
|
secondCamera->SetFieldOfView(60.0f);
|
|
second->GetTransform().SetWorldPosition({1.0f, 2.0f, 3.0f});
|
|
scene.CommitPendingAdditions();
|
|
secondCamera->MakeCurrent();
|
|
check(scene.GetCurrentCamera() == secondCamera && !firstCamera->IsCurrent(),
|
|
"the selected camera must be the current camera");
|
|
|
|
const Camera& camera = secondCamera->GetCamera(16.0f / 9.0f);
|
|
check(camera.GetPosition() == glm::vec3{1.0f, 2.0f, 3.0f} &&
|
|
close(camera.GetForward().z, -1.0f) && camera.GetFOVY() == 60.0f,
|
|
"a camera component must place its camera at its transform, looking down -Z");
|
|
|
|
const auto path = std::filesystem::temp_directory_path() / "destrum_camera_scene_test.json";
|
|
check(SceneSerializer::Save(scene, path), "scene with cameras must save");
|
|
check(SceneSerializer::Load(scene, path), "scene with cameras must load");
|
|
std::filesystem::remove(path);
|
|
|
|
const CameraComponent* loadedCamera = scene.GetCurrentCamera();
|
|
check(loadedCamera != nullptr && loadedCamera->GetGameObject()->GetName() == "second camera" &&
|
|
loadedCamera->GetFieldOfView() == 60.0f,
|
|
"scene round-trip must preserve the current camera");
|
|
|
|
scene.GetCurrentCamera()->GetGameObject()->SetActive(false);
|
|
check(scene.GetCurrentCamera() != nullptr &&
|
|
scene.GetCurrentCamera()->GetGameObject()->GetName() == "first camera",
|
|
"an inactive current camera must fall back to another camera");
|
|
|
|
SceneManager::GetInstance().Destroy();
|
|
}
|
|
|
|
void testHierarchyMoves()
|
|
{
|
|
Scene& scene = SceneManager::GetInstance().CreateScene("hierarchy");
|
|
GameObject* parent = scene.CreateGameObject("parent");
|
|
GameObject* a = scene.CreateGameObject("a");
|
|
GameObject* b = scene.CreateGameObject("b");
|
|
GameObject* c = scene.CreateGameObject("c");
|
|
GameObject* other = scene.CreateGameObject("other");
|
|
for (GameObject* child : {a, b, c}) {
|
|
child->GetTransform().SetParent(&parent->GetTransform(), false);
|
|
}
|
|
scene.CommitPendingAdditions();
|
|
|
|
const auto childNames = [](const GameObject& object) {
|
|
std::string names;
|
|
for (const Transform* child : object.GetTransform().GetChildren()) {
|
|
names += child->GetOwner()->GetName();
|
|
}
|
|
return names;
|
|
};
|
|
|
|
check(scene.MoveInHierarchy(*c, parent, a) && childNames(*parent) == "cab",
|
|
"moving before a sibling must reorder the children");
|
|
check(scene.MoveInHierarchy(*c, parent) && childNames(*parent) == "abc",
|
|
"moving without a sibling must put the object last");
|
|
check(scene.MoveInHierarchy(*a, other) && childNames(*parent) == "bc" && childNames(*other) == "a",
|
|
"moving onto another object must reparent it");
|
|
check(!scene.MoveInHierarchy(*other, a) && a->GetTransform().GetParent() == &other->GetTransform(),
|
|
"an object must not be moved under its own child");
|
|
check(scene.MoveInHierarchy(*a, nullptr, parent) && a->GetTransform().GetParent() == nullptr &&
|
|
scene.GetObjects().front().get() == a,
|
|
"moving to the root must unparent and order the roots");
|
|
check(scene.MoveInHierarchy(*c, parent, b) && childNames(*parent) == "cb", "reordering must still work");
|
|
|
|
const auto path = std::filesystem::temp_directory_path() / "destrum_hierarchy_scene.json";
|
|
check(SceneSerializer::Save(scene, path), "the hierarchy scene must save");
|
|
Scene& copy = SceneManager::GetInstance().CreateScene("hierarchy copy");
|
|
check(SceneSerializer::Load(copy, path), "the hierarchy scene must load");
|
|
std::filesystem::remove(path);
|
|
copy.CommitPendingAdditions();
|
|
const GameObject* loadedParent = findByName(copy, "parent");
|
|
check(loadedParent != nullptr && childNames(*loadedParent) == "cb", "sibling order must survive a save");
|
|
check(copy.GetObjects().front()->GetName() == "a", "root order must survive a save");
|
|
|
|
SceneManager::GetInstance().Destroy();
|
|
}
|
|
}
|
|
|
|
void RunCoreTests()
|
|
{
|
|
testTransforms();
|
|
testSceneFindGameObject();
|
|
testEventMutation();
|
|
testEventRemoval();
|
|
testComponentRemoval();
|
|
testEngineComponentRegistration();
|
|
testSceneCurrentCamera();
|
|
testHierarchyMoves();
|
|
}
|