新增脚本参数系统:声明式参数 + Inspector 自动控件 + 场景序列化

DeclareParams/ParamBuilder 声明字段,运行时反射读写;
Inspector 按类型生成控件,SceneSerializer 直通存取;
动画系统接入参数化裁剪;新增 OrbitMove 示例脚本
This commit is contained in:
2026-10-08 17:02:19 +08:00
parent f327f44269
commit ff955b9e63
11 changed files with 951 additions and 33 deletions
+184 -18
View File
@@ -4,6 +4,7 @@
#include <Engine/Scripting/ScriptRuntime.h>
#include <Engine/Core/Application.h>
#include <Engine/Core/Log.h>
#include <Engine/ECS/Components.h>
@@ -18,6 +19,28 @@ namespace Engine {
return *m_App;
}
// ---- 参数按名读写(ScriptBehavior 声明,经绑定表;写穿
// Item.params 由运行时 SetParam 完成)----
bool ScriptBehavior::WriteParam(const std::string& name,
const ParamValue& v) {
for (const auto& b : m_ParamBinds) {
if (b.name == name) {
return ParamBuilder::WriteBind(b, v);
}
}
return false;
}
bool ScriptBehavior::ReadParam(const std::string& name,
ParamValue& out) const {
for (const auto& b : m_ParamBinds) {
if (b.name == name) {
return ParamBuilder::ReadBind(b, out);
}
}
return false;
}
void ScriptRuntime::Update(f32 dt) {
Sync();
@@ -26,40 +49,73 @@ namespace Engine {
const auto e = static_cast<entt::entity>(bits);
const ECS::ScriptComponent* sc = reg.valid(e)
? reg.try_get<ECS::ScriptComponent>(e) : nullptr;
if (!sc) {
continue; // 延迟销毁队列里,下帧 Sync 清理
}
if (!sc->active) {
continue; // 组件总开关:全部实例停 tick(Unity disable)
}
for (auto& inst : list) {
if (!inst.behavior) {
continue;
}
// per-script 启停:按名查项(对账保证存在;新项要
// 下帧 Sync 才有实例,这里查不到就跳过)
const auto item = std::find_if(
sc->items.begin(), sc->items.end(),
[&](const ECS::ScriptComponent::Item& it) {
return it.name == inst.behavior->Name();
});
if (item == sc->items.end() || !item->active) {
// 启停翻转检测:wantOn = 组件总开关 && 项开关(组件
// 关闭时零查找;项按名查找沿用每帧既有的那一次)。
// 与上帧 inst.enabled 比较出沿:true→OnEnable,
// false→OnDisable(Unity enabled 翻转语义)
bool wantOn = false;
if (sc && sc->active) {
const auto item = std::find_if(
sc->items.begin(), sc->items.end(),
[&](const ECS::ScriptComponent::Item& it) {
return it.name == inst.behavior->Name();
});
wantOn = item != sc->items.end() && item->active;
}
if (!wantOn) {
if (inst.enabled) {
inst.enabled = false;
inst.behavior->OnDisable();
}
continue; // 禁用 = 实例保留停 OnUpdate
}
if (!inst.created) {
inst.behavior->OnCreate();
inst.created = true;
}
if (!inst.enabled) {
inst.enabled = true;
inst.behavior->OnEnable();
}
if (!inst.started) {
inst.started = true;
inst.behavior->OnStart(); // 首次 OnUpdate 前一次
}
inst.behavior->OnUpdate(dt);
}
}
}
// ---- 终局(GameFlow →Stopped 广播):全实例 OnDisable →
// OnDestroy。与 Sync 的孤儿清理同序,范围 = 全部且不重建;
// Application 的监听先于编辑器回滚注册(组合根构造序),
// OnDestroy 里读到的仍是上局实体 ----
void ScriptRuntime::EndPlay() {
for (auto& [bits, list] : m_Instances) {
for (auto& inst : list) {
if (!inst.behavior) {
continue;
}
if (inst.enabled) {
inst.enabled = false;
inst.behavior->OnDisable();
}
inst.behavior->OnDestroy();
}
}
m_Instances.clear();
}
void ScriptRuntime::Sync() {
auto& reg = m_Scene.GetRegistry();
// 1. 孤儿/改名清理:实体或组件没了 → 整组 OnDestroy;
// 实例名不在 items 里(改名/移除)→ 单个 OnDestroy
// 实例名不在 items 里(改名/移除)→ 单个 OnDestroy。
// 销毁次序 = OnDisable(守卫防双发)→ OnDestroy
for (auto it = m_Instances.begin(); it != m_Instances.end();) {
const auto e = static_cast<entt::entity>(it->first);
const ECS::ScriptComponent* sc =
@@ -67,9 +123,14 @@ namespace Engine {
: nullptr;
if (!sc) {
for (auto& inst : it->second) {
if (inst.behavior) {
inst.behavior->OnDestroy();
if (!inst.behavior) {
continue;
}
if (inst.enabled) {
inst.enabled = false;
inst.behavior->OnDisable();
}
inst.behavior->OnDestroy();
}
it = m_Instances.erase(it);
continue;
@@ -86,6 +147,10 @@ namespace Engine {
continue;
}
if (li->behavior) {
if (li->enabled) {
li->enabled = false;
li->behavior->OnDisable();
}
li->behavior->OnDestroy();
}
li = list.erase(li);
@@ -98,7 +163,9 @@ namespace Engine {
}
// 2. 缺失实例化:逐项按名查注册表(未知名警告——场景含未注册
// 脚本时弱前向兼容,跳过不影响其余脚本)
// 脚本时弱前向兼容,跳过不影响其余脚本)。灌值次序对齐
// Unity:声明(成员=声明默认)→ 组件存档覆盖 → 收值回调
// → OnCreate(= Awake,拿到最终参数值)
const auto view = reg.view<ECS::ScriptComponent>();
for (auto [e, sc] : view.each()) {
const u32 bits = static_cast<u32>(entt::to_integral(e));
@@ -124,6 +191,12 @@ namespace Engine {
}
behavior->Bind(m_Scene, e, m_App);
ParamBuilder pb;
behavior->DeclareParams(pb);
pb.Apply(item.params);
behavior->SetParamBinds(pb.TakeBinds());
behavior->OnParamsApplied();
ScriptInstance inst;
inst.behavior = std::move(behavior);
inst.behavior->OnCreate(); // Unity Awake 语义:实例化即回调
@@ -133,5 +206,98 @@ namespace Engine {
}
}
// ---- 参数热刷新(编辑期一次性):重新声明 + 按存档灌值 ----
void ScriptRuntime::RefreshParams(u32 entityBits) {
const auto it = m_Instances.find(entityBits);
if (it == m_Instances.end()) {
return;
}
auto& reg = m_Scene.GetRegistry();
const auto e = static_cast<entt::entity>(entityBits);
const ECS::ScriptComponent* sc = reg.valid(e)
? reg.try_get<ECS::ScriptComponent>(e) : nullptr;
if (!sc) {
return;
}
for (auto& inst : it->second) {
if (!inst.behavior) {
continue;
}
ParamBuilder pb;
inst.behavior->DeclareParams(pb);
const auto item = std::find_if(
sc->items.begin(), sc->items.end(),
[&](const ECS::ScriptComponent::Item& it2) {
return it2.name == inst.behavior->Name();
});
if (item != sc->items.end()) {
pb.Apply(item->params);
}
inst.behavior->SetParamBinds(pb.TakeBinds());
inst.behavior->OnParamsApplied();
}
}
// ---- 跨脚本按名参数访问 ----
bool ScriptRuntime::SetParam(u32 entityBits,
const std::string& scriptName,
const std::string& param,
const ParamValue& value) {
auto& reg = m_Scene.GetRegistry();
const auto e = static_cast<entt::entity>(entityBits);
ECS::ScriptComponent* sc = reg.valid(e)
? reg.try_get<ECS::ScriptComponent>(e) : nullptr;
if (!sc) {
return false;
}
const auto item = std::find_if(
sc->items.begin(), sc->items.end(),
[&](const ECS::ScriptComponent::Item& it) {
return it.name == scriptName;
});
if (item == sc->items.end()) {
return false;
}
// 目标实例成员直写(实例未建/未声明则跳过,存档仍写穿)
const auto ii = m_Instances.find(entityBits);
if (ii != m_Instances.end()) {
for (auto& inst : ii->second) {
if (inst.behavior &&
inst.behavior->Name() == scriptName) {
inst.behavior->WriteParam(param, value);
break;
}
}
}
// 写穿 Item.params:首次写新名一次分配,此后零分配
const auto pit = std::find_if(
item->params.begin(), item->params.end(),
[&](const ScriptParam& p) { return p.name == param; });
if (pit != item->params.end()) {
pit->value = value;
} else {
item->params.push_back({param, value});
}
return true;
}
bool ScriptRuntime::GetParam(u32 entityBits,
const std::string& scriptName,
const std::string& param,
ParamValue& out) const {
const auto ii = m_Instances.find(entityBits);
if (ii == m_Instances.end()) {
return false;
}
for (const auto& inst : ii->second) {
if (inst.behavior && inst.behavior->Name() == scriptName) {
return inst.behavior->ReadParam(param, out);
}
}
return false;
}
} // namespace Scripting
} // namespace Engine