Add script runtime lifecycle skeleton

This commit is contained in:
2026-03-26 20:45:41 +08:00
parent a78593e7e1
commit 9a2d77b81d
10 changed files with 813 additions and 0 deletions

View File

@@ -241,12 +241,17 @@ add_library(XCEngine STATIC
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/AudioClip/AudioLoader.cpp
# Scripting
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/IScriptRuntime.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/NullScriptRuntime.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/ScriptField.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/ScriptFieldStorage.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/ScriptComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/ScriptEngine.h
${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/NullScriptRuntime.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/ScriptField.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/ScriptFieldStorage.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/ScriptComponent.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/ScriptEngine.cpp
# Components
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/Component.h

View File

@@ -0,0 +1,52 @@
#pragma once
#include <cstdint>
namespace XCEngine {
namespace Components {
class GameObject;
class Scene;
}
namespace Scripting {
class ScriptComponent;
enum class ScriptLifecycleMethod {
Awake = 0,
OnEnable,
Start,
FixedUpdate,
Update,
LateUpdate,
OnDisable,
OnDestroy
};
struct ScriptRuntimeContext {
Components::Scene* scene = nullptr;
Components::GameObject* gameObject = nullptr;
ScriptComponent* component = nullptr;
uint64_t gameObjectUUID = 0;
uint64_t scriptComponentUUID = 0;
};
class IScriptRuntime {
public:
virtual ~IScriptRuntime() = default;
virtual void OnRuntimeStart(Components::Scene* scene) = 0;
virtual void OnRuntimeStop(Components::Scene* scene) = 0;
virtual bool CreateScriptInstance(const ScriptRuntimeContext& context) = 0;
virtual void DestroyScriptInstance(const ScriptRuntimeContext& context) = 0;
virtual void InvokeMethod(
const ScriptRuntimeContext& context,
ScriptLifecycleMethod method,
float deltaTime) = 0;
};
} // namespace Scripting
} // namespace XCEngine

View File

@@ -0,0 +1,23 @@
#pragma once
#include <XCEngine/Scripting/IScriptRuntime.h>
namespace XCEngine {
namespace Scripting {
class NullScriptRuntime : public IScriptRuntime {
public:
void OnRuntimeStart(Components::Scene* scene) override;
void OnRuntimeStop(Components::Scene* scene) override;
bool CreateScriptInstance(const ScriptRuntimeContext& context) override;
void DestroyScriptInstance(const ScriptRuntimeContext& context) override;
void InvokeMethod(
const ScriptRuntimeContext& context,
ScriptLifecycleMethod method,
float deltaTime) override;
};
} // namespace Scripting
} // namespace XCEngine

View File

@@ -36,6 +36,10 @@ public:
const ScriptFieldStorage& GetFieldStorage() const { return m_fieldStorage; }
void SetFieldStorage(const ScriptFieldStorage& fieldStorage) { m_fieldStorage = fieldStorage; }
void OnEnable() override;
void OnDisable() override;
void OnDestroy() override;
void Serialize(std::ostream& os) const override;
void Deserialize(std::istream& is) override;

View File

@@ -0,0 +1,88 @@
#pragma once
#include <XCEngine/Scripting/IScriptRuntime.h>
#include <XCEngine/Scripting/NullScriptRuntime.h>
#include <cstddef>
#include <cstdint>
#include <unordered_map>
#include <vector>
namespace XCEngine {
namespace Scripting {
class ScriptComponent;
class ScriptEngine {
public:
static ScriptEngine& Get();
void SetRuntime(IScriptRuntime* runtime);
IScriptRuntime* GetRuntime() const { return m_runtime; }
void OnRuntimeStart(Components::Scene* scene);
void OnRuntimeStop();
void OnFixedUpdate(float fixedDeltaTime);
void OnUpdate(float deltaTime);
void OnLateUpdate(float deltaTime);
void OnScriptComponentEnabled(ScriptComponent* component);
void OnScriptComponentDisabled(ScriptComponent* component);
void OnScriptComponentDestroyed(ScriptComponent* component);
bool IsRuntimeRunning() const { return m_runtimeRunning; }
Components::Scene* GetRuntimeScene() const { return m_runtimeScene; }
bool HasTrackedScriptComponent(const ScriptComponent* component) const;
bool HasRuntimeInstance(const ScriptComponent* component) const;
size_t GetTrackedScriptCount() const { return m_scriptOrder.size(); }
private:
struct ScriptInstanceKey {
uint64_t gameObjectUUID = 0;
uint64_t scriptComponentUUID = 0;
bool operator==(const ScriptInstanceKey& other) const {
return gameObjectUUID == other.gameObjectUUID
&& scriptComponentUUID == other.scriptComponentUUID;
}
};
struct ScriptInstanceKeyHasher {
size_t operator()(const ScriptInstanceKey& key) const;
};
struct ScriptInstanceState {
ScriptRuntimeContext context;
bool instanceCreated = false;
bool awakeCalled = false;
bool enabled = false;
bool startCalled = false;
bool startPending = false;
};
ScriptEngine() = default;
void CollectScriptComponents(Components::GameObject* gameObject);
ScriptInstanceState* TrackScriptComponent(ScriptComponent* component);
ScriptInstanceState* FindState(const ScriptComponent* component);
const ScriptInstanceState* FindState(const ScriptComponent* component) const;
void RemoveTrackedScriptComponent(const ScriptComponent* component);
bool ShouldScriptRun(const ScriptInstanceState& state) const;
bool EnsureScriptReady(ScriptInstanceState& state, bool invokeEnableIfNeeded);
void InvokeLifecycleMethod(ScriptInstanceState& state, ScriptLifecycleMethod method, float deltaTime = 0.0f);
void StopTrackingScript(ScriptInstanceState& state, bool runtimeStopping);
NullScriptRuntime m_nullRuntime;
IScriptRuntime* m_runtime = &m_nullRuntime;
Components::Scene* m_runtimeScene = nullptr;
bool m_runtimeRunning = false;
std::unordered_map<ScriptInstanceKey, ScriptInstanceState, ScriptInstanceKeyHasher> m_scriptStates;
std::vector<ScriptInstanceKey> m_scriptOrder;
};
} // namespace Scripting
} // namespace XCEngine

View File

@@ -0,0 +1,32 @@
#include "Scripting/NullScriptRuntime.h"
namespace XCEngine {
namespace Scripting {
void NullScriptRuntime::OnRuntimeStart(Components::Scene* scene) {
(void)scene;
}
void NullScriptRuntime::OnRuntimeStop(Components::Scene* scene) {
(void)scene;
}
bool NullScriptRuntime::CreateScriptInstance(const ScriptRuntimeContext& context) {
return context.component != nullptr;
}
void NullScriptRuntime::DestroyScriptInstance(const ScriptRuntimeContext& context) {
(void)context;
}
void NullScriptRuntime::InvokeMethod(
const ScriptRuntimeContext& context,
ScriptLifecycleMethod method,
float deltaTime) {
(void)context;
(void)method;
(void)deltaTime;
}
} // namespace Scripting
} // namespace XCEngine

View File

@@ -1,5 +1,7 @@
#include "Scripting/ScriptComponent.h"
#include "Scripting/ScriptEngine.h"
#include <random>
#include <sstream>
@@ -44,6 +46,18 @@ std::string ScriptComponent::GetFullClassName() const {
return m_namespaceName + "." + m_className;
}
void ScriptComponent::OnEnable() {
ScriptEngine::Get().OnScriptComponentEnabled(this);
}
void ScriptComponent::OnDisable() {
ScriptEngine::Get().OnScriptComponentDisabled(this);
}
void ScriptComponent::OnDestroy() {
ScriptEngine::Get().OnScriptComponentDestroyed(this);
}
void ScriptComponent::Serialize(std::ostream& os) const {
os << "scriptComponentUUID=" << m_scriptComponentUUID << ";";
os << "assembly=" << EscapeScriptString(m_assemblyName) << ";";

View File

@@ -0,0 +1,355 @@
#include "Scripting/ScriptEngine.h"
#include "Components/GameObject.h"
#include "Scene/Scene.h"
#include "Scripting/ScriptComponent.h"
#include <algorithm>
namespace XCEngine {
namespace Scripting {
ScriptEngine& ScriptEngine::Get() {
static ScriptEngine instance;
return instance;
}
void ScriptEngine::SetRuntime(IScriptRuntime* runtime) {
m_runtime = runtime ? runtime : &m_nullRuntime;
}
void ScriptEngine::OnRuntimeStart(Components::Scene* scene) {
OnRuntimeStop();
if (!scene) {
return;
}
m_runtimeScene = scene;
m_runtimeRunning = true;
m_runtime->OnRuntimeStart(scene);
for (Components::GameObject* root : scene->GetRootGameObjects()) {
CollectScriptComponents(root);
}
for (const ScriptInstanceKey& key : m_scriptOrder) {
auto it = m_scriptStates.find(key);
if (it == m_scriptStates.end()) {
continue;
}
ScriptInstanceState& state = it->second;
if (!ShouldScriptRun(state)) {
continue;
}
EnsureScriptReady(state, true);
}
}
void ScriptEngine::OnRuntimeStop() {
if (!m_runtimeRunning) {
m_runtimeScene = nullptr;
m_scriptStates.clear();
m_scriptOrder.clear();
return;
}
std::vector<ScriptInstanceKey> keys = m_scriptOrder;
for (const ScriptInstanceKey& key : keys) {
auto it = m_scriptStates.find(key);
if (it != m_scriptStates.end()) {
StopTrackingScript(it->second, true);
}
}
Components::Scene* stoppedScene = m_runtimeScene;
m_scriptStates.clear();
m_scriptOrder.clear();
m_runtimeRunning = false;
m_runtimeScene = nullptr;
m_runtime->OnRuntimeStop(stoppedScene);
}
void ScriptEngine::OnFixedUpdate(float fixedDeltaTime) {
if (!m_runtimeRunning) {
return;
}
for (const ScriptInstanceKey& key : m_scriptOrder) {
auto it = m_scriptStates.find(key);
if (it == m_scriptStates.end()) {
continue;
}
ScriptInstanceState& state = it->second;
if (!ShouldScriptRun(state) || !EnsureScriptReady(state, true) || !state.enabled) {
continue;
}
InvokeLifecycleMethod(state, ScriptLifecycleMethod::FixedUpdate, fixedDeltaTime);
}
}
void ScriptEngine::OnUpdate(float deltaTime) {
if (!m_runtimeRunning) {
return;
}
for (const ScriptInstanceKey& key : m_scriptOrder) {
auto it = m_scriptStates.find(key);
if (it == m_scriptStates.end()) {
continue;
}
ScriptInstanceState& state = it->second;
if (!ShouldScriptRun(state) || !EnsureScriptReady(state, true) || !state.enabled) {
continue;
}
if (state.startPending && !state.startCalled) {
InvokeLifecycleMethod(state, ScriptLifecycleMethod::Start);
state.startCalled = true;
state.startPending = false;
}
InvokeLifecycleMethod(state, ScriptLifecycleMethod::Update, deltaTime);
}
}
void ScriptEngine::OnLateUpdate(float deltaTime) {
if (!m_runtimeRunning) {
return;
}
for (const ScriptInstanceKey& key : m_scriptOrder) {
auto it = m_scriptStates.find(key);
if (it == m_scriptStates.end()) {
continue;
}
ScriptInstanceState& state = it->second;
if (!ShouldScriptRun(state) || !EnsureScriptReady(state, true) || !state.enabled) {
continue;
}
InvokeLifecycleMethod(state, ScriptLifecycleMethod::LateUpdate, deltaTime);
}
}
void ScriptEngine::OnScriptComponentEnabled(ScriptComponent* component) {
if (!m_runtimeRunning || !component) {
return;
}
ScriptInstanceState* state = TrackScriptComponent(component);
if (!state) {
return;
}
if (ShouldScriptRun(*state)) {
EnsureScriptReady(*state, true);
}
}
void ScriptEngine::OnScriptComponentDisabled(ScriptComponent* component) {
if (!m_runtimeRunning || !component) {
return;
}
ScriptInstanceState* state = FindState(component);
if (!state || !state->enabled) {
return;
}
InvokeLifecycleMethod(*state, ScriptLifecycleMethod::OnDisable);
state->enabled = false;
}
void ScriptEngine::OnScriptComponentDestroyed(ScriptComponent* component) {
if (!m_runtimeRunning || !component) {
return;
}
ScriptInstanceState* state = FindState(component);
if (!state) {
return;
}
StopTrackingScript(*state, false);
}
bool ScriptEngine::HasTrackedScriptComponent(const ScriptComponent* component) const {
return FindState(component) != nullptr;
}
bool ScriptEngine::HasRuntimeInstance(const ScriptComponent* component) const {
const ScriptInstanceState* state = FindState(component);
return state && state->instanceCreated;
}
size_t ScriptEngine::ScriptInstanceKeyHasher::operator()(const ScriptInstanceKey& key) const {
const size_t h1 = std::hash<uint64_t>{}(key.gameObjectUUID);
const size_t h2 = std::hash<uint64_t>{}(key.scriptComponentUUID);
return h1 ^ (h2 + 0x9e3779b97f4a7c15ULL + (h1 << 6) + (h1 >> 2));
}
void ScriptEngine::CollectScriptComponents(Components::GameObject* gameObject) {
if (!gameObject) {
return;
}
for (ScriptComponent* component : gameObject->GetComponents<ScriptComponent>()) {
TrackScriptComponent(component);
}
for (Components::GameObject* child : gameObject->GetChildren()) {
CollectScriptComponents(child);
}
}
ScriptEngine::ScriptInstanceState* ScriptEngine::TrackScriptComponent(ScriptComponent* component) {
if (!component || !component->GetGameObject()) {
return nullptr;
}
const ScriptInstanceKey key{
component->GetGameObject()->GetUUID(),
component->GetScriptComponentUUID()
};
auto it = m_scriptStates.find(key);
if (it != m_scriptStates.end()) {
it->second.context.scene = component->GetScene();
it->second.context.gameObject = component->GetGameObject();
it->second.context.component = component;
it->second.context.gameObjectUUID = component->GetGameObject()->GetUUID();
it->second.context.scriptComponentUUID = component->GetScriptComponentUUID();
return &it->second;
}
ScriptInstanceState state;
state.context.scene = component->GetScene();
state.context.gameObject = component->GetGameObject();
state.context.component = component;
state.context.gameObjectUUID = component->GetGameObject()->GetUUID();
state.context.scriptComponentUUID = component->GetScriptComponentUUID();
auto [insertedIt, inserted] = m_scriptStates.emplace(key, std::move(state));
if (inserted) {
m_scriptOrder.push_back(key);
}
return &insertedIt->second;
}
ScriptEngine::ScriptInstanceState* ScriptEngine::FindState(const ScriptComponent* component) {
if (!component || !component->GetGameObject()) {
return nullptr;
}
const ScriptInstanceKey key{
component->GetGameObject()->GetUUID(),
component->GetScriptComponentUUID()
};
auto it = m_scriptStates.find(key);
return it != m_scriptStates.end() ? &it->second : nullptr;
}
const ScriptEngine::ScriptInstanceState* ScriptEngine::FindState(const ScriptComponent* component) const {
if (!component || !component->GetGameObject()) {
return nullptr;
}
const ScriptInstanceKey key{
component->GetGameObject()->GetUUID(),
component->GetScriptComponentUUID()
};
auto it = m_scriptStates.find(key);
return it != m_scriptStates.end() ? &it->second : nullptr;
}
void ScriptEngine::RemoveTrackedScriptComponent(const ScriptComponent* component) {
if (!component || !component->GetGameObject()) {
return;
}
const ScriptInstanceKey key{
component->GetGameObject()->GetUUID(),
component->GetScriptComponentUUID()
};
m_scriptStates.erase(key);
m_scriptOrder.erase(
std::remove(m_scriptOrder.begin(), m_scriptOrder.end(), key),
m_scriptOrder.end());
}
bool ScriptEngine::ShouldScriptRun(const ScriptInstanceState& state) const {
return m_runtimeRunning
&& state.context.scene == m_runtimeScene
&& state.context.scene != nullptr
&& state.context.scene->IsActive()
&& state.context.gameObject != nullptr
&& state.context.gameObject->IsActiveInHierarchy()
&& state.context.component != nullptr
&& state.context.component->IsEnabled()
&& state.context.component->HasScriptClass();
}
bool ScriptEngine::EnsureScriptReady(ScriptInstanceState& state, bool invokeEnableIfNeeded) {
if (!state.context.component || !state.context.gameObject || !state.context.scene || !state.context.component->HasScriptClass()) {
return false;
}
if (!state.instanceCreated) {
if (!m_runtime->CreateScriptInstance(state.context)) {
return false;
}
state.instanceCreated = true;
}
if (!state.awakeCalled) {
InvokeLifecycleMethod(state, ScriptLifecycleMethod::Awake);
state.awakeCalled = true;
}
if (invokeEnableIfNeeded && !state.enabled && ShouldScriptRun(state)) {
InvokeLifecycleMethod(state, ScriptLifecycleMethod::OnEnable);
state.enabled = true;
if (!state.startCalled) {
state.startPending = true;
}
}
return true;
}
void ScriptEngine::InvokeLifecycleMethod(ScriptInstanceState& state, ScriptLifecycleMethod method, float deltaTime) {
m_runtime->InvokeMethod(state.context, method, deltaTime);
}
void ScriptEngine::StopTrackingScript(ScriptInstanceState& state, bool runtimeStopping) {
if (state.enabled) {
InvokeLifecycleMethod(state, ScriptLifecycleMethod::OnDisable);
state.enabled = false;
}
if (state.instanceCreated) {
InvokeLifecycleMethod(state, ScriptLifecycleMethod::OnDestroy);
m_runtime->DestroyScriptInstance(state.context);
state.instanceCreated = false;
}
state.startPending = false;
if (!runtimeStopping) {
RemoveTrackedScriptComponent(state.context.component);
}
}
} // namespace Scripting
} // namespace XCEngine

View File

@@ -5,6 +5,7 @@ project(XCEngine_ScriptingTests)
set(SCRIPTING_TEST_SOURCES
test_script_field_storage.cpp
test_script_component.cpp
test_script_engine.cpp
)
add_executable(scripting_tests ${SCRIPTING_TEST_SOURCES})

View File

@@ -0,0 +1,239 @@
#include <gtest/gtest.h>
#include <XCEngine/Scene/Scene.h>
#include <XCEngine/Scripting/IScriptRuntime.h>
#include <XCEngine/Scripting/ScriptComponent.h>
#include <XCEngine/Scripting/ScriptEngine.h>
#include <memory>
#include <string>
#include <utility>
#include <vector>
using namespace XCEngine::Components;
using namespace XCEngine::Scripting;
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 FakeScriptRuntime : public IScriptRuntime {
public:
void OnRuntimeStart(Scene* scene) override {
events.push_back("RuntimeStart:" + (scene ? scene->GetName() : std::string("null")));
}
void OnRuntimeStop(Scene* scene) override {
events.push_back("RuntimeStop:" + (scene ? scene->GetName() : std::string("null")));
}
bool CreateScriptInstance(const ScriptRuntimeContext& context) override {
events.push_back("Create:" + Describe(context));
return context.component != nullptr;
}
void DestroyScriptInstance(const ScriptRuntimeContext& context) override {
events.push_back("Destroy:" + Describe(context));
}
void InvokeMethod(
const ScriptRuntimeContext& context,
ScriptLifecycleMethod method,
float deltaTime) override {
(void)deltaTime;
events.push_back(LifecycleMethodToString(method) + ":" + Describe(context));
}
void Clear() {
events.clear();
}
std::vector<std::string> events;
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;
}
};
class ScriptEngineTest : public ::testing::Test {
protected:
void SetUp() override {
engine = &ScriptEngine::Get();
engine->OnRuntimeStop();
engine->SetRuntime(&runtime);
}
void TearDown() override {
engine->OnRuntimeStop();
engine->SetRuntime(nullptr);
scene.reset();
}
ScriptComponent* AddScriptComponent(GameObject* gameObject, const std::string& namespaceName, const std::string& className) {
ScriptComponent* component = gameObject->AddComponent<ScriptComponent>();
component->SetScriptClass("GameScripts", namespaceName, className);
return component;
}
Scene* CreateScene(const std::string& sceneName) {
scene = std::make_unique<Scene>(sceneName);
return scene.get();
}
ScriptEngine* engine = nullptr;
FakeScriptRuntime runtime;
std::unique_ptr<Scene> scene;
};
TEST_F(ScriptEngineTest, RuntimeStartTracksActiveScriptsAndInvokesAwakeThenOnEnable) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "PlayerController");
engine->OnRuntimeStart(runtimeScene);
EXPECT_TRUE(engine->IsRuntimeRunning());
EXPECT_EQ(engine->GetRuntimeScene(), runtimeScene);
EXPECT_EQ(engine->GetTrackedScriptCount(), 1u);
EXPECT_TRUE(engine->HasTrackedScriptComponent(component));
EXPECT_TRUE(engine->HasRuntimeInstance(component));
const std::vector<std::string> expected = {
"RuntimeStart:RuntimeScene",
"Create:Host:Gameplay.PlayerController",
"Awake:Host:Gameplay.PlayerController",
"OnEnable:Host:Gameplay.PlayerController"
};
EXPECT_EQ(runtime.events, expected);
}
TEST_F(ScriptEngineTest, UpdateInvokesStartOnlyOnceAndRunsPerFrameMethods) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
AddScriptComponent(host, "Gameplay", "Mover");
engine->OnRuntimeStart(runtimeScene);
runtime.Clear();
engine->OnUpdate(0.016f);
engine->OnFixedUpdate(0.02f);
engine->OnUpdate(0.016f);
engine->OnLateUpdate(0.016f);
const std::vector<std::string> expected = {
"Start:Host:Gameplay.Mover",
"Update:Host:Gameplay.Mover",
"FixedUpdate:Host:Gameplay.Mover",
"Update:Host:Gameplay.Mover",
"LateUpdate:Host:Gameplay.Mover"
};
EXPECT_EQ(runtime.events, expected);
}
TEST_F(ScriptEngineTest, InactiveObjectDefersCreationUntilActivated) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "SpawnedLater");
host->SetActive(false);
engine->OnRuntimeStart(runtimeScene);
ASSERT_EQ(engine->GetTrackedScriptCount(), 1u);
EXPECT_TRUE(engine->HasTrackedScriptComponent(component));
EXPECT_FALSE(engine->HasRuntimeInstance(component));
ASSERT_EQ(runtime.events.size(), 1u);
EXPECT_EQ(runtime.events[0], "RuntimeStart:RuntimeScene");
runtime.Clear();
host->SetActive(true);
engine->OnUpdate(0.016f);
const std::vector<std::string> expected = {
"Create:Host:Gameplay.SpawnedLater",
"Awake:Host:Gameplay.SpawnedLater",
"OnEnable:Host:Gameplay.SpawnedLater",
"Start:Host:Gameplay.SpawnedLater",
"Update:Host:Gameplay.SpawnedLater"
};
EXPECT_EQ(runtime.events, expected);
}
TEST_F(ScriptEngineTest, RuntimeStopInvokesDisableDestroyAndRuntimeStop) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
AddScriptComponent(host, "Gameplay", "ShutdownWatcher");
engine->OnRuntimeStart(runtimeScene);
runtime.Clear();
engine->OnRuntimeStop();
const std::vector<std::string> expected = {
"OnDisable:Host:Gameplay.ShutdownWatcher",
"OnDestroy:Host:Gameplay.ShutdownWatcher",
"Destroy:Host:Gameplay.ShutdownWatcher",
"RuntimeStop:RuntimeScene"
};
EXPECT_EQ(runtime.events, expected);
EXPECT_FALSE(engine->IsRuntimeRunning());
EXPECT_EQ(engine->GetRuntimeScene(), nullptr);
EXPECT_EQ(engine->GetTrackedScriptCount(), 0u);
}
TEST_F(ScriptEngineTest, ReEnablingScriptDoesNotInvokeStartTwice) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "ToggleWatcher");
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
runtime.Clear();
component->SetEnabled(false);
component->SetEnabled(true);
engine->OnUpdate(0.016f);
const std::vector<std::string> expected = {
"OnDisable:Host:Gameplay.ToggleWatcher",
"OnEnable:Host:Gameplay.ToggleWatcher",
"Update:Host:Gameplay.ToggleWatcher"
};
EXPECT_EQ(runtime.events, expected);
}
TEST_F(ScriptEngineTest, DestroyingGameObjectWhileRuntimeRunningDestroysTrackedScript) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
AddScriptComponent(host, "Gameplay", "DestroyWatcher");
engine->OnRuntimeStart(runtimeScene);
runtime.Clear();
runtimeScene->DestroyGameObject(host);
const std::vector<std::string> expected = {
"OnDisable:Host:Gameplay.DestroyWatcher",
"OnDestroy:Host:Gameplay.DestroyWatcher",
"Destroy:Host:Gameplay.DestroyWatcher"
};
EXPECT_EQ(runtime.events, expected);
EXPECT_EQ(engine->GetTrackedScriptCount(), 0u);
}
} // namespace