#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "TestUtils.h" using namespace test; namespace { void testCanvasLayout() { Scene& scene = SceneManager::GetInstance().CreateScene("canvas"); GameObject* hud = scene.CreateGameObject("hud"); auto* canvas = hud->AddComponent(); const auto addChild = [&](const std::string& name, GameObject& parent) { GameObject* child = scene.CreateGameObject(name); child->GetTransform().SetParent(&parent.GetTransform(), false); return child; }; GameObject* panel = addChild("panel", *hud); auto* panelRect = panel->AddComponent(); panelRect->SetSize({100.0f, 50.0f}); panelRect->SetPosition({10.0f, 0.0f}); panel->AddComponent(glm::vec4{1.0f, 0.0f, 0.0f, 1.0f}); GameObject* fill = addChild("fill", *panel); fill->AddComponent(glm::vec4{0.0f, 1.0f, 0.0f, 1.0f})->SetShape(UIImageShape::Ellipse); GameObject* corner = addChild("corner", *hud); auto* cornerRect = corner->AddComponent(); cornerRect->SetAnchor({1.0f, 1.0f}); cornerRect->SetPivot({1.0f, 1.0f}); cornerRect->SetPosition({-20.0f, -20.0f}); cornerRect->SetSize({40.0f, 40.0f}); corner->AddComponent(); GameObject* hidden = addChild("hidden", *hud); auto* hiddenRect = hidden->AddComponent(); hiddenRect->SetSize({20.0f, 20.0f}); hidden->AddComponent(); hidden->SetActive(false); GameObject* nested = addChild("nested", *hud); nested->AddComponent(); nested->AddComponent(); scene.CommitPendingAdditions(); auto& r2d = Renderer2D::GetInstance(); r2d.Clear(); canvas->Draw({1920.0f, 1080.0f}); const auto& shapes = r2d.GetShapes(); check(shapes.size() == 3, "a canvas must draw its active children and skip inactive ones and nested canvases"); check(shapes[0].type == Shape2DType::Rect && shapes[0].center == glm::vec2{970.0f, 540.0f} && shapes[0].halfSize == glm::vec2{50.0f, 25.0f}, "a centered rect must be placed around its anchor, offset by its position"); check(shapes[1].type == Shape2DType::Ellipse && shapes[1].center == shapes[0].center && shapes[1].halfSize == shapes[0].halfSize, "an object without a RectTransform must fill its parent and draw after it"); check(shapes[2].center == glm::vec2{1880.0f, 1040.0f}, "anchors and pivot must place a rect relative to the screen corner"); check(panelRect->GetScreenRect().min == glm::vec2{920.0f, 515.0f}, "the canvas must store where it drew each rect"); check(hiddenRect->GetScreenRect().size == glm::vec2{20.0f, 20.0f}, "hidden objects must still be laid out so the editor can show them"); r2d.Clear(); canvas->Draw({3840.0f, 2160.0f}); check(r2d.GetShapes().size() == 3 && r2d.GetShapes()[0].halfSize == glm::vec2{100.0f, 50.0f} && r2d.GetShapes()[0].center == glm::vec2{1940.0f, 1080.0f}, "sizes and offsets must scale with the screen height"); canvas->SetScaleMode(CanvasScaleMode::ConstantPixelSize); r2d.Clear(); canvas->Draw({3840.0f, 2160.0f}); check(r2d.GetShapes()[0].halfSize == glm::vec2{50.0f, 25.0f}, "a constant pixel size canvas must not scale"); check(Canvas::FindFor(*fill) == canvas && Canvas::FindFor(*nested) == nested->GetComponent(), "FindFor must return the closest canvas on the object or its parents"); check(cornerRect->GetParentRect().size == glm::vec2{3840.0f, 2160.0f}, "the canvas must store the parent rect each rect was laid out in"); cornerRect->ApplyAnchorPreset({0.0f, -1.0f}); check(cornerRect->GetAnchorMin() == glm::vec2{0.0f, 0.0f} && cornerRect->GetAnchorMax() == glm::vec2{0.0f, 1.0f} && cornerRect->GetPivot() == glm::vec2{0.0f, 0.5f} && cornerRect->GetPosition() == glm::vec2{0.0f} && cornerRect->GetSize() == glm::vec2{40.0f, 0.0f}, "an anchor preset must pin or stretch each axis and keep the size on pinned axes"); panelRect->Stretch(); check(panelRect->ComputeRect(UIRect{.min = {10.0f, 20.0f}, .size = {30.0f, 40.0f}}, 2.0f).size == glm::vec2{30.0f, 40.0f}, "a stretched rect must fill its parent"); r2d.Clear(); SceneManager::GetInstance().Destroy(); } void testUITextPlacement() { Scene& scene = SceneManager::GetInstance().CreateScene("ui text"); auto* text = scene.CreateGameObject("text")->AddComponent("hi", 20.0f); const UIRect rect{.min = {100.0f, 100.0f}, .size = {200.0f, 100.0f}}; check(text->PlaceText(rect, {50.0f, 20.0f}) == glm::vec2{175.0f, 140.0f}, "text must be centered by default"); text->SetAlignment(UITextAlign::End, UITextAlign::Start); check(text->PlaceText(rect, {50.0f, 20.0f}) == glm::vec2{250.0f, 100.0f}, "text must follow its alignment"); SceneManager::GetInstance().Destroy(); } void testHudRoundTrip() { RegisterEngineComponents(); ComponentFactory::Register("CrosshairUI"); ComponentFactory::Register("WinScreenUI"); ComponentFactory::Register("WinGoal"); ComponentFactory::Register("FirstPersonController"); Scene& scene = SceneManager::GetInstance().CreateScene("hud"); GameObject* player = scene.CreateGameObject("player"); player->AddComponent(); GameObject* goal = scene.CreateGameObject("goal"); auto* winGoal = goal->AddComponent(); GameObject* hud = scene.CreateGameObject("hud"); hud->AddComponent()->SetReferenceHeight(720.0f); GameObject* crosshair = scene.CreateGameObject("crosshair"); crosshair->GetTransform().SetParent(&hud->GetTransform(), false); auto* crosshairRect = crosshair->AddComponent(); crosshairRect->SetAnchors({0.25f, 0.5f}, {0.75f, 0.5f}); crosshair->AddComponent()->SetOutline(3.0f); GameObject* screen = scene.CreateGameObject("screen"); screen->GetTransform().SetParent(&hud->GetTransform(), false); auto* label = screen->AddComponent("YOU WIN", 64.0f); label->SetAlignment(UITextAlign::Start, UITextAlign::End); auto* crosshairUi = hud->AddComponent(); crosshairUi->SetPlayer(*player); crosshairUi->SetCrosshair(*crosshair); auto* winScreenUi = hud->AddComponent(); winScreenUi->SetGoal(*goal); winScreenUi->SetScreen(*screen); scene.CommitPendingAdditions(); crosshairUi->LateUpdate(0.0f); winScreenUi->LateUpdate(0.0f); check(!crosshair->IsActive(), "the crosshair must hide while the player isn't possessed"); check(!screen->IsActive(), "the win screen must hide until the goal is reached"); winGoal->Activate(*player); winScreenUi->LateUpdate(0.0f); check(screen->IsActive(), "the win screen must show once the goal is reached"); const auto path = std::filesystem::temp_directory_path() / "destrum_hud_scene.json"; check(SceneSerializer::Save(scene, path), "the hud scene must save"); Scene& copy = SceneManager::GetInstance().CreateScene("hud copy"); check(SceneSerializer::Load(copy, path), "the hud scene must load"); std::filesystem::remove(path); copy.CommitPendingAdditions(); GameObject* loadedHud = findByName(copy, "hud"); GameObject* loadedCrosshair = findByName(copy, "crosshair"); GameObject* loadedScreen = findByName(copy, "screen"); check(loadedHud != nullptr && loadedCrosshair != nullptr && loadedScreen != nullptr, "the hud objects must survive a round-trip"); check(loadedCrosshair->GetTransform().GetParent() == &loadedHud->GetTransform(), "ui objects must stay parented to their canvas"); const auto* loadedCanvas = loadedHud->GetComponent(); check(loadedCanvas != nullptr && loadedCanvas->GetReferenceHeight() == 720.0f, "canvas settings must round-trip"); const auto* loadedRect = loadedCrosshair->GetComponent(); check(loadedRect != nullptr && loadedRect->GetAnchorMin() == glm::vec2{0.25f, 0.5f} && loadedRect->GetAnchorMax() == glm::vec2{0.75f, 0.5f}, "rect transforms must round-trip"); const auto* loadedImage = loadedCrosshair->GetComponent(); check(loadedImage != nullptr && loadedImage->GetOutline() == 3.0f, "ui images must round-trip"); const auto* loadedLabel = loadedScreen->GetComponent(); check(loadedLabel != nullptr && loadedLabel->GetText() == "YOU WIN" && loadedLabel->GetFontSize() == 64.0f && loadedLabel->GetHorizontalAlign() == UITextAlign::Start && loadedLabel->GetVerticalAlign() == UITextAlign::End, "ui text must round-trip"); const auto* loadedCrosshairUi = loadedHud->GetComponent(); const auto* loadedWinScreenUi = loadedHud->GetComponent(); check(loadedCrosshairUi != nullptr && loadedCrosshairUi->GetPlayer().Get(copy) == findByName(copy, "player") && loadedCrosshairUi->GetCrosshair().Get(copy) == loadedCrosshair, "crosshair references must point at the loaded objects"); check(loadedWinScreenUi != nullptr && loadedWinScreenUi->GetGoal().Get(copy) == findByName(copy, "goal") && loadedWinScreenUi->GetScreen().Get(copy) == loadedScreen, "win screen references must point at the loaded objects"); SceneManager::GetInstance().Destroy(); } } void RunUiTests() { testCanvasLayout(); testUITextPlacement(); testHudRoundTrip(); }