Add script runtime lifecycle skeleton
This commit is contained in:
@@ -241,12 +241,17 @@ add_library(XCEngine STATIC
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/AudioClip/AudioLoader.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/AudioClip/AudioLoader.cpp
|
||||||
|
|
||||||
# Scripting
|
# 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/ScriptField.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/ScriptFieldStorage.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/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/ScriptField.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/ScriptFieldStorage.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/ScriptFieldStorage.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/ScriptComponent.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/ScriptComponent.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/ScriptEngine.cpp
|
||||||
|
|
||||||
# Components
|
# Components
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/Component.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/Component.h
|
||||||
|
|||||||
52
engine/include/XCEngine/Scripting/IScriptRuntime.h
Normal file
52
engine/include/XCEngine/Scripting/IScriptRuntime.h
Normal 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
|
||||||
23
engine/include/XCEngine/Scripting/NullScriptRuntime.h
Normal file
23
engine/include/XCEngine/Scripting/NullScriptRuntime.h
Normal 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
|
||||||
@@ -36,6 +36,10 @@ public:
|
|||||||
const ScriptFieldStorage& GetFieldStorage() const { return m_fieldStorage; }
|
const ScriptFieldStorage& GetFieldStorage() const { return m_fieldStorage; }
|
||||||
void SetFieldStorage(const ScriptFieldStorage& fieldStorage) { m_fieldStorage = 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 Serialize(std::ostream& os) const override;
|
||||||
void Deserialize(std::istream& is) override;
|
void Deserialize(std::istream& is) override;
|
||||||
|
|
||||||
|
|||||||
88
engine/include/XCEngine/Scripting/ScriptEngine.h
Normal file
88
engine/include/XCEngine/Scripting/ScriptEngine.h
Normal 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
|
||||||
32
engine/src/Scripting/NullScriptRuntime.cpp
Normal file
32
engine/src/Scripting/NullScriptRuntime.cpp
Normal 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
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "Scripting/ScriptComponent.h"
|
#include "Scripting/ScriptComponent.h"
|
||||||
|
|
||||||
|
#include "Scripting/ScriptEngine.h"
|
||||||
|
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
@@ -44,6 +46,18 @@ std::string ScriptComponent::GetFullClassName() const {
|
|||||||
return m_namespaceName + "." + m_className;
|
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 {
|
void ScriptComponent::Serialize(std::ostream& os) const {
|
||||||
os << "scriptComponentUUID=" << m_scriptComponentUUID << ";";
|
os << "scriptComponentUUID=" << m_scriptComponentUUID << ";";
|
||||||
os << "assembly=" << EscapeScriptString(m_assemblyName) << ";";
|
os << "assembly=" << EscapeScriptString(m_assemblyName) << ";";
|
||||||
|
|||||||
355
engine/src/Scripting/ScriptEngine.cpp
Normal file
355
engine/src/Scripting/ScriptEngine.cpp
Normal 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
|
||||||
@@ -5,6 +5,7 @@ project(XCEngine_ScriptingTests)
|
|||||||
set(SCRIPTING_TEST_SOURCES
|
set(SCRIPTING_TEST_SOURCES
|
||||||
test_script_field_storage.cpp
|
test_script_field_storage.cpp
|
||||||
test_script_component.cpp
|
test_script_component.cpp
|
||||||
|
test_script_engine.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(scripting_tests ${SCRIPTING_TEST_SOURCES})
|
add_executable(scripting_tests ${SCRIPTING_TEST_SOURCES})
|
||||||
|
|||||||
239
tests/scripting/test_script_engine.cpp
Normal file
239
tests/scripting/test_script_engine.cpp
Normal 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
|
||||||
Reference in New Issue
Block a user