新增 Prefab 系统:实体子树存取 + 编辑器 Save As Prefab/拖入实例化 + 脚本 Instantiate
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user