新增 Prefab 系统:实体子树存取 + 编辑器 Save As Prefab/拖入实例化 + 脚本 Instantiate

This commit is contained in:
2026-10-08 22:01:26 +08:00
parent b89f431896
commit ed7cb417bd
8 changed files with 643 additions and 208 deletions
+35 -5
View File
@@ -7,6 +7,7 @@
#include "ControllerEditorPanel.h" // kControllerDragPayload + 双击打开控制器编辑器
#include <Engine/Core/Log.h>
#include <Engine/Serialization/SceneSerializer.h>
#include <imgui.h>
@@ -27,10 +28,12 @@ namespace {
// 类型过滤(下标即身份):0 = All
constexpr const char* kTypeNames[] = {"All", "Models",
"Textures", "Scenes",
"Materials", "Controllers"};
"Materials", "Controllers",
"Prefabs"};
// 资产类别:按扩展名分派(过滤用)
enum class Kind { Model, Texture, Scene, Material, AnimClip, Other };
enum class Kind { Model, Texture, Scene, Material, AnimClip, Prefab,
Other };
Kind KindOf(const std::filesystem::path& p)
{
@@ -59,6 +62,10 @@ namespace {
{
return Kind::AnimClip;
}
if (ext == ".prefab")
{
return Kind::Prefab;
}
return Kind::Other;
}
@@ -71,6 +78,7 @@ namespace {
case 3: return kind == Kind::Scene;
case 4: return kind == Kind::Material;
case 5: return kind == Kind::AnimClip;
case 6: return kind == Kind::Prefab;
default: return true;
}
}
@@ -85,6 +93,7 @@ namespace {
case Kind::Scene: return "[S]";
case Kind::Material: return "[Ma]";
case Kind::AnimClip: return "[An]";
case Kind::Prefab: return "[Pf]";
default: return "[ ]";
}
}
@@ -121,7 +130,7 @@ void AssetBrowserPanel::Draw(const EditorContext& ctx)
}
ImGui::SameLine();
ImGui::SetNextItemWidth(110.0f);
ImGui::Combo("##type", &m_TypeFilter, kTypeNames, 6);
ImGui::Combo("##type", &m_TypeFilter, kTypeNames, 7);
ImGui::SameLine();
ImGui::SetNextItemWidth(140.0f);
ImGui::InputTextWithHint("##name", "filter", m_NameFilter,
@@ -296,6 +305,16 @@ void AssetBrowserPanel::DrawFileList(const EditorContext& ctx,
ImGui::EndDragDropSource();
}
// .prefab 拖拽源:拖到 Outliner(节点=挂其下 / 空白区=挂根)
// 实例化整棵子树(kPrefabDragPayload)
if (item.kind == Kind::Prefab && ImGui::BeginDragDropSource())
{
ImGui::SetDragDropPayload(kPrefabDragPayload,
assetPath.c_str(), assetPath.size() + 1);
ImGui::TextUnformatted(name.c_str());
ImGui::EndDragDropSource();
}
// 双击:.material 打开 Material Editor;.animctrl 打开
// Controller Editor;model/HDR 进缓存预热(路径去重,重复
// 双击零成本);场景加载含环境恢复/选中清理流程,提示走
@@ -328,6 +347,16 @@ void AssetBrowserPanel::DrawFileList(const EditorContext& ctx,
ENGINE_INFO("AssetBrowser: load scenes via File menu ({})",
assetPath);
}
else if (item.kind == Kind::Prefab && ctx.scene && ctx.assets)
{
// 双击 = 快捷实例化(授权位姿落点,选中交给 Outliner
// 手动改);拖拽实例化见 Outliner 落点
const auto root = Engine::Serialization::SceneSerializer::
InstantiatePrefab(*ctx.scene, *ctx.assets, assetPath);
ENGINE_INFO("AssetBrowser: prefab '{}' instantiated "
"(valid={})",
assetPath, root != entt::null);
}
}
// hover 信息卡(UE Content Browser 式):类型/大小/修改时间/加载状态
@@ -336,11 +365,12 @@ void AssetBrowserPanel::DrawFileList(const EditorContext& ctx,
ImGui::BeginTooltip();
ImGui::TextUnformatted(name.c_str());
ImGui::Separator();
// 与 Kind 枚举逐位对齐(Kind::Other = 5)——漏项 = tooltip
// 与 Kind 枚举逐位对齐(Kind::Other = 6)——漏项 = tooltip
// 数组越界读
static const char* kKindNames[] = {"Model", "Texture/Environment",
"Scene", "Material",
"Controller", "File"};
"Controller", "Prefab",
"File"};
ImGui::Text("type: %s", kKindNames[static_cast<int>(item.kind)]);
namespace fs = std::filesystem;
std::error_code ec;
+5
View File
@@ -18,6 +18,11 @@
#include <filesystem>
#include <string>
// Prefab 资产拖拽 payload(Content Browser 拖 .prefab → Outliner 落点
// 实例化;字符串 = 资产相对路径)。对齐 kMaterialDragPayload 模式:
// 常量住源面板头
inline constexpr const char* kPrefabDragPayload = "PREFAB_PATH";
class AssetBrowserPanel {
public:
void Draw(const EditorContext& ctx);
+69 -2
View File
@@ -7,12 +7,16 @@
//
#include "HierarchyPanel.h"
#include "AssetBrowserPanel.h" // kPrefabDragPayload(Content Browser 源)
#include "Engine/ECS/Components.h"
#include "Engine/ECS/EntityHandle.h" // CreateEntity 返回完整 EntityHandle
#include "Engine/Serialization/SceneSerializer.h"
#include "Engine/Core/Log.h"
#include <imgui.h>
#include <cstring>
#include <filesystem>
#include <string>
#include <vector>
@@ -33,6 +37,34 @@ namespace {
return "(unnamed)";
}
// Prefab 拖入实例化(树节点/空白区落点共用)。根实体挂在场景根、
// 保留授权位姿;返回根实体(失败 entt::null)
entt::entity InstantiatePrefabDrop(const EditorContext& ctx,
const char* path)
{
if (!ctx.scene || !ctx.assets || !path) {
return entt::null;
}
return Engine::Serialization::SceneSerializer::InstantiatePrefab(
*ctx.scene, *ctx.assets, path);
}
// 实体名 → 合法文件名(路径分隔符/保留符净化;空名兜底)
std::string PrefabFileName(std::string name)
{
for (char& c : name) {
if (c == '/' || c == '\\' || c == ':' || c == '*' ||
c == '?' || c == '"' || c == '<' || c == '>' ||
c == '|') {
c = '_';
}
}
if (name.empty() || name == "(unnamed)") {
name = "prefab";
}
return name;
}
// 递归绘制单节点(含子树)。返回值无意义,选中写 selected
void DrawEntityNode(const EditorContext& ctx, entt::entity e,
entt::entity& selected)
@@ -78,7 +110,9 @@ namespace {
ImGui::EndDragDropSource();
}
// ---- 拖放目标:挂到本实体下(keepWorld,世界位姿不变)----
// ---- 拖放目标:挂到本实体下(keepWorld,世界位姿不变);
// Prefab 拖入 = 实例化子树后挂到本实体下(keepWorld 保持
// 授权世界位姿)----
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kHierPayload)) {
@@ -91,6 +125,17 @@ namespace {
ctx.scene->SetParent(dragged, e);
}
}
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kPrefabDragPayload)) {
if (const entt::entity root =
InstantiatePrefabDrop(ctx,
static_cast<const char*>(
pl->Data));
root != entt::null) {
ctx.scene->SetParent(root, e); // keepWorld 默认 true
selected = root;
}
}
ImGui::EndDragDropTarget();
}
@@ -102,6 +147,19 @@ namespace {
ctx.scene->SetParent(child, e);
selected = child;
}
if (ImGui::MenuItem("Save As Prefab")) {
// 固定目录 + 实体名派生文件名(无文件对话框库,与场景
// File 菜单同策略;重名覆盖,改名走文件系统)
const std::string path =
"assets/prefabs/" +
PrefabFileName(DisplayName(reg, e)) + ".prefab";
std::error_code ec;
std::filesystem::create_directories("assets/prefabs", ec);
if (Serialization::SceneSerializer::SerializePrefab(
*ctx.scene, *ctx.assets, e, path)) {
ENGINE_INFO("Hierarchy: prefab saved to '{}'", path);
}
}
if (ctx.scene->GetParent(e) != entt::null &&
ImGui::MenuItem("Detach to Root")) {
ctx.scene->SetParent(e, entt::null);
@@ -194,7 +252,8 @@ void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
DrawEntityNode(ctx, e, selected);
});
// ---- 空白区 = 挂根落点(UE Outliner 拖到空白 = 顶层)----
// ---- 空白区 = 挂根落点(UE Outliner 拖到空白 = 顶层);
// Prefab 拖入 = 实例化到场景根(授权位姿)----
ImVec2 rem = ImGui::GetContentRegionAvail();
if (rem.y > 4.0f) {
ImGui::InvisibleButton("##hier_drop_root", rem);
@@ -206,6 +265,14 @@ void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
ctx.scene->SetParent(static_cast<entt::entity>(bits),
entt::null);
}
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kPrefabDragPayload)) {
if (const entt::entity root = InstantiatePrefabDrop(
ctx, static_cast<const char*>(pl->Data));
root != entt::null) {
selected = root;
}
}
ImGui::EndDragDropTarget();
}
}