Files
Destrum/tests/core_tests.cpp

334 lines
13 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 CountingComponent final : public Component {
public:
explicit CountingComponent(GameObject& owner) : Component(owner, "CountingComponent") {}
void Update(float) override { ++updates; }
void LateUpdate(float) override { ++lateUpdates; }
void FixedUpdate(float) override { ++fixedUpdates; }
std::string GetTypeName() const override { return "CountingComponent"; }
int updates{0};
int lateUpdates{0};
int fixedUpdates{0};
};
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",
"CylinderCollider",
"ConvexHullCollider",
"MeshCollider",
"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 testEditModeSkipsUpdates()
{
SceneManager& sceneManager = SceneManager::GetInstance();
Scene& scene = sceneManager.CreateScene("edit mode");
auto* counter = scene.CreateGameObject("counter")->AddComponent<CountingComponent>();
sceneManager.SetPlaying(false);
sceneManager.Update(0.1f);
sceneManager.FixedUpdate(0.1f);
sceneManager.LateUpdate(0.1f);
check(counter->updates == 0 && counter->fixedUpdates == 0 && counter->lateUpdates == 0,
"edit mode must not tick components");
check(scene.GetObjects().size() == 1, "edit mode must still add objects created in the editor");
sceneManager.SetPlaying(true);
sceneManager.Update(0.1f);
sceneManager.FixedUpdate(0.1f);
sceneManager.LateUpdate(0.1f);
check(counter->updates == 1 && counter->fixedUpdates == 1 && counter->lateUpdates == 1,
"play mode must tick components");
sceneManager.SetPaused(true);
sceneManager.Update(0.1f);
sceneManager.FixedUpdate(0.1f);
sceneManager.LateUpdate(0.1f);
sceneManager.SetPaused(false);
check(counter->updates == 1 && counter->fixedUpdates == 1 && counter->lateUpdates == 1,
"a paused game must not tick components");
sceneManager.Destroy();
}
}
void RunCoreTests()
{
testTransforms();
testSceneFindGameObject();
testEventMutation();
testEventRemoval();
testComponentRemoval();
testEngineComponentRegistration();
testSceneCurrentCamera();
testHierarchyMoves();
testEditModeSkipsUpdates();
}