新增 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
+10
View File
@@ -123,6 +123,16 @@ namespace Engine {
//(遍历安全);调用方持有的句柄此后悬空
void Destroy(ECS::Entity e);
// ---- Prefab 实例化(Unity Object.Instantiate 同位)----
// .prefab 文件 → 整棵子树追加进场景(UUID 全量再生,子树内
// Joint.target 自动重映射)。pos/rot = 根实体 local 位姿
//(根挂场景根时即世界位姿)。失败返回 entt::null;
// RigidBody/ScriptComponent 由 PhysicsWorld/ScriptRuntime
// 下一帧对账消化(与 Spawn 同语义)
ECS::Entity Instantiate(const std::string& prefabPath,
const glm::vec3& pos,
const glm::quat& rot = glm::quat{});
// ---- 组件启停(Unity SetActive / Behaviour.enabled 语义)----
// 自身组件总开关(整个脚本组件停 tick,实例保留)
void SetActive(bool on) {
@@ -23,6 +23,8 @@
#ifndef HELLOVULKAN_SCENESERIALIZER_H
#define HELLOVULKAN_SCENESERIALIZER_H
#include <Engine/ECS/Scene.h> // Entity + glm vec3/quat(Prefab API 签名用)
#include <string>
namespace Engine::ECS { class Scene; }
@@ -66,6 +68,31 @@ namespace Engine::Serialization {
static bool DeserializeFromString(ECS::Scene& scene,
Assets::AssetManager& assets,
const std::string& yamlText);
// ---- Prefab(实体子树存取;与场景共用实体段读写,实现见 .cpp)----
// 保存 root 子树(根 + 全部后代,DFS 序:父先于子)到 outPath。
// 格式 = 场景实体段子集:"Prefab:" 文档头 + Version + Entities
// 列表(组件段与场景逐键一致)。首实体 = 根(实例化约定)。
// 失败(根无效/无法写文件)返回 false
static bool SerializePrefab(const ECS::Scene& scene,
Assets::AssetManager& assets,
ECS::Entity root,
const std::string& outPath);
// 实例化 Prefab 到 scene(追加式,不换血)。UUID 全量再生——
// 同一 prefab 多次实例化、与场景既有实体均不冲突;子树内
// Joint.target 按旧→新重映射,指向子树外的引用保持原值。
// overridePos/overrideRot 非空时覆写根实体 local 位姿(根挂
// 场景根时即世界位姿);空指针 = 保留 prefab 授权位姿。
// 返回根实体(= 文件首实体);失败(解析失败/无实体)返回
// entt::null——逐实体容错与场景加载同语义,失败前可能已部分
// 实例化(单实体损坏只跳过该项)
static ECS::Entity InstantiatePrefab(
ECS::Scene& scene, Assets::AssetManager& assets,
const std::string& inPath,
const glm::vec3* overridePos = nullptr,
const glm::quat* overrideRot = nullptr);
};
} // namespace Engine::Serialization
+10
View File
@@ -10,6 +10,7 @@
#include <Engine/ECS/EntityHandle.h>
#include <Engine/Physics/PhysicsEvents.h>
#include <Engine/Physics/PhysicsWorld.h>
#include <Engine/Serialization/SceneSerializer.h>
#include <algorithm>
@@ -32,6 +33,15 @@ namespace Engine {
App().GetScene().DestroyEntity(e);
}
// Prefab 实例化(声明在 Script.h,实现下沉此处——同 Spawn/
// Destroy 模式;序列化器与 Application 门面都在引擎内可达)
ECS::Entity ScriptBehavior::Instantiate(const std::string& prefabPath,
const glm::vec3& pos,
const glm::quat& rot) {
return Serialization::SceneSerializer::InstantiatePrefab(
App().GetScene(), App().GetAssets(), prefabPath, &pos, &rot);
}
// ---- 参数按名读写(ScriptBehavior 声明,经绑定表;写穿
// Item.params 由运行时 SetParam 完成)----
bool ScriptBehavior::WriteParam(const std::string& name,
+399 -201
View File
@@ -271,6 +271,36 @@ namespace Engine::Serialization {
YAML::Node comps; // 缺省 = Null(无组件)
};
// 实体列表解析(场景加载 / Prefab 实例化共用):逐实体容错,
// 单实体损坏跳过继续,不动 registry
void ParseEntityList(const YAML::Node& entities,
std::vector<ParsedEntity>& out)
{
if (!entities || !entities.IsSequence()) {
return;
}
out.reserve(entities.size());
for (const auto& e : entities) {
try {
ParsedEntity pe;
pe.uuid = e["Entity"].as<u64>(0);
pe.name = e["Name"].as<std::string>(pe.name);
pe.parentUuid = e["Parent"].as<u64>(0);
if (const YAML::Node comps = e["Components"];
comps && comps.IsMap()) {
// 仅在 rhs 有效时赋值(僵尸节点 operator= 会抛)
pe.comps = comps;
}
out.push_back(std::move(pe));
}
catch (const std::exception& err) {
// 单实体损坏不炸全场:跳过继续
ENGINE_WARN("SceneSerializer: entity skipped ({})",
err.what());
}
}
}
// 写端:单实体 → Emitter(调用方已进入 Entities 序列)
void EmitEntity(YAML::Emitter& out, Assets::AssetManager& assets,
const ECS::Registry& reg, entt::entity e)
@@ -310,8 +340,8 @@ namespace Engine::Serialization {
if (!path.empty()) {
out << YAML::Key << "MeshRenderer" << YAML::Value
<< YAML::BeginMap;
out << YAML::Key << "Model" << YAML::Value << YAML::Flow
<< YAML::BeginMap;
out << YAML::Key << "Model" << YAML::Value
<< YAML::BeginMap;
if (const u64 guid = assets.GetOrCreateGUID(path);
guid != 0) {
out << YAML::Key << "Guid" << YAML::Value << guid;
@@ -347,12 +377,14 @@ namespace Engine::Serialization {
}
if (const auto* a = reg.try_get<ECS::AnimationComponent>(e)) {
out << YAML::Key << "Animation" << YAML::Value << YAML::Flow
<< YAML::BeginMap;
out << YAML::Key << "Animation" << YAML::Value
<< YAML::BeginMap; // block 风格:键多,单行不可读
out << YAML::Key << "ClipIndex" << YAML::Value << a->clipIndex;
out << YAML::Key << "Playing" << YAML::Value << a->playing;
out << YAML::Key << "Loop" << YAML::Value << a->loop;
out << YAML::Key << "Speed" << YAML::Value << a->speed;
out << YAML::Key << "RootMotion" << YAML::Value
<< a->applyRootMotion;
out << YAML::Key << "FadeDuration" << YAML::Value
<< a->fadeDuration;
out << YAML::Key << "Active" << YAML::Value << a->active;
@@ -379,8 +411,9 @@ namespace Engine::Serialization {
out << YAML::EndMap;
}
// 外部动画源(编辑器 attach 列表):clip 本体运行时并入
// Model 资产不落盘,加载端按此列表幂等重放
out << YAML::Key << "Attached" << YAML::Value << YAML::Flow
// Model 资产不落盘,加载端按此列表幂等重放。
// block 风格:路径长且条目多,flow 单行不可读
out << YAML::Key << "Attached" << YAML::Value
<< YAML::BeginSeq;
for (const auto& p : a->attachedAnimPaths) {
out << p;
@@ -390,7 +423,7 @@ namespace Engine::Serialization {
}
if (const auto* c = reg.try_get<ECS::CameraComponent>(e)) {
out << YAML::Key << "Camera" << YAML::Value << YAML::Flow
out << YAML::Key << "Attached" << YAML::Value
<< YAML::BeginMap;
out << YAML::Key << "Primary" << YAML::Value << c->primary;
out << YAML::Key << "Active" << YAML::Value << c->active;
@@ -583,185 +616,14 @@ namespace Engine::Serialization {
out << YAML::EndMap; // Entity
}
} // namespace
// ---- 保存 ----
// Emitter 构建段(文件版与内存版共用):documentName 空则不写
// 文档头 "Scene" 字段(快照无文件名,该字段是注释性的)
static std::string SerializeDoc(const ECS::Scene& scene,
Assets::AssetManager& assets,
const std::string& documentName)
{
// float32 无损往返需 9 位有效数字(std::numeric_limits
// <float>::max_digits10)
YAML::Emitter out;
out.SetFloatPrecision(9);
out << YAML::BeginMap;
if (!documentName.empty()) {
out << YAML::Key << "Scene" << YAML::Value << documentName;
}
out << YAML::Key << "Version" << YAML::Value << kSceneVersion;
const auto& s = scene.GetSettings();
out << YAML::Key << "Settings" << YAML::Value << YAML::BeginMap;
out << YAML::Key << "ClearColor" << YAML::Value;
EmitVec4(out, s.ClearColor);
out << YAML::EndMap;
out << YAML::Key << "Entities" << YAML::Value << YAML::BeginSeq;
const ECS::Registry& reg = scene.GetRegistry();
reg.view<ECS::IdComponent>().each(
[&](entt::entity e, const ECS::IdComponent&) {
EmitEntity(out, assets, reg, e);
});
out << YAML::EndSeq;
out << YAML::EndMap;
if (!out.good()) {
ENGINE_ERROR("SceneSerializer: emit failed: {}", out.GetLastError());
return {};
}
return {out.c_str(), out.size()};
}
bool SceneSerializer::Serialize(const ECS::Scene& scene,
Assets::AssetManager& assets,
const std::string& outPath)
{
try {
const std::string doc = SerializeDoc(
scene, assets,
std::filesystem::path(outPath).stem().string());
if (doc.empty()) {
return false; // emit 失败,日志已在 SerializeDoc 打过
}
std::ofstream file(outPath, std::ios::trunc);
if (!file) {
ENGINE_ERROR("SceneSerializer: cannot open '{}' for write",
outPath);
return false;
}
file << doc;
assets.FlushGUIDRegistry();
return true;
}
catch (const YAML::Exception& e) {
ENGINE_ERROR("SceneSerializer: save failed ({})", e.what());
return false;
}
catch (const std::exception& e) {
ENGINE_ERROR("SceneSerializer: save failed ({})", e.what());
return false;
}
}
std::string SceneSerializer::SerializeToString(
const ECS::Scene& scene,
Assets::AssetManager& assets,
const std::string& documentName)
{
try {
return SerializeDoc(scene, assets, documentName);
}
catch (const YAML::Exception& e) {
ENGINE_ERROR("SceneSerializer: snapshot failed ({})", e.what());
return {};
}
catch (const std::exception& e) {
ENGINE_ERROR("SceneSerializer: snapshot failed ({})", e.what());
return {};
}
}
// ---- 加载 ----
// 解析段(文件版与内存快照共用):root → 原子换血重建。
// sourceName 仅用于日志(文件路径或 "snapshot")
static bool ParseRoot(ECS::Scene& scene,
Assets::AssetManager& assets,
const YAML::Node& root,
const std::string& sourceName)
{
// Version 闸:高版本拒载(结构未知),缺失按 0 宽容旧档
if (const int version = root["Version"].as<int>(0);
version > kSceneVersion) {
ENGINE_ERROR("SceneSerializer: '{}' version {} > supported {}",
sourceName, version, kSceneVersion);
return false;
}
// ---- 旧档 v1 环境语义兼容读(v2 起环境 = 实体组件)----
// 僵尸节点安全:as<T>(fallback) 对缺失键返回 fallback;环境
// 键仅在真实存在时置位(避免新档误触发兼容建实体)
const std::string legacyEnv =
root["Environment"].as<std::string>("");
bool legacyEnvKey = !legacyEnv.empty();
glm::vec4 legacyAmbient{0.07f, 0.07f, 0.09f, 0.0f};
bool legacyAmbientKey = false;
bool legacyDrawSkybox = true;
bool legacySkyboxKey = false;
if (const YAML::Node st = root["Settings"]; st && st.IsMap()) {
auto& s = scene.GetSettings();
s.ClearColor = ReadVec4(st["ClearColor"], s.ClearColor);
if (const YAML::Node amb = st["AmbientColor"]) {
legacyAmbient = ReadVec4(amb, legacyAmbient);
legacyAmbientKey = true;
}
if (const YAML::Node ds = st["DrawSkybox"]) {
legacyDrawSkybox = ds.as<bool>(legacyDrawSkybox);
legacySkyboxKey = true;
}
}
// ---- 阶段 1:解析到中间结构(逐实体容错,不动 registry)----
std::vector<ParsedEntity> parsed;
const YAML::Node entities = root["Entities"];
if (entities && entities.IsSequence()) {
parsed.reserve(entities.size());
for (const auto& e : entities) {
try {
ParsedEntity pe;
pe.uuid = e["Entity"].as<u64>(0);
pe.name = e["Name"].as<std::string>(pe.name);
pe.parentUuid = e["Parent"].as<u64>(0);
if (const YAML::Node comps = e["Components"];
comps && comps.IsMap()) {
// 仅在 rhs 有效时赋值(僵尸节点 operator= 会抛)
pe.comps = comps;
}
parsed.push_back(std::move(pe));
}
catch (const std::exception& err) {
// 单实体损坏不炸全场:跳过继续
ENGINE_WARN("SceneSerializer: entity skipped ({})",
err.what());
}
}
}
// ---- 阶段 2:原子换血重建 ----
scene.Clear();
std::unordered_set<u64> seenUuids;
std::unordered_map<u64, entt::entity> byUuid; // 第二遍重链用
for (ParsedEntity& pe : parsed) {
auto handle = scene.CreateEntity(pe.name);
// UUID 重复/无效 → 重新生成(Prefab 实例化的 ID 重映射语义)
u64 uuid = pe.uuid;
if (uuid == 0 || !seenUuids.insert(uuid).second) {
uuid = UUID::New().Value();
}
handle.GetComponent<ECS::IdComponent>().uuid = UUID{uuid};
pe.uuid = uuid; // 回写最终 uuid(重生成后父引用仍可命中)
byUuid.emplace(uuid, static_cast<entt::entity>(handle));
const YAML::Node comps = pe.comps; // 拷贝初始化,缺省 Null
// 组件应用(场景加载 / Prefab 实例化共用):Components 节点 →
// 逐组件解析建池。handle 须为 CreateEntity 出厂态(IdComponent/
// Transform 出生即有,其余组件按需 AddComponent)。资产引用
// 解析与预载语义见各 Resolve* 辅助
void ApplyComponents(ECS::EntityHandle handle,
const YAML::Node& comps,
Assets::AssetManager& assets)
{
if (const YAML::Node n = comps["Transform"]) {
auto& t = handle.GetComponent<ECS::TransformComponent>();
t.SetPosition(ReadVec3(n["Position"], t.GetPosition()));
@@ -812,6 +674,8 @@ namespace Engine::Serialization {
a.playing = n["Playing"].as<bool>(a.playing);
a.loop = n["Loop"].as<bool>(a.loop);
a.speed = n["Speed"].as<f32>(a.speed);
a.applyRootMotion = n["RootMotion"].as<bool>(
a.applyRootMotion); // 缺键 = 旧行为(保留根位移)
a.fadeDuration =
n["FadeDuration"].as<f32>(a.fadeDuration); // 缺键回退 0.3
a.active = n["Active"].as<bool>(true);
@@ -1030,6 +894,200 @@ namespace Engine::Serialization {
}
}
// 父子重链(场景加载 / Prefab 实例化共用;两遍解析:父可能
// 出现在子之后)。keepWorld=false:local TRS 已从文件就位,
// 只建链标脏。防环由 SetParent 兜底(手编档成环 → 告警 + 挂根)。
// byUuid = 创建期构建的最终 uuid → entity 映射
void RelinkParents(
ECS::Scene& scene, const std::vector<ParsedEntity>& parsed,
const std::unordered_map<u64, entt::entity>& byUuid)
{
for (const ParsedEntity& pe : parsed) {
if (pe.parentUuid == 0) continue;
const auto c = byUuid.find(pe.uuid);
const auto p = byUuid.find(pe.parentUuid);
if (c == byUuid.end() || p == byUuid.end()) {
ENGINE_WARN("SceneSerializer: parent {} missing for "
"entity {}, kept as root",
pe.parentUuid, pe.uuid);
continue;
}
scene.SetParent(c->second, p->second, /*keepWorld=*/false);
}
}
// 子树收集(Prefab 写端):DFS 根 → 后代(父先于子,可读性 +
// 实例化"首实体 = 根"约定)
void CollectSubtree(const ECS::Registry& reg, ECS::Entity root,
std::vector<ECS::Entity>& out)
{
out.push_back(root);
if (const auto* h = reg.try_get<ECS::HierarchyComponent>(root)) {
for (const ECS::Entity c : h->children) {
if (reg.valid(c)) {
CollectSubtree(reg, c, out);
}
}
}
}
} // namespace
// ---- 保存 ----
// Emitter 构建段(文件版与内存版共用):documentName 空则不写
// 文档头 "Scene" 字段(快照无文件名,该字段是注释性的)
static std::string SerializeDoc(const ECS::Scene& scene,
Assets::AssetManager& assets,
const std::string& documentName)
{
// float32 无损往返需 9 位有效数字(std::numeric_limits
// <float>::max_digits10)
YAML::Emitter out;
out.SetFloatPrecision(9);
out << YAML::BeginMap;
if (!documentName.empty()) {
out << YAML::Key << "Scene" << YAML::Value << documentName;
}
out << YAML::Key << "Version" << YAML::Value << kSceneVersion;
const auto& s = scene.GetSettings();
out << YAML::Key << "Settings" << YAML::Value << YAML::BeginMap;
out << YAML::Key << "ClearColor" << YAML::Value;
EmitVec4(out, s.ClearColor);
out << YAML::EndMap;
out << YAML::Key << "Entities" << YAML::Value << YAML::BeginSeq;
const ECS::Registry& reg = scene.GetRegistry();
reg.view<ECS::IdComponent>().each(
[&](entt::entity e, const ECS::IdComponent&) {
EmitEntity(out, assets, reg, e);
});
out << YAML::EndSeq;
out << YAML::EndMap;
if (!out.good()) {
ENGINE_ERROR("SceneSerializer: emit failed: {}", out.GetLastError());
return {};
}
return {out.c_str(), out.size()};
}
bool SceneSerializer::Serialize(const ECS::Scene& scene,
Assets::AssetManager& assets,
const std::string& outPath)
{
try {
const std::string doc = SerializeDoc(
scene, assets,
std::filesystem::path(outPath).stem().string());
if (doc.empty()) {
return false; // emit 失败,日志已在 SerializeDoc 打过
}
std::ofstream file(outPath, std::ios::trunc);
if (!file) {
ENGINE_ERROR("SceneSerializer: cannot open '{}' for write",
outPath);
return false;
}
file << doc;
assets.FlushGUIDRegistry();
return true;
}
catch (const YAML::Exception& e) {
ENGINE_ERROR("SceneSerializer: save failed ({})", e.what());
return false;
}
catch (const std::exception& e) {
ENGINE_ERROR("SceneSerializer: save failed ({})", e.what());
return false;
}
}
std::string SceneSerializer::SerializeToString(
const ECS::Scene& scene,
Assets::AssetManager& assets,
const std::string& documentName)
{
try {
return SerializeDoc(scene, assets, documentName);
}
catch (const YAML::Exception& e) {
ENGINE_ERROR("SceneSerializer: snapshot failed ({})", e.what());
return {};
}
catch (const std::exception& e) {
ENGINE_ERROR("SceneSerializer: snapshot failed ({})", e.what());
return {};
}
}
// ---- 加载 ----
// 解析段(文件版与内存快照共用):root → 原子换血重建。
// sourceName 仅用于日志(文件路径或 "snapshot")
static bool ParseRoot(ECS::Scene& scene,
Assets::AssetManager& assets,
const YAML::Node& root,
const std::string& sourceName)
{
// Version 闸:高版本拒载(结构未知),缺失按 0 宽容旧档
if (const int version = root["Version"].as<int>(0);
version > kSceneVersion) {
ENGINE_ERROR("SceneSerializer: '{}' version {} > supported {}",
sourceName, version, kSceneVersion);
return false;
}
// ---- 旧档 v1 环境语义兼容读(v2 起环境 = 实体组件)----
// 僵尸节点安全:as<T>(fallback) 对缺失键返回 fallback;环境
// 键仅在真实存在时置位(避免新档误触发兼容建实体)
const std::string legacyEnv =
root["Environment"].as<std::string>("");
bool legacyEnvKey = !legacyEnv.empty();
glm::vec4 legacyAmbient{0.07f, 0.07f, 0.09f, 0.0f};
bool legacyAmbientKey = false;
bool legacyDrawSkybox = true;
bool legacySkyboxKey = false;
if (const YAML::Node st = root["Settings"]; st && st.IsMap()) {
auto& s = scene.GetSettings();
s.ClearColor = ReadVec4(st["ClearColor"], s.ClearColor);
if (const YAML::Node amb = st["AmbientColor"]) {
legacyAmbient = ReadVec4(amb, legacyAmbient);
legacyAmbientKey = true;
}
if (const YAML::Node ds = st["DrawSkybox"]) {
legacyDrawSkybox = ds.as<bool>(legacyDrawSkybox);
legacySkyboxKey = true;
}
}
// ---- 阶段 1:解析到中间结构(逐实体容错,不动 registry)----
std::vector<ParsedEntity> parsed;
ParseEntityList(root["Entities"], parsed);
// ---- 阶段 2:原子换血重建 ----
scene.Clear();
std::unordered_set<u64> seenUuids;
std::unordered_map<u64, entt::entity> byUuid; // 第二遍重链用
for (ParsedEntity& pe : parsed) {
auto handle = scene.CreateEntity(pe.name);
// UUID 重复/无效 → 重新生成(Prefab 实例化的 ID 重映射语义)
u64 uuid = pe.uuid;
if (uuid == 0 || !seenUuids.insert(uuid).second) {
uuid = UUID::New().Value();
}
handle.GetComponent<ECS::IdComponent>().uuid = UUID{uuid};
pe.uuid = uuid; // 回写最终 uuid(重生成后父引用仍可命中)
byUuid.emplace(uuid, static_cast<entt::entity>(handle));
ApplyComponents(handle, pe.comps, assets);
}
// ---- 旧档环境兼容迁移:v1 的 Environment/Settings.AmbientColor/
// DrawSkybox → 补建一个 Environment 实体(视觉等价)。仅当旧键
// 真实存在且实体列表没有环境组件时触发(新档缺省键不触发;
@@ -1051,21 +1109,8 @@ namespace Engine::Serialization {
legacyEnv);
}
// ---- 阶段 3:重链父子(两遍解析:父可能出现在子之后)。
// keepWorld=false:local TRS 已从文件就位(相对父语义),
// 只建链标脏。防环由 SetParent 兜底(手编档成环 → 告警 + 挂根)
for (const ParsedEntity& pe : parsed) {
if (pe.parentUuid == 0) continue;
const auto c = byUuid.find(pe.uuid);
const auto p = byUuid.find(pe.parentUuid);
if (c == byUuid.end() || p == byUuid.end()) {
ENGINE_WARN("SceneSerializer: parent {} missing for entity "
"{}, kept as root",
pe.parentUuid, pe.uuid);
continue;
}
scene.SetParent(c->second, p->second, /*keepWorld=*/false);
}
// ---- 阶段 3:重链父子(两遍解析:父可能出现在子之后)----
RelinkParents(scene, parsed, byUuid);
// 加载在 OnImGuiRender 段,错过本帧 UpdateTransforms:
// 显式补算,否则首帧 World = 单位阵
@@ -1123,4 +1168,157 @@ namespace Engine::Serialization {
return ParseRoot(scene, assets, root, "snapshot");
}
// ---- Prefab(实体子树存取;与场景共用实体段读写)----
bool SceneSerializer::SerializePrefab(const ECS::Scene& scene,
Assets::AssetManager& assets,
ECS::Entity root,
const std::string& outPath)
{
const ECS::Registry& reg = scene.GetRegistry();
if (!scene.IsAlive(root)) {
ENGINE_ERROR("SceneSerializer: prefab root invalid, '{}' not "
"written", outPath);
return false;
}
try {
YAML::Emitter out;
out.SetFloatPrecision(9);
out << YAML::BeginMap;
out << YAML::Key << "Prefab" << YAML::Value
<< std::filesystem::path(outPath).stem().string();
out << YAML::Key << "Version" << YAML::Value << kSceneVersion;
out << YAML::Key << "Entities" << YAML::Value << YAML::BeginSeq;
std::vector<ECS::Entity> subtree;
CollectSubtree(reg, root, subtree);
for (const ECS::Entity e : subtree) {
EmitEntity(out, assets, reg, e);
}
out << YAML::EndSeq;
out << YAML::EndMap;
if (!out.good()) {
ENGINE_ERROR("SceneSerializer: prefab emit failed: {}",
out.GetLastError());
return false;
}
std::ofstream file(outPath, std::ios::trunc);
if (!file) {
ENGINE_ERROR("SceneSerializer: cannot open '{}' for write",
outPath);
return false;
}
file.write(out.c_str(), static_cast<std::streamsize>(out.size()));
assets.FlushGUIDRegistry();
return true;
}
catch (const YAML::Exception& e) {
ENGINE_ERROR("SceneSerializer: prefab save failed ({})", e.what());
return false;
}
catch (const std::exception& e) {
ENGINE_ERROR("SceneSerializer: prefab save failed ({})", e.what());
return false;
}
}
ECS::Entity SceneSerializer::InstantiatePrefab(
ECS::Scene& scene, Assets::AssetManager& assets,
const std::string& inPath,
const glm::vec3* overridePos, const glm::quat* overrideRot)
{
YAML::Node root;
try {
root = YAML::LoadFile(inPath);
}
catch (const YAML::Exception& e) {
ENGINE_ERROR("SceneSerializer: cannot parse prefab '{}' ({})",
inPath, e.what());
return entt::null;
}
catch (const std::exception& e) {
ENGINE_ERROR("SceneSerializer: cannot read prefab '{}' ({})",
inPath, e.what());
return entt::null;
}
// 版本闸同场景(实体段同构:高版本拒载,缺失按 0 宽容)
if (const int version = root["Version"].as<int>(0);
version > kSceneVersion) {
ENGINE_ERROR("SceneSerializer: prefab '{}' version {} > "
"supported {}", inPath, version, kSceneVersion);
return entt::null;
}
// ---- 解析到中间结构(逐实体容错,同场景加载)----
std::vector<ParsedEntity> parsed;
ParseEntityList(root["Entities"], parsed);
if (parsed.empty()) {
ENGINE_ERROR("SceneSerializer: prefab '{}' has no entities",
inPath);
return entt::null;
}
// ---- 追加式实例化(不换血)。UUID 全量再生:同一 prefab 多次
// 实例化、与场景既有实体均天然不冲突。old2new 供子树内引用
// (Joint.target)重映射;父引用由 RelinkParents 走 byUuid 消化
std::unordered_map<u64, entt::entity> byUuid;
std::unordered_map<u64, u64> old2new;
ECS::Entity rootEntity = entt::null; // 约定 = 文件首实体
for (ParsedEntity& pe : parsed) {
auto handle = scene.CreateEntity(pe.name);
if (rootEntity == entt::null) {
rootEntity = handle;
}
const u64 uuid = UUID::New().Value();
old2new.emplace(pe.uuid, uuid);
pe.uuid = uuid;
handle.GetComponent<ECS::IdComponent>().uuid = UUID{uuid};
byUuid.emplace(uuid, static_cast<entt::entity>(handle));
ApplyComponents(handle, pe.comps, assets);
}
// 子树内 Joint.target 重映射(旧 → 新);指向子树外的引用保持
// 原值(悬空语义与场景加载一致,PhysicsWorld 惰性反查落空)
auto& reg = scene.GetRegistry();
for (const ParsedEntity& pe : parsed) {
const auto self = byUuid.find(pe.uuid);
if (self == byUuid.end()) {
continue;
}
if (auto* jc = reg.try_get<ECS::JointComponent>(self->second)) {
if (const auto it = old2new.find(jc->target.Value());
it != old2new.end()) {
jc->target = UUID{it->second};
}
}
}
RelinkParents(scene, parsed, byUuid);
// 根位姿覆写(local 语义:实例根挂场景根时即世界位姿)
if (rootEntity != entt::null && (overridePos || overrideRot)) {
auto& t = reg.get<ECS::TransformComponent>(rootEntity);
if (overridePos) {
t.SetPosition(*overridePos);
}
if (overrideRot) {
t.SetRotationQuat(*overrideRot);
}
}
// 实例化常发生在 OnImGuiRender/脚本回调(错过本帧
// UpdateTransforms):显式补算 world;uuid 被 CreateEntity 后
// 覆写,全量重建索引(同场景加载语义)
scene.UpdateTransforms();
scene.RebuildUuidIndex();
ENGINE_INFO("SceneSerializer: prefab '{}' instantiated "
"({} entities)", inPath, parsed.size());
return rootEntity;
}
} // namespace Engine::Serialization