serialization: scene save/load (Unity-style YAML + UE-style GUID registry)

Engine-layer SceneSerializer (yaml-cpp submodule): entities/components
to versioned YAML, atomic clear-and-rebuild on load with per-entity
fault tolerance, missing-field defaults, unknown-component skip and
UUID dedup. Asset references are {Guid, Path} pairs resolved through
the new AssetManager GUID registry (assets/asset_registry.yaml) with
path fallback; procedural:// pseudo-paths rebuild from parameters.
Editor File menu gets Save/Load Scene modals; app re-resolves stale
entity handles via onSceneLoaded callback.
This commit is contained in:
2026-10-03 14:47:02 +08:00
parent ffe33e9967
commit 007d175314
14 changed files with 867 additions and 1 deletions
@@ -0,0 +1,467 @@
//
// Created by atdunbg on 2026/10/3.
//
// SceneSerializer:场景 ↔ YAML。结构见头文件注释与计划 v3。
//
// 原子性: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 <cmath>
#include <filesystem>
#include <fstream>
#include <string>
#include <string_view>
#include <unordered_set>
#include <vector>
namespace Engine::Serialization {
namespace {
// ---- 写出辅助:glm → 数组 ----
std::vector<f32> Vec3Arr(const glm::vec3& v) { return {v.x, v.y, v.z}; }
std::vector<f32> Vec4Arr(const glm::vec4& v) { return {v.x, v.y, v.z, v.w}; }
std::vector<f32> QuatArr(const glm::quat& q) { return {q.w, q.x, q.y, q.z}; }
// ---- 读入辅助:缺字段/类型错 → 默认值(as<T>(fallback) 语义)----
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;
}
// 资产引用双写:{Guid, Path}(procedural 只有 Path,Guid=0 不写)
void WriteModelRef(YAML::Node& node, Assets::AssetManager& assets,
ModelRef ref)
{
const std::string& path = assets.Models().Path(ref);
if (path.empty()) {
ENGINE_WARN("SceneSerializer: entity has invalid model ref, "
"MeshRenderer skipped");
return;
}
YAML::Node m;
if (const u64 guid = assets.GetOrCreateGUID(path); guid != 0) {
m["Guid"] = guid;
}
m["Path"] = path;
node["Model"] = m;
}
// ---- 中间结构:解析全部成功才碰 registry ----
struct ParsedEntity
{
u64 uuid = 0;
std::string name = "entity";
YAML::Node transform, meshRenderer, animation, camera, light,
rigidBody; // 未出现 = undefined
};
void SerializeEntity(YAML::Node& out, Assets::AssetManager& assets,
const ECS::Registry& reg, entt::entity e)
{
const auto& id = reg.get<ECS::IdComponent>(e);
YAML::Node ent;
ent["Entity"] = id.uuid.Value();
if (const auto* name = reg.try_get<ECS::NameComponent>(e)) {
ent["Name"] = name->name;
}
YAML::Node comps;
if (const auto* t = reg.try_get<ECS::TransformComponent>(e)) {
YAML::Node n;
n["Position"] = Vec3Arr(t->GetPosition());
n["Rotation"] = QuatArr(t->GetRotationQuat()); // (w,x,y,z) 无损
n["Scale"] = Vec3Arr(t->GetScale());
comps["Transform"] = n;
}
if (const auto* mr = reg.try_get<ECS::MeshRendererComponent>(e)) {
YAML::Node n;
WriteModelRef(n, assets, mr->model);
if (const YAML::Node m = n["Model"]) comps["MeshRenderer"] = n;
}
if (const auto* a = reg.try_get<ECS::AnimationComponent>(e)) {
YAML::Node n;
n["ClipIndex"] = a->clipIndex;
n["Playing"] = a->playing;
n["Loop"] = a->loop;
n["Speed"] = a->speed; // fade*/time 是运行态,不存
comps["Animation"] = n;
}
if (const auto* c = reg.try_get<ECS::CameraComponent>(e)) {
YAML::Node n;
n["Primary"] = c->primary; // 相机不存投影参数(调用方传入)
comps["Camera"] = n;
}
if (const auto* l = reg.try_get<ECS::LightComponent>(e)) {
YAML::Node n;
n["Type"] = static_cast<u8>(l->type);
n["Direction"] = Vec3Arr(l->direction);
n["Color"] = Vec4Arr(l->color);
n["Radius"] = l->radius;
n["CastShadow"] = l->castShadow;
comps["Light"] = n;
}
if (const auto* rb = reg.try_get<ECS::RigidBodyComponent>(e)) {
YAML::Node n;
n["Type"] = static_cast<u8>(rb->type);
n["Collider"] = static_cast<u8>(rb->collider);
n["BoxHalfExtents"] = Vec3Arr(rb->boxHalfExtents);
n["SphereRadius"] = rb->sphereRadius;
n["Mass"] = rb->mass;
n["GravityScale"] = rb->gravityScale;
n["Friction"] = rb->friction;
n["Restitution"] = rb->restitution;
n["IsSensor"] = rb->isSensor;
// bodyId/bodyValid/velocity 由 PhysicsWorld 对账重建,不存
comps["RigidBody"] = n;
}
if (comps.size() > 0) ent["Components"] = comps;
out.push_back(ent);
}
} // namespace
// ---- 保存 ----
bool SceneSerializer::Serialize(const ECS::Scene& scene,
Assets::AssetManager& assets,
const std::string& outPath,
const std::string& environmentPath)
{
try {
YAML::Node root;
root["Scene"] = std::filesystem::path(outPath).stem().string();
root["Version"] = kSceneVersion;
if (!environmentPath.empty()) {
root["Environment"] = environmentPath;
}
const auto& s = scene.GetSettings();
YAML::Node st;
st["ClearColor"] = Vec4Arr(s.ClearColor);
st["AmbientColor"] = Vec4Arr(s.AmbientColor);
st["DrawSkybox"] = s.DrawSkybox;
root["Settings"] = st;
YAML::Node entities(YAML::NodeType::Sequence);
const ECS::Registry& reg = scene.GetRegistry();
reg.view<ECS::IdComponent>().each(
[&](entt::entity e, const ECS::IdComponent&) {
SerializeEntity(entities, assets, reg, e);
});
root["Entities"] = entities;
std::ofstream out(outPath, std::ios::trunc);
if (!out) {
ENGINE_ERROR("SceneSerializer: cannot open '{}' for write",
outPath);
return false;
}
out << root;
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;
}
}
// ---- 加载 ----
bool SceneSerializer::Deserialize(ECS::Scene& scene,
Assets::AssetManager& assets,
const std::string& inPath,
std::string& outEnvironmentPath)
{
outEnvironmentPath.clear();
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;
}
// Version 闸:高版本拒载(结构未知),缺失按 0 宽容旧档
if (const int version = root["Version"].as<int>(0);
version > kSceneVersion) {
ENGINE_ERROR("SceneSerializer: '{}' version {} > supported {}",
inPath, version, kSceneVersion);
return false;
}
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.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()) {
pe.transform = comps["Transform"];
pe.meshRenderer = comps["MeshRenderer"];
pe.animation = comps["Animation"];
pe.camera = comps["Camera"];
pe.light = comps["Light"];
pe.rigidBody = comps["RigidBody"];
// 未识别的组件键自然跳过(前向兼容)
}
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};
if (pe.transform) {
const YAML::Node n = pe.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 (pe.meshRenderer) {
const YAML::Node n = pe.meshRenderer;
const u64 guid = n["Model"]["Guid"].as<u64>(0);
const auto path = n["Model"]["Path"].as<std::string>("");
if (ModelRef ref = ResolveModel(assets, guid, path);
ref.Valid()) {
handle.AddComponent<ECS::MeshRendererComponent>(
ECS::MeshRendererComponent{ref});
}
else {
ENGINE_WARN("SceneSerializer: model unresolved "
"(guid={}, path='{}'), MeshRenderer skipped",
guid, path);
}
}
if (pe.animation) {
const YAML::Node n = pe.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);
}
if (pe.camera) {
const YAML::Node n = pe.camera;
auto& c = handle.AddComponent<ECS::CameraComponent>();
c.primary = n["Primary"].as<bool>(false);
}
if (pe.light) {
const YAML::Node n = pe.light;
auto& l = handle.AddComponent<ECS::LightComponent>();
if (const u8 type = n["Type"].as<u8>(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);
}
if (pe.rigidBody) {
const YAML::Node n = pe.rigidBody;
auto& rb = handle.AddComponent<ECS::RigidBodyComponent>();
if (const u8 type = n["Type"].as<u8>(0); type <= 2) {
rb.type = static_cast<ECS::RigidBodyComponent::BodyType>(
type);
}
if (const u8 col = n["Collider"].as<u8>(0); col <= 2) {
rb.collider =
static_cast<ECS::RigidBodyComponent::ColliderType>(
col);
}
rb.boxHalfExtents = ReadVec3(n["BoxHalfExtents"],
rb.boxHalfExtents);
rb.sphereRadius = n["SphereRadius"].as<f32>(rb.sphereRadius);
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);
rb.isSensor = n["IsSensor"].as<bool>(rb.isSensor);
// bodyId/bodyValid/velocity 保持默认(对账重建)
}
}
// 加载在 OnImGuiRender 段,错过本帧 UpdateTransforms:
// 显式补算,否则首帧 World = 单位阵
scene.UpdateTransforms();
ENGINE_INFO("SceneSerializer: loaded {} entities from '{}'",
parsed.size(), inPath);
return true;
}
} // namespace Engine::Serialization