Files
XCEngine/tests/Scene/test_scene_runtime.cpp

692 lines
25 KiB
C++

#include <gtest/gtest.h>
#include <XCEngine/Scene/SceneRuntime.h>
#include <XCEngine/Scripting/IScriptRuntime.h>
#include <XCEngine/Scripting/ScriptComponent.h>
#include <XCEngine/Scripting/ScriptEngine.h>
#include <XCEngine/UI/Runtime/UIScreenStackController.h>
#include <XCEngine/UI/Runtime/UIScreenTypes.h>
#include <XCEngine/UI/Runtime/UISystem.h>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <memory>
#include <string>
#include <vector>
using namespace XCEngine::Components;
using namespace XCEngine::Scripting;
using XCEngine::UI::Runtime::UIScreenAsset;
namespace fs = std::filesystem;
namespace {
std::string LifecycleMethodToString(ScriptLifecycleMethod method) {
switch (method) {
case ScriptLifecycleMethod::Awake: return "Awake";
case ScriptLifecycleMethod::OnEnable: return "OnEnable";
case ScriptLifecycleMethod::Start: return "Start";
case ScriptLifecycleMethod::FixedUpdate: return "FixedUpdate";
case ScriptLifecycleMethod::Update: return "Update";
case ScriptLifecycleMethod::LateUpdate: return "LateUpdate";
case ScriptLifecycleMethod::OnDisable: return "OnDisable";
case ScriptLifecycleMethod::OnDestroy: return "OnDestroy";
}
return "Unknown";
}
class OrderedObserverComponent : public Component {
public:
explicit OrderedObserverComponent(std::vector<std::string>* events)
: m_events(events) {
}
std::string GetName() const override { return "OrderedObserver"; }
void Start() override {
if (m_events) {
m_events->push_back("NativeStart:" + GetGameObject()->GetName());
}
}
void FixedUpdate() override {
if (m_events) {
m_events->push_back("NativeFixedUpdate:" + GetGameObject()->GetName());
}
}
void Update(float deltaTime) override {
(void)deltaTime;
if (m_events) {
m_events->push_back("NativeUpdate:" + GetGameObject()->GetName());
}
}
void LateUpdate(float deltaTime) override {
(void)deltaTime;
if (m_events) {
m_events->push_back("NativeLateUpdate:" + GetGameObject()->GetName());
}
}
private:
std::vector<std::string>* m_events = nullptr;
};
class TempFileScope {
public:
TempFileScope(std::string stem, std::string extension, std::string contents) {
const auto uniqueId = std::to_string(
std::chrono::steady_clock::now().time_since_epoch().count());
m_path = fs::temp_directory_path() / (std::move(stem) + "_" + uniqueId + std::move(extension));
std::ofstream output(m_path, std::ios::binary | std::ios::trunc);
output << contents;
}
~TempFileScope() {
std::error_code ec;
fs::remove(m_path, ec);
}
const fs::path& Path() const {
return m_path;
}
private:
fs::path m_path = {};
};
std::string BuildViewMarkup(const char* heroTitle) {
return
"<View name=\"Runtime Screen\">\n"
" <Column id=\"root\" padding=\"18\" gap=\"10\">\n"
" <Card id=\"hero\" title=\"" + std::string(heroTitle) + "\" subtitle=\"Shared XCUI runtime layer\" />\n"
" <Text id=\"status\" text=\"Ready for play\" />\n"
" </Column>\n"
"</View>\n";
}
UIScreenAsset BuildScreenAsset(const fs::path& viewPath, const char* screenId) {
UIScreenAsset screen = {};
screen.screenId = screenId;
screen.documentPath = viewPath.string();
return screen;
}
bool DrawDataContainsText(
const XCEngine::UI::UIDrawData& drawData,
const std::string& text) {
for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) {
for (const XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) {
if (command.type == XCEngine::UI::UIDrawCommandType::Text &&
command.text == text) {
return true;
}
}
}
return false;
}
const XCEngine::UI::UIDrawCommand* FindFirstFilledRectCommand(
const XCEngine::UI::UIDrawData& drawData) {
for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) {
for (const XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) {
if (command.type == XCEngine::UI::UIDrawCommandType::FilledRect) {
return &command;
}
}
}
return nullptr;
}
const XCEngine::UI::Runtime::UISystemPresentedLayer* FindPresentedLayerById(
const XCEngine::UI::Runtime::UISystemFrameResult& frame,
XCEngine::UI::Runtime::UIScreenLayerId layerId) {
for (const XCEngine::UI::Runtime::UISystemPresentedLayer& layer : frame.layers) {
if (layer.layerId == layerId) {
return &layer;
}
}
return nullptr;
}
class RecordingScriptRuntime : public IScriptRuntime {
public:
explicit RecordingScriptRuntime(std::vector<std::string>* events)
: m_events(events) {
}
void OnRuntimeStart(Scene* scene) override {
if (m_events) {
m_events->push_back("RuntimeStart:" + (scene ? scene->GetName() : std::string("null")));
}
}
void OnRuntimeStop(Scene* scene) override {
if (m_events) {
m_events->push_back("RuntimeStop:" + (scene ? scene->GetName() : std::string("null")));
}
}
bool TryGetAvailableScriptClasses(
std::vector<ScriptClassDescriptor>& outClasses) const override {
outClasses.clear();
return false;
}
bool TryGetClassFieldMetadata(
const std::string& assemblyName,
const std::string& namespaceName,
const std::string& className,
std::vector<ScriptFieldMetadata>& outFields) const override {
(void)assemblyName;
(void)namespaceName;
(void)className;
outFields.clear();
return false;
}
bool TryGetClassFieldDefaultValues(
const std::string& assemblyName,
const std::string& namespaceName,
const std::string& className,
std::vector<ScriptFieldDefaultValue>& outFields) const override {
(void)assemblyName;
(void)namespaceName;
(void)className;
outFields.clear();
return false;
}
bool TrySetManagedFieldValue(
const ScriptRuntimeContext& context,
const std::string& fieldName,
const ScriptFieldValue& value) override {
(void)context;
(void)fieldName;
(void)value;
return true;
}
bool TryGetManagedFieldValue(
const ScriptRuntimeContext& context,
const std::string& fieldName,
ScriptFieldValue& outValue) const override {
(void)context;
(void)fieldName;
(void)outValue;
return false;
}
void SyncManagedFieldsToStorage(const ScriptRuntimeContext& context) override {
(void)context;
}
bool CreateScriptInstance(const ScriptRuntimeContext& context) override {
if (m_events) {
m_events->push_back("Create:" + Describe(context));
}
return true;
}
void DestroyScriptInstance(const ScriptRuntimeContext& context) override {
if (m_events) {
m_events->push_back("Destroy:" + Describe(context));
}
}
void InvokeMethod(
const ScriptRuntimeContext& context,
ScriptLifecycleMethod method,
float deltaTime) override {
(void)deltaTime;
if (m_events) {
m_events->push_back(LifecycleMethodToString(method) + ":" + Describe(context));
}
}
private:
static std::string Describe(const ScriptRuntimeContext& context) {
const std::string gameObjectName = context.gameObject ? context.gameObject->GetName() : "null";
const std::string className = context.component ? context.component->GetFullClassName() : "null";
return gameObjectName + ":" + className;
}
std::vector<std::string>* m_events = nullptr;
};
class SceneRuntimeTest : public ::testing::Test {
protected:
void SetUp() override {
scriptEngine = &ScriptEngine::Get();
scriptEngine->OnRuntimeStop();
runtimeImpl = std::make_unique<RecordingScriptRuntime>(&events);
scriptEngine->SetRuntime(runtimeImpl.get());
}
void TearDown() override {
runtime.Stop();
scriptEngine->OnRuntimeStop();
scriptEngine->SetRuntime(nullptr);
runtimeImpl.reset();
scene.reset();
}
Scene* CreateScene(const std::string& sceneName) {
scene = std::make_unique<Scene>(sceneName);
return scene.get();
}
ScriptComponent* AddScript(GameObject* gameObject, const std::string& namespaceName, const std::string& className) {
ScriptComponent* component = gameObject->AddComponent<ScriptComponent>();
component->SetScriptClass("GameScripts", namespaceName, className);
return component;
}
std::vector<std::string> events;
std::unique_ptr<Scene> scene;
std::unique_ptr<RecordingScriptRuntime> runtimeImpl;
ScriptEngine* scriptEngine = nullptr;
SceneRuntime runtime;
};
TEST_F(SceneRuntimeTest, StartAndStopForwardToScriptEngine) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
AddScript(host, "Gameplay", "Bootstrap");
runtime.Start(runtimeScene);
runtime.Stop();
const std::vector<std::string> expected = {
"RuntimeStart:RuntimeScene",
"Create:Host:Gameplay.Bootstrap",
"Awake:Host:Gameplay.Bootstrap",
"OnEnable:Host:Gameplay.Bootstrap",
"OnDisable:Host:Gameplay.Bootstrap",
"OnDestroy:Host:Gameplay.Bootstrap",
"Destroy:Host:Gameplay.Bootstrap",
"RuntimeStop:RuntimeScene"
};
EXPECT_EQ(events, expected);
EXPECT_FALSE(runtime.IsRunning());
EXPECT_EQ(runtime.GetScene(), nullptr);
}
TEST_F(SceneRuntimeTest, FrameOrderRunsScriptLifecycleBeforeNativeComponents) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
host->AddComponent<OrderedObserverComponent>(&events);
AddScript(host, "Gameplay", "Mover");
runtime.Start(runtimeScene);
events.clear();
runtime.FixedUpdate(0.02f);
runtime.Update(0.016f);
runtime.LateUpdate(0.016f);
const std::vector<std::string> expected = {
"FixedUpdate:Host:Gameplay.Mover",
"NativeFixedUpdate:Host",
"Start:Host:Gameplay.Mover",
"Update:Host:Gameplay.Mover",
"NativeStart:Host",
"NativeUpdate:Host",
"LateUpdate:Host:Gameplay.Mover",
"NativeLateUpdate:Host"
};
EXPECT_EQ(events, expected);
}
TEST_F(SceneRuntimeTest, InactiveSceneSkipsFrameExecution) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
host->AddComponent<OrderedObserverComponent>(&events);
AddScript(host, "Gameplay", "PausedScript");
runtimeScene->SetActive(false);
runtime.Start(runtimeScene);
events.clear();
runtime.FixedUpdate(0.02f);
runtime.Update(0.016f);
runtime.LateUpdate(0.016f);
EXPECT_TRUE(events.empty());
}
TEST_F(SceneRuntimeTest, StartingNewSceneStopsPreviousRuntimeFirst) {
Scene* firstScene = CreateScene("FirstScene");
GameObject* firstHost = firstScene->CreateGameObject("FirstHost");
AddScript(firstHost, "Gameplay", "FirstScript");
runtime.Start(firstScene);
std::unique_ptr<Scene> secondScene = std::make_unique<Scene>("SecondScene");
GameObject* secondHost = secondScene->CreateGameObject("SecondHost");
ScriptComponent* secondScript = AddScript(secondHost, "Gameplay", "SecondScript");
events.clear();
runtime.Start(secondScene.get());
const std::vector<std::string> expected = {
"OnDisable:FirstHost:Gameplay.FirstScript",
"OnDestroy:FirstHost:Gameplay.FirstScript",
"Destroy:FirstHost:Gameplay.FirstScript",
"RuntimeStop:FirstScene",
"RuntimeStart:SecondScene",
"Create:SecondHost:Gameplay.SecondScript",
"Awake:SecondHost:Gameplay.SecondScript",
"OnEnable:SecondHost:Gameplay.SecondScript"
};
EXPECT_EQ(events, expected);
EXPECT_EQ(runtime.GetScene(), secondScene.get());
EXPECT_TRUE(scriptEngine->HasRuntimeInstance(secondScript));
runtime.Stop();
}
TEST_F(SceneRuntimeTest, UpdateTicksUiRuntimeAndClearsQueuedInputEvents) {
Scene* runtimeScene = CreateScene("RuntimeScene");
runtime.Start(runtimeScene);
runtime.SetUIViewportRect(XCEngine::UI::UIRect(0.0f, 0.0f, 800.0f, 480.0f));
runtime.SetUIFocused(true);
TempFileScope menuView("xcui_scene_runtime_menu", ".xcui", BuildViewMarkup("Runtime Menu"));
const auto layerId = runtime.GetUIScreenStackController().PushMenu(
BuildScreenAsset(menuView.Path(), "runtime.menu"),
"menu");
ASSERT_NE(layerId, 0u);
XCEngine::UI::UIInputEvent textEvent = {};
textEvent.type = XCEngine::UI::UIInputEventType::Character;
textEvent.character = 'A';
runtime.QueueUIInputEvent(textEvent);
runtime.Update(0.016f);
const auto& firstFrame = runtime.GetLastUIFrame();
ASSERT_EQ(firstFrame.presentedLayerCount, 1u);
ASSERT_EQ(firstFrame.layers.size(), 1u);
EXPECT_EQ(firstFrame.frameIndex, 1u);
EXPECT_EQ(firstFrame.layers.front().stats.inputEventCount, 1u);
EXPECT_TRUE(DrawDataContainsText(firstFrame.drawData, "Runtime Menu"));
runtime.Update(0.016f);
const auto& secondFrame = runtime.GetLastUIFrame();
ASSERT_EQ(secondFrame.presentedLayerCount, 1u);
ASSERT_EQ(secondFrame.layers.size(), 1u);
EXPECT_EQ(secondFrame.frameIndex, 2u);
EXPECT_EQ(secondFrame.layers.front().stats.inputEventCount, 0u);
}
TEST_F(SceneRuntimeTest, StopClearsUiRuntimeState) {
Scene* runtimeScene = CreateScene("RuntimeScene");
runtime.Start(runtimeScene);
runtime.SetUIViewportRect(XCEngine::UI::UIRect(0.0f, 0.0f, 800.0f, 480.0f));
runtime.SetUIFocused(true);
TempFileScope menuView("xcui_scene_runtime_pause", ".xcui", BuildViewMarkup("Pause Menu"));
const auto layerId = runtime.GetUIScreenStackController().PushMenu(
BuildScreenAsset(menuView.Path(), "runtime.pause"),
"pause");
ASSERT_NE(layerId, 0u);
runtime.Update(0.016f);
ASSERT_EQ(runtime.GetUISystem().GetLayerCount(), 1u);
ASSERT_EQ(runtime.GetUIScreenStackController().GetEntryCount(), 1u);
ASSERT_EQ(runtime.GetLastUIFrame().presentedLayerCount, 1u);
runtime.Stop();
EXPECT_FALSE(runtime.IsRunning());
EXPECT_EQ(runtime.GetScene(), nullptr);
EXPECT_EQ(runtime.GetUISystem().GetLayerCount(), 0u);
EXPECT_EQ(runtime.GetUIScreenStackController().GetEntryCount(), 0u);
EXPECT_EQ(runtime.GetLastUIFrame().presentedLayerCount, 0u);
EXPECT_TRUE(runtime.GetLastUIFrame().layers.empty());
}
TEST_F(SceneRuntimeTest, ClearQueuedUiInputEventsPreventsPendingDelivery) {
Scene* runtimeScene = CreateScene("RuntimeScene");
runtime.Start(runtimeScene);
runtime.SetUIViewportRect(XCEngine::UI::UIRect(0.0f, 0.0f, 800.0f, 480.0f));
runtime.SetUIFocused(true);
TempFileScope menuView("xcui_scene_runtime_clear_input", ".xcui", BuildViewMarkup("Clear Input Menu"));
const auto layerId = runtime.GetUIScreenStackController().PushMenu(
BuildScreenAsset(menuView.Path(), "runtime.clear.input"),
"clear-input");
ASSERT_NE(layerId, 0u);
XCEngine::UI::UIInputEvent textEvent = {};
textEvent.type = XCEngine::UI::UIInputEventType::Character;
textEvent.character = 'A';
runtime.QueueUIInputEvent(textEvent);
XCEngine::UI::UIInputEvent keyEvent = {};
keyEvent.type = XCEngine::UI::UIInputEventType::KeyDown;
keyEvent.keyCode = 13;
runtime.QueueUIInputEvent(keyEvent);
runtime.ClearQueuedUIInputEvents();
runtime.Update(0.016f);
const auto& clearedFrame = runtime.GetLastUIFrame();
ASSERT_EQ(clearedFrame.presentedLayerCount, 1u);
ASSERT_EQ(clearedFrame.layers.size(), 1u);
EXPECT_EQ(clearedFrame.frameIndex, 1u);
EXPECT_EQ(clearedFrame.layers.front().layerId, layerId);
EXPECT_EQ(clearedFrame.layers.front().stats.inputEventCount, 0u);
runtime.QueueUIInputEvent(textEvent);
runtime.Update(0.016f);
const auto& deliveredFrame = runtime.GetLastUIFrame();
ASSERT_EQ(deliveredFrame.presentedLayerCount, 1u);
ASSERT_EQ(deliveredFrame.layers.size(), 1u);
EXPECT_EQ(deliveredFrame.frameIndex, 2u);
EXPECT_EQ(deliveredFrame.layers.front().stats.inputEventCount, 1u);
}
TEST_F(SceneRuntimeTest, ViewportPersistsAcrossFramesAndResetsAfterStop) {
Scene* runtimeScene = CreateScene("RuntimeScene");
runtime.Start(runtimeScene);
runtime.SetUIViewportRect(XCEngine::UI::UIRect(32.0f, 48.0f, 900.0f, 500.0f));
runtime.SetUIFocused(true);
TempFileScope menuView("xcui_scene_runtime_viewport", ".xcui", BuildViewMarkup("Viewport Menu"));
const UIScreenAsset screenAsset = BuildScreenAsset(menuView.Path(), "runtime.viewport.menu");
ASSERT_NE(runtime.GetUIScreenStackController().PushMenu(screenAsset, "viewport-menu"), 0u);
runtime.Update(0.016f);
const auto& firstFrame = runtime.GetLastUIFrame();
const auto* firstBackground = FindFirstFilledRectCommand(firstFrame.drawData);
ASSERT_NE(firstBackground, nullptr);
EXPECT_EQ(firstFrame.frameIndex, 1u);
EXPECT_FLOAT_EQ(firstBackground->rect.x, 32.0f);
EXPECT_FLOAT_EQ(firstBackground->rect.y, 48.0f);
EXPECT_FLOAT_EQ(firstBackground->rect.width, 900.0f);
EXPECT_FLOAT_EQ(firstBackground->rect.height, 500.0f);
runtime.Update(0.016f);
const auto& secondFrame = runtime.GetLastUIFrame();
const auto* secondBackground = FindFirstFilledRectCommand(secondFrame.drawData);
ASSERT_NE(secondBackground, nullptr);
EXPECT_EQ(secondFrame.frameIndex, 2u);
EXPECT_FLOAT_EQ(secondBackground->rect.x, 32.0f);
EXPECT_FLOAT_EQ(secondBackground->rect.y, 48.0f);
EXPECT_FLOAT_EQ(secondBackground->rect.width, 900.0f);
EXPECT_FLOAT_EQ(secondBackground->rect.height, 500.0f);
runtime.Stop();
runtime.Start(runtimeScene);
runtime.SetUIFocused(true);
ASSERT_NE(runtime.GetUIScreenStackController().PushMenu(screenAsset, "viewport-menu-reset"), 0u);
runtime.Update(0.016f);
const auto& restartedFrame = runtime.GetLastUIFrame();
const auto* restartedBackground = FindFirstFilledRectCommand(restartedFrame.drawData);
ASSERT_NE(restartedBackground, nullptr);
EXPECT_EQ(restartedFrame.frameIndex, 1u);
EXPECT_FLOAT_EQ(restartedBackground->rect.x, 0.0f);
EXPECT_FLOAT_EQ(restartedBackground->rect.y, 0.0f);
EXPECT_FLOAT_EQ(restartedBackground->rect.width, 640.0f);
EXPECT_FLOAT_EQ(restartedBackground->rect.height, 360.0f);
}
TEST_F(SceneRuntimeTest, LayeredSceneUiRoutesInputOnlyToTopInteractivePresentedLayer) {
Scene* runtimeScene = CreateScene("RuntimeScene");
runtime.Start(runtimeScene);
runtime.SetUIViewportRect(XCEngine::UI::UIRect(0.0f, 0.0f, 1280.0f, 720.0f));
runtime.SetUIFocused(true);
TempFileScope gameplayView("xcui_scene_runtime_gameplay", ".xcui", BuildViewMarkup("Gameplay Layer"));
TempFileScope overlayView("xcui_scene_runtime_overlay", ".xcui", BuildViewMarkup("Overlay Layer"));
XCEngine::UI::Runtime::UIScreenLayerOptions gameplayOptions = {};
gameplayOptions.debugName = "gameplay";
gameplayOptions.acceptsInput = true;
gameplayOptions.blocksLayersBelow = false;
XCEngine::UI::Runtime::UIScreenLayerOptions overlayOptions = {};
overlayOptions.debugName = "overlay";
overlayOptions.acceptsInput = true;
overlayOptions.blocksLayersBelow = false;
const auto gameplayLayerId = runtime.GetUIScreenStackController().PushScreen(
BuildScreenAsset(gameplayView.Path(), "runtime.gameplay"),
gameplayOptions);
const auto overlayLayerId = runtime.GetUIScreenStackController().PushScreen(
BuildScreenAsset(overlayView.Path(), "runtime.overlay"),
overlayOptions);
ASSERT_NE(gameplayLayerId, 0u);
ASSERT_NE(overlayLayerId, 0u);
XCEngine::UI::UIInputEvent textEvent = {};
textEvent.type = XCEngine::UI::UIInputEventType::Character;
textEvent.character = 'I';
runtime.QueueUIInputEvent(textEvent);
runtime.Update(0.016f);
const auto& frame = runtime.GetLastUIFrame();
ASSERT_EQ(frame.presentedLayerCount, 2u);
ASSERT_EQ(frame.skippedLayerCount, 0u);
ASSERT_EQ(frame.layers.size(), 2u);
const auto* gameplayLayer = FindPresentedLayerById(frame, gameplayLayerId);
const auto* overlayLayer = FindPresentedLayerById(frame, overlayLayerId);
ASSERT_NE(gameplayLayer, nullptr);
ASSERT_NE(overlayLayer, nullptr);
EXPECT_EQ(gameplayLayer->stats.inputEventCount, 0u);
EXPECT_EQ(overlayLayer->stats.inputEventCount, 1u);
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Gameplay Layer"));
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Overlay Layer"));
}
TEST_F(SceneRuntimeTest, BlockingLayerSkipsLowerLayersAndOwnsQueuedInput) {
Scene* runtimeScene = CreateScene("RuntimeScene");
runtime.Start(runtimeScene);
runtime.SetUIViewportRect(XCEngine::UI::UIRect(0.0f, 0.0f, 1280.0f, 720.0f));
runtime.SetUIFocused(true);
TempFileScope gameplayView("xcui_scene_runtime_blocked_gameplay", ".xcui", BuildViewMarkup("Blocked Gameplay"));
TempFileScope modalView("xcui_scene_runtime_modal", ".xcui", BuildViewMarkup("Pause Modal"));
XCEngine::UI::Runtime::UIScreenLayerOptions gameplayOptions = {};
gameplayOptions.debugName = "gameplay";
gameplayOptions.acceptsInput = true;
gameplayOptions.blocksLayersBelow = false;
const auto gameplayLayerId = runtime.GetUIScreenStackController().PushScreen(
BuildScreenAsset(gameplayView.Path(), "runtime.blocked.gameplay"),
gameplayOptions);
const auto modalLayerId = runtime.GetUIScreenStackController().PushModal(
BuildScreenAsset(modalView.Path(), "runtime.pause.modal"),
"pause-modal");
ASSERT_NE(gameplayLayerId, 0u);
ASSERT_NE(modalLayerId, 0u);
XCEngine::UI::UIInputEvent textEvent = {};
textEvent.type = XCEngine::UI::UIInputEventType::Character;
textEvent.character = 'P';
runtime.QueueUIInputEvent(textEvent);
runtime.Update(0.016f);
const auto& frame = runtime.GetLastUIFrame();
ASSERT_EQ(frame.presentedLayerCount, 1u);
ASSERT_EQ(frame.skippedLayerCount, 1u);
ASSERT_EQ(frame.layers.size(), 1u);
const auto* modalLayer = FindPresentedLayerById(frame, modalLayerId);
ASSERT_NE(modalLayer, nullptr);
EXPECT_EQ(modalLayer->stats.inputEventCount, 1u);
EXPECT_EQ(FindPresentedLayerById(frame, gameplayLayerId), nullptr);
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Pause Modal"));
EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Blocked Gameplay"));
}
TEST_F(SceneRuntimeTest, HiddenTopLayerDoesNotStealInputFromVisibleUnderlyingLayer) {
Scene* runtimeScene = CreateScene("RuntimeScene");
runtime.Start(runtimeScene);
runtime.SetUIViewportRect(XCEngine::UI::UIRect(0.0f, 0.0f, 1280.0f, 720.0f));
runtime.SetUIFocused(true);
TempFileScope gameplayView("xcui_scene_runtime_visible_gameplay", ".xcui", BuildViewMarkup("Visible Gameplay"));
TempFileScope hiddenOverlayView("xcui_scene_runtime_hidden_overlay", ".xcui", BuildViewMarkup("Hidden Overlay"));
XCEngine::UI::Runtime::UIScreenLayerOptions gameplayOptions = {};
gameplayOptions.debugName = "gameplay";
gameplayOptions.acceptsInput = true;
gameplayOptions.blocksLayersBelow = false;
XCEngine::UI::Runtime::UIScreenLayerOptions hiddenOverlayOptions = {};
hiddenOverlayOptions.debugName = "hidden-overlay";
hiddenOverlayOptions.visible = false;
hiddenOverlayOptions.acceptsInput = true;
hiddenOverlayOptions.blocksLayersBelow = false;
const auto gameplayLayerId = runtime.GetUIScreenStackController().PushScreen(
BuildScreenAsset(gameplayView.Path(), "runtime.visible.gameplay"),
gameplayOptions);
const auto hiddenOverlayLayerId = runtime.GetUIScreenStackController().PushScreen(
BuildScreenAsset(hiddenOverlayView.Path(), "runtime.hidden.overlay"),
hiddenOverlayOptions);
ASSERT_NE(gameplayLayerId, 0u);
ASSERT_NE(hiddenOverlayLayerId, 0u);
XCEngine::UI::UIInputEvent textEvent = {};
textEvent.type = XCEngine::UI::UIInputEventType::Character;
textEvent.character = 'W';
runtime.QueueUIInputEvent(textEvent);
runtime.Update(0.016f);
const auto& frame = runtime.GetLastUIFrame();
ASSERT_EQ(frame.presentedLayerCount, 1u);
ASSERT_EQ(frame.skippedLayerCount, 1u);
ASSERT_EQ(frame.layers.size(), 1u);
const auto* gameplayLayer = FindPresentedLayerById(frame, gameplayLayerId);
ASSERT_NE(gameplayLayer, nullptr);
EXPECT_EQ(gameplayLayer->stats.inputEventCount, 1u);
EXPECT_EQ(FindPresentedLayerById(frame, hiddenOverlayLayerId), nullptr);
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Visible Gameplay"));
EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Hidden Overlay"));
}
} // namespace