2026-03-26 20:45:41 +08:00
|
|
|
#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);
|
2026-03-27 16:30:16 +08:00
|
|
|
m_runtimeSceneCreatedSubscription = scene->OnGameObjectCreated().Subscribe(
|
|
|
|
|
[this](Components::GameObject* gameObject) {
|
|
|
|
|
HandleGameObjectCreated(gameObject);
|
|
|
|
|
});
|
2026-03-26 20:45:41 +08:00
|
|
|
|
|
|
|
|
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() {
|
2026-03-27 16:30:16 +08:00
|
|
|
if (m_runtimeScene && m_runtimeSceneCreatedSubscription != 0) {
|
|
|
|
|
m_runtimeScene->OnGameObjectCreated().Unsubscribe(m_runtimeSceneCreatedSubscription);
|
|
|
|
|
m_runtimeScene->OnGameObjectCreated().ProcessUnsubscribes();
|
|
|
|
|
m_runtimeSceneCreatedSubscription = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 20:45:41 +08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 16:30:16 +08:00
|
|
|
void ScriptEngine::EnsureTrackedScriptsReady(Components::GameObject* gameObject) {
|
|
|
|
|
if (!gameObject) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (ScriptComponent* component : gameObject->GetComponents<ScriptComponent>()) {
|
|
|
|
|
ScriptInstanceState* state = FindState(component);
|
|
|
|
|
if (!state || !ShouldScriptRun(*state)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EnsureScriptReady(*state, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (Components::GameObject* child : gameObject->GetChildren()) {
|
|
|
|
|
EnsureTrackedScriptsReady(child);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ScriptEngine::HandleGameObjectCreated(Components::GameObject* gameObject) {
|
|
|
|
|
if (!m_runtimeRunning || !gameObject || gameObject->GetScene() != m_runtimeScene) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CollectScriptComponents(gameObject);
|
|
|
|
|
EnsureTrackedScriptsReady(gameObject);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 20:45:41 +08:00
|
|
|
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
|