fix(scripts): prevent duplicate script attachment per entity

The per-entity script list allowed picking the same script class
twice; the name-keyed runtime silently ignores duplicates, leaving
the extra rows dead weight with unusable enable bits. Enforce
DisallowMultipleComponent semantics: the dropdown of each row only
offers unattached names (own name kept for echo), "+ Add Script"
disables when every registered script is attached, and scene loading
dedupes as a safety net for hand-edited files.
This commit is contained in:
2026-10-04 00:57:45 +08:00
parent fd2d8f9b34
commit 7d02d344e5
2 changed files with 59 additions and 13 deletions
+48 -12
View File
@@ -569,23 +569,41 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
// 多脚本列表(Unity 语义):每行 = 脚本下拉 + 独立启停 + 删除;
// 实例生命周期由 ScriptRuntime 对账(删行/改名 → 下帧 OnDestroy)
const auto& names = Engine::Scripting::ScriptRegistry::Names();
std::vector<const char*> options;
options.push_back("<none>");
for (const auto& n : names)
{
options.push_back(n.c_str());
}
int removeIdx = -1;
for (int i = 0; i < static_cast<int>(sc->items.size()); ++i)
{
auto& item = sc->items[static_cast<size_t>(i)];
ImGui::PushID(i);
int cur = 0;
for (size_t k = 0; k < names.size(); ++k)
// 去重下拉(DisallowMultipleComponent 语义):可选项 =
// "<none>" + 未挂名 + 自身名(已挂的别人的名不出现,结构上
// 选不出重复);自身名置于末尾保证已挂项正常回显
std::vector<const char*> options;
options.push_back("<none>");
for (const auto& n : names)
{
if (names[k] == item.name)
if (n == item.name)
{
cur = static_cast<int>(k) + 1;
continue;
}
const bool attached = std::any_of(
sc->items.begin(), sc->items.end(),
[&](const Engine::ECS::ScriptComponent::Item& it)
{ return it.name == n; });
if (!attached)
{
options.push_back(n.c_str());
}
}
if (!item.name.empty())
{
options.push_back(item.name.c_str());
}
int cur = 0;
for (size_t k = 0; k < options.size(); ++k)
{
if (item.name == options[k])
{
cur = static_cast<int>(k);
}
}
ImGui::SetNextItemWidth(
@@ -593,7 +611,7 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
if (ImGui::Combo("##script", &cur, options.data(),
static_cast<int>(options.size())))
{
item.name = (cur <= 0) ? "" : names[static_cast<size_t>(cur) - 1];
item.name = (cur <= 0) ? "" : options[static_cast<size_t>(cur)];
}
ImGui::SameLine();
ImGui::Checkbox("##en", &item.active);
@@ -615,7 +633,25 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
{
sc->items.erase(sc->items.begin() + removeIdx);
}
if (ImGui::Button("+ Add Script"))
// 已注册脚本全部挂上 → 禁止再加(去重语义,无意义项)
const bool allAttached = !names.empty() &&
std::all_of(names.begin(), names.end(),
[&](const std::string& n)
{
return std::any_of(
sc->items.begin(), sc->items.end(),
[&](const Engine::ECS::ScriptComponent::Item& it)
{ return it.name == n; });
});
if (allAttached)
{
ImGui::BeginDisabled(true);
ImGui::Button("+ Add Script");
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::TextDisabled("all registered scripts attached");
}
else if (ImGui::Button("+ Add Script"))
{
sc->items.push_back({});
}