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:
@@ -569,23 +569,41 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
|||||||
// 多脚本列表(Unity 语义):每行 = 脚本下拉 + 独立启停 + 删除;
|
// 多脚本列表(Unity 语义):每行 = 脚本下拉 + 独立启停 + 删除;
|
||||||
// 实例生命周期由 ScriptRuntime 对账(删行/改名 → 下帧 OnDestroy)
|
// 实例生命周期由 ScriptRuntime 对账(删行/改名 → 下帧 OnDestroy)
|
||||||
const auto& names = Engine::Scripting::ScriptRegistry::Names();
|
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;
|
int removeIdx = -1;
|
||||||
for (int i = 0; i < static_cast<int>(sc->items.size()); ++i)
|
for (int i = 0; i < static_cast<int>(sc->items.size()); ++i)
|
||||||
{
|
{
|
||||||
auto& item = sc->items[static_cast<size_t>(i)];
|
auto& item = sc->items[static_cast<size_t>(i)];
|
||||||
ImGui::PushID(i);
|
ImGui::PushID(i);
|
||||||
int cur = 0;
|
// 去重下拉(DisallowMultipleComponent 语义):可选项 =
|
||||||
for (size_t k = 0; k < names.size(); ++k)
|
// "<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(
|
ImGui::SetNextItemWidth(
|
||||||
@@ -593,7 +611,7 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
|||||||
if (ImGui::Combo("##script", &cur, options.data(),
|
if (ImGui::Combo("##script", &cur, options.data(),
|
||||||
static_cast<int>(options.size())))
|
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::SameLine();
|
||||||
ImGui::Checkbox("##en", &item.active);
|
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);
|
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({});
|
sc->items.push_back({});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <glm/gtc/quaternion.hpp>
|
#include <glm/gtc/quaternion.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
@@ -672,7 +673,16 @@ namespace Engine::Serialization {
|
|||||||
ECS::ScriptComponent::Item item;
|
ECS::ScriptComponent::Item item;
|
||||||
item.name = it["Name"].as<std::string>("");
|
item.name = it["Name"].as<std::string>("");
|
||||||
item.active = it["Active"].as<bool>(true);
|
item.active = it["Active"].as<bool>(true);
|
||||||
sc.items.push_back(std::move(item));
|
// 去重兜底:同名项只收第一份(UI 已杜绝重复,
|
||||||
|
// 手编场景仍可能写出重复)
|
||||||
|
const bool dup = std::any_of(
|
||||||
|
sc.items.begin(), sc.items.end(),
|
||||||
|
[&](const ECS::ScriptComponent::Item& e) {
|
||||||
|
return e.name == item.name;
|
||||||
|
});
|
||||||
|
if (!dup) {
|
||||||
|
sc.items.push_back(std::move(item));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 旧格式:Script: {ScriptName: ..., Active: ...}
|
// 旧格式:Script: {ScriptName: ..., Active: ...}
|
||||||
|
|||||||
Reference in New Issue
Block a user