feat: Play-In-Editor (PIE) with in-memory scene snapshot

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.
This commit is contained in:
2026-10-04 02:22:49 +08:00
parent c949e8684b
commit 2577809ee4
6 changed files with 291 additions and 59 deletions
+124 -54
View File
@@ -384,47 +384,65 @@ namespace Engine::Serialization {
// ---- 保存 ----
// 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 {
// float32 无损往返需 9 位有效数字(std::numeric_limits
// <float>::max_digits10)
YAML::Emitter out;
out.SetFloatPrecision(9);
out << YAML::BeginMap;
out << YAML::Key << "Scene" << YAML::Value
<< std::filesystem::path(outPath).stem().string();
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 false;
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);
@@ -433,7 +451,7 @@ namespace Engine::Serialization {
outPath);
return false;
}
file << out.c_str();
file << doc;
return true;
}
catch (const YAML::Exception& e) {
@@ -446,35 +464,42 @@ namespace Engine::Serialization {
}
}
// ---- 加载 ----
bool SceneSerializer::Deserialize(ECS::Scene& scene,
Assets::AssetManager& assets,
const std::string& inPath,
std::string& outEnvironmentPath)
std::string SceneSerializer::SerializeToString(
const ECS::Scene& scene,
Assets::AssetManager& assets,
const std::string& documentName,
const std::string& environmentPath)
{
outEnvironmentPath.clear();
YAML::Node root;
try {
root = YAML::LoadFile(inPath);
return SerializeDoc(scene, assets, documentName, environmentPath);
}
catch (const YAML::Exception& e) {
ENGINE_ERROR("SceneSerializer: cannot parse '{}' ({})", inPath,
e.what());
return false;
ENGINE_ERROR("SceneSerializer: snapshot failed ({})", e.what());
return {};
}
catch (const std::exception& e) {
ENGINE_ERROR("SceneSerializer: cannot read '{}' ({})", inPath,
e.what());
return false;
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 {}",
inPath, version, kSceneVersion);
sourceName, version, kSceneVersion);
return false;
}
@@ -714,8 +739,53 @@ namespace Engine::Serialization {
scene.UpdateTransforms();
ENGINE_INFO("SceneSerializer: loaded {} entities from '{}'",
parsed.size(), inPath);
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