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:
@@ -0,0 +1,240 @@
|
|||||||
|
//
|
||||||
|
// AssetBrowserPanel.cpp:项目资产浏览器实现。
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "AssetBrowserPanel.h"
|
||||||
|
|
||||||
|
#include <Engine/Core/Log.h>
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// 固定根:下标与 Editor 无耦合,仅面板内部使用
|
||||||
|
constexpr const char* kRootNames[] = {"Assets", "Scenes"};
|
||||||
|
constexpr const char* kRootPaths[] = {"assets", "scenes"};
|
||||||
|
|
||||||
|
// 类型过滤(下标即身份):0 = All
|
||||||
|
constexpr const char* kTypeNames[] = {"All", "Models", "Textures",
|
||||||
|
"Scenes"};
|
||||||
|
|
||||||
|
// 资产类别:按扩展名分派(过滤用)
|
||||||
|
enum class Kind { Model, Texture, Scene, Other };
|
||||||
|
|
||||||
|
Kind KindOf(const std::filesystem::path& p)
|
||||||
|
{
|
||||||
|
std::string ext = p.extension().string();
|
||||||
|
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||||
|
[](unsigned char c) { return std::tolower(c); });
|
||||||
|
if (ext == ".gltf" || ext == ".glb" || ext == ".fbx" ||
|
||||||
|
ext == ".obj")
|
||||||
|
{
|
||||||
|
return Kind::Model;
|
||||||
|
}
|
||||||
|
if (ext == ".hdr" || ext == ".png" || ext == ".jpg" ||
|
||||||
|
ext == ".jpeg")
|
||||||
|
{
|
||||||
|
return Kind::Texture;
|
||||||
|
}
|
||||||
|
if (ext == ".scene")
|
||||||
|
{
|
||||||
|
return Kind::Scene;
|
||||||
|
}
|
||||||
|
return Kind::Other;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PassFilter(int typeFilter, Kind kind)
|
||||||
|
{
|
||||||
|
switch (typeFilter)
|
||||||
|
{
|
||||||
|
case 1: return kind == Kind::Model;
|
||||||
|
case 2: return kind == Kind::Texture;
|
||||||
|
case 3: return kind == Kind::Scene;
|
||||||
|
default: return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表条目前缀符号(ImGui 默认字体无图标字形,用 ASCII 标签)
|
||||||
|
const char* KindTag(Kind kind)
|
||||||
|
{
|
||||||
|
switch (kind)
|
||||||
|
{
|
||||||
|
case Kind::Model: return "[M]";
|
||||||
|
case Kind::Texture: return "[T]";
|
||||||
|
case Kind::Scene: return "[S]";
|
||||||
|
default: return "[ ]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 相对路径规范化为正斜杠(AssetManager 键与场景文件统一 POSIX 风格)
|
||||||
|
std::string ToAssetPath(const std::filesystem::path& p)
|
||||||
|
{
|
||||||
|
std::string s = p.generic_string();
|
||||||
|
std::replace(s.begin(), s.end(), '\\', '/');
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void AssetBrowserPanel::Draw(const EditorContext& ctx)
|
||||||
|
{
|
||||||
|
if (!ImGui::Begin("Asset Browser"))
|
||||||
|
{
|
||||||
|
ImGui::End();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 顶栏:根切换 + 类型过滤 + 名字过滤 ----
|
||||||
|
if (ImGui::BeginTabBar("##roots"))
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 2; ++i)
|
||||||
|
{
|
||||||
|
if (ImGui::BeginTabItem(kRootNames[i]))
|
||||||
|
{
|
||||||
|
m_Root = i;
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndTabBar();
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::SetNextItemWidth(110.0f);
|
||||||
|
ImGui::Combo("##type", &m_TypeFilter, kTypeNames, 4);
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::SetNextItemWidth(140.0f);
|
||||||
|
ImGui::InputTextWithHint("##name", "filter", m_NameFilter,
|
||||||
|
sizeof(m_NameFilter));
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
const std::filesystem::path root(kRootPaths[m_Root]);
|
||||||
|
|
||||||
|
// ---- 左:目录树 | 右:文件列表 ----
|
||||||
|
const float treeWidth = 180.0f;
|
||||||
|
ImGui::BeginChild("##tree", ImVec2(treeWidth, 0.0f),
|
||||||
|
ImGuiChildFlags_ResizeX | ImGuiChildFlags_Borders);
|
||||||
|
if (std::filesystem::exists(root))
|
||||||
|
{
|
||||||
|
ImGui::SetNextItemOpen(true, ImGuiCond_Once);
|
||||||
|
DrawDirTree(ctx, root);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui::TextDisabled("(no '%s' dir)", kRootPaths[m_Root]);
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::BeginChild("##files", ImVec2(0.0f, 0.0f),
|
||||||
|
ImGuiChildFlags_Borders);
|
||||||
|
if (std::filesystem::exists(root))
|
||||||
|
{
|
||||||
|
DrawFileList(ctx, root);
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetBrowserPanel::DrawDirTree(const EditorContext& ctx,
|
||||||
|
const std::filesystem::path& dir)
|
||||||
|
{
|
||||||
|
for (const auto& entry : std::filesystem::directory_iterator(dir))
|
||||||
|
{
|
||||||
|
if (!entry.is_directory())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const std::string name = entry.path().filename().string();
|
||||||
|
if (ImGui::TreeNode(name.c_str()))
|
||||||
|
{
|
||||||
|
DrawDirTree(ctx, entry.path());
|
||||||
|
ImGui::TreePop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetBrowserPanel::DrawFileList(const EditorContext& ctx,
|
||||||
|
const std::filesystem::path& dir)
|
||||||
|
{
|
||||||
|
// 收集 + 过滤 + 排序(名字序;目录迭代序不稳定)
|
||||||
|
struct Item
|
||||||
|
{
|
||||||
|
std::filesystem::path path;
|
||||||
|
Kind kind;
|
||||||
|
};
|
||||||
|
std::vector<Item> items;
|
||||||
|
const std::string filter = m_NameFilter;
|
||||||
|
for (const auto& entry : std::filesystem::directory_iterator(dir))
|
||||||
|
{
|
||||||
|
if (entry.is_directory())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const Kind kind = KindOf(entry.path());
|
||||||
|
if (!PassFilter(m_TypeFilter, kind))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!filter.empty())
|
||||||
|
{
|
||||||
|
const std::string name = entry.path().filename().string();
|
||||||
|
if (name.find(filter) == std::string::npos)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items.push_back({entry.path(), kind});
|
||||||
|
}
|
||||||
|
std::sort(items.begin(), items.end(),
|
||||||
|
[](const Item& a, const Item& b)
|
||||||
|
{
|
||||||
|
return a.path.filename().string() <
|
||||||
|
b.path.filename().string();
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const auto& item : items)
|
||||||
|
{
|
||||||
|
const std::string name = item.path.filename().string();
|
||||||
|
const std::string assetPath = ToAssetPath(item.path);
|
||||||
|
ImGui::PushID(name.c_str());
|
||||||
|
|
||||||
|
ImGui::TextUnformatted(KindTag(item.kind));
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Selectable(name.c_str());
|
||||||
|
|
||||||
|
// 双击预热:model/HDR 进缓存(路径去重,重复双击零成本);
|
||||||
|
// 场景加载含环境恢复/选中清理流程,提示走 File 菜单
|
||||||
|
if (ImGui::IsItemHovered() &&
|
||||||
|
ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
|
||||||
|
{
|
||||||
|
if (item.kind == Kind::Model && ctx.assets)
|
||||||
|
{
|
||||||
|
const auto ref = ctx.assets->LoadModel(assetPath);
|
||||||
|
ENGINE_INFO("AssetBrowser: model '{}' loaded (valid={})",
|
||||||
|
assetPath, ref.Valid());
|
||||||
|
}
|
||||||
|
else if (item.kind == Kind::Texture && ctx.assets)
|
||||||
|
{
|
||||||
|
const auto ref = ctx.assets->LoadEnvironment(assetPath);
|
||||||
|
ENGINE_INFO("AssetBrowser: env '{}' loaded (valid={})",
|
||||||
|
assetPath, ref.Valid());
|
||||||
|
}
|
||||||
|
else if (item.kind == Kind::Scene)
|
||||||
|
{
|
||||||
|
ENGINE_INFO("AssetBrowser: load scenes via File menu ({})",
|
||||||
|
assetPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tooltip:完整相对路径(面板窄时文件名截断仍可辨识)
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui::SetTooltip("%s", assetPath.c_str());
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
//
|
||||||
|
// AssetBrowserPanel:项目资产浏览器(Unity Project 窗口简化版)。
|
||||||
|
//
|
||||||
|
// 左侧目录树 + 右侧文件列表(类型过滤 + 名字过滤)。与 AssetManager
|
||||||
|
// 的集成点是"双击预热":model/HDR 进缓存去重(重复双击零成本),
|
||||||
|
// .scene 提示走 File 菜单(加载含环境恢复/选中清理等流程,不在面板
|
||||||
|
// 重复)。拖拽到视口/Inspector 属后续迭代。
|
||||||
|
//
|
||||||
|
// 目录每帧扫描:项目级目录条目有限(几十量级),filesystem 迭代开销
|
||||||
|
// 可忽略;资产规模上来后再加缓存脏标。
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef HELLOVULKAN_ASSETBROWSERPANEL_H
|
||||||
|
#define HELLOVULKAN_ASSETBROWSERPANEL_H
|
||||||
|
|
||||||
|
#include "EditorContext.h"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class AssetBrowserPanel {
|
||||||
|
public:
|
||||||
|
void Draw(const EditorContext& ctx);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// 目录树节点(递归);右侧列表画 dir 下的文件
|
||||||
|
void DrawDirTree(const EditorContext& ctx,
|
||||||
|
const std::filesystem::path& dir);
|
||||||
|
void DrawFileList(const EditorContext& ctx,
|
||||||
|
const std::filesystem::path& dir);
|
||||||
|
|
||||||
|
// 固定根(下标即身份):项目资产 / 场景文件
|
||||||
|
int m_Root = 0;
|
||||||
|
// 类型过滤(0 = All;其余见 kTypeNames)
|
||||||
|
int m_TypeFilter = 0;
|
||||||
|
// 名字子串过滤(空 = 不过滤)
|
||||||
|
char m_NameFilter[64] = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif //HELLOVULKAN_ASSETBROWSERPANEL_H
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#include "Editor.h"
|
#include "Editor.h"
|
||||||
|
#include "AssetBrowserPanel.h"
|
||||||
#include "HierarchyPanel.h"
|
#include "HierarchyPanel.h"
|
||||||
#include "InspectorPanel.h"
|
#include "InspectorPanel.h"
|
||||||
#include "ViewportPanel.h"
|
#include "ViewportPanel.h"
|
||||||
@@ -40,6 +41,7 @@ void Editor::Init(const EditorContext& ctx)
|
|||||||
m_Inspector = std::make_unique<InspectorPanel>();
|
m_Inspector = std::make_unique<InspectorPanel>();
|
||||||
m_Viewport = std::make_unique<ViewportPanel>();
|
m_Viewport = std::make_unique<ViewportPanel>();
|
||||||
m_Stats = std::make_unique<StatsPanel>();
|
m_Stats = std::make_unique<StatsPanel>();
|
||||||
|
m_AssetBrowser = std::make_unique<AssetBrowserPanel>();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Editor::Draw()
|
void Editor::Draw()
|
||||||
@@ -82,6 +84,7 @@ void Editor::Draw()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (m_ShowStats) m_Stats->Draw(m_Ctx);
|
if (m_ShowStats) m_Stats->Draw(m_Ctx);
|
||||||
|
if (m_ShowAssetBrowser) m_AssetBrowser->Draw(m_Ctx);
|
||||||
|
|
||||||
DrawQuitModal();
|
DrawQuitModal();
|
||||||
DrawSceneModals();
|
DrawSceneModals();
|
||||||
@@ -154,6 +157,7 @@ void Editor::DrawMenuBar()
|
|||||||
ImGui::MenuItem("Inspector", nullptr, &m_ShowInspector);
|
ImGui::MenuItem("Inspector", nullptr, &m_ShowInspector);
|
||||||
ImGui::MenuItem("Viewport", nullptr, &m_ShowViewport);
|
ImGui::MenuItem("Viewport", nullptr, &m_ShowViewport);
|
||||||
ImGui::MenuItem("Stats", nullptr, &m_ShowStats);
|
ImGui::MenuItem("Stats", nullptr, &m_ShowStats);
|
||||||
|
ImGui::MenuItem("Asset Browser", nullptr, &m_ShowAssetBrowser);
|
||||||
ImGui::EndMenu();
|
ImGui::EndMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +188,7 @@ void Editor::DrawDockSpace()
|
|||||||
|
|
||||||
ImGui::DockBuilderDockWindow("Hierarchy", dockLeft);
|
ImGui::DockBuilderDockWindow("Hierarchy", dockLeft);
|
||||||
ImGui::DockBuilderDockWindow("Stats", dockStats);
|
ImGui::DockBuilderDockWindow("Stats", dockStats);
|
||||||
|
ImGui::DockBuilderDockWindow("Asset Browser", dockStats);
|
||||||
ImGui::DockBuilderDockWindow("Inspector", dockRight);
|
ImGui::DockBuilderDockWindow("Inspector", dockRight);
|
||||||
ImGui::DockBuilderDockWindow("Viewport", dockCenter);
|
ImGui::DockBuilderDockWindow("Viewport", dockCenter);
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class HierarchyPanel;
|
|||||||
class InspectorPanel;
|
class InspectorPanel;
|
||||||
class ViewportPanel;
|
class ViewportPanel;
|
||||||
class StatsPanel;
|
class StatsPanel;
|
||||||
|
class AssetBrowserPanel;
|
||||||
|
|
||||||
class Editor {
|
class Editor {
|
||||||
public:
|
public:
|
||||||
@@ -86,6 +87,7 @@ private:
|
|||||||
std::unique_ptr<InspectorPanel> m_Inspector;
|
std::unique_ptr<InspectorPanel> m_Inspector;
|
||||||
std::unique_ptr<ViewportPanel> m_Viewport;
|
std::unique_ptr<ViewportPanel> m_Viewport;
|
||||||
std::unique_ptr<StatsPanel> m_Stats;
|
std::unique_ptr<StatsPanel> m_Stats;
|
||||||
|
std::unique_ptr<AssetBrowserPanel> m_AssetBrowser;
|
||||||
|
|
||||||
entt::entity m_Selected{entt::null};
|
entt::entity m_Selected{entt::null};
|
||||||
|
|
||||||
@@ -94,6 +96,7 @@ private:
|
|||||||
bool m_ShowInspector = true;
|
bool m_ShowInspector = true;
|
||||||
bool m_ShowViewport = true;
|
bool m_ShowViewport = true;
|
||||||
bool m_ShowStats = true;
|
bool m_ShowStats = true;
|
||||||
|
bool m_ShowAssetBrowser = true;
|
||||||
|
|
||||||
// 默认布局只建一次(imgui.ini 已有布局则跳过)
|
// 默认布局只建一次(imgui.ini 已有布局则跳过)
|
||||||
bool m_LayoutBuilt = false;
|
bool m_LayoutBuilt = false;
|
||||||
|
|||||||
@@ -566,37 +566,58 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
|||||||
{
|
{
|
||||||
ImGui::BeginDisabled(true);
|
ImGui::BeginDisabled(true);
|
||||||
}
|
}
|
||||||
// 下拉列出全部已注册脚本(注册序);"<none>" = 清空(对账自动销毁实例)
|
// 多脚本列表(Unity 语义):每行 = 脚本下拉 + 独立启停 + 删除;
|
||||||
|
// 实例生命周期由 ScriptRuntime 对账(删行/改名 → 下帧 OnDestroy)
|
||||||
const auto& names = Engine::Scripting::ScriptRegistry::Names();
|
const auto& names = Engine::Scripting::ScriptRegistry::Names();
|
||||||
std::vector<const char*> items;
|
std::vector<const char*> options;
|
||||||
items.push_back("<none>");
|
options.push_back("<none>");
|
||||||
for (const auto& n : names)
|
for (const auto& n : names)
|
||||||
{
|
{
|
||||||
items.push_back(n.c_str());
|
options.push_back(n.c_str());
|
||||||
}
|
}
|
||||||
int cur = 0;
|
int removeIdx = -1;
|
||||||
for (size_t i = 0; i < names.size(); ++i)
|
for (int i = 0; i < static_cast<int>(sc->items.size()); ++i)
|
||||||
{
|
{
|
||||||
if (names[i] == sc->scriptName)
|
auto& item = sc->items[static_cast<size_t>(i)];
|
||||||
|
ImGui::PushID(i);
|
||||||
|
int cur = 0;
|
||||||
|
for (size_t k = 0; k < names.size(); ++k)
|
||||||
{
|
{
|
||||||
cur = static_cast<int>(i) + 1;
|
if (names[k] == item.name)
|
||||||
|
{
|
||||||
|
cur = static_cast<int>(k) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::SetNextItemWidth(
|
||||||
|
ImGui::GetContentRegionAvail().x - 64.0f);
|
||||||
|
if (ImGui::Combo("##script", &cur, options.data(),
|
||||||
|
static_cast<int>(options.size())))
|
||||||
|
{
|
||||||
|
item.name = (cur <= 0) ? "" : names[static_cast<size_t>(cur) - 1];
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Checkbox("##en", &item.active);
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("-", ImVec2(24.0f, 0.0f)))
|
||||||
|
{
|
||||||
|
removeIdx = i;
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
// 已填名但注册表查无此类:场景加载早于脚本注册/改名都会走到这
|
||||||
|
if (!item.name.empty() && cur == 0)
|
||||||
|
{
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
|
||||||
|
"! not registered: %s",
|
||||||
|
item.name.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::Combo("script", &cur, items.data(),
|
if (removeIdx >= 0)
|
||||||
static_cast<int>(items.size())))
|
|
||||||
{
|
{
|
||||||
sc->scriptName = (cur <= 0) ? "" : names[static_cast<size_t>(cur) - 1];
|
sc->items.erase(sc->items.begin() + removeIdx);
|
||||||
}
|
}
|
||||||
if (sc->scriptName.empty())
|
if (ImGui::Button("+ Add Script"))
|
||||||
{
|
{
|
||||||
ImGui::TextDisabled("no script assigned");
|
sc->items.push_back({});
|
||||||
}
|
|
||||||
// 已填名但注册表查无此类:场景加载早于脚本注册/改名都会走到这
|
|
||||||
else if (cur == 0)
|
|
||||||
{
|
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
|
|
||||||
"! script not registered: %s",
|
|
||||||
sc->scriptName.c_str());
|
|
||||||
}
|
}
|
||||||
if (!sc->active)
|
if (!sc->active)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -282,12 +282,21 @@ namespace Engine::ECS {
|
|||||||
// 实体原点 = 脚底(胶囊底端),符合角色 pivot 习惯
|
// 实体原点 = 脚底(胶囊底端),符合角色 pivot 习惯
|
||||||
};
|
};
|
||||||
|
|
||||||
// 脚本组件:只存脚本类名(注册表键,POD 可序列化)。C++ 原生脚本经
|
// 脚本组件:多脚本挂载(Unity 语义——一个 GameObject 挂 N 个
|
||||||
// ENGINE_REGISTER_SCRIPT 自注册;将来 Lua/C# 脚本同键解析。
|
// MonoBehaviour,各自有 enabled)。每项 = 脚本类名(注册表键,
|
||||||
|
// 将来 Lua/C# 同键解析)+ 独立启停位;组件级 active 是总开关。
|
||||||
// 实例生命周期由 ScriptRuntime 对账管理(不进组件,保持可拷贝)
|
// 实例生命周期由 ScriptRuntime 对账管理(不进组件,保持可拷贝)
|
||||||
struct ScriptComponent {
|
struct ScriptComponent {
|
||||||
bool active = true; // 禁用 = 实例保留但停 OnUpdate(Unity disable)
|
// 单个脚本项
|
||||||
std::string scriptName;
|
struct Item {
|
||||||
|
std::string name;
|
||||||
|
bool active = true; // 禁用 = 实例保留但停 OnUpdate
|
||||||
|
|
||||||
|
bool operator==(const Item&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool active = true; // 组件总开关(全部停 tick,实例保留)
|
||||||
|
std::vector<Item> items;
|
||||||
|
|
||||||
bool operator==(const ScriptComponent&) const = default;
|
bool operator==(const ScriptComponent&) const = default;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
//
|
//
|
||||||
// ScriptRuntime.h:脚本生命周期驱动。对账式管理(组件加/删/改名 ↔
|
// ScriptRuntime.h:脚本生命周期驱动。对账式管理(组件项加/删/改名 ↔
|
||||||
// 实例建/毁),照 PhysicsWorld.SyncScene 模式——每帧对账,编辑器操作
|
// 实例建/毁),照 PhysicsWorld.SyncScene 模式——每帧对账,编辑器操作
|
||||||
// 即时生效,场景加载/实体销毁自动消化。
|
// 即时生效,场景加载/实体销毁自动消化。
|
||||||
//
|
//
|
||||||
|
// 多脚本:一个实体可挂任意多个脚本项(ScriptComponent::items),实例
|
||||||
|
// 按名字与 items 对账——重命名/删除走 OnDestroy 清理,未动的实例不重建。
|
||||||
|
//
|
||||||
|
|
||||||
#ifndef HELLOVULKAN_SCRIPT_RUNTIME_H
|
#ifndef HELLOVULKAN_SCRIPT_RUNTIME_H
|
||||||
#define HELLOVULKAN_SCRIPT_RUNTIME_H
|
#define HELLOVULKAN_SCRIPT_RUNTIME_H
|
||||||
@@ -12,6 +15,7 @@
|
|||||||
#include <Engine/Scripting/Script.h>
|
#include <Engine/Scripting/Script.h>
|
||||||
|
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
|
|
||||||
@@ -37,8 +41,9 @@ namespace Engine {
|
|||||||
void Sync();
|
void Sync();
|
||||||
|
|
||||||
ECS::Scene& m_Scene;
|
ECS::Scene& m_Scene;
|
||||||
Application* m_App; // 注入脚本上下文
|
Application* m_App; // 注入脚本上下文
|
||||||
std::unordered_map<u32, ScriptInstance> m_Instances; // entityBits
|
// entityBits → 实例列表(成员按名字与 items 对齐,无序)
|
||||||
|
std::unordered_map<u32, std::vector<ScriptInstance>> m_Instances;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Scripting
|
} // namespace Scripting
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// ScriptRuntime.cpp:脚本对账与生命周期驱动。
|
// ScriptRuntime.cpp:脚本对账与生命周期驱动(多脚本挂载)。
|
||||||
//
|
//
|
||||||
|
|
||||||
#include <Engine/Scripting/ScriptRuntime.h>
|
#include <Engine/Scripting/ScriptRuntime.h>
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
#include <Engine/Core/Log.h>
|
#include <Engine/Core/Log.h>
|
||||||
#include <Engine/ECS/Components.h>
|
#include <Engine/ECS/Components.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
namespace Scripting {
|
namespace Scripting {
|
||||||
|
|
||||||
@@ -20,7 +22,7 @@ namespace Engine {
|
|||||||
Sync();
|
Sync();
|
||||||
|
|
||||||
auto& reg = m_Scene.GetRegistry();
|
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 auto e = static_cast<entt::entity>(bits);
|
||||||
const ECS::ScriptComponent* sc = reg.valid(e)
|
const ECS::ScriptComponent* sc = reg.valid(e)
|
||||||
? reg.try_get<ECS::ScriptComponent>(e) : nullptr;
|
? reg.try_get<ECS::ScriptComponent>(e) : nullptr;
|
||||||
@@ -28,57 +30,106 @@ namespace Engine {
|
|||||||
continue; // 延迟销毁队列里,下帧 Sync 清理
|
continue; // 延迟销毁队列里,下帧 Sync 清理
|
||||||
}
|
}
|
||||||
if (!sc->active) {
|
if (!sc->active) {
|
||||||
continue; // 禁用 = 保留实例停 tick(Unity disable 语义)
|
continue; // 组件总开关:全部实例停 tick(Unity disable)
|
||||||
}
|
}
|
||||||
if (!inst.created) {
|
for (auto& inst : list) {
|
||||||
inst.behavior->OnCreate();
|
if (!inst.behavior) {
|
||||||
inst.created = true;
|
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() {
|
void ScriptRuntime::Sync() {
|
||||||
auto& reg = m_Scene.GetRegistry();
|
auto& reg = m_Scene.GetRegistry();
|
||||||
|
|
||||||
// 1. 孤儿/改名清理:实体或组件没了、脚本名变了 → OnDestroy + 删除
|
// 1. 孤儿/改名清理:实体或组件没了 → 整组 OnDestroy;
|
||||||
|
// 实例名不在 items 里(改名/移除)→ 单个 OnDestroy
|
||||||
for (auto it = m_Instances.begin(); it != m_Instances.end();) {
|
for (auto it = m_Instances.begin(); it != m_Instances.end();) {
|
||||||
const auto e = static_cast<entt::entity>(it->first);
|
const auto e = static_cast<entt::entity>(it->first);
|
||||||
const ECS::ScriptComponent* sc =
|
const ECS::ScriptComponent* sc =
|
||||||
reg.valid(e) ? reg.try_get<ECS::ScriptComponent>(e) : nullptr;
|
reg.valid(e) ? reg.try_get<ECS::ScriptComponent>(e)
|
||||||
const bool stale = !sc || !it->second.behavior ||
|
: nullptr;
|
||||||
it->second.behavior->Name() != sc->scriptName;
|
if (!sc) {
|
||||||
if (stale) {
|
for (auto& inst : it->second) {
|
||||||
if (it->second.behavior) {
|
if (inst.behavior) {
|
||||||
it->second.behavior->OnDestroy();
|
inst.behavior->OnDestroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
it = m_Instances.erase(it);
|
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 {
|
} else {
|
||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 缺失实例化:按名查注册表(未知名警告——场景含未注册脚本时
|
// 2. 缺失实例化:逐项按名查注册表(未知名警告——场景含未注册
|
||||||
// 弱前向兼容,跳过不影响其余实体)
|
// 脚本时弱前向兼容,跳过不影响其余脚本)
|
||||||
const auto view = reg.view<ECS::ScriptComponent>();
|
const auto view = reg.view<ECS::ScriptComponent>();
|
||||||
for (auto [e, sc] : view.each()) {
|
for (auto [e, sc] : view.each()) {
|
||||||
const u32 bits = static_cast<u32>(entt::to_integral(e));
|
const u32 bits = static_cast<u32>(entt::to_integral(e));
|
||||||
if (m_Instances.contains(bits) || sc.scriptName.empty()) {
|
auto& list = m_Instances[bits];
|
||||||
continue;
|
for (const auto& item : sc.items) {
|
||||||
}
|
if (item.name.empty()) {
|
||||||
auto behavior = ScriptRegistry::Create(sc.scriptName);
|
continue;
|
||||||
if (!behavior) {
|
}
|
||||||
ENGINE_WARN("Script '{}' not registered (entity {})",
|
const bool exists = std::any_of(
|
||||||
sc.scriptName, bits);
|
list.begin(), list.end(),
|
||||||
continue;
|
[&](const ScriptInstance& inst) {
|
||||||
}
|
return inst.behavior &&
|
||||||
behavior->Bind(m_Scene, e, m_App);
|
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;
|
ScriptInstance inst;
|
||||||
inst.behavior = std::move(behavior);
|
inst.behavior = std::move(behavior);
|
||||||
inst.behavior->OnCreate(); // Unity Awake 语义:实例化即回调
|
inst.behavior->OnCreate(); // Unity Awake 语义:实例化即回调
|
||||||
inst.created = true;
|
inst.created = true;
|
||||||
m_Instances.emplace(bits, std::move(inst));
|
list.push_back(std::move(inst));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -350,12 +350,20 @@ namespace Engine::Serialization {
|
|||||||
out << YAML::EndMap;
|
out << YAML::EndMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 脚本只存类名(注册表键);未注册名字加载后运行时警告跳过
|
// 脚本:多脚本列表(每项 = 名字 + 独立启停位);未注册名字
|
||||||
|
// 加载后运行时警告跳过
|
||||||
if (const auto* sc = reg.try_get<ECS::ScriptComponent>(e)) {
|
if (const auto* sc = reg.try_get<ECS::ScriptComponent>(e)) {
|
||||||
out << YAML::Key << "Script" << YAML::Value << YAML::BeginMap;
|
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 << "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;
|
out << YAML::EndMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -655,11 +663,25 @@ namespace Engine::Serialization {
|
|||||||
// 状态(速度/着地)由 PhysicsWorld 对账重建,不存
|
// 状态(速度/着地)由 PhysicsWorld 对账重建,不存
|
||||||
}
|
}
|
||||||
|
|
||||||
// 脚本:只存类名;未注册名字由 ScriptRuntime 运行时警告跳过
|
// 脚本:优先读多脚本 Items;无 Items 回退旧单脚本键(向前兼容)
|
||||||
if (const YAML::Node n = comps["Script"]) {
|
if (const YAML::Node n = comps["Script"]) {
|
||||||
auto& sc = handle.AddComponent<ECS::ScriptComponent>();
|
auto& sc = handle.AddComponent<ECS::ScriptComponent>();
|
||||||
sc.scriptName = n["ScriptName"].as<std::string>("");
|
|
||||||
sc.active = n["Active"].as<bool>(true);
|
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