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
+8
View File
@@ -7,6 +7,7 @@
#include <Engine/Core/Log.h>
#include <Engine/ECS/Scene.h>
#include <Engine/Physics/PhysicsWorld.h>
#include <Engine/Scripting/ScriptRuntime.h>
#include <Engine/Renderer/Renderer.h>
#include <Engine/Assets/AssetManager.h>
#include <Engine/Vulkan/Device.h>
@@ -102,6 +103,9 @@ namespace Engine {
// 物理调试图元 → 渲染侧(PhysicsDebugPass 每帧上传绘制)
m_Renderer->SetPhysicsDebugSource(&m_Physics->GetDebugFrame());
// 脚本生命周期(对账式,组件增删/改名即时生效)
m_Scripts = CreateScope<Scripting::ScriptRuntime>(*m_Scene);
while (m_Running) {
const auto frameStart = std::chrono::steady_clock::now();
@@ -121,6 +125,10 @@ namespace Engine {
// SyncScene 对账仍跑(拖静态体照常同步)
m_Physics->Step(frame.Dt());
// 脚本更新(物理之后、业务 OnUpdate 之前,Unity FixedUpdate→Update
// 语义;吃 scaled dt——timeScale=0 暂停时 dt=0 脚本自然停摆)
m_Scripts->Update(frame.Dt());
OnUpdate(frame);
// 延迟销毁在渲染前统一 flush,保证渲染遍历时池结构不变
+75
View File
@@ -0,0 +1,75 @@
//
// ScriptRuntime.cpp:脚本对账与生命周期驱动。
//
#include <Engine/Scripting/ScriptRuntime.h>
#include <Engine/Core/Log.h>
#include <Engine/ECS/Components.h>
namespace Engine {
namespace Scripting {
void ScriptRuntime::Update(f32 dt) {
Sync();
auto& reg = m_Scene.GetRegistry();
for (auto& [bits, inst] : m_Instances) {
const auto e = static_cast<entt::entity>(bits);
if (!reg.valid(e) || !reg.any_of<ECS::ScriptComponent>(e)) {
continue; // 延迟销毁队列里,下帧 Sync 清理
}
if (!inst.created) {
inst.behavior->OnCreate();
inst.created = true;
}
inst.behavior->OnUpdate(dt);
}
}
void ScriptRuntime::Sync() {
auto& reg = m_Scene.GetRegistry();
// 1. 孤儿/改名清理:实体或组件没了、脚本名变了 → OnDestroy + 删除
for (auto it = m_Instances.begin(); it != m_Instances.end();) {
const auto e = static_cast<entt::entity>(it->first);
const ECS::ScriptComponent* sc =
reg.valid(e) ? reg.try_get<ECS::ScriptComponent>(e) : nullptr;
const bool stale = !sc || !it->second.behavior ||
it->second.behavior->Name() != sc->scriptName;
if (stale) {
if (it->second.behavior) {
it->second.behavior->OnDestroy();
}
it = m_Instances.erase(it);
} else {
++it;
}
}
// 2. 缺失实例化:按名查注册表(未知名警告——场景含未注册脚本时
// 弱前向兼容,跳过不影响其余实体)
const auto view = reg.view<ECS::ScriptComponent>();
for (auto [e, sc] : view.each()) {
const u32 bits = static_cast<u32>(entt::to_integral(e));
if (m_Instances.contains(bits) || sc.scriptName.empty()) {
continue;
}
auto behavior = ScriptRegistry::Create(sc.scriptName);
if (!behavior) {
ENGINE_WARN("Script '{}' not registered (entity {})",
sc.scriptName, bits);
continue;
}
behavior->Bind(m_Scene, e);
ScriptInstance inst;
inst.behavior = std::move(behavior);
inst.behavior->OnCreate(); // Unity Awake 语义:实例化即回调
inst.created = true;
m_Instances.emplace(bits, std::move(inst));
}
}
} // namespace Scripting
} // namespace Engine
@@ -343,6 +343,14 @@ namespace Engine::Serialization {
out << YAML::EndMap;
}
// 脚本只存类名(注册表键);未注册名字加载后运行时警告跳过
if (const auto* sc = reg.try_get<ECS::ScriptComponent>(e)) {
out << YAML::Key << "Script" << YAML::Value << YAML::BeginMap;
out << YAML::Key << "ScriptName" << YAML::Value
<< sc->scriptName;
out << YAML::EndMap;
}
out << YAML::EndMap; // Components
out << YAML::EndMap; // Entity
}
@@ -628,6 +636,12 @@ namespace Engine::Serialization {
cc.stepHeight = n["StepHeight"].as<f32>(cc.stepHeight);
// 状态(速度/着地)由 PhysicsWorld 对账重建,不存
}
// 脚本:只存类名;未注册名字由 ScriptRuntime 运行时警告跳过
if (const YAML::Node n = comps["Script"]) {
auto& sc = handle.AddComponent<ECS::ScriptComponent>();
sc.scriptName = n["ScriptName"].as<std::string>("");
}
}
// 加载在 OnImGuiRender 段,错过本帧 UpdateTransforms: