feat(engine): C++ native script system (ScriptBehavior + registry)

ScriptComponent stores only a script name string (POD, serializable);
instances are reconciled by ScriptRuntime each frame (PhysicsWorld
pattern): orphan/rename cleanup with OnDestroy, per-name instantiation
from a file-level self-registering registry (ENGINE_REGISTER_SCRIPT),
OnCreate on bind, OnUpdate after physics step on scaled dt. Engine
framerate loop only sees the base interface, so future Lua/C# behaviors
plug in as foreign ScriptBehavior implementations with zero engine
changes. Ships a Spinner sample script and Inspector script picker.
This commit is contained in:
2026-10-03 18:02:49 +08:00
parent ea6beee593
commit 3403822a52
10 changed files with 348 additions and 2 deletions
+39
View File
@@ -9,6 +9,7 @@
#include "Engine/ECS/Components.h"
#include "Engine/Renderer/Model.h"
#include "Engine/Physics/PhysicsWorld.h" // Apply Impulse 调试(GetPhysics 门面)
#include "Engine/Scripting/Script.h" // 脚本下拉(ScriptRegistry::Names)
#include <imgui.h>
@@ -126,6 +127,11 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
{
reg.emplace<Engine::ECS::CharacterControllerComponent>(selected);
}
if (!reg.all_of<Engine::ECS::ScriptComponent>(selected) &&
ImGui::MenuItem("Script"))
{
reg.emplace<Engine::ECS::ScriptComponent>(selected);
}
ImGui::EndPopup();
}
ImGui::Separator();
@@ -428,6 +434,39 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
}
}
// ---- Script(按名解析注册表脚本;实例生命周期在 ScriptRuntime)----
if (auto* sc = reg.try_get<Engine::ECS::ScriptComponent>(selected))
{
if (ImGui::CollapsingHeader("Script", ImGuiTreeNodeFlags_DefaultOpen))
{
// 下拉列出全部已注册脚本(注册序);"<none>" = 清空(对账自动销毁实例)
const auto& names = Engine::Scripting::ScriptRegistry::Names();
std::vector<const char*> items;
items.push_back("<none>");
for (const auto& n : names)
{
items.push_back(n.c_str());
}
int cur = 0;
for (size_t i = 0; i < names.size(); ++i)
{
if (names[i] == sc->scriptName)
{
cur = static_cast<int>(i) + 1;
}
}
if (ImGui::Combo("script", &cur, items.data(),
static_cast<int>(items.size())))
{
sc->scriptName = (cur <= 0) ? "" : names[static_cast<size_t>(cur) - 1];
}
if (sc->scriptName.empty())
{
ImGui::TextDisabled("no script assigned");
}
}
}
// ---- Animation ----
if (auto* anim = reg.try_get<Engine::ECS::AnimationComponent>(selected))
{