新增 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();
}
}
+88
View File
@@ -0,0 +1,88 @@
//
// PrefabTest.cpp:阶段2 验收脚本——脚本侧 Instantiate(prefab) 运行时
// 实例化链路。
//
// PrefabTest(挂任意空实体,作为实例化锚点):
// OnStart 按参数路径实例化一次 prefab;Interval > 0 时周期性再实例化
// (多实例验收:每次 UUID 全量再生、物理体独立、沿 X 向排开下落)。
//
// 覆盖的引擎设施:
// - ScriptBehavior::Instantiate(path, pos, rot)(Unity Object.
// Instantiate 同位)→ SceneSerializer::InstantiatePrefab
// - prefab 子树含 RigidBody/ScriptComponent 时由 PhysicsWorld/
// ScriptRuntime 下一帧对账消化(与 Spawn 同语义)
//
// 验收步骤:
// 1. 编辑器搭一个实体子树(如 Dynamic 盒刚体 + MeshRenderer 的箱子,
// 可再挂个子实体验证层级一起走)→ Outliner 右键 Save As Prefab
// (落盘 assets/prefabs/<名字>.prefab)
// 2. 建空实体挂 PrefabTest,参数 PrefabPath 填该路径 → Play:
// 锚点处出现实例;Interval 设 1.0 可看连续多实例物理下落互不串扰
// 3. 编辑态也可从 Content Browser 把 .prefab 拖进 Outliner(节点=
// 挂其下,空白区=挂根)或双击直接实例化——UUID 独立、资产引用有效
//
#include <Engine/Scripting/Script.h>
#include <glm/glm.hpp>
using namespace Engine; // f32 / ScriptBehavior 等免前缀(对齐 TestShoot)
namespace {
using Engine::Scripting::ParamBuilder;
class PrefabTest final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "PrefabTest"; }
void DeclareParams(ParamBuilder& pb) override {
// prefab 资产路径(工作目录相对,同场景/资产管理约定)
pb.Field("PrefabPath", &m_PrefabPath,
std::string{"assets/prefabs/crate.prefab"});
// 周期实例化间隔(秒;<=0 = 仅 OnStart 实例化一次)
pb.Field("Interval", &m_Interval, 0.0f);
// 相邻实例的 X 向排开间距(多实例不叠在一起)
pb.Field("SpreadX", &m_SpreadX, 2.0f);
}
void OnStart() override {
m_Timer = m_Interval;
SpawnInstance();
}
void OnUpdate(f32 dt) override {
if (m_Interval <= 0.0f) {
return;
}
m_Timer -= dt;
if (m_Timer <= 0.0f) {
m_Timer = m_Interval;
SpawnInstance();
}
}
private:
void SpawnInstance() {
// 锚点实体位姿 + 排开偏移;旋转 = 锚点自身旋转
const auto& tr = Transform();
const glm::vec3 pos =
tr.GetPosition() + glm::vec3{m_SpreadX * f32(m_Count),
0.0f, 0.0f};
const glm::quat rot = tr.GetRotationQuat();
if (Instantiate(m_PrefabPath, pos, rot) == entt::null) {
return; // 路径无效/解析失败:日志已在序列化器里打过
}
++m_Count;
}
std::string m_PrefabPath;
f32 m_Interval = 0.0f;
f32 m_SpreadX = 2.0f;
f32 m_Timer = 0.0f;
int m_Count = 0; // 已实例化份数(排开偏移基准)
};
ENGINE_REGISTER_SCRIPT(PrefabTest)
} // namespace