DeclareParams/ParamBuilder 声明字段,运行时反射读写; Inspector 按类型生成控件,SceneSerializer 直通存取; 动画系统接入参数化裁剪;新增 OrbitMove 示例脚本
1127 lines
52 KiB
C++
1127 lines
52 KiB
C++
//
|
||
// Created by atdunbg on 2026/10/3.
|
||
//
|
||
// SceneSerializer:场景 ↔ YAML。结构见头文件注释。
|
||
//
|
||
// 写端:手动 Emitter 逐键输出(短集合用 Flow 单行数组/内联 map,
|
||
// 浮点 9 位精度保证 float32 无损往返)。
|
||
// 读端:yaml-cpp 新版对"僵尸节点"(缺失键的返回值)硬化——operator=、
|
||
// size()、Type()、链式 operator[] 一律抛 InvalidNode,只有 IsDefined/
|
||
// as<T>(fallback)/begin() 安全。因此读侧铁律:每层节点先
|
||
// `const Node n = parent["Key"]` 拷贝初始化 + `if (n)` 判空再访问,
|
||
// 绝不对可能缺失的键做赋值或链式下标。
|
||
//
|
||
// 原子性:Deserialize 先解析到中间结构(逐实体容错),全部成功才
|
||
// scene.Clear() 换血重建——解析失败不动现场。
|
||
//
|
||
|
||
#include <Engine/Serialization/SceneSerializer.h>
|
||
#include <Engine/ECS/Scene.h>
|
||
#include <Engine/ECS/EntityHandle.h>
|
||
#include <Engine/ECS/Components.h>
|
||
#include <Engine/Assets/AssetManager.h>
|
||
#include <Engine/Core/Log.h>
|
||
#include <Engine/Core/UUID.h>
|
||
#include <Engine/Scripting/Script.h>
|
||
|
||
#include <yaml-cpp/yaml.h>
|
||
|
||
#include <glm/glm.hpp>
|
||
#include <glm/gtc/quaternion.hpp>
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <filesystem>
|
||
#include <fstream>
|
||
#include <string>
|
||
#include <string_view>
|
||
#include <unordered_map>
|
||
#include <unordered_set>
|
||
#include <vector>
|
||
|
||
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<f32>(def.x), n[1].as<f32>(def.y),
|
||
n[2].as<f32>(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<f32>(def.x), n[1].as<f32>(def.y)};
|
||
}
|
||
|
||
// ---- 脚本参数读端:按声明 schema 收值(调用方保证键已知)----
|
||
// 数值臂互转(YAML 标量无类型:"3" 作 f32/i32 均自然,i32 声明
|
||
// 遇浮点值截断);形不符回声明默认 + 警告(加载期一次,不刷屏)
|
||
template<typename T>
|
||
bool TryAs(const YAML::Node& n, T& out)
|
||
{
|
||
try {
|
||
out = n.as<T>();
|
||
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<f32>(n, v)) return {v};
|
||
i32 iv{};
|
||
if (TryAs<i32>(n, iv)) return {static_cast<f32>(iv)};
|
||
break;
|
||
}
|
||
case Scripting::ParamType::I32: {
|
||
i32 v{};
|
||
if (TryAs<i32>(n, v)) return {v};
|
||
f32 fv{};
|
||
if (TryAs<f32>(n, fv)) return {static_cast<i32>(fv)};
|
||
break;
|
||
}
|
||
case Scripting::ParamType::Bool: {
|
||
bool v{};
|
||
if (TryAs<bool>(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<f32>(def.x), n[1].as<f32>(def.y),
|
||
n[2].as<f32>(def.z), n[3].as<f32>(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<f32>(1.0f), x = n[1].as<f32>(0.0f);
|
||
const f32 y = n[2].as<f32>(0.0f), z = n[3].as<f32>(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 <size_t N>
|
||
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/";
|
||
|
||
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<u8>(c[0]), static_cast<u8>(c[1]),
|
||
static_cast<u8>(c[2]), static_cast<u8>(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<u8>(c[0]), static_cast<u8>(c[1]),
|
||
static_cast<u8>(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(无组件)
|
||
};
|
||
|
||
// 写端:单实体 → Emitter(调用方已进入 Entities 序列)
|
||
void EmitEntity(YAML::Emitter& out, Assets::AssetManager& assets,
|
||
const ECS::Registry& reg, entt::entity e)
|
||
{
|
||
const auto& id = reg.get<ECS::IdComponent>(e);
|
||
|
||
out << YAML::BeginMap;
|
||
out << YAML::Key << "Entity" << YAML::Value << id.uuid.Value();
|
||
if (const auto* name = reg.try_get<ECS::NameComponent>(e)) {
|
||
out << YAML::Key << "Name" << YAML::Value << name->name;
|
||
}
|
||
// 层级(UE Actor attach):父存 UUID(跨存档稳定);
|
||
// 悬空父(不该发生)按根实体写
|
||
if (const auto* h = reg.try_get<ECS::HierarchyComponent>(e);
|
||
h && h->parent != entt::null && reg.valid(h->parent)) {
|
||
out << YAML::Key << "Parent" << YAML::Value
|
||
<< reg.get<ECS::IdComponent>(h->parent).uuid.Value();
|
||
}
|
||
|
||
out << YAML::Key << "Components" << YAML::Value << YAML::BeginMap;
|
||
|
||
if (const auto* t = reg.try_get<ECS::TransformComponent>(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<ECS::MeshRendererComponent>(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::Flow
|
||
<< 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<ECS::AnimationComponent>(e)) {
|
||
out << YAML::Key << "Animation" << YAML::Value << YAML::Flow
|
||
<< YAML::BeginMap;
|
||
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 << "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<int>(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 资产不落盘,加载端按此列表幂等重放
|
||
out << YAML::Key << "Attached" << YAML::Value << YAML::Flow
|
||
<< YAML::BeginSeq;
|
||
for (const auto& p : a->attachedAnimPaths) {
|
||
out << p;
|
||
}
|
||
out << YAML::EndSeq;
|
||
out << YAML::EndMap;
|
||
}
|
||
|
||
if (const auto* c = reg.try_get<ECS::CameraComponent>(e)) {
|
||
out << YAML::Key << "Camera" << YAML::Value << YAML::Flow
|
||
<< 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<int>(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<ECS::LightComponent>(e)) {
|
||
out << YAML::Key << "Light" << YAML::Value << YAML::BeginMap;
|
||
out << YAML::Key << "Type" << YAML::Value
|
||
<< static_cast<int>(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<ECS::SkyboxComponent>(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<ECS::SkyLightComponent>(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<ECS::RigidBodyComponent>(e)) {
|
||
out << YAML::Key << "RigidBody" << YAML::Value
|
||
<< YAML::BeginMap;
|
||
out << YAML::Key << "Type" << YAML::Value
|
||
<< static_cast<int>(rb->type);
|
||
out << YAML::Key << "Collider" << YAML::Value
|
||
<< static_cast<int>(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<int>(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<ECS::JointComponent>(e)) {
|
||
out << YAML::Key << "Joint" << YAML::Value
|
||
<< YAML::BeginMap;
|
||
out << YAML::Key << "Type" << YAML::Value
|
||
<< static_cast<int>(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<ECS::CharacterControllerComponent>(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<ECS::ScriptComponent>(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<f32>(p.value.v);
|
||
break;
|
||
case Engine::Scripting::ParamType::I32:
|
||
out << YAML::Key << p.name << YAML::Value
|
||
<< std::get<i32>(p.value.v);
|
||
break;
|
||
case Engine::Scripting::ParamType::Bool:
|
||
out << YAML::Key << p.name << YAML::Value
|
||
<< std::get<bool>(p.value.v);
|
||
break;
|
||
case Engine::Scripting::ParamType::String:
|
||
out << YAML::Key << p.name << YAML::Value
|
||
<< std::get<std::string>(p.value.v);
|
||
break;
|
||
case Engine::Scripting::ParamType::Vec2:
|
||
out << YAML::Key << p.name << YAML::Value;
|
||
EmitVec2(out, std::get<glm::vec2>(p.value.v));
|
||
break;
|
||
case Engine::Scripting::ParamType::Vec3:
|
||
out << YAML::Key << p.name << YAML::Value;
|
||
EmitVec3(out, std::get<glm::vec3>(p.value.v));
|
||
break;
|
||
}
|
||
}
|
||
out << YAML::EndMap;
|
||
}
|
||
out << YAML::EndMap;
|
||
}
|
||
out << YAML::EndSeq;
|
||
out << YAML::EndMap;
|
||
}
|
||
|
||
out << YAML::EndMap; // Components
|
||
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
|
||
|
||
if (const YAML::Node n = comps["Transform"]) {
|
||
auto& t = handle.GetComponent<ECS::TransformComponent>();
|
||
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<u64>(0);
|
||
const auto path = m["Path"].as<std::string>("");
|
||
if (ModelRef ref = ResolveModel(assets, guid, path);
|
||
ref.Valid()) {
|
||
handle.AddComponent<ECS::MeshRendererComponent>(
|
||
ECS::MeshRendererComponent{ref});
|
||
auto& mrComp =
|
||
handle.GetComponent<ECS::MeshRendererComponent>();
|
||
mrComp.active =
|
||
n["Active"].as<bool>(true); // 缺省 true 向前兼容
|
||
// 材质覆盖表:逐项解析(guid 反查 → path 兜底)
|
||
// 并预载;加载失败保留路径(收集期回退内建材质)
|
||
if (const YAML::Node ovs = n["MaterialOverrides"]) {
|
||
for (const auto& ov : ovs) {
|
||
const u32 slot = ov["Slot"].as<u32>(
|
||
0xFFFFFFFFu);
|
||
const auto opath = ResolveMaterialPath(
|
||
assets, ov["Guid"].as<u64>(0),
|
||
ov["Path"].as<std::string>(""));
|
||
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<ECS::AnimationComponent>();
|
||
a.clipIndex = n["ClipIndex"].as<int>(a.clipIndex);
|
||
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.fadeDuration =
|
||
n["FadeDuration"].as<f32>(a.fadeDuration); // 缺键回退 0.3
|
||
a.active = n["Active"].as<bool>(true);
|
||
// Controller 模式缺键回退 Direct(老场景零迁移);
|
||
// mode 越界值也视为 Direct
|
||
if (const int m = n["Mode"].as<int>(0); m == 1) {
|
||
a.mode = ECS::AnimationComponent::AnimMode::Controller;
|
||
}
|
||
a.controllerPath = n["ControllerPath"].as<std::string>("");
|
||
// 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<std::string>("")] =
|
||
it.second.as<float>(0.0f);
|
||
}
|
||
}
|
||
if (const YAML::Node bp = n["BoolParams"]) {
|
||
for (const auto& it : bp) {
|
||
a.boolParams[it.first.as<std::string>("")] =
|
||
it.second.as<bool>(false);
|
||
}
|
||
}
|
||
if (const YAML::Node att = n["Attached"]) {
|
||
for (const auto& it : att) {
|
||
a.attachedAnimPaths.push_back(
|
||
it.as<std::string>(""));
|
||
}
|
||
}
|
||
}
|
||
|
||
if (const YAML::Node n = comps["Camera"]) {
|
||
auto& c = handle.AddComponent<ECS::CameraComponent>();
|
||
c.primary = n["Primary"].as<bool>(false);
|
||
c.active = n["Active"].as<bool>(true);
|
||
// 投影参数缺省回退默认值(Version 向后兼容:老场景无新键
|
||
// 可载)。ProjectionMode 越界值视为透视
|
||
if (const int pm = n["ProjectionMode"].as<int>(0);
|
||
pm == 1) {
|
||
c.camera.SetProjectionMode(
|
||
Engine::Camera::ProjectionMode::Orthographic);
|
||
}
|
||
c.camera.SetFovDeg(n["FovDeg"].as<f32>(c.camera.GetFovDeg()));
|
||
c.camera.SetClip(n["NearZ"].as<f32>(c.camera.GetNearZ()),
|
||
n["FarZ"].as<f32>(c.camera.GetFarZ()));
|
||
c.camera.SetOrthoSize(
|
||
n["OrthoSize"].as<f32>(c.camera.GetOrthoSize()));
|
||
}
|
||
|
||
if (const YAML::Node n = comps["Light"]) {
|
||
auto& l = handle.AddComponent<ECS::LightComponent>();
|
||
if (const int type = n["Type"].as<int>(0); type >= 0 &&
|
||
type <= 1) {
|
||
l.type = static_cast<ECS::LightComponent::Type>(type);
|
||
}
|
||
l.direction = ReadVec3(n["Direction"], l.direction);
|
||
l.color = ReadVec4(n["Color"], l.color);
|
||
l.radius = n["Radius"].as<f32>(l.radius);
|
||
l.castShadow = n["CastShadow"].as<bool>(l.castShadow);
|
||
l.active = n["Active"].as<bool>(true);
|
||
}
|
||
|
||
if (const YAML::Node n = comps["RigidBody"]) {
|
||
auto& rb = handle.AddComponent<ECS::RigidBodyComponent>();
|
||
if (const int type = n["Type"].as<int>(0);
|
||
type >= 0 && type <= 2) {
|
||
rb.type =
|
||
static_cast<ECS::RigidBodyComponent::BodyType>(type);
|
||
}
|
||
if (const int col = n["Collider"].as<int>(0);
|
||
col >= 0 && col <= 5) {
|
||
rb.collider =
|
||
static_cast<ECS::RigidBodyComponent::ColliderType>(
|
||
col);
|
||
}
|
||
rb.boxHalfExtents = ReadVec3(n["BoxHalfExtents"],
|
||
rb.boxHalfExtents);
|
||
rb.sphereRadius = n["SphereRadius"].as<f32>(rb.sphereRadius);
|
||
rb.capsuleRadius = n["CapsuleRadius"].as<f32>(rb.capsuleRadius);
|
||
rb.capsuleHalfHeight =
|
||
n["CapsuleHalfHeight"].as<f32>(rb.capsuleHalfHeight);
|
||
rb.cylinderRadius =
|
||
n["CylinderRadius"].as<f32>(rb.cylinderRadius);
|
||
rb.cylinderHalfHeight =
|
||
n["CylinderHalfHeight"].as<f32>(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<int>(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<f32>(c.radius);
|
||
c.capsuleHalfHeight =
|
||
item["CapsuleHalfHeight"].as<f32>(
|
||
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<f32>(rb.mass);
|
||
rb.gravityScale = n["GravityScale"].as<f32>(rb.gravityScale);
|
||
rb.friction = n["Friction"].as<f32>(rb.friction);
|
||
rb.restitution = n["Restitution"].as<f32>(rb.restitution);
|
||
// 新键 IsTrigger;旧场景 IsSensor 兼容读(缺键 as 返回 fallback 安全)
|
||
rb.isTrigger = n["IsTrigger"].as<bool>(
|
||
n["IsSensor"].as<bool>(rb.isTrigger));
|
||
rb.active = n["Active"].as<bool>(true);
|
||
// bodyId/bodyValid/velocity 保持默认(对账重建)
|
||
}
|
||
|
||
// Joint:target 只存 UUID,加载后由 PhysicsWorld 惰性反查
|
||
//(两遍解析不需要——UUID 再生/加载顺序均由逐帧解析消化)
|
||
if (const YAML::Node n = comps["Joint"]) {
|
||
auto& jc = handle.AddComponent<ECS::JointComponent>();
|
||
if (const int type = n["Type"].as<int>(0);
|
||
type >= 0 && type <= 2) {
|
||
jc.type =
|
||
static_cast<ECS::JointComponent::Type>(type);
|
||
}
|
||
jc.target = UUID{n["Target"].as<u64>(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<bool>(jc.enableLimit);
|
||
jc.limitMin = n["LimitMin"].as<f32>(jc.limitMin);
|
||
jc.limitMax = n["LimitMax"].as<f32>(jc.limitMax);
|
||
jc.active = n["Active"].as<bool>(true);
|
||
}
|
||
|
||
if (const YAML::Node n = comps["CharacterController"]) {
|
||
auto& cc =
|
||
handle.AddComponent<ECS::CharacterControllerComponent>();
|
||
cc.radius = n["Radius"].as<f32>(cc.radius);
|
||
cc.capsuleHalfHeight =
|
||
n["CapsuleHalfHeight"].as<f32>(cc.capsuleHalfHeight);
|
||
cc.mass = n["Mass"].as<f32>(cc.mass);
|
||
cc.maxSlopeAngle = n["MaxSlopeAngle"].as<f32>(cc.maxSlopeAngle);
|
||
cc.stepHeight = n["StepHeight"].as<f32>(cc.stepHeight);
|
||
cc.gravityScale = n["GravityScale"].as<f32>(cc.gravityScale);
|
||
cc.active = n["Active"].as<bool>(true);
|
||
// 状态(速度/着地)由 PhysicsWorld 对账重建,不存
|
||
}
|
||
|
||
// 脚本:优先读多脚本 Items;无 Items 回退旧单脚本键(向前兼容)
|
||
if (const YAML::Node n = comps["Script"]) {
|
||
auto& sc = handle.AddComponent<ECS::ScriptComponent>();
|
||
sc.active = n["Active"].as<bool>(true);
|
||
if (const YAML::Node items = n["Items"]) {
|
||
for (const auto& it : items) {
|
||
ECS::ScriptComponent::Item item;
|
||
item.name = it["Name"].as<std::string>("");
|
||
item.active = it["Active"].as<bool>(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<std::string>("");
|
||
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<std::string>("");
|
||
if (!item.name.empty()) {
|
||
sc.items.push_back(std::move(item));
|
||
}
|
||
}
|
||
}
|
||
|
||
if (const YAML::Node n = comps["Skybox"]) {
|
||
auto& sb = handle.AddComponent<ECS::SkyboxComponent>();
|
||
sb.active = n["Active"].as<bool>(true);
|
||
sb.envPath = n["EnvMap"].as<std::string>("");
|
||
}
|
||
|
||
if (const YAML::Node n = comps["SkyLight"]) {
|
||
auto& sl = handle.AddComponent<ECS::SkyLightComponent>();
|
||
sl.ambientColor = ReadVec4(n["Ambient"], sl.ambientColor);
|
||
sl.active = n["Active"].as<bool>(true);
|
||
}
|
||
}
|
||
|
||
// ---- 旧档环境兼容迁移:v1 的 Environment/Settings.AmbientColor/
|
||
// DrawSkybox → 补建一个 Environment 实体(视觉等价)。仅当旧键
|
||
// 真实存在且实体列表没有环境组件时触发(新档缺省键不触发;
|
||
// 快照恢复同理——快照里组件已实体化,不走这里)
|
||
const bool legacyEnvPresent =
|
||
legacyEnvKey || legacyAmbientKey || legacySkyboxKey;
|
||
const bool hasEnvComponents =
|
||
scene.GetRegistry().view<ECS::SkyboxComponent>().size() > 0 ||
|
||
scene.GetRegistry().view<ECS::SkyLightComponent>().size() > 0;
|
||
if (legacyEnvPresent && !hasEnvComponents) {
|
||
auto env = scene.CreateEntity("Environment");
|
||
auto& sb = env.AddComponent<ECS::SkyboxComponent>();
|
||
sb.envPath = legacyEnv;
|
||
sb.active = legacyDrawSkybox;
|
||
auto& sl = env.AddComponent<ECS::SkyLightComponent>();
|
||
sl.ambientColor = legacyAmbient;
|
||
ENGINE_INFO("SceneSerializer: legacy environment migrated to "
|
||
"'Environment' entity (env='{}')",
|
||
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);
|
||
}
|
||
|
||
// 加载在 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");
|
||
}
|
||
|
||
} // namespace Engine::Serialization
|