Editor toolbar gains Play/Stop (Ctrl+P) with UE semantics: - Play: serialize current scene to an in-memory YAML snapshot via new SceneSerializer::SerializeToString, switch display to kDisplayDirect (primary camera entity renders), unfreeze time (timeScale 0 -> 1) - Stop: DeserializeFromString atomically rebuilds the pre-play scene, back to kDisplayViewport (editor camera), refreeze time; selection cleared and gizmo drag reset since all entity handles dangle - SceneSerializer refactored: SerializeDoc/ParseRoot shared by file and memory variants; file Save/Load behavior unchanged - Running state disables gizmo + editor hotkeys in the viewport (input goes to game scripts), draws yellow PLAYING hint and a no-camera warning; File->Save/Load disabled while playing (UE behavior) Physics bodies/constraints/characters and script instances are reconciled automatically by the existing per-frame sync on restore.
792 lines
35 KiB
C++
792 lines
35 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 <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_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 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::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;
|
||
}
|
||
|
||
// ---- 中间结构:解析全部成功才碰 registry ----
|
||
// comps 持 Components 节点(仅在键存在且为 map 时赋值——
|
||
// yaml-cpp 新版对僵尸节点做 operator= 会抛异常)
|
||
|
||
struct ParsedEntity
|
||
{
|
||
u64 uuid = 0;
|
||
std::string name = "entity";
|
||
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;
|
||
}
|
||
|
||
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;
|
||
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 << "Active" << YAML::Value << a->active;
|
||
// 外部动画源(编辑器 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;
|
||
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* 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 << "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;
|
||
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,
|
||
const std::string& environmentPath)
|
||
{
|
||
// 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;
|
||
if (!environmentPath.empty()) {
|
||
out << YAML::Key << "Environment" << YAML::Value
|
||
<< environmentPath;
|
||
}
|
||
|
||
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::Key << "AmbientColor" << YAML::Value;
|
||
EmitVec4(out, s.AmbientColor);
|
||
out << YAML::Key << "DrawSkybox" << YAML::Value << s.DrawSkybox;
|
||
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,
|
||
const std::string& environmentPath)
|
||
{
|
||
try {
|
||
const std::string doc = SerializeDoc(
|
||
scene, assets,
|
||
std::filesystem::path(outPath).stem().string(),
|
||
environmentPath);
|
||
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;
|
||
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,
|
||
const std::string& environmentPath)
|
||
{
|
||
try {
|
||
return SerializeDoc(scene, assets, documentName, environmentPath);
|
||
}
|
||
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,
|
||
std::string& outEnvironmentPath)
|
||
{
|
||
outEnvironmentPath.clear();
|
||
|
||
// Version 闸:高版本拒载(结构未知),缺失按 0 宽容旧档
|
||
if (const int version = root["Version"].as<int>(0);
|
||
version > kSceneVersion) {
|
||
ENGINE_ERROR("SceneSerializer: '{}' version {} > supported {}",
|
||
sourceName, version, kSceneVersion);
|
||
return false;
|
||
}
|
||
|
||
// 僵尸节点安全:as<T>(fallback) 对缺失键返回 fallback
|
||
outEnvironmentPath = root["Environment"].as<std::string>("");
|
||
|
||
if (const YAML::Node st = root["Settings"]; st && st.IsMap()) {
|
||
auto& s = scene.GetSettings();
|
||
s.ClearColor = ReadVec4(st["ClearColor"], s.ClearColor);
|
||
s.AmbientColor = ReadVec4(st["AmbientColor"], s.AmbientColor);
|
||
s.DrawSkybox = st["DrawSkybox"].as<bool>(s.DrawSkybox);
|
||
}
|
||
|
||
// ---- 阶段 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);
|
||
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;
|
||
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};
|
||
|
||
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});
|
||
handle.GetComponent<ECS::MeshRendererComponent>().active =
|
||
n["Active"].as<bool>(true); // 缺省 true 向前兼容
|
||
}
|
||
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.active = n["Active"].as<bool>(true);
|
||
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);
|
||
}
|
||
|
||
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.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);
|
||
// 去重兜底:同名项只收第一份(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));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 加载在 OnImGuiRender 段,错过本帧 UpdateTransforms:
|
||
// 显式补算,否则首帧 World = 单位阵
|
||
scene.UpdateTransforms();
|
||
|
||
ENGINE_INFO("SceneSerializer: loaded {} entities from '{}'",
|
||
parsed.size(), sourceName);
|
||
return true;
|
||
}
|
||
|
||
bool SceneSerializer::Deserialize(ECS::Scene& scene,
|
||
Assets::AssetManager& assets,
|
||
const std::string& inPath,
|
||
std::string& outEnvironmentPath)
|
||
{
|
||
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, outEnvironmentPath);
|
||
}
|
||
|
||
bool SceneSerializer::DeserializeFromString(
|
||
ECS::Scene& scene,
|
||
Assets::AssetManager& assets,
|
||
const std::string& yamlText,
|
||
std::string& outEnvironmentPath)
|
||
{
|
||
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", outEnvironmentPath);
|
||
}
|
||
|
||
} // namespace Engine::Serialization
|