feat(editor): multi-script entities + asset browser panel
ScriptComponent now holds a list of {name, active} items (Unity
semantics: one entity, many scripts, each with its own enable bit;
component-level active stays as the master switch). ScriptRuntime
reconciles instances by name per entity -- rename/remove destroys
only the affected instances, untouched ones keep ticking. Scenes
persist Script.Items; old single-script key still loads.
AssetBrowserPanel (lightweight Unity Project window): fixed roots
(assets/scenes) as tabs, recursive directory tree, file list with
type filter (models/textures/scenes) and name substring filter,
double-click preloads models/HDRs into the asset caches (path-
deduped). Scene files point to the File menu for now; drag & drop
is a later iteration. Docked with Stats by default in fresh layouts.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// ScriptRuntime.cpp:脚本对账与生命周期驱动。
|
||||
// ScriptRuntime.cpp:脚本对账与生命周期驱动(多脚本挂载)。
|
||||
//
|
||||
|
||||
#include <Engine/Scripting/ScriptRuntime.h>
|
||||
@@ -7,6 +7,8 @@
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/ECS/Components.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace Engine {
|
||||
namespace Scripting {
|
||||
|
||||
@@ -20,7 +22,7 @@ namespace Engine {
|
||||
Sync();
|
||||
|
||||
auto& reg = m_Scene.GetRegistry();
|
||||
for (auto& [bits, inst] : m_Instances) {
|
||||
for (auto& [bits, list] : m_Instances) {
|
||||
const auto e = static_cast<entt::entity>(bits);
|
||||
const ECS::ScriptComponent* sc = reg.valid(e)
|
||||
? reg.try_get<ECS::ScriptComponent>(e) : nullptr;
|
||||
@@ -28,57 +30,106 @@ namespace Engine {
|
||||
continue; // 延迟销毁队列里,下帧 Sync 清理
|
||||
}
|
||||
if (!sc->active) {
|
||||
continue; // 禁用 = 保留实例停 tick(Unity disable 语义)
|
||||
continue; // 组件总开关:全部实例停 tick(Unity disable)
|
||||
}
|
||||
if (!inst.created) {
|
||||
inst.behavior->OnCreate();
|
||||
inst.created = true;
|
||||
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) {
|
||||
continue; // 禁用 = 实例保留停 OnUpdate
|
||||
}
|
||||
if (!inst.created) {
|
||||
inst.behavior->OnCreate();
|
||||
inst.created = true;
|
||||
}
|
||||
inst.behavior->OnUpdate(dt);
|
||||
}
|
||||
inst.behavior->OnUpdate(dt);
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptRuntime::Sync() {
|
||||
auto& reg = m_Scene.GetRegistry();
|
||||
|
||||
// 1. 孤儿/改名清理:实体或组件没了、脚本名变了 → OnDestroy + 删除
|
||||
// 1. 孤儿/改名清理:实体或组件没了 → 整组 OnDestroy;
|
||||
// 实例名不在 items 里(改名/移除)→ 单个 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();
|
||||
reg.valid(e) ? reg.try_get<ECS::ScriptComponent>(e)
|
||||
: nullptr;
|
||||
if (!sc) {
|
||||
for (auto& inst : it->second) {
|
||||
if (inst.behavior) {
|
||||
inst.behavior->OnDestroy();
|
||||
}
|
||||
}
|
||||
it = m_Instances.erase(it);
|
||||
continue;
|
||||
}
|
||||
auto& list = it->second;
|
||||
for (auto li = list.begin(); li != list.end();) {
|
||||
const bool keep = li->behavior &&
|
||||
std::any_of(sc->items.begin(), sc->items.end(),
|
||||
[&](const ECS::ScriptComponent::Item& item) {
|
||||
return item.name == li->behavior->Name();
|
||||
});
|
||||
if (keep) {
|
||||
++li;
|
||||
continue;
|
||||
}
|
||||
if (li->behavior) {
|
||||
li->behavior->OnDestroy();
|
||||
}
|
||||
li = list.erase(li);
|
||||
}
|
||||
if (list.empty()) {
|
||||
it = m_Instances.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 缺失实例化:按名查注册表(未知名警告——场景含未注册脚本时
|
||||
// 弱前向兼容,跳过不影响其余实体)
|
||||
// 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, m_App);
|
||||
auto& list = m_Instances[bits];
|
||||
for (const auto& item : sc.items) {
|
||||
if (item.name.empty()) {
|
||||
continue;
|
||||
}
|
||||
const bool exists = std::any_of(
|
||||
list.begin(), list.end(),
|
||||
[&](const ScriptInstance& inst) {
|
||||
return inst.behavior &&
|
||||
inst.behavior->Name() == item.name;
|
||||
});
|
||||
if (exists) {
|
||||
continue;
|
||||
}
|
||||
auto behavior = ScriptRegistry::Create(item.name);
|
||||
if (!behavior) {
|
||||
ENGINE_WARN("Script '{}' not registered (entity {})",
|
||||
item.name, bits);
|
||||
continue;
|
||||
}
|
||||
behavior->Bind(m_Scene, e, m_App);
|
||||
|
||||
ScriptInstance inst;
|
||||
inst.behavior = std::move(behavior);
|
||||
inst.behavior->OnCreate(); // Unity Awake 语义:实例化即回调
|
||||
inst.created = true;
|
||||
m_Instances.emplace(bits, std::move(inst));
|
||||
ScriptInstance inst;
|
||||
inst.behavior = std::move(behavior);
|
||||
inst.behavior->OnCreate(); // Unity Awake 语义:实例化即回调
|
||||
inst.created = true;
|
||||
list.push_back(std::move(inst));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -350,12 +350,20 @@ 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::Key << "Active" << YAML::Value << sc->active;
|
||||
out << YAML::Key << "Items" << YAML::Value << YAML::BeginSeq;
|
||||
for (const auto& item : sc->items) {
|
||||
// flow map 单行一项,与场景格式定案一致
|
||||
out << YAML::BeginMap;
|
||||
out << YAML::Key << "Name" << YAML::Value << item.name;
|
||||
out << YAML::Key << "Active" << YAML::Value << item.active;
|
||||
out << YAML::EndMap;
|
||||
}
|
||||
out << YAML::EndSeq;
|
||||
out << YAML::EndMap;
|
||||
}
|
||||
|
||||
@@ -655,11 +663,25 @@ namespace Engine::Serialization {
|
||||
// 状态(速度/着地)由 PhysicsWorld 对账重建,不存
|
||||
}
|
||||
|
||||
// 脚本:只存类名;未注册名字由 ScriptRuntime 运行时警告跳过
|
||||
// 脚本:优先读多脚本 Items;无 Items 回退旧单脚本键(向前兼容)
|
||||
if (const YAML::Node n = comps["Script"]) {
|
||||
auto& sc = handle.AddComponent<ECS::ScriptComponent>();
|
||||
sc.scriptName = n["ScriptName"].as<std::string>("");
|
||||
sc.active = n["Active"].as<bool>(true);
|
||||
if (const YAML::Node items = n["Items"]) {
|
||||
for (const auto& it : items) {
|
||||
ECS::ScriptComponent::Item item;
|
||||
item.name = it["Name"].as<std::string>("");
|
||||
item.active = it["Active"].as<bool>(true);
|
||||
sc.items.push_back(std::move(item));
|
||||
}
|
||||
} else {
|
||||
// 旧格式:Script: {ScriptName: ..., Active: ...}
|
||||
ECS::ScriptComponent::Item item;
|
||||
item.name = n["ScriptName"].as<std::string>("");
|
||||
if (!item.name.empty()) {
|
||||
sc.items.push_back(std::move(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user