// // Created by atdunbg on 2026/10/3. // // SceneSerializer:场景 ↔ YAML。结构见头文件注释。 // // 写端:手动 Emitter 逐键输出(短集合用 Flow 单行数组/内联 map, // 浮点 9 位精度保证 float32 无损往返)。 // 读端:yaml-cpp 新版对"僵尸节点"(缺失键的返回值)硬化——operator=、 // size()、Type()、链式 operator[] 一律抛 InvalidNode,只有 IsDefined/ // as(fallback)/begin() 安全。因此读侧铁律:每层节点先 // `const Node n = parent["Key"]` 拷贝初始化 + `if (n)` 判空再访问, // 绝不对可能缺失的键做赋值或链式下标。 // // 原子性:Deserialize 先解析到中间结构(逐实体容错),全部成功才 // scene.Clear() 换血重建——解析失败不动现场。 // #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Engine::Serialization { namespace { // ---- 写端辅助:Flow 单行数组(Position: [0, 1.2, -6])---- void EmitVec3(YAML::Emitter& e, const glm::vec3& v) { e << YAML::Flow << YAML::BeginSeq << v.x << v.y << v.z << YAML::EndSeq; } void EmitVec2(YAML::Emitter& e, const glm::vec2& v) { e << YAML::Flow << YAML::BeginSeq << v.x << v.y << YAML::EndSeq; } void EmitVec4(YAML::Emitter& e, const glm::vec4& v) { e << YAML::Flow << YAML::BeginSeq << v.x << v.y << v.z << v.w << YAML::EndSeq; } void EmitQuat(YAML::Emitter& e, const glm::quat& q) { e << YAML::Flow << YAML::BeginSeq << q.w << q.x << q.y << q.z << YAML::EndSeq; } // ---- 读端辅助:缺字段/类型错/僵尸节点 → 默认值 ---- glm::vec3 ReadVec3(const YAML::Node& n, const glm::vec3& def) { if (!n || !n.IsSequence() || n.size() < 3) return def; return {n[0].as(def.x), n[1].as(def.y), n[2].as(def.z)}; } glm::vec2 ReadVec2(const YAML::Node& n, const glm::vec2& def) { if (!n || !n.IsSequence() || n.size() < 2) return def; return {n[0].as(def.x), n[1].as(def.y)}; } // ---- 脚本参数读端:按声明 schema 收值(调用方保证键已知)---- // 数值臂互转(YAML 标量无类型:"3" 作 f32/i32 均自然,i32 声明 // 遇浮点值截断);形不符回声明默认 + 警告(加载期一次,不刷屏) template bool TryAs(const YAML::Node& n, T& out) { try { out = n.as(); return true; } catch (const std::exception&) { return false; // 类型不符 / 僵尸节点硬化异常统一兜住 } } Scripting::ParamValue ReadParamValue( const YAML::Node& n, const Scripting::ParamBuilder::Desc& d) { switch (d.type) { case Scripting::ParamType::F32: { f32 v{}; if (TryAs(n, v)) return {v}; i32 iv{}; if (TryAs(n, iv)) return {static_cast(iv)}; break; } case Scripting::ParamType::I32: { i32 v{}; if (TryAs(n, v)) return {v}; f32 fv{}; if (TryAs(n, fv)) return {static_cast(fv)}; break; } case Scripting::ParamType::Bool: { bool v{}; if (TryAs(n, v)) return {v}; break; } case Scripting::ParamType::String: { if (n.IsScalar()) return {n.Scalar()}; break; } case Scripting::ParamType::Vec2: { if (n && n.IsSequence() && n.size() >= 2) { return {ReadVec2(n, glm::vec2{0.0f})}; } break; } case Scripting::ParamType::Vec3: { if (n && n.IsSequence() && n.size() >= 3) { return {ReadVec3(n, glm::vec3{0.0f})}; } break; } } ENGINE_WARN("Script param '{}' type mismatch, use default", d.name); return d.def; } glm::vec4 ReadVec4(const YAML::Node& n, const glm::vec4& def) { if (!n || !n.IsSequence() || n.size() < 4) return def; return {n[0].as(def.x), n[1].as(def.y), n[2].as(def.z), n[3].as(def.w)}; } // [w,x,y,z] → 归一化 quat;非法/缺失回退单位四元数(防手改档出 NaN) glm::quat ReadQuat(const YAML::Node& n) { if (!n || !n.IsSequence() || n.size() < 4) return {}; const f32 w = n[0].as(1.0f), x = n[1].as(0.0f); const f32 y = n[2].as(0.0f), z = n[3].as(0.0f); const f32 len = std::sqrt(x * x + y * y + z * z + w * w); return len > 1e-8f ? glm::quat(w, x, y, z) / len : glm::quat{}; } // "a,b,c[,d]" → N 个 int(procedural 伪路径参数解析) template bool ParseCsvInts(const std::string& s, int (&out)[N]) { size_t pos = 0; for (size_t i = 0; i < N; ++i) { const size_t next = (i + 1 < N) ? s.find(',', pos) : std::string::npos; if (i + 1 < N && next == std::string::npos) return false; try { out[i] = std::stoi(s.substr(pos, next - pos)); } catch (const std::exception&) { return false; } pos = (next == std::string::npos) ? s.size() : next + 1; } return true; } // procedural:// 伪路径 → 参数反解重建(确定性键必然命中缓存; // 绝不对伪路径调 LoadModel——那会走 assimp 必炸) ModelRef ResolveProcedural(Assets::AssetManager& assets, const std::string& path) { constexpr std::string_view kPlane = "procedural://plane/"; constexpr std::string_view kTPlane = "procedural://transparent-plane/"; constexpr std::string_view kCube = "procedural://cube/"; if (path.rfind(kCube, 0) == 0) { // "{halfSize}/{r},{g},{b}" const std::string rest = path.substr(kCube.size()); const auto slash = rest.find('/'); if (slash == std::string::npos) return {}; int c[3]; if (!ParseCsvInts(rest.substr(slash + 1), c)) return {}; try { return assets.CreateCube( std::stof(rest.substr(0, slash)), static_cast(c[0]), static_cast(c[1]), static_cast(c[2])); } catch (const std::exception&) { return {}; } } if (path.rfind(kTPlane, 0) == 0) { // "{halfSize}/{r},{g},{b},{a}" const std::string rest = path.substr(kTPlane.size()); const auto slash = rest.find('/'); if (slash == std::string::npos) return {}; int c[4]; if (!ParseCsvInts(rest.substr(slash + 1), c)) return {}; try { return assets.CreateTransparentPlane( std::stof(rest.substr(0, slash)), static_cast(c[0]), static_cast(c[1]), static_cast(c[2]), static_cast(c[3])); } catch (const std::exception&) { return {}; } } if (path.rfind(kPlane, 0) == 0) { // "{halfSize}/{r},{g},{b}" const std::string rest = path.substr(kPlane.size()); const auto slash = rest.find('/'); if (slash == std::string::npos) return {}; int c[3]; if (!ParseCsvInts(rest.substr(slash + 1), c)) return {}; try { return assets.CreatePlane( std::stof(rest.substr(0, slash)), static_cast(c[0]), static_cast(c[1]), static_cast(c[2])); } catch (const std::exception&) { return {}; } } return {}; // 未知伪路径 } // 模型引用解析降级链:procedural 反解 → GUID 主键 → Path 兜底。 // 加载成功后 GetOrCreateGUID 回写注册表(注册表丢档自愈) ModelRef ResolveModel(Assets::AssetManager& assets, u64 guid, const std::string& path) { if (ModelRef ref = ResolveProcedural(assets, path); ref.Valid()) { return ref; } std::string resolved; if (guid != 0) resolved = assets.ResolveGUID(guid); if (resolved.empty()) resolved = path; if (resolved.empty()) return {}; ModelRef ref = assets.LoadModel(resolved); if (ref.Valid()) assets.GetOrCreateGUID(resolved); return ref; } // 材质覆盖路径解析:guid 反查优先(资产移动不断引用),path // 兜底;解析成功即预载——"收集期只查缓存"不变量在此保证 //(游戏形态无编辑器,场景读后预载是唯一自动加载点),未加载 // 成功也返回路径(收集期回退内建材质,不丢实体) std::string ResolveMaterialPath(Assets::AssetManager& assets, u64 guid, const std::string& path) { std::string resolved; if (guid != 0) resolved = assets.ResolveGUID(guid); if (resolved.empty()) resolved = path; if (resolved.empty()) return {}; if (!assets.IsMaterialLoaded(resolved)) { assets.LoadMaterialAsset(resolved); } assets.GetOrCreateGUID(resolved); return resolved; } // ---- 中间结构:解析全部成功才碰 registry ---- // comps 持 Components 节点(仅在键存在且为 map 时赋值—— // yaml-cpp 新版对僵尸节点做 operator= 会抛异常) struct ParsedEntity { u64 uuid = 0; std::string name = "entity"; u64 parentUuid = 0; // 0 = 根实体(层级键缺省) YAML::Node comps; // 缺省 = Null(无组件) }; // 实体列表解析(场景加载 / Prefab 实例化共用):逐实体容错, // 单实体损坏跳过继续,不动 registry void ParseEntityList(const YAML::Node& entities, std::vector& out) { if (!entities || !entities.IsSequence()) { return; } out.reserve(entities.size()); for (const auto& e : entities) { try { ParsedEntity pe; pe.uuid = e["Entity"].as(0); pe.name = e["Name"].as(pe.name); pe.parentUuid = e["Parent"].as(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) { const auto& id = reg.get(e); out << YAML::BeginMap; out << YAML::Key << "Entity" << YAML::Value << id.uuid.Value(); if (const auto* name = reg.try_get(e)) { out << YAML::Key << "Name" << YAML::Value << name->name; } // 层级(UE Actor attach):父存 UUID(跨存档稳定); // 悬空父(不该发生)按根实体写 if (const auto* h = reg.try_get(e); h && h->parent != entt::null && reg.valid(h->parent)) { out << YAML::Key << "Parent" << YAML::Value << reg.get(h->parent).uuid.Value(); } out << YAML::Key << "Components" << YAML::Value << YAML::BeginMap; if (const auto* t = reg.try_get(e)) { out << YAML::Key << "Transform" << YAML::Value << YAML::BeginMap; out << YAML::Key << "Position" << YAML::Value; EmitVec3(out, t->GetPosition()); out << YAML::Key << "Rotation" << YAML::Value; EmitQuat(out, t->GetRotationQuat()); // (w,x,y,z) 无损 out << YAML::Key << "Scale" << YAML::Value; EmitVec3(out, t->GetScale()); out << YAML::EndMap; } if (const auto* mr = reg.try_get(e)) { // ModelRef → {Guid, Path}:Path 反查为空 = 无效引用,跳过 const std::string& path = assets.Models().Path(mr->model); if (!path.empty()) { out << YAML::Key << "MeshRenderer" << YAML::Value << 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; } out << YAML::Key << "Path" << YAML::Value << path; out << YAML::EndMap; // Model(flow) out << YAML::Key << "Active" << YAML::Value << mr->active; // 材质覆盖表(UE Material Slot):seq of {Slot,Guid,Path} if (!mr->materialOverrides.empty()) { out << YAML::Key << "MaterialOverrides" << YAML::Value << YAML::BeginSeq; for (const auto& [slot, mpath] : mr->materialOverrides) { out << YAML::BeginMap; out << YAML::Key << "Slot" << YAML::Value << slot; if (const u64 guid = assets.GetOrCreateGUID(mpath); guid != 0) { out << YAML::Key << "Guid" << YAML::Value << guid; } out << YAML::Key << "Path" << YAML::Value << mpath; out << YAML::EndMap; } out << YAML::EndSeq; } out << YAML::EndMap; // MeshRenderer } else { ENGINE_WARN("SceneSerializer: entity has invalid model " "ref, MeshRenderer skipped"); } } if (const auto* a = reg.try_get(e)) { 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; // Controller 模式(Unity Animator 语义):mode/路径/参数 // 实例值落盘;状态机播放头瞬态不落盘(重启从 entry 开始) out << YAML::Key << "Mode" << YAML::Value << static_cast(a->mode); out << YAML::Key << "ControllerPath" << YAML::Value << a->controllerPath; if (!a->floatParams.empty()) { out << YAML::Key << "FloatParams" << YAML::Value << YAML::BeginMap; for (const auto& [k, v] : a->floatParams) { out << YAML::Key << k << YAML::Value << v; } out << YAML::EndMap; } if (!a->boolParams.empty()) { out << YAML::Key << "BoolParams" << YAML::Value << YAML::BeginMap; for (const auto& [k, v] : a->boolParams) { out << YAML::Key << k << YAML::Value << v; } out << YAML::EndMap; } // 外部动画源(编辑器 attach 列表):clip 本体运行时并入 // Model 资产不落盘,加载端按此列表幂等重放。 // block 风格:路径长且条目多,flow 单行不可读 out << YAML::Key << "Attached" << YAML::Value << YAML::BeginSeq; for (const auto& p : a->attachedAnimPaths) { out << p; } out << YAML::EndSeq; out << YAML::EndMap; } if (const auto* c = reg.try_get(e)) { out << YAML::Key << "Attached" << YAML::Value << YAML::BeginMap; out << YAML::Key << "Primary" << YAML::Value << c->primary; out << YAML::Key << "Active" << YAML::Value << c->active; // 投影参数(Camera 成员):老场景缺键时读侧回退默认值 out << YAML::Key << "ProjectionMode" << YAML::Value << static_cast(c->camera.GetProjectionMode()); out << YAML::Key << "FovDeg" << YAML::Value << c->camera.GetFovDeg(); out << YAML::Key << "NearZ" << YAML::Value << c->camera.GetNearZ(); out << YAML::Key << "FarZ" << YAML::Value << c->camera.GetFarZ(); out << YAML::Key << "OrthoSize" << YAML::Value << c->camera.GetOrthoSize(); out << YAML::EndMap; } if (const auto* l = reg.try_get(e)) { out << YAML::Key << "Light" << YAML::Value << YAML::BeginMap; out << YAML::Key << "Type" << YAML::Value << static_cast(l->type); out << YAML::Key << "Direction" << YAML::Value; EmitVec3(out, l->direction); out << YAML::Key << "Color" << YAML::Value; EmitVec4(out, l->color); out << YAML::Key << "Radius" << YAML::Value << l->radius; out << YAML::Key << "CastShadow" << YAML::Value << l->castShadow; out << YAML::Key << "Active" << YAML::Value << l->active; out << YAML::EndMap; } if (const auto* sb = reg.try_get(e)) { out << YAML::Key << "Skybox" << YAML::Value << YAML::BeginMap; out << YAML::Key << "Active" << YAML::Value << sb->active; out << YAML::Key << "EnvMap" << YAML::Value << sb->envPath; out << YAML::EndMap; } if (const auto* sl = reg.try_get(e)) { out << YAML::Key << "SkyLight" << YAML::Value << YAML::BeginMap; out << YAML::Key << "Ambient" << YAML::Value; EmitVec4(out, sl->ambientColor); out << YAML::Key << "Active" << YAML::Value << sl->active; out << YAML::EndMap; } if (const auto* rb = reg.try_get(e)) { out << YAML::Key << "RigidBody" << YAML::Value << YAML::BeginMap; out << YAML::Key << "Type" << YAML::Value << static_cast(rb->type); out << YAML::Key << "Collider" << YAML::Value << static_cast(rb->collider); out << YAML::Key << "BoxHalfExtents" << YAML::Value; EmitVec3(out, rb->boxHalfExtents); out << YAML::Key << "SphereRadius" << YAML::Value << rb->sphereRadius; out << YAML::Key << "CapsuleRadius" << YAML::Value << rb->capsuleRadius; out << YAML::Key << "CapsuleHalfHeight" << YAML::Value << rb->capsuleHalfHeight; out << YAML::Key << "CylinderRadius" << YAML::Value << rb->cylinderRadius; out << YAML::Key << "CylinderHalfHeight" << YAML::Value << rb->cylinderHalfHeight; if (!rb->compound.empty()) { out << YAML::Key << "Compound" << YAML::Value << YAML::BeginSeq; for (const auto& c : rb->compound) { out << YAML::BeginMap; out << YAML::Key << "Shape" << YAML::Value << static_cast(c.shape); out << YAML::Key << "BoxHalfExtents" << YAML::Value; EmitVec3(out, c.boxHalfExtents); out << YAML::Key << "Radius" << YAML::Value << c.radius; out << YAML::Key << "CapsuleHalfHeight" << YAML::Value << c.capsuleHalfHeight; out << YAML::Key << "Position" << YAML::Value; EmitVec3(out, c.position); out << YAML::Key << "Rotation" << YAML::Value; EmitQuat(out, c.rotation); // (w,x,y,z) out << YAML::EndMap; } out << YAML::EndSeq; } out << YAML::Key << "Mass" << YAML::Value << rb->mass; out << YAML::Key << "GravityScale" << YAML::Value << rb->gravityScale; out << YAML::Key << "Friction" << YAML::Value << rb->friction; out << YAML::Key << "Restitution" << YAML::Value << rb->restitution; out << YAML::Key << "IsTrigger" << YAML::Value << rb->isTrigger; // bodyId/bodyValid/velocity 由 PhysicsWorld 对账重建,不存 out << YAML::Key << "Active" << YAML::Value << rb->active; out << YAML::EndMap; } if (const auto* jc = reg.try_get(e)) { out << YAML::Key << "Joint" << YAML::Value << YAML::BeginMap; out << YAML::Key << "Type" << YAML::Value << static_cast(jc->type); // 实体引用存 UUID(跨存档稳定;target=0 未设置) out << YAML::Key << "Target" << YAML::Value << jc->target.Value(); out << YAML::Key << "AnchorA" << YAML::Value; EmitVec3(out, jc->anchorA); out << YAML::Key << "AnchorB" << YAML::Value; EmitVec3(out, jc->anchorB); out << YAML::Key << "HingeAxis" << YAML::Value; EmitVec3(out, jc->hingeAxis); out << YAML::Key << "EnableLimit" << YAML::Value << jc->enableLimit; out << YAML::Key << "LimitMin" << YAML::Value << jc->limitMin; out << YAML::Key << "LimitMax" << YAML::Value << jc->limitMax; out << YAML::Key << "Active" << YAML::Value << jc->active; out << YAML::EndMap; } // 角色只存参数(速度/着地等状态不序列化) if (const auto* cc = reg.try_get(e)) { out << YAML::Key << "CharacterController" << YAML::Value << YAML::BeginMap; out << YAML::Key << "Radius" << YAML::Value << cc->radius; out << YAML::Key << "CapsuleHalfHeight" << YAML::Value << cc->capsuleHalfHeight; out << YAML::Key << "Mass" << YAML::Value << cc->mass; out << YAML::Key << "MaxSlopeAngle" << YAML::Value << cc->maxSlopeAngle; out << YAML::Key << "StepHeight" << YAML::Value << cc->stepHeight; out << YAML::Key << "GravityScale" << YAML::Value << cc->gravityScale; out << YAML::Key << "Active" << YAML::Value << cc->active; out << YAML::EndMap; } // 脚本:多脚本列表(每项 = 名字 + 独立启停位);未注册名字 // 加载后运行时警告跳过 if (const auto* sc = reg.try_get(e)) { out << YAML::Key << "Script" << YAML::Value << YAML::BeginMap; 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; if (!item.params.empty()) { out << YAML::Key << "Params" << YAML::Value << YAML::BeginMap; for (const auto& p : item.params) { switch (p.value.Type()) { case Engine::Scripting::ParamType::F32: out << YAML::Key << p.name << YAML::Value << std::get(p.value.v); break; case Engine::Scripting::ParamType::I32: out << YAML::Key << p.name << YAML::Value << std::get(p.value.v); break; case Engine::Scripting::ParamType::Bool: out << YAML::Key << p.name << YAML::Value << std::get(p.value.v); break; case Engine::Scripting::ParamType::String: out << YAML::Key << p.name << YAML::Value << std::get(p.value.v); break; case Engine::Scripting::ParamType::Vec2: out << YAML::Key << p.name << YAML::Value; EmitVec2(out, std::get(p.value.v)); break; case Engine::Scripting::ParamType::Vec3: out << YAML::Key << p.name << YAML::Value; EmitVec3(out, std::get(p.value.v)); break; } } out << YAML::EndMap; } out << YAML::EndMap; } out << YAML::EndSeq; out << YAML::EndMap; } out << YAML::EndMap; // Components out << YAML::EndMap; // Entity } // 组件应用(场景加载 / 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(); t.SetPosition(ReadVec3(n["Position"], t.GetPosition())); t.SetRotationQuat(ReadQuat(n["Rotation"])); t.SetScale(ReadVec3(n["Scale"], t.GetScale())); } if (const YAML::Node n = comps["MeshRenderer"]) { if (const YAML::Node m = n["Model"]) { const u64 guid = m["Guid"].as(0); const auto path = m["Path"].as(""); if (ModelRef ref = ResolveModel(assets, guid, path); ref.Valid()) { handle.AddComponent( ECS::MeshRendererComponent{ref}); auto& mrComp = handle.GetComponent(); mrComp.active = n["Active"].as(true); // 缺省 true 向前兼容 // 材质覆盖表:逐项解析(guid 反查 → path 兜底) // 并预载;加载失败保留路径(收集期回退内建材质) if (const YAML::Node ovs = n["MaterialOverrides"]) { for (const auto& ov : ovs) { const u32 slot = ov["Slot"].as( 0xFFFFFFFFu); const auto opath = ResolveMaterialPath( assets, ov["Guid"].as(0), ov["Path"].as("")); if (slot == 0xFFFFFFFFu || opath.empty()) { continue; } mrComp.materialOverrides[slot] = opath; } } } else { ENGINE_WARN("SceneSerializer: model unresolved " "(guid={}, path='{}'), MeshRenderer " "skipped", guid, path); } } } if (const YAML::Node n = comps["Animation"]) { auto& a = handle.AddComponent(); a.clipIndex = n["ClipIndex"].as(a.clipIndex); a.playing = n["Playing"].as(a.playing); a.loop = n["Loop"].as(a.loop); a.speed = n["Speed"].as(a.speed); a.applyRootMotion = n["RootMotion"].as( a.applyRootMotion); // 缺键 = 旧行为(保留根位移) a.fadeDuration = n["FadeDuration"].as(a.fadeDuration); // 缺键回退 0.3 a.active = n["Active"].as(true); // Controller 模式缺键回退 Direct(老场景零迁移); // mode 越界值也视为 Direct if (const int m = n["Mode"].as(0); m == 1) { a.mode = ECS::AnimationComponent::AnimMode::Controller; } a.controllerPath = n["ControllerPath"].as(""); // Controller 预载(收集期只查缓存的不变量在此保证; // 解析失败保留路径,System Find 落空 → rest 兜底) if (!a.controllerPath.empty() && !assets.IsControllerLoaded(a.controllerPath)) { assets.LoadController(a.controllerPath); } if (const YAML::Node fp = n["FloatParams"]) { for (const auto& it : fp) { a.floatParams[it.first.as("")] = it.second.as(0.0f); } } if (const YAML::Node bp = n["BoolParams"]) { for (const auto& it : bp) { a.boolParams[it.first.as("")] = it.second.as(false); } } if (const YAML::Node att = n["Attached"]) { for (const auto& it : att) { a.attachedAnimPaths.push_back( it.as("")); } } } if (const YAML::Node n = comps["Camera"]) { auto& c = handle.AddComponent(); c.primary = n["Primary"].as(false); c.active = n["Active"].as(true); // 投影参数缺省回退默认值(Version 向后兼容:老场景无新键 // 可载)。ProjectionMode 越界值视为透视 if (const int pm = n["ProjectionMode"].as(0); pm == 1) { c.camera.SetProjectionMode( Engine::Camera::ProjectionMode::Orthographic); } c.camera.SetFovDeg(n["FovDeg"].as(c.camera.GetFovDeg())); c.camera.SetClip(n["NearZ"].as(c.camera.GetNearZ()), n["FarZ"].as(c.camera.GetFarZ())); c.camera.SetOrthoSize( n["OrthoSize"].as(c.camera.GetOrthoSize())); } if (const YAML::Node n = comps["Light"]) { auto& l = handle.AddComponent(); if (const int type = n["Type"].as(0); type >= 0 && type <= 1) { l.type = static_cast(type); } l.direction = ReadVec3(n["Direction"], l.direction); l.color = ReadVec4(n["Color"], l.color); l.radius = n["Radius"].as(l.radius); l.castShadow = n["CastShadow"].as(l.castShadow); l.active = n["Active"].as(true); } if (const YAML::Node n = comps["RigidBody"]) { auto& rb = handle.AddComponent(); if (const int type = n["Type"].as(0); type >= 0 && type <= 2) { rb.type = static_cast(type); } if (const int col = n["Collider"].as(0); col >= 0 && col <= 5) { rb.collider = static_cast( col); } rb.boxHalfExtents = ReadVec3(n["BoxHalfExtents"], rb.boxHalfExtents); rb.sphereRadius = n["SphereRadius"].as(rb.sphereRadius); rb.capsuleRadius = n["CapsuleRadius"].as(rb.capsuleRadius); rb.capsuleHalfHeight = n["CapsuleHalfHeight"].as(rb.capsuleHalfHeight); rb.cylinderRadius = n["CylinderRadius"].as(rb.cylinderRadius); rb.cylinderHalfHeight = n["CylinderHalfHeight"].as(rb.cylinderHalfHeight); // 僵尸节点铁律:拷贝初始化 + 判空再遍历(缺 Compound 键安全) if (const YAML::Node cs = n["Compound"]) { for (const auto& item : cs) { ECS::RigidBodyComponent::CompoundChild c; if (const int sh = item["Shape"].as(0); sh >= 0 && sh <= 2) { c.shape = static_cast< ECS::RigidBodyComponent::CompoundChild::Shape>( sh); } c.boxHalfExtents = ReadVec3(item["BoxHalfExtents"], c.boxHalfExtents); c.radius = item["Radius"].as(c.radius); c.capsuleHalfHeight = item["CapsuleHalfHeight"].as( c.capsuleHalfHeight); c.position = ReadVec3(item["Position"], c.position); c.rotation = ReadQuat(item["Rotation"]); rb.compound.push_back(std::move(c)); } } rb.mass = n["Mass"].as(rb.mass); rb.gravityScale = n["GravityScale"].as(rb.gravityScale); rb.friction = n["Friction"].as(rb.friction); rb.restitution = n["Restitution"].as(rb.restitution); // 新键 IsTrigger;旧场景 IsSensor 兼容读(缺键 as 返回 fallback 安全) rb.isTrigger = n["IsTrigger"].as( n["IsSensor"].as(rb.isTrigger)); rb.active = n["Active"].as(true); // bodyId/bodyValid/velocity 保持默认(对账重建) } // Joint:target 只存 UUID,加载后由 PhysicsWorld 惰性反查 //(两遍解析不需要——UUID 再生/加载顺序均由逐帧解析消化) if (const YAML::Node n = comps["Joint"]) { auto& jc = handle.AddComponent(); if (const int type = n["Type"].as(0); type >= 0 && type <= 2) { jc.type = static_cast(type); } jc.target = UUID{n["Target"].as(0)}; jc.anchorA = ReadVec3(n["AnchorA"], jc.anchorA); jc.anchorB = ReadVec3(n["AnchorB"], jc.anchorB); jc.hingeAxis = ReadVec3(n["HingeAxis"], jc.hingeAxis); jc.enableLimit = n["EnableLimit"].as(jc.enableLimit); jc.limitMin = n["LimitMin"].as(jc.limitMin); jc.limitMax = n["LimitMax"].as(jc.limitMax); jc.active = n["Active"].as(true); } if (const YAML::Node n = comps["CharacterController"]) { auto& cc = handle.AddComponent(); cc.radius = n["Radius"].as(cc.radius); cc.capsuleHalfHeight = n["CapsuleHalfHeight"].as(cc.capsuleHalfHeight); cc.mass = n["Mass"].as(cc.mass); cc.maxSlopeAngle = n["MaxSlopeAngle"].as(cc.maxSlopeAngle); cc.stepHeight = n["StepHeight"].as(cc.stepHeight); cc.gravityScale = n["GravityScale"].as(cc.gravityScale); cc.active = n["Active"].as(true); // 状态(速度/着地)由 PhysicsWorld 对账重建,不存 } // 脚本:优先读多脚本 Items;无 Items 回退旧单脚本键(向前兼容) if (const YAML::Node n = comps["Script"]) { auto& sc = handle.AddComponent(); sc.active = n["Active"].as(true); if (const YAML::Node items = n["Items"]) { for (const auto& it : items) { ECS::ScriptComponent::Item item; item.name = it["Name"].as(""); item.active = it["Active"].as(true); // 参数:按脚本 schema 对齐收值——未知键忽略(前向 // 兼容),缺失键不存(运行时用声明默认) const YAML::Node ps = it["Params"]; if (ps) { const auto& schema = Scripting::ScriptRegistry::Schema(item.name); for (const auto& kv : ps) { const std::string pname = kv.first.as(""); const auto dit = std::find_if( schema.begin(), schema.end(), [&](const Scripting::ParamBuilder::Desc& d) { return d.name == pname; }); if (dit == schema.end()) { continue; } item.params.push_back( {pname, ReadParamValue(kv.second, *dit)}); } } // 去重兜底:同名项只收第一份(UI 已杜绝重复, // 手编场景仍可能写出重复) const bool dup = std::any_of( sc.items.begin(), sc.items.end(), [&](const ECS::ScriptComponent::Item& e) { return e.name == item.name; }); if (!dup) { sc.items.push_back(std::move(item)); } } } else { // 旧格式:Script: {ScriptName: ..., Active: ...} ECS::ScriptComponent::Item item; item.name = n["ScriptName"].as(""); if (!item.name.empty()) { sc.items.push_back(std::move(item)); } } } if (const YAML::Node n = comps["Skybox"]) { auto& sb = handle.AddComponent(); sb.active = n["Active"].as(true); sb.envPath = n["EnvMap"].as(""); } if (const YAML::Node n = comps["SkyLight"]) { auto& sl = handle.AddComponent(); sl.ambientColor = ReadVec4(n["Ambient"], sl.ambientColor); sl.active = n["Active"].as(true); } } // 父子重链(场景加载 / Prefab 实例化共用;两遍解析:父可能 // 出现在子之后)。keepWorld=false:local TRS 已从文件就位, // 只建链标脏。防环由 SetParent 兜底(手编档成环 → 告警 + 挂根)。 // byUuid = 创建期构建的最终 uuid → entity 映射 void RelinkParents( ECS::Scene& scene, const std::vector& parsed, const std::unordered_map& 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& out) { out.push_back(root); if (const auto* h = reg.try_get(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 // ::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().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(0); version > kSceneVersion) { ENGINE_ERROR("SceneSerializer: '{}' version {} > supported {}", sourceName, version, kSceneVersion); return false; } // ---- 旧档 v1 环境语义兼容读(v2 起环境 = 实体组件)---- // 僵尸节点安全:as(fallback) 对缺失键返回 fallback;环境 // 键仅在真实存在时置位(避免新档误触发兼容建实体) const std::string legacyEnv = root["Environment"].as(""); 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(legacyDrawSkybox); legacySkyboxKey = true; } } // ---- 阶段 1:解析到中间结构(逐实体容错,不动 registry)---- std::vector parsed; ParseEntityList(root["Entities"], parsed); // ---- 阶段 2:原子换血重建 ---- scene.Clear(); std::unordered_set seenUuids; std::unordered_map 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().uuid = UUID{uuid}; pe.uuid = uuid; // 回写最终 uuid(重生成后父引用仍可命中) byUuid.emplace(uuid, static_cast(handle)); ApplyComponents(handle, pe.comps, assets); } // ---- 旧档环境兼容迁移:v1 的 Environment/Settings.AmbientColor/ // DrawSkybox → 补建一个 Environment 实体(视觉等价)。仅当旧键 // 真实存在且实体列表没有环境组件时触发(新档缺省键不触发; // 快照恢复同理——快照里组件已实体化,不走这里) const bool legacyEnvPresent = legacyEnvKey || legacyAmbientKey || legacySkyboxKey; const bool hasEnvComponents = scene.GetRegistry().view().size() > 0 || scene.GetRegistry().view().size() > 0; if (legacyEnvPresent && !hasEnvComponents) { auto env = scene.CreateEntity("Environment"); auto& sb = env.AddComponent(); sb.envPath = legacyEnv; sb.active = legacyDrawSkybox; auto& sl = env.AddComponent(); sl.ambientColor = legacyAmbient; ENGINE_INFO("SceneSerializer: legacy environment migrated to " "'Environment' entity (env='{}')", legacyEnv); } // ---- 阶段 3:重链父子(两遍解析:父可能出现在子之后)---- RelinkParents(scene, parsed, byUuid); // 加载在 OnImGuiRender 段,错过本帧 UpdateTransforms: // 显式补算,否则首帧 World = 单位阵 scene.UpdateTransforms(); // 序列化加载绕过了 CreateEntity 的 UUID 增量维护(565 行直接覆盖 // IdComponent.uuid),全量重建一次索引,否则 FindEntityByUUID 会全失效 scene.RebuildUuidIndex(); ENGINE_INFO("SceneSerializer: loaded {} entities from '{}'", parsed.size(), sourceName); return true; } bool SceneSerializer::Deserialize(ECS::Scene& scene, Assets::AssetManager& assets, const std::string& inPath) { YAML::Node root; try { root = YAML::LoadFile(inPath); } catch (const YAML::Exception& e) { ENGINE_ERROR("SceneSerializer: cannot parse '{}' ({})", inPath, e.what()); return false; } catch (const std::exception& e) { ENGINE_ERROR("SceneSerializer: cannot read '{}' ({})", inPath, e.what()); return false; } return ParseRoot(scene, assets, root, inPath); } bool SceneSerializer::DeserializeFromString( ECS::Scene& scene, Assets::AssetManager& assets, const std::string& yamlText) { YAML::Node root; try { root = YAML::Load(yamlText); } catch (const YAML::Exception& e) { ENGINE_ERROR("SceneSerializer: snapshot parse failed ({})", e.what()); return false; } catch (const std::exception& e) { ENGINE_ERROR("SceneSerializer: snapshot parse failed ({})", e.what()); return false; } 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 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(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(0); version > kSceneVersion) { ENGINE_ERROR("SceneSerializer: prefab '{}' version {} > " "supported {}", inPath, version, kSceneVersion); return entt::null; } // ---- 解析到中间结构(逐实体容错,同场景加载)---- std::vector 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 byUuid; std::unordered_map 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().uuid = UUID{uuid}; byUuid.emplace(uuid, static_cast(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(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(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