场景加载优化

This commit is contained in:
Rowland 2026-07-27 17:02:26 +08:00
parent ddeba5bbbc
commit 31a408d813
31 changed files with 2812 additions and 144 deletions

View File

@ -21,6 +21,7 @@
#include "MetaCoreRuntimeInput/MetaCoreRuntimeInteraction.h"
#include "MetaCoreScene/MetaCoreScenePackage.h"
#include "MetaCoreScene/MetaCoreScene.h"
#include "MetaCoreScene/MetaCoreSceneWorld.h"
#include "MetaCoreScripting/MetaCoreScripting.h"
#include <filesystem>
@ -30,6 +31,7 @@
#include <iostream>
#include <memory>
#include <unordered_map>
#include <thread>
#include <vector>
namespace {
@ -450,10 +452,50 @@ int main(int argc, char* argv[]) {
startupSceneDocument = MetaCore::MetaCoreLoadStartupSceneDocument(projectPath);
}
MetaCore::MetaCoreScene scene;
std::unordered_map<MetaCore::MetaCoreAssetGuid, std::filesystem::path, MetaCore::MetaCoreAssetGuidHasher> runtimeScenePackages;
if (deliveryDocument) for (const auto& entry : deliveryDocument->ScenePackages)
runtimeScenePackages[entry.SceneGuid] = packageRoot / entry.PackagePath;
MetaCore::MetaCoreSceneWorld sceneWorld(
scene,
[&](const MetaCore::MetaCoreAssetGuid& sceneGuid) -> std::optional<MetaCore::MetaCoreSceneDocument> {
if (const auto path = runtimeScenePackages.find(sceneGuid); path != runtimeScenePackages.end())
return MetaCore::MetaCoreReadScenePackage(path->second);
if (!deliveryDocument) {
std::error_code scanError;
const auto scenesRoot = projectRoot / "Scenes";
if (std::filesystem::is_directory(scenesRoot, scanError)) {
for (std::filesystem::recursive_directory_iterator iterator(
scenesRoot, std::filesystem::directory_options::skip_permission_denied, scanError), end;
!scanError && iterator != end; iterator.increment(scanError)) {
if (!iterator->is_regular_file(scanError)) continue;
auto candidate = MetaCore::MetaCoreReadScenePackage(iterator->path());
if (candidate && candidate->SceneGuid == sceneGuid) return candidate;
}
}
}
return std::nullopt;
}
);
if (startupSceneDocument.has_value()) {
MetaCore::MetaCoreSceneSnapshot snapshot;
snapshot.GameObjects = startupSceneDocument->GameObjects;
scene.RestoreSnapshot(snapshot);
std::string sceneLoadError;
if (!sceneWorld.LoadDocumentNow(*startupSceneDocument, MetaCore::MetaCoreSceneLoadMode::Replace, &sceneLoadError)) {
errors << "MCB6004: startup SceneWorld load failed: " << sceneLoadError << '\n';
return 2;
}
// AlwaysLoaded SubScenes are part of the startup transaction from the
// runtime's point of view: settle their background reads before systems scan ECS.
const std::size_t expectedStartupScenes = 1U + static_cast<std::size_t>(std::count_if(
startupSceneDocument->SubScenes.begin(), startupSceneDocument->SubScenes.end(),
[](const auto& subScene) { return subScene.LoadPolicy == MetaCore::MetaCoreSubSceneLoadPolicy::AlwaysLoaded; }
));
for (int iteration = 0; iteration < 1000 && sceneWorld.GetLoadedScenes().size() < expectedStartupScenes; ++iteration) {
sceneWorld.Tick();
std::this_thread::yield();
}
if (sceneWorld.GetLoadedScenes().size() < expectedStartupScenes) {
errors << "MCB6004: one or more AlwaysLoaded SubScenes failed to load\n";
return 2;
}
output << "MetaCorePlayer: loaded startup scene from project "
<< projectPath.string() << '\n';
} else if (!deliveryDocument) {
@ -704,6 +746,8 @@ int main(int argc, char* argv[]) {
MetaCore::MetaCoreSceneView interactionSceneView = MetaCoreBuildPlayerSceneView();
const auto interactionSnapshot = interactionCameraSync.BuildSnapshot(scene);
(void)MetaCore::MetaCoreSceneRenderSync::TryBuildSceneViewFromPrimaryCamera(interactionSnapshot, interactionSceneView);
sceneWorld.UpdateStreaming({interactionSceneView.CameraPosition});
sceneWorld.Tick();
runtimeInteraction.Update(scene, viewportRenderer, physicsWorld, interactionSceneView,
window.GetInput(), inputViewport, uiConsumption.Mouse);
fixedAccumulatorSeconds += std::clamp(frameDeltaSeconds, 0.0, physicsSettings.MaxFrameDelta);

View File

@ -561,14 +561,19 @@ set(METACORE_SCENE_HEADERS
Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h
Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h
Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h
Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreObjectReference.h
Source/MetaCoreScene/Public/MetaCoreScene/MetaCorePrefab.h
Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScenePackage.h
Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScene.h
Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneWorld.h
Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneSerializer.h
)
set(METACORE_SCENE_SOURCES
Source/MetaCoreScene/Private/MetaCoreScenePackage.cpp
Source/MetaCoreScene/Private/MetaCoreScene.cpp
Source/MetaCoreScene/Private/MetaCoreSceneWorld.cpp
Source/MetaCoreScene/Private/MetaCorePrefab.cpp
Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp
)
@ -579,6 +584,7 @@ metacore_generate_reflection(
Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h
Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h
Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneDocument.h
Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreObjectReference.h
Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScene.h
)
@ -1001,6 +1007,17 @@ metacore_stage_material_templates(MetaCorePlayer)
if(METACORE_BUILD_TESTS)
enable_testing()
add_executable(MetaCoreSceneTests tests/MetaCoreSceneTests.cpp)
target_link_libraries(MetaCoreSceneTests PRIVATE MetaCoreScene)
target_compile_options(MetaCoreSceneTests PRIVATE ${METACORE_COMMON_WARNINGS})
add_test(NAME MetaCoreSceneTests COMMAND MetaCoreSceneTests)
add_executable(MetaCoreSceneBenchmark tests/MetaCoreSceneBenchmark.cpp)
target_link_libraries(MetaCoreSceneBenchmark PRIVATE MetaCoreScene)
target_compile_options(MetaCoreSceneBenchmark PRIVATE ${METACORE_COMMON_WARNINGS})
add_test(NAME MetaCoreSceneBenchmark COMMAND MetaCoreSceneBenchmark)
set_tests_properties(MetaCoreSceneBenchmark PROPERTIES LABELS "benchmark")
add_executable(MetaCoreDeliveryTests
tests/MetaCoreDeliveryTests.cpp
Source/MetaCoreRender/Private/MetaCoreGlibcCompat.cpp

View File

@ -12,6 +12,7 @@
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h"
#include "MetaCoreRendering/MetaCoreRendering.h"
#include "MetaCoreScene/MetaCoreScenePackage.h"
#include "MetaCoreScene/MetaCorePrefab.h"
#include "MetaCoreScene/MetaCoreSceneSerializer.h"
#include "MetaCoreScripting/MetaCoreScripting.h"
@ -30,6 +31,7 @@
#include <sstream>
#include <system_error>
#include <unordered_set>
#include <unordered_map>
#if defined(__linux__)
#include <sys/utsname.h>
#endif
@ -562,6 +564,20 @@ std::optional<MetaCoreRuntimeDeliveryDocument> MetaCoreReadRuntimeDeliveryDocume
if (module.is_string()) document.ScriptModules.emplace_back(module.get<std::string>());
}
}
if (value->contains("scene_packages") && (*value)["scene_packages"].is_array()) {
for (const auto& scene : (*value)["scene_packages"]) {
MetaCoreRuntimeDeliveryDocument::SceneEntry entry;
entry.SceneGuid = MetaCoreAssetGuid::Parse(scene.value("guid", "")).value_or(MetaCoreAssetGuid{});
entry.PackagePath = scene.value("path", "");
entry.SourceHash = scene.value("source_hash", std::uint64_t{});
if (scene.contains("dependencies")) for (const auto& dependency : scene["dependencies"]) {
if (dependency.is_string()) {
if (const auto guid = MetaCoreAssetGuid::Parse(dependency.get<std::string>())) entry.Dependencies.push_back(*guid);
}
}
document.ScenePackages.push_back(std::move(entry));
}
}
if (document.FormatVersion != GMetaCoreRuntimeDeliveryVersion) {
SetIssue(issue, MetaCoreBuildErrorCode::ManifestVersionUnsupported, "Runtime delivery format is unsupported", path);
return std::nullopt;
@ -582,18 +598,36 @@ std::optional<MetaCoreRuntimeDeliveryDocument> MetaCoreReadRuntimeDeliveryDocume
return std::nullopt;
}
}
for (const auto& scene : document.ScenePackages) {
if (!scene.SceneGuid.IsValid() || !IsSafeRelativePath(scene.PackagePath)) {
SetIssue(issue, MetaCoreBuildErrorCode::UnsafePath, "Runtime delivery contains an invalid scene entry", path);
return std::nullopt;
}
}
return document;
}
bool MetaCoreWriteRuntimeDeliveryDocument(const std::filesystem::path& path, const MetaCoreRuntimeDeliveryDocument& document) {
json modules = json::array();
for (const auto& module : document.ScriptModules) modules.push_back(module.generic_string());
json scenes = json::array();
for (const auto& scene : document.ScenePackages) {
json dependencies = json::array();
for (const auto& dependency : scene.Dependencies) dependencies.push_back(dependency.ToString());
scenes.push_back({
{"guid", scene.SceneGuid.ToString()},
{"path", scene.PackagePath.generic_string()},
{"source_hash", scene.SourceHash},
{"dependencies", std::move(dependencies)}
});
}
return WriteJson(path, {
{"format_version", document.FormatVersion}, {"runtime_abi_major", document.RuntimeAbiMajor},
{"runtime_abi_minor", document.RuntimeAbiMinor}, {"build_id", document.BuildId},
{"content_root", document.ContentRoot.generic_string()}, {"startup_scene", document.StartupScene.generic_string()},
{"runtime_config", document.RuntimeConfig.generic_string()}, {"ui_manifest", document.UiManifest.generic_string()},
{"release_manifest", document.ReleaseManifest.generic_string()}, {"script_modules", std::move(modules)}
{"release_manifest", document.ReleaseManifest.generic_string()}, {"script_modules", std::move(modules)},
{"scene_packages", std::move(scenes)}
});
}
@ -832,6 +866,7 @@ MetaCoreBuildResult MetaCoreBuildPipeline::Run(const MetaCoreBuildRequest& reque
MetaCoreRegisterRuntimeDataGeneratedTypes(inputRegistry);
MetaCoreRegisterRuntimeInputGeneratedTypes(inputRegistry);
MetaCoreRegisterPhysicsGeneratedTypes(inputRegistry);
const MetaCoreTypeRegistry sceneRegistry = MetaCoreBuildScenePackageTypeRegistry();
const auto runtimeProjectPath = request.ProjectRoot / project->RuntimeDirectory / "ProjectRuntime.mcruntimecfg";
auto runtimeProject = MetaCoreReadRuntimeProjectDocument(runtimeProjectPath, inputRegistry);
if (!runtimeProject) return fail(MetaCoreBuildErrorCode::CookFailed, "Runtime project configuration is missing or unreadable", runtimeProjectPath);
@ -883,6 +918,7 @@ MetaCoreBuildResult MetaCoreBuildPipeline::Run(const MetaCoreBuildRequest& reque
const auto cookedScene = staging / "Content" / "Scenes" / cookedSceneName;
const auto cookablePhysicsMeshes = MetaCoreCollectCookablePhysicsMeshes(request.ProjectRoot, inputRegistry, staging / "Content" / "Physics");
std::optional<MetaCoreSceneDocument> startupSceneDocument;
std::vector<MetaCoreRuntimeDeliveryDocument::SceneEntry> stagedSceneEntries;
if (startupSource.extension() == ".json") {
const auto registry = MetaCoreBuildScenePackageTypeRegistry();
const auto scene = MetaCoreSceneSerializer::LoadSceneFromJson(absoluteStartup, registry);
@ -903,6 +939,131 @@ MetaCoreBuildResult MetaCoreBuildPipeline::Run(const MetaCoreBuildRequest& reque
std::filesystem::copy_file(absoluteStartup, cookedScene, std::filesystem::copy_options::overwrite_existing, error);
if (error) return fail(MetaCoreBuildErrorCode::CookFailed, "Failed to copy startup scene", absoluteStartup);
}
if (startupSceneDocument) {
if (!startupSceneDocument->SceneGuid.IsValid())
return fail(MetaCoreBuildErrorCode::CookFailed, "Startup scene has no stable SceneGuid", absoluteStartup);
std::unordered_map<MetaCoreAssetGuid, std::filesystem::path, MetaCoreAssetGuidHasher> sceneSources;
std::error_code sceneScanError;
const auto sceneRoot = request.ProjectRoot / "Scenes";
if (std::filesystem::is_directory(sceneRoot, sceneScanError)) {
for (std::filesystem::recursive_directory_iterator iterator(
sceneRoot, std::filesystem::directory_options::skip_permission_denied, sceneScanError), end;
!sceneScanError && iterator != end; iterator.increment(sceneScanError)) {
if (!iterator->is_regular_file(sceneScanError)) continue;
const auto path = iterator->path();
if (!path.filename().string().ends_with(".mcscene.json")) continue;
if (const auto candidate = MetaCoreSceneSerializer::LoadSceneFromJson(path, sceneRegistry);
candidate && candidate->SceneGuid.IsValid()) {
sceneSources[candidate->SceneGuid] = path;
}
}
}
sceneSources[startupSceneDocument->SceneGuid] = absoluteStartup;
std::unordered_map<MetaCoreAssetGuid, MetaCorePrefabDocument, MetaCoreAssetGuidHasher> prefabDocuments;
std::error_code prefabScanError;
const auto prefabRoot = request.ProjectRoot / "Assets";
if (std::filesystem::is_directory(prefabRoot, prefabScanError)) {
for (std::filesystem::recursive_directory_iterator iterator(
prefabRoot, std::filesystem::directory_options::skip_permission_denied, prefabScanError), end;
!prefabScanError && iterator != end; iterator.increment(prefabScanError)) {
if (!iterator->is_regular_file(prefabScanError) ||
!iterator->path().filename().string().ends_with(".mcprefab.json")) continue;
if (auto prefab = MetaCoreSceneSerializer::LoadPrefabFromJson(iterator->path(), sceneRegistry);
prefab && prefab->PrefabGuid.IsValid())
prefabDocuments[prefab->PrefabGuid] = std::move(*prefab);
}
}
MetaCorePrefabResolver prefabResolver(
[&](const MetaCoreAssetGuid& guid) -> std::optional<MetaCorePrefabDocument> {
const auto iterator = prefabDocuments.find(guid);
return iterator == prefabDocuments.end() ? std::nullopt : std::optional(iterator->second);
}
);
std::unordered_set<MetaCoreAssetGuid, MetaCoreAssetGuidHasher> visitedScenes;
std::unordered_set<MetaCoreAssetGuid, MetaCoreAssetGuidHasher> activeScenes;
std::string sceneDependencyError;
std::function<bool(
const MetaCoreSceneDocument&,
const std::filesystem::path&,
const std::filesystem::path&
)> stageSceneClosure;
stageSceneClosure = [&](const MetaCoreSceneDocument& sceneDocument,
const std::filesystem::path& sourcePath,
const std::filesystem::path& outputPath) {
if (visitedScenes.contains(sceneDocument.SceneGuid)) return true;
if (!activeScenes.insert(sceneDocument.SceneGuid).second) {
sceneDependencyError = "SubScene dependency cycle at " + sceneDocument.SceneGuid.ToString();
return false;
}
for (const auto& instance : sceneDocument.PrefabInstances) {
const auto prefabIssues = prefabResolver.ValidateDependencies(instance.PrefabAssetGuid);
if (!prefabIssues.empty()) {
sceneDependencyError = "Prefab dependency validation failed for " +
instance.PrefabAssetGuid.ToString() + ": " + prefabIssues.front();
return false;
}
}
std::vector<MetaCoreAssetGuid> dependencies;
for (const auto& subScene : sceneDocument.SubScenes)
if (subScene.SceneGuid.IsValid()) dependencies.push_back(subScene.SceneGuid);
for (const auto& object : sceneDocument.GameObjects) {
if (object.PhysicsConstraint &&
object.PhysicsConstraint->TargetObjectReference.Strength == MetaCoreObjectReferenceStrength::Hard &&
object.PhysicsConstraint->TargetObjectReference.SceneGuid.IsValid())
dependencies.push_back(object.PhysicsConstraint->TargetObjectReference.SceneGuid);
if (object.TimelineDirector) for (const auto& binding : object.TimelineDirector->Bindings)
if (binding.ObjectReference.Strength == MetaCoreObjectReferenceStrength::Hard &&
binding.ObjectReference.SceneGuid.IsValid())
dependencies.push_back(binding.ObjectReference.SceneGuid);
}
std::sort(dependencies.begin(), dependencies.end(), [](const auto& lhs, const auto& rhs) {
return lhs.ToString() < rhs.ToString();
});
dependencies.erase(std::unique(dependencies.begin(), dependencies.end()), dependencies.end());
for (const auto& dependency : dependencies) {
if (dependency == sceneDocument.SceneGuid) continue;
auto source = sceneSources.find(dependency);
if (source == sceneSources.end()) {
const auto declared = std::find_if(sceneDocument.SubScenes.begin(), sceneDocument.SubScenes.end(),
[&](const auto& subScene) { return subScene.SceneGuid == dependency && !subScene.ScenePath.empty(); });
if (declared != sceneDocument.SubScenes.end() && IsSafeRelativePath(declared->ScenePath))
source = sceneSources.emplace(dependency, request.ProjectRoot / declared->ScenePath).first;
}
if (source == sceneSources.end() || !std::filesystem::is_regular_file(source->second)) {
sceneDependencyError = "Required Scene dependency is missing: " + dependency.ToString();
return false;
}
auto dependencyDocument = source->second.extension() == ".json"
? MetaCoreSceneSerializer::LoadSceneFromJson(source->second, sceneRegistry)
: MetaCoreReadScenePackage(source->second);
if (!dependencyDocument || dependencyDocument->SceneGuid != dependency) {
sceneDependencyError = "Scene dependency GUID/path mismatch: " + dependency.ToString();
return false;
}
const auto relativeOutput = std::filesystem::path("Content") / "Scenes" /
(dependency.ToString() + ".mcscene");
if (!stageSceneClosure(*dependencyDocument, source->second, staging / relativeOutput)) return false;
}
if (outputPath != cookedScene && !MetaCoreWriteScenePackage(outputPath, sceneDocument)) {
sceneDependencyError = "Failed to cook dependent Scene: " + sourcePath.string();
return false;
}
stagedSceneEntries.push_back({
sceneDocument.SceneGuid,
outputPath.lexically_relative(staging),
MetaCoreHashFile(sourcePath).value_or(0),
dependencies
});
activeScenes.erase(sceneDocument.SceneGuid);
visitedScenes.insert(sceneDocument.SceneGuid);
return true;
};
if (!stageSceneClosure(*startupSceneDocument, absoluteStartup, cookedScene))
return fail(MetaCoreBuildErrorCode::DependencyMissing, sceneDependencyError, absoluteStartup);
}
const auto currentCookRoot = request.ProjectRoot / "Library" / "Cooked" / profile->TargetPlatform / profile->Name;
const auto editorCookRoot = request.ProjectRoot / "Library" / "Cooked" / profile->TargetPlatform / "Editor";
const auto legacyCookRoot = request.ProjectRoot / "Library" / "Cooked" / "Windows";
@ -1036,6 +1197,7 @@ MetaCoreBuildResult MetaCoreBuildPipeline::Run(const MetaCoreBuildRequest& reque
MetaCoreRuntimeDeliveryDocument delivery;
delivery.BuildId = result.BuildId;
delivery.StartupScene = std::filesystem::path("Content") / "Scenes" / cookedSceneName;
delivery.ScenePackages = stagedSceneEntries;
if (!stagedScriptModule.empty()) delivery.ScriptModules.push_back(std::filesystem::path("Project") / "Scripts" / stagedScriptModule.filename());
if (!MetaCoreWriteRuntimeDeliveryDocument(staging / "Project" / "MetaCore.runtimeproject", delivery))
return fail(MetaCoreBuildErrorCode::StageFailed, "Failed to write runtime project document", staging);

View File

@ -1,5 +1,7 @@
#pragma once
#include "MetaCoreFoundation/MetaCoreAssetGuid.h"
#include <atomic>
#include <cstdint>
#include <filesystem>
@ -62,6 +64,13 @@ struct MetaCoreRuntimeDeliveryDocument {
std::filesystem::path UiManifest = "Content/Runtime/Ui.mcruntime";
std::filesystem::path ReleaseManifest = "Manifest/MetaCore.release.json";
std::vector<std::filesystem::path> ScriptModules{};
struct SceneEntry {
MetaCoreAssetGuid SceneGuid{};
std::filesystem::path PackagePath{};
std::uint64_t SourceHash = 0;
std::vector<MetaCoreAssetGuid> Dependencies{};
};
std::vector<SceneEntry> ScenePackages{};
};
struct MetaCoreReleaseFileEntry {

View File

@ -26,6 +26,8 @@
#include "MetaCoreFoundation/MetaCoreRuntimeAssetRegistry.h"
#include "MetaCoreScene/MetaCoreScenePackage.h"
#include "MetaCoreScene/MetaCoreSceneSerializer.h"
#include "MetaCoreScene/MetaCoreSceneWorld.h"
#include "MetaCoreScene/MetaCorePrefab.h"
#include <nlohmann/json.hpp>
#include <imgui.h>
@ -9869,6 +9871,7 @@ public:
CurrentSceneGuid_ = MetaCoreAssetGuid{};
CurrentScenePath_.clear();
CurrentSceneName_.clear();
ScenePaths_.clear();
SavedSnapshotBytes_.clear();
Dirty_ = false;
}
@ -9893,6 +9896,18 @@ public:
}
bool LoadScene(MetaCoreEditorContext& editorContext, const std::filesystem::path& relativeScenePath) override {
return LoadSceneWithMode(editorContext, relativeScenePath, MetaCoreSceneLoadMode::Replace);
}
bool LoadSceneAdditive(MetaCoreEditorContext& editorContext, const std::filesystem::path& relativeScenePath) override {
return LoadSceneWithMode(editorContext, relativeScenePath, MetaCoreSceneLoadMode::Additive);
}
bool LoadSceneWithMode(
MetaCoreEditorContext& editorContext,
const std::filesystem::path& relativeScenePath,
MetaCoreSceneLoadMode loadMode
) {
if (AssetDatabaseService_ == nullptr ||
ReflectionRegistry_ == nullptr ||
!AssetDatabaseService_->HasProject() ||
@ -9944,15 +9959,12 @@ public:
if(!migrationFailed)for(const auto&change:pending){auto&animator=*sceneDocument->GameObjects[change.ObjectIndex].Animator;animator.ControllerAssetGuid=change.Controller;animator.AvatarAssetGuid=change.Avatar;migratedLegacyAnimators=true;}
}
MetaCoreEditorStateSnapshot editorStateSnapshot;
editorStateSnapshot.SceneSnapshot.Environment = sceneDocument->Environment;
editorStateSnapshot.SceneSnapshot.GameObjects = sceneDocument->GameObjects;
editorStateSnapshot.SelectionSnapshot = sceneDocument->Selection;
editorContext.RestoreStateSnapshot(editorStateSnapshot);
editorContext.GetCommandService().Clear();
// 从 .meta 提取或生成 Guid
MetaCoreAssetGuid sceneGuid{};
MetaCoreAssetGuid sceneGuid = sceneDocument->SceneGuid;
if (const auto record = AssetDatabaseService_->FindAssetByRelativePath(normalizedScenePath);
record.has_value() && record->Guid.IsValid()) {
sceneGuid = record->Guid;
}
const std::filesystem::path metaPath = absoluteScenePath.string() + ".meta";
if (std::filesystem::exists(metaPath)) {
sceneGuid = MetaCoreAssetRegistry::Get().ResolvePathToGuid(normalizedScenePath);
@ -9960,12 +9972,42 @@ public:
if (!sceneGuid.IsValid()) {
sceneGuid = MetaCoreAssetGuid::Generate();
}
sceneDocument->SceneGuid = sceneGuid;
for (auto& object : sceneDocument->GameObjects) object.SceneGuid = sceneGuid;
if (MetaCoreSceneWorld* world = editorContext.GetSceneWorld()) {
world->SetSceneLoader([this](const MetaCoreAssetGuid& guid) -> std::optional<MetaCoreSceneDocument> {
if (AssetDatabaseService_ == nullptr || ReflectionRegistry_ == nullptr || !AssetDatabaseService_->HasProject())
return std::nullopt;
const auto record = AssetDatabaseService_->FindAssetByGuid(guid);
if (!record) return std::nullopt;
const auto relative = !record->PackagePath.empty() ? record->PackagePath : record->RelativePath;
const auto absolute = AssetDatabaseService_->GetProjectDescriptor().RootPath / relative;
if (relative.extension() == ".mcscene") {
if (auto package = MetaCoreReadScenePackage(absolute)) return package;
}
return MetaCoreSceneSerializer::LoadSceneFromJson(absolute, ReflectionRegistry_->GetTypeRegistry());
});
std::string loadError;
if (!world->LoadDocumentNow(*sceneDocument, loadMode, &loadError)) {
editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Scene", "场景组合加载失败: " + loadError);
return false;
}
} else {
MetaCoreEditorStateSnapshot editorStateSnapshot;
editorStateSnapshot.SceneSnapshot.Environment = sceneDocument->Environment;
editorStateSnapshot.SceneSnapshot.GameObjects = sceneDocument->GameObjects;
editorStateSnapshot.SelectionSnapshot = sceneDocument->Selection;
editorContext.RestoreStateSnapshot(editorStateSnapshot);
}
editorContext.GetCommandService().Clear();
CurrentSceneGuid_ = sceneGuid;
CurrentScenePath_ = normalizedScenePath;
CurrentSceneName_ = sceneDocument->Name.empty()
? MetaCoreSceneDisplayNameFromPath(normalizedScenePath)
: sceneDocument->Name;
ScenePaths_[sceneGuid] = normalizedScenePath;
SavedSnapshotBytes_ = MetaCoreSerializeSceneSnapshotBytes(
ReflectionRegistry_->GetTypeRegistry(),
editorContext.GetScene().CaptureSnapshot()
@ -9986,6 +10028,30 @@ public:
return true;
}
bool UnloadScene(MetaCoreEditorContext& editorContext, const MetaCoreAssetGuid& sceneGuid, bool force) override {
MetaCoreSceneWorld* world = editorContext.GetSceneWorld();
if (world == nullptr || !world->UnloadScene(sceneGuid, force)) return false;
ScenePaths_.erase(sceneGuid);
if (CurrentSceneGuid_ == sceneGuid) {
CurrentSceneGuid_ = world->GetActiveSceneGuid();
const auto path = ScenePaths_.find(CurrentSceneGuid_);
CurrentScenePath_ = path == ScenePaths_.end() ? std::filesystem::path{} : path->second;
CurrentSceneName_ = CurrentScenePath_.stem().string();
}
editorContext.ClearSelection();
return true;
}
bool SetActiveScene(MetaCoreEditorContext& editorContext, const MetaCoreAssetGuid& sceneGuid) override {
MetaCoreSceneWorld* world = editorContext.GetSceneWorld();
if (world == nullptr || !world->SetActiveScene(sceneGuid)) return false;
CurrentSceneGuid_ = sceneGuid;
const auto path = ScenePaths_.find(sceneGuid);
CurrentScenePath_ = path == ScenePaths_.end() ? std::filesystem::path{} : path->second;
CurrentSceneName_ = CurrentScenePath_.stem().string();
return true;
}
bool SaveCurrentScene(MetaCoreEditorContext& editorContext) override {
return CurrentScenePath_.empty()
? SaveSceneAs(editorContext, std::filesystem::path("Scenes") / "Untitled.mcscene.json")
@ -10019,9 +10085,17 @@ public:
MetaCoreSceneDocument sceneDocument;
sceneDocument.Name = CurrentSceneName_.empty() ? MetaCoreSceneDisplayNameFromPath(normalizedScenePath) : CurrentSceneName_;
const MetaCoreSceneSnapshot sceneSnapshot = editorContext.GetScene().CaptureSnapshot();
sceneDocument.Environment = sceneSnapshot.Environment;
sceneDocument.GameObjects = sceneSnapshot.GameObjects;
if (MetaCoreSceneWorld* world = editorContext.GetSceneWorld(); world != nullptr && CurrentSceneGuid_.IsValid()) {
const auto captured = world->CaptureSceneDocument(CurrentSceneGuid_);
if (!captured) return false;
sceneDocument = *captured;
sceneDocument.Name = CurrentSceneName_.empty() ? MetaCoreSceneDisplayNameFromPath(normalizedScenePath) : CurrentSceneName_;
} else {
const MetaCoreSceneSnapshot sceneSnapshot = editorContext.GetScene().CaptureSnapshot();
sceneDocument.Environment = sceneSnapshot.Environment;
sceneDocument.GameObjects = sceneSnapshot.GameObjects;
}
sceneDocument.SceneGuid = sceneGuid;
sceneDocument.Selection.SelectedObjectIds = editorContext.GetSelectedObjectIds();
sceneDocument.Selection.ActiveObjectId = editorContext.GetActiveObjectId();
sceneDocument.Selection.SelectionAnchorId = editorContext.GetSelectionAnchorId();
@ -10047,6 +10121,7 @@ public:
CurrentSceneGuid_ = sceneGuid;
CurrentScenePath_ = normalizedScenePath;
CurrentSceneName_ = sceneDocument.Name;
ScenePaths_[sceneGuid] = normalizedScenePath;
SavedSnapshotBytes_ = MetaCoreSerializeSceneSnapshotBytes(
ReflectionRegistry_->GetTypeRegistry(),
editorContext.GetScene().CaptureSnapshot()
@ -10072,6 +10147,24 @@ public:
return true;
}
bool SaveAllScenes(MetaCoreEditorContext& editorContext) override {
MetaCoreSceneWorld* world = editorContext.GetSceneWorld();
if (world == nullptr) return SaveCurrentScene(editorContext);
const MetaCoreAssetGuid previous = world->GetActiveSceneGuid();
bool success = true;
const auto paths = ScenePaths_;
for (const auto& [guid, path] : paths) {
if (!SetActiveScene(editorContext, guid) || path.empty() || !SaveSceneAs(editorContext, path)) success = false;
}
if (previous.IsValid()) (void)SetActiveScene(editorContext, previous);
return success;
}
[[nodiscard]] std::vector<MetaCoreLoadedSceneInfo> GetOpenScenes(const MetaCoreEditorContext& editorContext) const override {
const MetaCoreSceneWorld* world = editorContext.GetSceneWorld();
return world == nullptr ? std::vector<MetaCoreLoadedSceneInfo>{} : world->GetLoadedScenes();
}
void UpdateDirtyState(const MetaCoreSceneSnapshot& snapshot) override {
if (ReflectionRegistry_ == nullptr) {
SetDirtyState(true);
@ -10111,6 +10204,7 @@ private:
std::string CurrentSceneName_{};
std::vector<std::byte> SavedSnapshotBytes_{};
bool Dirty_ = false;
std::unordered_map<MetaCoreAssetGuid, std::filesystem::path, MetaCoreAssetGuidHasher> ScenePaths_{};
};
class MetaCoreBuiltinSelectionService final : public MetaCoreISelectionService {
@ -11420,6 +11514,7 @@ public:
State_ = MetaCorePlayModeState::Edit;
HasPrePlaySnapshot_ = false;
HasPendingStopRestoreSnapshot_ = false;
HasPrePlayWorldSnapshot_ = false;
CommandRecordingSuppressed_ = false;
ElapsedTimeSeconds_ = 0.0;
FixedTimeAccumulatorSeconds_ = 0.0;
@ -11456,6 +11551,12 @@ public:
}
PrePlaySnapshot_ = editorContext.CaptureStateSnapshot();
HasPrePlaySnapshot_ = true;
if (const MetaCoreSceneWorld* world = editorContext.GetSceneWorld()) {
PrePlayWorldSnapshot_ = world->CaptureWorldSnapshot();
HasPrePlayWorldSnapshot_ = true;
} else {
HasPrePlayWorldSnapshot_ = false;
}
MetaCoreDebugLog(
"PlayMode",
"Enter captured prePlaySnapshot objects=" +
@ -11709,6 +11810,12 @@ private:
" " + MetaCoreDebugSceneSummary(editorContext)
);
editorContext.RestoreStateSnapshot(PendingStopRestoreSnapshot_);
if (HasPrePlayWorldSnapshot_) {
if (MetaCoreSceneWorld* world = editorContext.GetSceneWorld())
world->RestoreWorldSnapshot(PrePlayWorldSnapshot_);
HasPrePlayWorldSnapshot_ = false;
PrePlayWorldSnapshot_ = {};
}
HasPendingStopRestoreSnapshot_ = false;
PendingStopRestoreSnapshot_ = {};
MetaCoreDebugLog("PlayMode", "Apply pending stop restore end " + MetaCoreDebugSceneSummary(editorContext));
@ -11718,8 +11825,10 @@ private:
MetaCorePlayModeState State_ = MetaCorePlayModeState::Edit;
MetaCoreEditorStateSnapshot PrePlaySnapshot_{};
MetaCoreEditorStateSnapshot PendingStopRestoreSnapshot_{};
MetaCoreSceneWorldSnapshot PrePlayWorldSnapshot_{};
bool HasPrePlaySnapshot_ = false;
bool HasPendingStopRestoreSnapshot_ = false;
bool HasPrePlayWorldSnapshot_ = false;
bool CommandRecordingSuppressed_ = false;
double ElapsedTimeSeconds_ = 0.0;
double FixedTimeStepSeconds_ = 1.0 / 60.0;
@ -11837,6 +11946,7 @@ std::optional<std::filesystem::path> MetaCoreBuiltinPrefabService::CreatePrefabF
}
MetaCorePrefabDocument prefabDocument;
prefabDocument.PrefabGuid = prefabGuid;
prefabDocument.Name = rootObject ? rootObject.GetName() : normalizedPrefabPath.stem().string();
std::vector<MetaCoreGameObjectData> prefabDataObjects;
@ -11844,6 +11954,7 @@ std::optional<std::filesystem::path> MetaCoreBuiltinPrefabService::CreatePrefabF
for (const MetaCoreGameObject& gameObject : prefabObjects) {
MetaCoreGameObjectData data;
data.Id = gameObject.GetId();
data.StableObjectGuid = MetaCoreBuildDeterministicGuid(prefabGuid, data.Id);
data.Name = gameObject.GetName();
data.ParentId = gameObject.GetParentId();
data.Transform = gameObject.GetComponent<MetaCoreTransformComponent>();
@ -11902,6 +12013,10 @@ std::optional<MetaCoreId> MetaCoreBuiltinPrefabService::InstantiatePrefabDocumen
}
MetaCoreId instantiatedRootId = 0;
const MetaCoreAssetGuid instanceGuid = MetaCoreAssetGuid::Generate();
const MetaCoreAssetGuid targetSceneGuid = editorContext.GetSceneWorld() != nullptr
? editorContext.GetSceneWorld()->GetActiveSceneGuid()
: MetaCoreAssetGuid{};
const bool created = editorContext.ExecuteSnapshotCommand("实例化 Prefab", [&]() {
std::unordered_map<MetaCoreId, MetaCoreId> idMap;
std::vector<MetaCoreGameObject> clonedObjects;
@ -11925,6 +12040,17 @@ std::optional<MetaCoreId> MetaCoreBuiltinPrefabService::InstantiatePrefabDocumen
}
clonedObject.GetComponent<MetaCoreTransformComponent>() = sourceObject.Transform;
if (targetSceneGuid.IsValid()) {
const MetaCoreAssetGuid nodeGuid = sourceObject.StableObjectGuid.IsValid()
? sourceObject.StableObjectGuid
: MetaCoreBuildDeterministicGuid(prefabAssetGuid, sourceObject.Id);
clonedObject.AddComponent<MetaCoreSceneObjectIdentityComponent>(
MetaCoreSceneObjectIdentityComponent{
targetSceneGuid,
MetaCoreBuildDeterministicGuid(instanceGuid, nodeGuid)
}
);
}
if (sourceObject.Camera.has_value()) clonedObject.AddComponent<MetaCoreCameraComponent>(*sourceObject.Camera);
if (sourceObject.Light.has_value()) clonedObject.AddComponent<MetaCoreLightComponent>(*sourceObject.Light);
if (sourceObject.Rotator.has_value()) clonedObject.AddComponent<MetaCoreRotatorComponent>(*sourceObject.Rotator);
@ -11942,11 +12068,14 @@ std::optional<MetaCoreId> MetaCoreBuiltinPrefabService::InstantiatePrefabDocumen
if (sourceObject.DecalProjector.has_value()) clonedObject.AddComponent<MetaCoreDecalProjectorComponent>(*sourceObject.DecalProjector);
if (sourceObject.Terrain.has_value()) clonedObject.AddComponent<MetaCoreTerrainComponent>(*sourceObject.Terrain);
clonedObject.AddComponent<MetaCorePrefabInstanceMetadata>(MetaCorePrefabInstanceMetadata{
prefabAssetGuid,
sourceObject.Id,
0
});
MetaCorePrefabInstanceMetadata metadata;
metadata.PrefabAssetGuid = prefabAssetGuid;
metadata.PrefabObjectId = sourceObject.Id;
metadata.InstanceGuid = instanceGuid;
metadata.PrefabNodeGuid = sourceObject.StableObjectGuid.IsValid()
? sourceObject.StableObjectGuid
: MetaCoreBuildDeterministicGuid(prefabAssetGuid, sourceObject.Id);
clonedObject.AddComponent<MetaCorePrefabInstanceMetadata>(metadata);
}
for (MetaCoreGameObject& clonedObject : clonedObjects) {
@ -11981,7 +12110,23 @@ std::optional<MetaCoreId> MetaCoreBuiltinPrefabService::InstantiatePrefab(
if (!prefabDocument.has_value()) {
return std::nullopt;
}
return InstantiatePrefabDocument(editorContext, prefabAssetGuid, *prefabDocument, forcedParentId);
MetaCorePrefabResolver resolver([this](const MetaCoreAssetGuid& guid) {
return LoadPrefabDocument(guid);
});
std::string resolveError;
const auto resolved = resolver.Resolve(prefabAssetGuid, &resolveError);
if (!resolved) {
editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Prefab", "Prefab 依赖解析失败: " + resolveError);
return std::nullopt;
}
if (!resolved->OrphanOverrides.empty()) {
editorContext.AddConsoleMessage(
MetaCoreLogLevel::Warning,
"Prefab",
"Prefab 包含 " + std::to_string(resolved->OrphanOverrides.size()) + " 个孤立 Override"
);
}
return InstantiatePrefabDocument(editorContext, prefabAssetGuid, resolved->Document, forcedParentId);
}
bool MetaCoreBuiltinPrefabService::ApplySelectedPrefabInstance(MetaCoreEditorContext& editorContext) {
@ -12198,6 +12343,11 @@ std::optional<MetaCoreId> MetaCoreBuiltinSceneEditingService::CreateGameObject(
}
MetaCoreGameObject object = editorContext.GetScene().CreateGameObject(objectName, parentId);
if (MetaCoreSceneWorld* world = editorContext.GetSceneWorld(); world != nullptr && world->GetActiveSceneGuid().IsValid()) {
object.AddComponent<MetaCoreSceneObjectIdentityComponent>(MetaCoreSceneObjectIdentityComponent{
world->GetActiveSceneGuid(), MetaCoreAssetGuid::Generate()
});
}
if (withMeshRenderer) {
auto& meshRenderer = object.AddComponent<MetaCoreMeshRendererComponent>(MetaCoreMeshRendererComponent{});
if (objectName == "Plane") {
@ -12251,6 +12401,11 @@ std::optional<MetaCoreId> MetaCoreBuiltinSceneEditingService::CreateCamera(
}
MetaCoreGameObject cameraObject = editorContext.GetScene().CreateGameObject("Camera", parentId);
if (MetaCoreSceneWorld* world = editorContext.GetSceneWorld(); world != nullptr && world->GetActiveSceneGuid().IsValid()) {
cameraObject.AddComponent<MetaCoreSceneObjectIdentityComponent>(MetaCoreSceneObjectIdentityComponent{
world->GetActiveSceneGuid(), MetaCoreAssetGuid::Generate()
});
}
MetaCoreCameraComponent& camera = cameraObject.AddComponent<MetaCoreCameraComponent>(MetaCoreCameraComponent{});
camera.IsPrimary = !hasEnabledPrimaryCamera;
@ -12388,6 +12543,11 @@ std::optional<MetaCoreId> MetaCoreBuiltinSceneEditingService::InstantiateModelAs
? resolvedModelPath.filename().generic_string()
: assetRecord->RelativePath.filename().generic_string());
MetaCoreGameObject modelRootObject = editorContext.GetScene().CreateGameObject(modelRootName, externalParentId);
if (MetaCoreSceneWorld* world = editorContext.GetSceneWorld(); world != nullptr && world->GetActiveSceneGuid().IsValid()) {
modelRootObject.AddComponent<MetaCoreSceneObjectIdentityComponent>(MetaCoreSceneObjectIdentityComponent{
world->GetActiveSceneGuid(), MetaCoreAssetGuid::Generate()
});
}
instantiatedRootId = modelRootObject.GetId();
std::vector<MetaCoreId> nodeToIdMap(importedDocument->Nodes.size(), 0);
@ -12479,6 +12639,11 @@ std::optional<MetaCoreId> MetaCoreBuiltinSceneEditingService::InstantiateModelAs
for (std::size_t i = 0; i < importedDocument->Nodes.size(); ++i) {
const auto& nodeMeta = importedDocument->Nodes[i];
MetaCoreGameObject gameObject = editorContext.GetScene().CreateGameObject(nodeMeta.Name, 0);
if (MetaCoreSceneWorld* world = editorContext.GetSceneWorld(); world != nullptr && world->GetActiveSceneGuid().IsValid()) {
gameObject.AddComponent<MetaCoreSceneObjectIdentityComponent>(MetaCoreSceneObjectIdentityComponent{
world->GetActiveSceneGuid(), MetaCoreAssetGuid::Generate()
});
}
nodeToIdMap[i] = gameObject.GetId();
// 立即同步变换组件
@ -12590,6 +12755,29 @@ bool MetaCoreBuiltinSceneEditingService::ReparentSelection(MetaCoreEditorContext
return false;
}
MetaCoreAssetGuid targetSceneGuid{};
if (newParentId != 0) {
const auto parent = editorContext.GetScene().FindGameObject(newParentId);
if (parent && parent.HasComponent<MetaCoreSceneObjectIdentityComponent>()) {
targetSceneGuid = parent.GetComponent<MetaCoreSceneObjectIdentityComponent>().SceneGuid;
}
}
for (MetaCoreId selectedId : selectedIds) {
const auto selected = editorContext.GetScene().FindGameObject(selectedId);
if (!selected || !selected.HasComponent<MetaCoreSceneObjectIdentityComponent>()) {
continue;
}
const auto selectedScene = selected.GetComponent<MetaCoreSceneObjectIdentityComponent>().SceneGuid;
if (targetSceneGuid.IsValid() && selectedScene.IsValid() && targetSceneGuid != selectedScene) {
editorContext.AddConsoleMessage(
MetaCoreLogLevel::Warning,
"Hierarchy",
"禁止跨场景父子关系;请使用 Move Subtree To Scene"
);
return false;
}
}
const bool keepWorldTransform = editorContext.GetReparentTransformRule() == MetaCoreReparentTransformRule::KeepWorld;
const bool reparented = editorContext.ExecuteSnapshotCommand("重挂接对象", [&]() {
return editorContext.GetScene().ReparentGameObjects(selectedIds, newParentId, keepWorldTransform);

View File

@ -2829,23 +2829,48 @@ static std::array<char, 256> GProjectRenamePathBuffer{};
BeginRename(editorContext, requestedRenameId);
}
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 8));
ImGui::TextColored(ImVec4(0.4F, 0.6F, 1.0F, 1.0F), " (S) ");
ImGui::SameLine();
const std::string sceneLabel =
scenePersistenceService != nullptr && scenePersistenceService->HasOpenScene()
? scenePersistenceService->GetCurrentSceneDisplayName()
: "Untitled";
ImGui::TextUnformatted(sceneLabel.c_str());
if (scenePersistenceService != nullptr && scenePersistenceService->IsSceneDirty()) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.90F, 0.74F, 0.22F, 1.0F), "*");
}
ImGui::PopStyleVar();
ImGui::Separator();
for (MetaCoreId rootId : editorContext.GetScene().GetRootObjectIds()) {
DrawHierarchyNode(editorContext, rootId);
const auto openScenes = scenePersistenceService != nullptr
? scenePersistenceService->GetOpenScenes(editorContext)
: std::vector<MetaCoreLoadedSceneInfo>{};
if (!openScenes.empty()) {
for (const auto& loadedScene : openScenes) {
ImGuiTreeNodeFlags sceneFlags = ImGuiTreeNodeFlags_DefaultOpen |
ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_OpenOnArrow;
if (loadedScene.Active) sceneFlags |= ImGuiTreeNodeFlags_Selected;
const std::string label = std::string(loadedScene.Active ? "(A) " : "(S) ") +
(loadedScene.Name.empty() ? loadedScene.SceneGuid.ToString() : loadedScene.Name) +
(loadedScene.Dirty ? " *" : "");
const bool open = ImGui::TreeNodeEx(
reinterpret_cast<void*>(static_cast<intptr_t>(MetaCoreAssetGuidHasher{}(loadedScene.SceneGuid))),
sceneFlags,
"%s",
label.c_str()
);
if (ImGui::IsItemClicked() && !loadedScene.Active)
(void)scenePersistenceService->SetActiveScene(editorContext, loadedScene.SceneGuid);
if (ImGui::BeginPopupContextItem()) {
if (ImGui::MenuItem("设为活动场景", nullptr, false, !loadedScene.Active))
(void)scenePersistenceService->SetActiveScene(editorContext, loadedScene.SceneGuid);
if (ImGui::MenuItem("保存全部场景"))
(void)scenePersistenceService->SaveAllScenes(editorContext);
if (ImGui::MenuItem("卸载场景", nullptr, false, openScenes.size() > 1))
(void)scenePersistenceService->UnloadScene(editorContext, loadedScene.SceneGuid, false);
ImGui::EndPopup();
}
if (open) {
for (MetaCoreId rootId : editorContext.GetScene().GetRootObjectIds()) {
const auto object = editorContext.GetScene().FindGameObject(rootId);
if (!object || !object.HasComponent<MetaCoreSceneObjectIdentityComponent>()) continue;
if (object.GetComponent<MetaCoreSceneObjectIdentityComponent>().SceneGuid == loadedScene.SceneGuid)
DrawHierarchyNode(editorContext, rootId);
}
ImGui::TreePop();
}
}
} else {
ImGui::TextColored(ImVec4(0.4F, 0.6F, 1.0F, 1.0F), "(S) Untitled");
ImGui::Separator();
for (MetaCoreId rootId : editorContext.GetScene().GetRootObjectIds()) DrawHierarchyNode(editorContext, rootId);
}
DrawRootDropTarget(editorContext);
@ -9320,7 +9345,10 @@ public:
editorContext.ClearSelectedAsset();
editorContext.ClearSelectedAssetSubId();
} else if (entry.AssetRecord.has_value() && entry.AssetRecord->Type == "scene" && scenePersistenceService) {
(void)scenePersistenceService->LoadScene(editorContext, entry.RelativePath);
if (ImGui::GetIO().KeyCtrl)
(void)scenePersistenceService->LoadSceneAdditive(editorContext, entry.RelativePath);
else
(void)scenePersistenceService->LoadScene(editorContext, entry.RelativePath);
} else if (entry.AssetRecord.has_value() && entry.AssetRecord->Type == "model") {
(void)MetaCoreInstantiateModelAsset(editorContext, entry.AssetRecord->Guid, std::nullopt);
} else if (entry.AssetRecord.has_value() && entry.AssetRecord->Type == "prefab") {
@ -9381,6 +9409,13 @@ public:
if (!entry.IsDirectory && ImGui::MenuItem("打开外部程序")) {
MetaCoreOpenPathExternally(project.RootPath / entry.RelativePath);
}
if (!entry.IsDirectory &&
entry.AssetRecord.has_value() &&
entry.AssetRecord->Type == "scene" &&
scenePersistenceService != nullptr &&
ImGui::MenuItem("Additive 打开场景")) {
(void)scenePersistenceService->LoadSceneAdditive(editorContext, entry.RelativePath);
}
if (!entry.IsDirectory &&
entry.AssetRecord.has_value() &&
entry.AssetRecord->Type == "ui_document" &&

View File

@ -2362,6 +2362,7 @@ bool MetaCoreEditorApp::Initialize(int argc, char* argv[]) {
LogService_,
ModuleRegistry_
);
EditorContext_->SetSceneWorld(&SceneWorld_);
MetaCoreTraceStartup("metacore.app: load startup scene");
bool loadedStartupScene = false;
@ -2467,6 +2468,18 @@ int MetaCoreEditorApp::Run() {
playModeService != nullptr && EditorContext_ != nullptr) {
playModeService->Tick(*EditorContext_, playModeDeltaSeconds);
}
if (EditorContext_ != nullptr) {
std::vector<glm::vec3> streamingSources;
for (const auto object : Scene_.GetGameObjects()) {
if (object.HasComponent<MetaCoreCameraComponent>() &&
object.GetComponent<MetaCoreCameraComponent>().Enabled &&
object.GetComponent<MetaCoreCameraComponent>().IsPrimary) {
streamingSources.push_back(object.GetComponent<MetaCoreTransformComponent>().Position);
}
}
SceneWorld_.UpdateStreaming(streamingSources);
SceneWorld_.Tick();
}
#if defined(_WIN32)
ImGui_ImplWin32_NewFrame();
@ -2821,9 +2834,7 @@ void MetaCoreEditorApp::DrawEditorFrame() {
viewportState.Focused &&
!ImGui::GetIO().WantCaptureKeyboard &&
EditorContext_->GetInput().WasKeyPressed(MetaCoreInputKey::Focus)) {
if (MetaCoreGameObject selectedObject = EditorContext_->GetSelectedGameObject(); selectedObject) {
EditorContext_->GetCameraController().FocusGameObject(selectedObject);
}
SceneInteractionService_.FocusSelectedObject(*EditorContext_);
}
// 4. 同步尺寸给 Filament (SetViewportRect 内部会处理 Resize)

View File

@ -31,10 +31,10 @@ void MetaCoreEditorCameraController::Update(
// Zoom: Alt + Right Drag or Mouse Wheel
if (viewportState.Hovered && altDown && ImGui::IsMouseDragging(ImGuiMouseButton_Right)) {
Distance_ *= (1.0F - io.MouseDelta.y * 0.01F);
Distance_ = std::clamp(Distance_, 0.5F, 100.0F);
Distance_ = std::clamp(Distance_, 0.01F, 10000000.0F);
} else if (viewportState.Hovered && std::abs(io.MouseWheel) > 0.001F) {
Distance_ *= (io.MouseWheel > 0.0F) ? 0.88F : 1.12F;
Distance_ = std::clamp(Distance_, 0.5F, 100.0F);
Distance_ = std::clamp(Distance_, 0.01F, 10000000.0F);
}
// Orbit: Alt + Left Drag
@ -97,13 +97,16 @@ void MetaCoreEditorCameraController::Update(
void MetaCoreEditorCameraController::FocusGameObject(const MetaCoreGameObject& gameObject) {
Target_ = gameObject.GetComponent<MetaCoreTransformComponent>().Position;
const float scaleRadius = std::max({gameObject.GetComponent<MetaCoreTransformComponent>().Scale.x, gameObject.GetComponent<MetaCoreTransformComponent>().Scale.y, gameObject.GetComponent<MetaCoreTransformComponent>().Scale.z});
const auto& scale = gameObject.GetComponent<MetaCoreTransformComponent>().Scale;
const float scaleRadius = std::max({std::abs(scale.x), std::abs(scale.y), std::abs(scale.z), 0.5F});
FramingRadius_ = scaleRadius;
Distance_ = std::max(2.0F, scaleRadius * 4.0F + 1.0F);
}
void MetaCoreEditorCameraController::FocusBounds(const glm::vec3& center, float radius) {
Target_ = center;
Distance_ = std::max(2.0F, radius * 3.5F + 1.0F);
FramingRadius_ = std::max(0.01F, std::isfinite(radius) ? radius : 1.0F);
Distance_ = std::max(2.0F, FramingRadius_ * 3.5F + 1.0F);
}
void MetaCoreEditorCameraController::ApplySceneView(const MetaCoreSceneView& sceneView) {
@ -111,6 +114,7 @@ void MetaCoreEditorCameraController::ApplySceneView(const MetaCoreSceneView& sce
FieldOfViewDegrees_ = sceneView.VerticalFieldOfViewDegrees;
const glm::vec3 offset = sceneView.CameraPosition - sceneView.CameraTarget;
Distance_ = std::max(0.1F, glm::length(offset));
FramingRadius_ = std::max(1.0F, (sceneView.FarClip - Distance_) * 0.25F);
if (Distance_ > 0.001F) {
const glm::vec3 forward = glm::normalize(sceneView.CameraTarget - sceneView.CameraPosition);
@ -125,6 +129,12 @@ MetaCoreSceneView MetaCoreEditorCameraController::BuildSceneView() const {
sceneView.CameraTarget = Target_;
sceneView.CameraUp = glm::vec3(0.0F, 0.0F, 1.0F); // Z-up!
sceneView.VerticalFieldOfViewDegrees = FieldOfViewDegrees_;
sceneView.NearClip = std::clamp(Distance_ * 0.0001F, 0.01F, 1.0F);
sceneView.FarClip = std::clamp(
std::max({1000.0F, Distance_ * 2.0F, Distance_ + FramingRadius_ * 4.0F}),
sceneView.NearClip + 1.0F,
100000000.0F
);
return sceneView;
}

View File

@ -31,6 +31,7 @@ private:
float FieldOfViewDegrees_ = 60.0F;
float MoveSpeed_ = 6.0F;
float MoveSpeedBoost_ = 4.0F;
float FramingRadius_ = 10.0F;
};
} // namespace MetaCore

View File

@ -31,6 +31,7 @@
#include <limits>
#include <optional>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace MetaCore {
@ -213,6 +214,60 @@ MetaCoreResolvedPickGeometry MetaCoreResolvePickGeometry(
return MetaCoreResolvedPickGeometry{};
}
void MetaCoreAccumulateTransformedBounds(
const MetaCorePickBounds& localBounds,
const glm::mat4& worldMatrix,
bool& initialized,
glm::vec3& boundsMin,
glm::vec3& boundsMax
) {
for (int cornerIndex = 0; cornerIndex < 8; ++cornerIndex) {
const glm::vec3 localPoint(
(cornerIndex & 1) != 0 ? localBounds.Max.x : localBounds.Min.x,
(cornerIndex & 2) != 0 ? localBounds.Max.y : localBounds.Min.y,
(cornerIndex & 4) != 0 ? localBounds.Max.z : localBounds.Min.z
);
const glm::vec3 worldPoint = glm::vec3(worldMatrix * glm::vec4(localPoint, 1.0F));
if (!MetaCoreIsFiniteVec3(worldPoint)) continue;
if (!initialized) {
boundsMin = worldPoint;
boundsMax = worldPoint;
initialized = true;
} else {
boundsMin = glm::min(boundsMin, worldPoint);
boundsMax = glm::max(boundsMax, worldPoint);
}
}
}
bool MetaCoreComputeWorldRenderBounds(
const MetaCoreEditorContext& editorContext,
const std::unordered_set<MetaCoreId>& objectIds,
glm::vec3& boundsMin,
glm::vec3& boundsMax
) {
const auto assetEditingService =
editorContext.GetModuleRegistry().ResolveService<MetaCoreIAssetEditingService>();
std::unordered_map<MetaCoreAssetGuid, std::optional<MetaCoreModelAssetDocument>, MetaCoreAssetGuidHasher>
modelAssetCache;
bool initialized = false;
for (const auto& object : editorContext.GetScene().GetGameObjects()) {
if (!objectIds.empty() && !objectIds.contains(object.GetId())) continue;
if (!object.HasComponent<MetaCoreMeshRendererComponent>()) continue;
const auto& renderer = object.GetComponent<MetaCoreMeshRendererComponent>();
if (!renderer.Visible) continue;
const auto geometry = MetaCoreResolvePickGeometry(renderer, assetEditingService, modelAssetCache);
MetaCoreAccumulateTransformedBounds(
geometry.Bounds,
MetaCoreBuildWorldTransformMatrix(editorContext.GetScene(), object.GetId()),
initialized,
boundsMin,
boundsMax
);
}
return initialized;
}
bool MetaCoreIntersectLocalRayWithAabb(
const glm::vec3& rayOrigin,
const glm::vec3& rayDirection,
@ -660,92 +715,37 @@ void MetaCoreSceneInteractionService::FocusSelectedObject(MetaCoreEditorContext&
return;
}
if (selectedObjectIds.size() == 1) {
MetaCoreGameObject selectedObject = editorContext.GetSelectedGameObject();
if (selectedObject) {
editorContext.GetCameraController().FocusGameObject(selectedObject);
}
return;
std::unordered_set<MetaCoreId> focusObjectIds;
for (const MetaCoreId selectedObjectId : selectedObjectIds) {
const auto subtree = editorContext.GetScene().GetSubtreeObjectIds(selectedObjectId);
focusObjectIds.insert(subtree.begin(), subtree.end());
}
bool initialized = false;
glm::vec3 boundsMin(0.0F);
glm::vec3 boundsMax(0.0F);
for (const MetaCoreId selectedObjectId : selectedObjectIds) {
const MetaCoreGameObject selectedObject = editorContext.GetScene().FindGameObject(selectedObjectId);
if (!selectedObject) {
continue;
}
const float extent = std::max({
std::abs(selectedObject.GetComponent<MetaCoreTransformComponent>().Scale.x),
std::abs(selectedObject.GetComponent<MetaCoreTransformComponent>().Scale.y),
std::abs(selectedObject.GetComponent<MetaCoreTransformComponent>().Scale.z),
0.5F
});
const glm::vec3 objectMin = selectedObject.GetComponent<MetaCoreTransformComponent>().Position - glm::vec3(extent);
const glm::vec3 objectMax = selectedObject.GetComponent<MetaCoreTransformComponent>().Position + glm::vec3(extent);
if (!initialized) {
boundsMin = objectMin;
boundsMax = objectMax;
initialized = true;
continue;
}
boundsMin = glm::min(boundsMin, objectMin);
boundsMax = glm::max(boundsMax, objectMax);
}
if (!initialized) {
if (!MetaCoreComputeWorldRenderBounds(editorContext, focusObjectIds, boundsMin, boundsMax)) {
const MetaCoreGameObject selectedObject = editorContext.GetSelectedGameObject();
if (selectedObject) editorContext.GetCameraController().FocusGameObject(selectedObject);
return;
}
const glm::vec3 center = (boundsMin + boundsMax) * 0.5F;
const glm::vec3 size = boundsMax - boundsMin;
const float radius = std::max({size.x, size.y, size.z, 1.0F}) * 0.5F;
const float radius = std::max(glm::length(size) * 0.5F, 0.5F);
editorContext.GetCameraController().FocusBounds(center, radius);
}
void MetaCoreSceneInteractionService::FocusVisibleScene(MetaCoreEditorContext& editorContext) const {
const auto& gameObjects = editorContext.GetScene().GetGameObjects();
bool initialized = false;
glm::vec3 boundsMin(0.0F);
glm::vec3 boundsMax(0.0F);
for (const MetaCoreGameObject& gameObject : gameObjects) {
if (gameObject.HasComponent<MetaCoreMeshRendererComponent>() && !gameObject.GetComponent<MetaCoreMeshRendererComponent>().Visible) {
continue;
}
const float extent = std::max({
std::abs(gameObject.GetComponent<MetaCoreTransformComponent>().Scale.x),
std::abs(gameObject.GetComponent<MetaCoreTransformComponent>().Scale.y),
std::abs(gameObject.GetComponent<MetaCoreTransformComponent>().Scale.z),
0.5F
});
const glm::vec3 objectMin = gameObject.GetComponent<MetaCoreTransformComponent>().Position - glm::vec3(extent);
const glm::vec3 objectMax = gameObject.GetComponent<MetaCoreTransformComponent>().Position + glm::vec3(extent);
if (!initialized) {
boundsMin = objectMin;
boundsMax = objectMax;
initialized = true;
continue;
}
boundsMin = glm::min(boundsMin, objectMin);
boundsMax = glm::max(boundsMax, objectMax);
}
if (!initialized) {
if (!MetaCoreComputeWorldRenderBounds(editorContext, {}, boundsMin, boundsMax)) {
editorContext.GetCameraController().FocusBounds(glm::vec3(0.0F, 0.5F, 0.0F), 2.0F);
return;
}
const glm::vec3 center = (boundsMin + boundsMax) * 0.5F;
const glm::vec3 size = boundsMax - boundsMin;
const float radius = std::max({size.x, size.y, size.z, 1.0F}) * 0.5F;
const float radius = std::max(glm::length(size) * 0.5F, 0.5F);
editorContext.GetCameraController().FocusBounds(center, radius);
}

View File

@ -9,6 +9,7 @@
#include "MetaCoreRender/MetaCoreEditorViewportRenderer.h"
#include "MetaCoreRender/MetaCoreRenderDevice.h"
#include "MetaCoreScene/MetaCoreScene.h"
#include "MetaCoreScene/MetaCoreSceneWorld.h"
#include <memory>
#include <vector>
@ -38,6 +39,7 @@ private:
MetaCoreRenderDevice RenderDevice_{};
MetaCoreEditorViewportRenderer ViewportRenderer_{};
MetaCoreScene Scene_{};
MetaCoreSceneWorld SceneWorld_{Scene_};
MetaCoreLogService LogService_{};
MetaCoreEditorModuleRegistry ModuleRegistry_{};
MetaCoreSceneInteractionService SceneInteractionService_{};

View File

@ -28,6 +28,7 @@ class MetaCoreGameObject;
class MetaCoreEditorModuleRegistry;
class MetaCoreEditorCameraController;
class MetaCorePhysicsWorld;
class MetaCoreSceneWorld;
enum class MetaCoreGizmoOperation {
None = 0,
@ -125,6 +126,8 @@ public:
[[nodiscard]] MetaCoreEditorViewportRenderer& GetViewportRenderer();
[[nodiscard]] MetaCoreScene& GetScene();
[[nodiscard]] const MetaCoreScene& GetScene() const;
void SetSceneWorld(MetaCoreSceneWorld* sceneWorld) { SceneWorld_ = sceneWorld; }
[[nodiscard]] MetaCoreSceneWorld* GetSceneWorld() const { return SceneWorld_; }
[[nodiscard]] MetaCoreLogService& GetLogService();
[[nodiscard]] const MetaCoreLogService& GetLogService() const;
[[nodiscard]] MetaCoreEditorModuleRegistry& GetModuleRegistry();
@ -219,6 +222,7 @@ private:
MetaCoreRenderDevice& RenderDevice_;
MetaCoreEditorViewportRenderer& ViewportRenderer_;
MetaCoreScene& Scene_;
MetaCoreSceneWorld* SceneWorld_ = nullptr;
MetaCoreLogService& LogService_;
MetaCoreEditorModuleRegistry& ModuleRegistry_;
std::unique_ptr<MetaCoreEditorCameraController> CameraController_;

View File

@ -7,6 +7,7 @@
#include "MetaCoreFoundation/MetaCorePackage.h"
#include "MetaCoreFoundation/MetaCoreReflection.h"
#include "MetaCoreScene/MetaCoreScene.h"
#include "MetaCoreScene/MetaCoreSceneWorld.h"
#include "MetaCoreScripting/MetaCoreScripting.h"
#include <cstddef>
@ -310,8 +311,13 @@ public:
[[nodiscard]] virtual std::vector<std::filesystem::path> GetKnownScenePaths() const = 0;
virtual bool LoadStartupScene(MetaCoreEditorContext& editorContext) = 0;
virtual bool LoadScene(MetaCoreEditorContext& editorContext, const std::filesystem::path& relativeScenePath) = 0;
virtual bool LoadSceneAdditive(MetaCoreEditorContext& editorContext, const std::filesystem::path& relativeScenePath) = 0;
virtual bool UnloadScene(MetaCoreEditorContext& editorContext, const MetaCoreAssetGuid& sceneGuid, bool force) = 0;
virtual bool SetActiveScene(MetaCoreEditorContext& editorContext, const MetaCoreAssetGuid& sceneGuid) = 0;
virtual bool SaveCurrentScene(MetaCoreEditorContext& editorContext) = 0;
virtual bool SaveSceneAs(MetaCoreEditorContext& editorContext, const std::filesystem::path& relativeScenePath) = 0;
virtual bool SaveAllScenes(MetaCoreEditorContext& editorContext) = 0;
[[nodiscard]] virtual std::vector<MetaCoreLoadedSceneInfo> GetOpenScenes(const MetaCoreEditorContext& editorContext) const = 0;
virtual void UpdateDirtyState(const MetaCoreSceneSnapshot& snapshot) = 0;
};

View File

@ -183,8 +183,18 @@ void MetaCoreFilamentMaterialDebugLog(const std::string& message) {
return stream.str();
}
[[nodiscard]] bool MetaCorePreserveGltfBlendAlphaMode() {
const char* preserveBlend = std::getenv("METACORE_GLTF_PRESERVE_BLEND_ALPHA");
[[nodiscard]] bool MetaCoreForceGltfBlendAsMask() {
const char* forceMask = std::getenv("METACORE_GLTF_FORCE_BLEND_AS_MASK");
if (forceMask == nullptr) {
return false;
}
return std::strcmp(forceMask, "1") == 0 ||
std::strcmp(forceMask, "true") == 0 ||
std::strcmp(forceMask, "TRUE") == 0;
}
[[nodiscard]] bool MetaCorePreserveDoubleSidedGltfBlend() {
const char* preserveBlend = std::getenv("METACORE_GLTF_PRESERVE_DOUBLE_SIDED_BLEND");
if (preserveBlend == nullptr) {
return false;
}
@ -245,17 +255,29 @@ public:
private:
void NormalizeAlphaMode(filament::gltfio::MaterialKey* config, const char* label) {
if (config == nullptr ||
MetaCorePreserveGltfBlendAlphaMode() ||
config->alphaMode != filament::gltfio::AlphaMode::BLEND) {
return;
}
const bool forceAllBlendAsMask = MetaCoreForceGltfBlendAsMask();
const bool stabilizeDoubleSidedCutout =
config->doubleSided && !MetaCorePreserveDoubleSidedGltfBlend();
if (!forceAllBlendAsMask && !stabilizeDoubleSidedCutout) {
return;
}
config->alphaMode = filament::gltfio::AlphaMode::MASK;
if (!LoggedBlendAsMask_) {
MetaCoreFilamentDebugLog(
"GLTF alphaMode=BLEND is rendered as MASK by default for stable foliage depth. "
"Set METACORE_GLTF_PRESERVE_BLEND_ALPHA=1 to keep GLTF blend materials."
);
if (forceAllBlendAsMask) {
MetaCoreFilamentDebugLog(
"GLTF alphaMode=BLEND is forced to MASK by METACORE_GLTF_FORCE_BLEND_AS_MASK."
);
} else {
MetaCoreFilamentDebugLog(
"GLTF double-sided BLEND materials use MASK for stable foliage depth. "
"Set METACORE_GLTF_PRESERVE_DOUBLE_SIDED_BLEND=1 for true double-sided transparency."
);
}
LoggedBlendAsMask_ = true;
}
if (label != nullptr && label[0] != '\0') {
@ -1620,6 +1642,12 @@ public:
CurrentSceneView_.NearClip, CurrentSceneView_.FarClip);
const glm::mat4 viewProjection = projectionMatrix * viewMatrix;
const MetaCoreViewFrustum frustum = MetaCoreExtractFrustum(viewProjection);
// gltfio already owns exact per-primitive bounds for imported assets. The ECS
// snapshot currently only has a unit transform proxy for these nodes, which is
// not a conservative bound for large or offset meshes. Running that proxy
// through the engine BVH / software HZB causes whole model sections to pop as
// their node origins cross the view frustum. Keep hierarchy/layer/distance
// visibility here, then leave geometric frustum culling to Filament.
std::vector<MetaCoreBvhItem> visibilityItems;
visibilityItems.reserve(resolvedSnapshot.Renderables.size());
for (const auto& renderable : resolvedSnapshot.Renderables) {
@ -1628,6 +1656,9 @@ public:
MetaCoreAccumulateVisibilityResult(RenderingStatistics_, lodCulled ? MetaCoreVisibilityResult::LodCulled : MetaCoreVisibilityResult::DistanceCulled);
continue;
}
if (MetaCoreUsesNativeGltfVisibility(renderable)) {
continue;
}
visibilityItems.push_back({renderable.ObjectId, renderable.WorldBounds, renderable.RenderingLayerMask,
renderable.MaxDrawDistance, !renderable.StaticForCulling});
}
@ -1652,7 +1683,40 @@ public:
};
std::unordered_set<MetaCoreId> currentVisible;
for (auto& renderable : resolvedSnapshot.Renderables) {
if (!renderable.Visible || !bvhVisibleSet.contains(renderable.ObjectId)) { renderable.Visible=false; continue; }
if (!renderable.Visible) {
continue;
}
if (MetaCoreUsesNativeGltfVisibility(renderable)) {
const bool layerVisible =
(renderable.RenderingLayerMask & CurrentSceneView_.CullingMask) != 0U;
const bool distanceVisible =
renderable.MaxDrawDistance <= 0.0F ||
glm::distance(
CurrentSceneView_.CameraPosition,
glm::vec3(renderable.WorldMatrix[3])
) <= renderable.MaxDrawDistance;
renderable.Visible = layerVisible && distanceVisible;
if (renderable.Visible) {
currentVisible.insert(renderable.ObjectId);
MetaCoreAccumulateVisibilityResult(
RenderingStatistics_,
MetaCoreVisibilityResult::Visible,
renderable.TriangleCount
);
RenderingStatistics_.EstimatedVideoMemoryBytes +=
renderable.EstimatedVideoMemoryBytes;
} else {
MetaCoreAccumulateVisibilityResult(
RenderingStatistics_,
MetaCoreVisibilityResult::DistanceCulled
);
}
continue;
}
if (!bvhVisibleSet.contains(renderable.ObjectId)) {
renderable.Visible = false;
continue;
}
bool queryVisible=true;glm::vec4 screenBounds{};float nearestDepth=0.0F,farthestDepth=0.0F;
if(RenderSettings_.EnableOcclusion&&projectBounds(renderable.WorldBounds,screenBounds,nearestDepth,farthestDepth))queryVisible=PreviousHzb_.IsVisible(screenBounds,nearestDepth);
const bool newlyVisible=!LastFrameVisibleObjects_.contains(renderable.ObjectId);
@ -1663,7 +1727,7 @@ public:
}
constexpr std::uint32_t hzbWidth=64U,hzbHeight=36U;
std::vector<float> softwareDepth(static_cast<std::size_t>(hzbWidth)*hzbHeight,CurrentSceneView_.FarClip);
for(const auto& renderable:resolvedSnapshot.Renderables){if(!renderable.Visible||!renderable.StaticForCulling)continue;glm::vec4 bounds{};float nearestDepth=0.0F,farthestDepth=0.0F;if(!projectBounds(renderable.WorldBounds,bounds,nearestDepth,farthestDepth))continue;const std::uint32_t minX=std::min(hzbWidth-1U,static_cast<std::uint32_t>(std::clamp(bounds.x,0.0F,1.0F)*hzbWidth)),maxX=std::min(hzbWidth-1U,static_cast<std::uint32_t>(std::clamp(bounds.z,0.0F,1.0F)*hzbWidth));const std::uint32_t minY=std::min(hzbHeight-1U,static_cast<std::uint32_t>(std::clamp(bounds.y,0.0F,1.0F)*hzbHeight)),maxY=std::min(hzbHeight-1U,static_cast<std::uint32_t>(std::clamp(bounds.w,0.0F,1.0F)*hzbHeight));for(std::uint32_t y=minY;y<=maxY;++y)for(std::uint32_t x=minX;x<=maxX;++x)softwareDepth[static_cast<std::size_t>(y)*hzbWidth+x]=std::min(softwareDepth[static_cast<std::size_t>(y)*hzbWidth+x],farthestDepth);}
for(const auto& renderable:resolvedSnapshot.Renderables){if(!renderable.Visible||!renderable.StaticForCulling||MetaCoreUsesNativeGltfVisibility(renderable))continue;glm::vec4 bounds{};float nearestDepth=0.0F,farthestDepth=0.0F;if(!projectBounds(renderable.WorldBounds,bounds,nearestDepth,farthestDepth))continue;const std::uint32_t minX=std::min(hzbWidth-1U,static_cast<std::uint32_t>(std::clamp(bounds.x,0.0F,1.0F)*hzbWidth)),maxX=std::min(hzbWidth-1U,static_cast<std::uint32_t>(std::clamp(bounds.z,0.0F,1.0F)*hzbWidth));const std::uint32_t minY=std::min(hzbHeight-1U,static_cast<std::uint32_t>(std::clamp(bounds.y,0.0F,1.0F)*hzbHeight)),maxY=std::min(hzbHeight-1U,static_cast<std::uint32_t>(std::clamp(bounds.w,0.0F,1.0F)*hzbHeight));for(std::uint32_t y=minY;y<=maxY;++y)for(std::uint32_t x=minX;x<=maxX;++x)softwareDepth[static_cast<std::size_t>(y)*hzbWidth+x]=std::min(softwareDepth[static_cast<std::size_t>(y)*hzbWidth+x],farthestDepth);}
(void)PreviousHzb_.Build(hzbWidth,hzbHeight,softwareDepth);LastFrameVisibleObjects_=std::move(currentVisible);PreviousVisibilityCameraPosition_=CurrentSceneView_.CameraPosition;PreviousVisibilityCameraDirection_=cameraDirection;HasPreviousVisibilityCamera_=true;
std::vector<MetaCoreInstanceCandidate> instanceCandidates;
instanceCandidates.reserve(snapshot.Renderables.size());

View File

@ -60,6 +60,14 @@ struct MetaCoreRenderSyncRenderable {
std::uint64_t EstimatedVideoMemoryBytes = 0U;
};
[[nodiscard]] inline bool MetaCoreUsesNativeGltfVisibility(
const MetaCoreRenderSyncRenderable& renderable
) {
return renderable.MeshSource == MetaCoreMeshSourceKind::Asset &&
!renderable.SourceModelPath.empty() &&
renderable.HostRootId != 0;
}
struct MetaCoreRenderSyncCamera {
MetaCoreId ObjectId = 0;
bool IsPrimary = false;

View File

@ -0,0 +1,388 @@
#include "MetaCoreScene/MetaCorePrefab.h"
#include "MetaCoreScene/MetaCoreSceneWorld.h"
#include <algorithm>
#include <nlohmann/json.hpp>
#include <unordered_set>
namespace MetaCore {
namespace {
using json = nlohmann::json;
glm::vec3 ReadVec3(std::string_view value, bool& valid) {
try {
const json parsed = json::parse(value);
if (!parsed.is_array() || parsed.size() < 3) return {};
valid = true;
return {parsed[0].get<float>(), parsed[1].get<float>(), parsed[2].get<float>()};
} catch (...) {
return {};
}
}
template <typename T>
bool ReadScalar(std::string_view value, T& output) {
try {
output = json::parse(value).get<T>();
return true;
} catch (...) {
return false;
}
}
MetaCoreGameObjectData* FindNode(MetaCorePrefabDocument& document, const MetaCoreAssetGuid& nodeGuid) {
const auto iterator = std::find_if(document.GameObjects.begin(), document.GameObjects.end(), [&](const auto& object) {
return object.StableObjectGuid == nodeGuid;
});
return iterator == document.GameObjects.end() ? nullptr : &*iterator;
}
const MetaCoreGameObjectData* FindNode(const MetaCorePrefabDocument& document, const MetaCoreAssetGuid& nodeGuid) {
const auto iterator = std::find_if(document.GameObjects.begin(), document.GameObjects.end(), [&](const auto& object) {
return object.StableObjectGuid == nodeGuid;
});
return iterator == document.GameObjects.end() ? nullptr : &*iterator;
}
void NormalizePrefab(MetaCorePrefabDocument& document) {
if (!document.PrefabGuid.IsValid()) document.PrefabGuid = MetaCoreAssetGuid::Generate();
for (auto& object : document.GameObjects) {
if (!object.StableObjectGuid.IsValid())
object.StableObjectGuid = MetaCoreBuildDeterministicGuid(document.PrefabGuid, object.Id);
}
document.SchemaVersion = GMetaCorePrefabSchemaVersion;
}
void MergeVariantObjects(MetaCorePrefabDocument& target, const MetaCorePrefabDocument& layer) {
for (const auto& object : layer.GameObjects) {
const auto iterator = std::find_if(target.GameObjects.begin(), target.GameObjects.end(), [&](const auto& current) {
return current.StableObjectGuid == object.StableObjectGuid;
});
if (iterator == target.GameObjects.end()) target.GameObjects.push_back(object);
else *iterator = object;
}
}
} // namespace
MetaCorePrefabOverrideRegistry::MetaCorePrefabOverrideRegistry() {
RegisterPropertyHandler("Name", [](MetaCoreGameObjectData& object, std::string_view path, std::string_view value) {
return path == "Value" && ReadScalar(value, object.Name);
});
RegisterPropertyHandler("Transform", [](MetaCoreGameObjectData& object, std::string_view path, std::string_view value) {
bool valid = false;
const glm::vec3 parsed = ReadVec3(value, valid);
if (!valid) return false;
if (path == "Position") object.Transform.Position = parsed;
else if (path == "RotationEulerDegrees") object.Transform.RotationEulerDegrees = parsed;
else if (path == "Scale") object.Transform.Scale = parsed;
else return false;
return true;
});
RegisterPropertyHandler("Active", [](MetaCoreGameObjectData& object, std::string_view path, std::string_view value) {
if (path != "Active") return false;
if (!object.Active) object.Active = MetaCoreActiveComponent{};
return ReadScalar(value, object.Active->Active);
});
RegisterPropertyHandler("Camera", [](MetaCoreGameObjectData& object, std::string_view path, std::string_view value) {
if (!object.Camera) object.Camera = MetaCoreCameraComponent{};
if (path == "FieldOfViewDegrees") return ReadScalar(value, object.Camera->FieldOfViewDegrees);
if (path == "NearClip") return ReadScalar(value, object.Camera->NearClip);
if (path == "FarClip") return ReadScalar(value, object.Camera->FarClip);
if (path == "Enabled") return ReadScalar(value, object.Camera->Enabled);
if (path == "IsPrimary") return ReadScalar(value, object.Camera->IsPrimary);
return false;
});
RegisterPropertyHandler("Light", [](MetaCoreGameObjectData& object, std::string_view path, std::string_view value) {
if (!object.Light) object.Light = MetaCoreLightComponent{};
if (path == "Intensity") return ReadScalar(value, object.Light->Intensity);
if (path == "Enabled") return ReadScalar(value, object.Light->Enabled);
if (path == "Color") {
bool valid = false;
const auto parsed = ReadVec3(value, valid);
if (valid) object.Light->Color = parsed;
return valid;
}
return false;
});
}
void MetaCorePrefabOverrideRegistry::RegisterPropertyHandler(std::string componentTypeId, PropertyHandler handler) {
PropertyHandlers_[std::move(componentTypeId)] = std::move(handler);
}
bool MetaCorePrefabOverrideRegistry::Apply(
MetaCorePrefabDocument& document,
const MetaCorePrefabOverrideDocument& overrideValue
) const {
MetaCoreGameObjectData* object = FindNode(document, overrideValue.TargetNodeGuid);
if (!object) return false;
switch (overrideValue.Operation) {
case MetaCorePrefabOverrideOperation::SetProperty: {
const auto handler = PropertyHandlers_.find(overrideValue.ComponentTypeId);
return handler != PropertyHandlers_.end() &&
handler->second(*object, overrideValue.PropertyPath, overrideValue.ValueJson);
}
case MetaCorePrefabOverrideOperation::AddComponent:
if (overrideValue.ComponentTypeId == "Active") object->Active = MetaCoreActiveComponent{};
else if (overrideValue.ComponentTypeId == "Camera") object->Camera = MetaCoreCameraComponent{};
else if (overrideValue.ComponentTypeId == "Light") object->Light = MetaCoreLightComponent{};
else if (overrideValue.ComponentTypeId == "MeshRenderer") object->MeshRenderer = MetaCoreMeshRendererComponent{};
else if (overrideValue.ComponentTypeId == "Collider") object->Collider = MetaCoreColliderComponent{};
else if (overrideValue.ComponentTypeId == "RigidBody") object->RigidBody = MetaCoreRigidBodyComponent{};
else if (overrideValue.ComponentTypeId == "Rotator") object->Rotator = MetaCoreRotatorComponent{};
else if (overrideValue.ComponentTypeId == "Animator") object->Animator = MetaCoreAnimatorComponent{};
else if (overrideValue.ComponentTypeId == "TimelineDirector") object->TimelineDirector = MetaCoreTimelineDirectorComponent{};
else if (overrideValue.ComponentTypeId == "Script") object->Script = MetaCoreScriptComponent{};
else if (overrideValue.ComponentTypeId == "UiRenderer") object->UiRenderer = MetaCoreUiRendererComponent{};
else if (overrideValue.ComponentTypeId == "Interactable") object->Interactable = MetaCoreInteractableComponent{};
else if (overrideValue.ComponentTypeId == "PhysicsConstraint") object->PhysicsConstraint = MetaCorePhysicsConstraintComponent{};
else if (overrideValue.ComponentTypeId == "CharacterController") object->CharacterController = MetaCoreCharacterControllerComponent{};
else if (overrideValue.ComponentTypeId == "ReflectionProbe") object->ReflectionProbe = MetaCoreReflectionProbeComponent{};
else if (overrideValue.ComponentTypeId == "LodGroup") object->LodGroup = MetaCoreLodGroupComponent{};
else if (overrideValue.ComponentTypeId == "ParticleEmitter") object->ParticleEmitter = MetaCoreParticleEmitterComponent{};
else if (overrideValue.ComponentTypeId == "LineRenderer") object->LineRenderer = MetaCoreLineRendererComponent{};
else if (overrideValue.ComponentTypeId == "TrailRenderer") object->TrailRenderer = MetaCoreTrailRendererComponent{};
else if (overrideValue.ComponentTypeId == "DecalProjector") object->DecalProjector = MetaCoreDecalProjectorComponent{};
else if (overrideValue.ComponentTypeId == "Terrain") object->Terrain = MetaCoreTerrainComponent{};
else return false;
return true;
case MetaCorePrefabOverrideOperation::RemoveComponent:
if (overrideValue.ComponentTypeId == "Active") object->Active.reset();
else if (overrideValue.ComponentTypeId == "Camera") object->Camera.reset();
else if (overrideValue.ComponentTypeId == "Light") object->Light.reset();
else if (overrideValue.ComponentTypeId == "MeshRenderer") object->MeshRenderer.reset();
else if (overrideValue.ComponentTypeId == "Collider") object->Collider.reset();
else if (overrideValue.ComponentTypeId == "RigidBody") object->RigidBody.reset();
else if (overrideValue.ComponentTypeId == "Rotator") object->Rotator.reset();
else if (overrideValue.ComponentTypeId == "Animator") object->Animator.reset();
else if (overrideValue.ComponentTypeId == "TimelineDirector") object->TimelineDirector.reset();
else if (overrideValue.ComponentTypeId == "Script") object->Script.reset();
else if (overrideValue.ComponentTypeId == "UiRenderer") object->UiRenderer.reset();
else if (overrideValue.ComponentTypeId == "Interactable") object->Interactable.reset();
else if (overrideValue.ComponentTypeId == "PhysicsConstraint") object->PhysicsConstraint.reset();
else if (overrideValue.ComponentTypeId == "CharacterController") object->CharacterController.reset();
else if (overrideValue.ComponentTypeId == "ReflectionProbe") object->ReflectionProbe.reset();
else if (overrideValue.ComponentTypeId == "LodGroup") object->LodGroup.reset();
else if (overrideValue.ComponentTypeId == "ParticleEmitter") object->ParticleEmitter.reset();
else if (overrideValue.ComponentTypeId == "LineRenderer") object->LineRenderer.reset();
else if (overrideValue.ComponentTypeId == "TrailRenderer") object->TrailRenderer.reset();
else if (overrideValue.ComponentTypeId == "DecalProjector") object->DecalProjector.reset();
else if (overrideValue.ComponentTypeId == "Terrain") object->Terrain.reset();
else return false;
return true;
case MetaCorePrefabOverrideOperation::RemoveChild: {
std::unordered_set<MetaCoreId> removed{object->Id};
bool changed = true;
while (changed) {
changed = false;
for (const auto& candidate : document.GameObjects)
if (removed.contains(candidate.ParentId) && removed.insert(candidate.Id).second) changed = true;
}
std::erase_if(document.GameObjects, [&](const auto& candidate) { return removed.contains(candidate.Id); });
return true;
}
case MetaCorePrefabOverrideOperation::Reparent: {
const auto parentGuid = MetaCoreAssetGuid::Parse(overrideValue.ValueJson);
if (!parentGuid) return false;
const auto* parent = FindNode(document, *parentGuid);
if (!parent || parent->Id == object->Id) return false;
object->ParentId = parent->Id;
return true;
}
case MetaCorePrefabOverrideOperation::AddChild: {
try {
const json value = json::parse(overrideValue.ValueJson);
MetaCoreGameObjectData child;
child.Id = 1;
for (const auto& current : document.GameObjects) child.Id = std::max(child.Id, current.Id + 1);
child.ParentId = object->Id;
child.Name = value.value("Name", std::string("GameObject"));
child.StableObjectGuid = MetaCoreAssetGuid::Parse(value.value("StableObjectGuid", std::string{}))
.value_or(MetaCoreBuildDeterministicGuid(document.PrefabGuid, child.Id));
if (value.contains("Position")) {
bool valid = false;
child.Transform.Position = ReadVec3(value["Position"].dump(), valid);
if (!valid) return false;
}
if (value.contains("RotationEulerDegrees")) {
bool valid = false;
child.Transform.RotationEulerDegrees = ReadVec3(value["RotationEulerDegrees"].dump(), valid);
if (!valid) return false;
}
if (value.contains("Scale")) {
bool valid = false;
child.Transform.Scale = ReadVec3(value["Scale"].dump(), valid);
if (!valid) return false;
}
document.GameObjects.push_back(std::move(child));
return true;
} catch (...) {
return false;
}
}
default:
return false;
}
}
MetaCorePrefabResolver::MetaCorePrefabResolver(PrefabLoader loader, MetaCorePrefabOverrideRegistry registry)
: Loader_(std::move(loader)), OverrideRegistry_(std::move(registry)) {}
std::optional<MetaCoreResolvedPrefab> MetaCorePrefabResolver::Resolve(
const MetaCoreAssetGuid& prefabGuid,
std::string* error
) const {
std::vector<MetaCoreAssetGuid> stack;
return ResolveInternal(prefabGuid, stack, error);
}
std::optional<MetaCoreResolvedPrefab> MetaCorePrefabResolver::ResolveInternal(
const MetaCoreAssetGuid& prefabGuid,
std::vector<MetaCoreAssetGuid>& stack,
std::string* error
) const {
if (!prefabGuid.IsValid() || !Loader_) {
if (error) *error = "Prefab GUID is invalid or no loader is configured";
return std::nullopt;
}
if (std::find(stack.begin(), stack.end(), prefabGuid) != stack.end()) {
if (error) *error = "Prefab base/nesting dependency cycle at " + prefabGuid.ToString();
return std::nullopt;
}
auto layer = Loader_(prefabGuid);
if (!layer) {
if (error) *error = "Prefab asset is missing: " + prefabGuid.ToString();
return std::nullopt;
}
if (layer->SchemaVersion > GMetaCorePrefabSchemaVersion) {
if (error) *error = "Prefab schema is newer than this engine: " + prefabGuid.ToString();
return std::nullopt;
}
if (!layer->PrefabGuid.IsValid()) layer->PrefabGuid = prefabGuid;
NormalizePrefab(*layer);
stack.push_back(prefabGuid);
MetaCoreResolvedPrefab result;
if (layer->BasePrefabGuid.IsValid()) {
auto base = ResolveInternal(layer->BasePrefabGuid, stack, error);
if (!base) {
stack.pop_back();
return std::nullopt;
}
result = std::move(*base);
result.Document.PrefabGuid = layer->PrefabGuid;
result.Document.BasePrefabGuid = layer->BasePrefabGuid;
result.Document.Name = layer->Name;
MergeVariantObjects(result.Document, *layer);
} else {
result.Document = *layer;
}
result.DependencyChain.push_back(prefabGuid);
for (const auto& value : layer->Overrides)
if (!OverrideRegistry_.Apply(result.Document, value)) result.OrphanOverrides.push_back(value);
for (const auto& nested : layer->NestedPrefabs) {
auto nestedResult = ResolveInternal(nested.PrefabAssetGuid, stack, error);
if (!nestedResult) {
stack.pop_back();
return std::nullopt;
}
for (const auto& value : nested.Overrides)
if (!OverrideRegistry_.Apply(nestedResult->Document, value)) result.OrphanOverrides.push_back(value);
const auto* mount = FindNode(result.Document, nested.MountNodeGuid);
if (!mount) {
MetaCorePrefabOverrideDocument orphan;
orphan.TargetNodeGuid = nested.MountNodeGuid;
orphan.ComponentTypeId = "NestedPrefab";
orphan.ValueJson = nested.PrefabAssetGuid.ToString();
result.OrphanOverrides.push_back(std::move(orphan));
continue;
}
const MetaCoreAssetGuid instanceGuid = nested.InstanceGuid.IsValid()
? nested.InstanceGuid
: MetaCoreBuildDeterministicGuid(layer->PrefabGuid, nested.PrefabAssetGuid);
std::unordered_map<MetaCoreId, MetaCoreId> remap;
for (const auto& object : nestedResult->Document.GameObjects)
remap[object.Id] = MetaCoreIdGenerator::Generate();
for (auto object : nestedResult->Document.GameObjects) {
const MetaCoreId oldId = object.Id;
object.Id = remap.at(oldId);
if (object.ParentId == 0) object.ParentId = mount->Id;
else if (const auto parent = remap.find(object.ParentId); parent != remap.end()) object.ParentId = parent->second;
else {
if (error) *error = "Nested prefab contains a missing parent object";
stack.pop_back();
return std::nullopt;
}
object.StableObjectGuid = MetaCoreBuildDeterministicGuid(instanceGuid, object.StableObjectGuid);
result.Document.GameObjects.push_back(std::move(object));
}
result.DependencyChain.insert(result.DependencyChain.end(), nestedResult->DependencyChain.begin(), nestedResult->DependencyChain.end());
result.OrphanOverrides.insert(result.OrphanOverrides.end(), nestedResult->OrphanOverrides.begin(), nestedResult->OrphanOverrides.end());
}
stack.pop_back();
return result;
}
std::optional<std::vector<MetaCoreGameObjectData>> MetaCorePrefabResolver::Instantiate(
const MetaCoreAssetGuid& prefabGuid,
const MetaCoreAssetGuid& instanceGuid,
const std::vector<MetaCorePrefabOverrideDocument>& instanceOverrides,
std::string* error
) const {
if (!instanceGuid.IsValid()) {
if (error) *error = "Prefab instance GUID is invalid";
return std::nullopt;
}
auto resolved = Resolve(prefabGuid, error);
if (!resolved) return std::nullopt;
for (const auto& value : instanceOverrides)
if (!OverrideRegistry_.Apply(resolved->Document, value)) resolved->OrphanOverrides.push_back(value);
std::unordered_map<MetaCoreId, MetaCoreId> remap;
for (const auto& object : resolved->Document.GameObjects) remap[object.Id] = MetaCoreIdGenerator::Generate();
for (auto& object : resolved->Document.GameObjects) {
const MetaCoreId oldId = object.Id;
const MetaCoreAssetGuid nodeGuid = object.StableObjectGuid;
object.Id = remap.at(oldId);
if (object.ParentId != 0) {
const auto parent = remap.find(object.ParentId);
if (parent == remap.end()) {
if (error) *error = "Prefab contains a missing parent object";
return std::nullopt;
}
object.ParentId = parent->second;
}
object.StableObjectGuid = MetaCoreBuildDeterministicGuid(instanceGuid, nodeGuid);
MetaCorePrefabInstanceMetadata metadata;
metadata.PrefabAssetGuid = prefabGuid;
metadata.PrefabObjectId = oldId;
metadata.InstanceGuid = instanceGuid;
metadata.PrefabNodeGuid = nodeGuid;
object.PrefabInstance = metadata;
}
const MetaCoreId rootId = [&] {
const auto root = std::find_if(resolved->Document.GameObjects.begin(), resolved->Document.GameObjects.end(),
[](const auto& object) { return object.ParentId == 0; });
return root == resolved->Document.GameObjects.end() ? MetaCoreId{} : root->Id;
}();
for (auto& object : resolved->Document.GameObjects)
if (object.PrefabInstance) object.PrefabInstance->PrefabInstanceRootId = rootId;
return resolved->Document.GameObjects;
}
std::vector<std::string> MetaCorePrefabResolver::ValidateDependencies(const MetaCoreAssetGuid& prefabGuid) const {
std::string error;
const auto result = Resolve(prefabGuid, &error);
std::vector<std::string> issues;
if (!result) issues.push_back(error);
else if (!result->OrphanOverrides.empty())
issues.push_back("Prefab contains " + std::to_string(result->OrphanOverrides.size()) + " orphan override(s)");
return issues;
}
} // namespace MetaCore

View File

@ -465,6 +465,11 @@ MetaCoreSceneSnapshot MetaCoreScene::CaptureSnapshot() const {
MetaCoreGameObjectData data;
data.Id = obj.GetId();
data.ParentId = obj.GetParentId();
if (obj.HasComponent<MetaCoreSceneObjectIdentityComponent>()) {
const auto& identity = obj.GetComponent<MetaCoreSceneObjectIdentityComponent>();
data.SceneGuid = identity.SceneGuid;
data.StableObjectGuid = identity.StableObjectGuid;
}
data.Name = obj.GetName();
data.Transform = obj.GetComponent<MetaCoreTransformComponent>();
@ -523,6 +528,11 @@ void MetaCoreScene::RestoreSnapshot(const MetaCoreSceneSnapshot& snapshot) {
maxId = std::max(maxId, data.Id);
MetaCoreGameObject obj = CreateGameObjectWithId(data.Id, data.Name, data.ParentId);
obj.GetComponent<MetaCoreTransformComponent>() = data.Transform;
if (data.SceneGuid.IsValid() || data.StableObjectGuid.IsValid()) {
obj.AddComponent<MetaCoreSceneObjectIdentityComponent>(
MetaCoreSceneObjectIdentityComponent{data.SceneGuid, data.StableObjectGuid}
);
}
if (data.Camera.has_value())
obj.AddComponent<MetaCoreCameraComponent>(data.Camera.value());

View File

@ -15,7 +15,8 @@ namespace {
[[nodiscard]] MetaCorePackageDocument MetaCoreBuildTypedScenePackage(
const MetaCoreAssetGuid& assetGuid,
const std::string& objectName,
std::vector<std::byte> payload
std::vector<std::byte> payload,
const MetaCoreSceneDocument& scene
) {
MetaCorePackageDocument document;
document.Header.PackageType = MetaCorePackageType::Scene;
@ -29,6 +30,17 @@ namespace {
0,
static_cast<std::uint64_t>(payload.size())
});
for (const auto& subScene : scene.SubScenes) {
if (subScene.SceneGuid.IsValid())
document.Dependencies.push_back({subScene.SceneGuid, "SubScene"});
}
for (const auto& instance : scene.PrefabInstances) {
if (instance.PrefabAssetGuid.IsValid())
document.Dependencies.push_back({instance.PrefabAssetGuid, "PrefabInstance"});
}
document.CustomVersions.push_back({"MetaCore.Scene", scene.SchemaVersion});
for (const auto& component : scene.ComponentVersions)
document.CustomVersions.push_back({"MetaCore.Component." + component.TypeId, component.Version});
document.PayloadSections.push_back(std::move(payload));
return document;
}
@ -110,9 +122,10 @@ bool MetaCoreWriteScenePackage(
const std::string objectName = path.stem().string().empty() ? "Scene" : path.stem().string();
MetaCorePackageDocument package = MetaCoreBuildTypedScenePackage(
MetaCoreAssetGuid::Generate(),
document.SceneGuid.IsValid() ? document.SceneGuid : MetaCoreAssetGuid::Generate(),
objectName,
*payload
*payload,
document
);
return MetaCoreWritePackageFile(path, std::move(package), registry);
}
@ -123,7 +136,9 @@ std::optional<MetaCoreSceneDocument> MetaCoreReadScenePackage(const std::filesys
if (!package.has_value() || package->Header.PackageType != MetaCorePackageType::Scene) {
return std::nullopt;
}
return MetaCoreReadTypedPayload<MetaCoreSceneDocument>(*package, registry, "MetaCoreSceneDocument");
auto scene = MetaCoreReadTypedPayload<MetaCoreSceneDocument>(*package, registry, "MetaCoreSceneDocument");
if (!scene || scene->SchemaVersion > GMetaCoreSceneSchemaVersion) return std::nullopt;
return scene;
}
std::optional<std::filesystem::path> MetaCoreResolveStartupScenePackagePath(const std::filesystem::path& projectFilePath) {

View File

@ -1,10 +1,12 @@
#include "MetaCoreScene/MetaCoreSceneSerializer.h"
#include "MetaCoreFoundation/MetaCoreReflection.h"
#include "MetaCoreScene/MetaCoreSceneWorld.h"
#include <nlohmann/json.hpp>
#include <fstream>
#include <iostream>
#include <sstream>
#include <span>
#include <unordered_map>
using json = nlohmann::json;
@ -33,6 +35,23 @@ static MetaCoreAssetGuid StringToGuid(const std::string& str) {
return str.empty() ? MetaCoreAssetGuid{} : MetaCoreAssetGuid::Parse(str).value_or(MetaCoreAssetGuid{});
}
static json ObjectReferenceToJson(const MetaCoreObjectReference& reference) {
return {
{"SceneGuid", GuidToString(reference.SceneGuid)},
{"StableObjectGuid", GuidToString(reference.StableObjectGuid)},
{"Strength", static_cast<int>(reference.Strength)}
};
}
static MetaCoreObjectReference JsonToObjectReference(const json& value) {
MetaCoreObjectReference result;
if (!value.is_object()) return result;
result.SceneGuid = StringToGuid(value.value("SceneGuid", std::string{}));
result.StableObjectGuid = StringToGuid(value.value("StableObjectGuid", std::string{}));
result.Strength = static_cast<MetaCoreObjectReferenceStrength>(value.value("Strength", 0));
return result;
}
template <typename T>
static std::string ReflectedValueToHex(const T& value, const MetaCoreTypeRegistry& registry) {
const auto bytes = MetaCoreSerializeToBytes(value, registry); if (!bytes) return {};
@ -89,6 +108,10 @@ static json GameObjectDataToJson(const MetaCoreGameObjectData& obj, const MetaCo
objJson["Id"] = obj.Id;
} else if (objField.Name == "ParentId") {
objJson["ParentId"] = obj.ParentId;
} else if (objField.Name == "SceneGuid") {
objJson["SceneGuid"] = GuidToString(obj.SceneGuid);
} else if (objField.Name == "StableObjectGuid") {
objJson["StableObjectGuid"] = GuidToString(obj.StableObjectGuid);
} else if (objField.Name == "Name") {
objJson["Name"] = obj.Name;
} else if (objField.Name == "Transform") {
@ -205,7 +228,11 @@ static json GameObjectDataToJson(const MetaCoreGameObjectData& obj, const MetaCo
} else if (objField.Name == "TimelineDirector" && obj.TimelineDirector.has_value()) {
const auto& director = *obj.TimelineDirector;
json bindings = json::array();
for (const auto& binding : director.Bindings) bindings.push_back({{"Slot", binding.Slot}, {"ObjectId", binding.ObjectId}});
for (const auto& binding : director.Bindings) bindings.push_back({
{"Slot", binding.Slot},
{"ObjectId", binding.ObjectId},
{"ObjectReference", ObjectReferenceToJson(binding.ObjectReference)}
});
objJson["TimelineDirector"] = {{"Enabled", director.Enabled}, {"TimelineAssetGuid", GuidToString(director.TimelineAssetGuid)},
{"PlayOnStart", director.PlayOnStart}, {"Loop", director.Loop}, {"Speed", director.Speed}, {"Bindings", std::move(bindings)}};
} else if (objField.Name == "Script" && obj.Script.has_value()) {
@ -261,7 +288,8 @@ static json GameObjectDataToJson(const MetaCoreGameObjectData& obj, const MetaCo
} else if (objField.Name == "PhysicsConstraint" && obj.PhysicsConstraint.has_value()) {
const auto& value = *obj.PhysicsConstraint;
objJson["PhysicsConstraint"] = {{"Enabled", value.Enabled}, {"Type", static_cast<int>(value.Type)},
{"TargetObjectId", value.TargetObjectId}, {"Anchor", Vec3ToJson(value.Anchor)},
{"TargetObjectId", value.TargetObjectId}, {"TargetObjectReference", ObjectReferenceToJson(value.TargetObjectReference)},
{"Anchor", Vec3ToJson(value.Anchor)},
{"TargetAnchor", Vec3ToJson(value.TargetAnchor)}, {"Axis", Vec3ToJson(value.Axis)},
{"LinearLowerLimit", Vec3ToJson(value.LinearLowerLimit)}, {"LinearUpperLimit", Vec3ToJson(value.LinearUpperLimit)},
{"AngularLowerLimit", Vec3ToJson(value.AngularLowerLimit)}, {"AngularUpperLimit", Vec3ToJson(value.AngularUpperLimit)},
@ -310,6 +338,8 @@ static json GameObjectDataToJson(const MetaCoreGameObjectData& obj, const MetaCo
if (prefabField.Name == "PrefabAssetGuid") prefabJson["PrefabAssetGuid"] = GuidToString(obj.PrefabInstance->PrefabAssetGuid);
else if (prefabField.Name == "PrefabObjectId") prefabJson["PrefabObjectId"] = obj.PrefabInstance->PrefabObjectId;
else if (prefabField.Name == "PrefabInstanceRootId") prefabJson["PrefabInstanceRootId"] = obj.PrefabInstance->PrefabInstanceRootId;
else if (prefabField.Name == "InstanceGuid") prefabJson["InstanceGuid"] = GuidToString(obj.PrefabInstance->InstanceGuid);
else if (prefabField.Name == "PrefabNodeGuid") prefabJson["PrefabNodeGuid"] = GuidToString(obj.PrefabInstance->PrefabNodeGuid);
}
objJson["PrefabInstance"] = prefabJson;
}
@ -329,6 +359,10 @@ static MetaCoreGameObjectData JsonToGameObjectData(const json& objJson, const Me
obj.Id = objJson["Id"].get<MetaCoreId>();
} else if (objField.Name == "ParentId" && objJson.contains("ParentId")) {
obj.ParentId = objJson["ParentId"].get<MetaCoreId>();
} else if (objField.Name == "SceneGuid" && objJson.contains("SceneGuid")) {
obj.SceneGuid = StringToGuid(objJson["SceneGuid"].get<std::string>());
} else if (objField.Name == "StableObjectGuid" && objJson.contains("StableObjectGuid")) {
obj.StableObjectGuid = StringToGuid(objJson["StableObjectGuid"].get<std::string>());
} else if (objField.Name == "Name" && objJson.contains("Name")) {
obj.Name = objJson["Name"].get<std::string>();
} else if (objField.Name == "Transform" && objJson.contains("Transform")) {
@ -457,7 +491,13 @@ static MetaCoreGameObjectData JsonToGameObjectData(const json& objJson, const Me
director.PlayOnStart = value.value("PlayOnStart", true);
director.Loop = value.value("Loop", false);
director.Speed = value.value("Speed", 1.0F);
for (const auto& binding : value.value("Bindings", json::array())) director.Bindings.push_back({binding.value("Slot", ""), binding.value("ObjectId", MetaCoreId{})});
for (const auto& binding : value.value("Bindings", json::array())) {
MetaCoreTimelineBinding result;
result.Slot = binding.value("Slot", "");
result.ObjectId = binding.value("ObjectId", MetaCoreId{});
if (binding.contains("ObjectReference")) result.ObjectReference = JsonToObjectReference(binding["ObjectReference"]);
director.Bindings.push_back(std::move(result));
}
obj.TimelineDirector = std::move(director);
} else if (objField.Name == "Script" && objJson.contains("Script")) {
const auto& scriptJson = objJson["Script"];
@ -537,6 +577,7 @@ static MetaCoreGameObjectData JsonToGameObjectData(const json& objJson, const Me
MetaCorePhysicsConstraintComponent component;
component.Enabled = value.value("Enabled", true); component.Type = static_cast<MetaCorePhysicsConstraintType>(value.value("Type", 0));
component.TargetObjectId = value.value("TargetObjectId", MetaCoreId{0});
if (value.contains("TargetObjectReference")) component.TargetObjectReference = JsonToObjectReference(value["TargetObjectReference"]);
if (value.contains("Anchor")) component.Anchor = JsonToVec3(value["Anchor"]);
if (value.contains("TargetAnchor")) component.TargetAnchor = JsonToVec3(value["TargetAnchor"]);
if (value.contains("Axis")) component.Axis = JsonToVec3(value["Axis"]);
@ -595,6 +636,8 @@ static MetaCoreGameObjectData JsonToGameObjectData(const json& objJson, const Me
if (prefabField.Name == "PrefabAssetGuid" && prefabJson.contains("PrefabAssetGuid")) prefab.PrefabAssetGuid = StringToGuid(prefabJson["PrefabAssetGuid"].get<std::string>());
else if (prefabField.Name == "PrefabObjectId" && prefabJson.contains("PrefabObjectId")) prefab.PrefabObjectId = prefabJson["PrefabObjectId"].get<MetaCoreId>();
else if (prefabField.Name == "PrefabInstanceRootId" && prefabJson.contains("PrefabInstanceRootId")) prefab.PrefabInstanceRootId = prefabJson["PrefabInstanceRootId"].get<MetaCoreId>();
else if (prefabField.Name == "InstanceGuid" && prefabJson.contains("InstanceGuid")) prefab.InstanceGuid = StringToGuid(prefabJson["InstanceGuid"].get<std::string>());
else if (prefabField.Name == "PrefabNodeGuid" && prefabJson.contains("PrefabNodeGuid")) prefab.PrefabNodeGuid = StringToGuid(prefabJson["PrefabNodeGuid"].get<std::string>());
}
obj.PrefabInstance = prefab;
}
@ -603,13 +646,120 @@ static MetaCoreGameObjectData JsonToGameObjectData(const json& objJson, const Me
return obj;
}
static json PrefabOverrideToJson(const MetaCorePrefabOverrideDocument& value) {
return {
{"TargetNodeGuid", GuidToString(value.TargetNodeGuid)},
{"ComponentTypeId", value.ComponentTypeId},
{"PropertyPath", value.PropertyPath},
{"Operation", static_cast<int>(value.Operation)},
{"ValueJson", value.ValueJson}
};
}
static MetaCorePrefabOverrideDocument JsonToPrefabOverride(const json& value) {
MetaCorePrefabOverrideDocument result;
result.TargetNodeGuid = StringToGuid(value.value("TargetNodeGuid", std::string{}));
result.ComponentTypeId = value.value("ComponentTypeId", std::string{});
result.PropertyPath = value.value("PropertyPath", std::string{});
result.Operation = static_cast<MetaCorePrefabOverrideOperation>(value.value("Operation", 0));
result.ValueJson = value.value("ValueJson", std::string{});
return result;
}
static json SchemaVersionsToJson(const std::vector<MetaCoreSchemaVersionEntry>& versions) {
json result = json::array();
for (const auto& version : versions) {
result.push_back({{"TypeId", version.TypeId}, {"Version", version.Version}});
}
return result;
}
static std::vector<MetaCoreSchemaVersionEntry> JsonToSchemaVersions(const json& values) {
std::vector<MetaCoreSchemaVersionEntry> result;
if (!values.is_array()) return result;
for (const auto& value : values) {
result.push_back({value.value("TypeId", std::string{}), value.value("Version", 1U)});
}
return result;
}
static MetaCoreAssetGuid BuildLegacyDocumentGuid(std::string_view kind, std::string_view name) {
// Legacy documents do not carry an asset GUID. Derive one from persisted
// content so repeated migration/save runs are deterministic.
std::uint64_t first = 1469598103934665603ULL;
std::uint64_t second = 1099511628211ULL;
const auto mix = [&](std::string_view value) {
for (const unsigned char character : value) {
first = (first ^ character) * 1099511628211ULL;
second = (second + character + 0x9e3779b97f4a7c15ULL) * 14029467366897019727ULL;
}
};
mix(kind);
mix(name);
MetaCoreAssetGuid result;
for (std::size_t index = 0; index < 8; ++index) {
result.Bytes[index] = static_cast<std::uint8_t>(first >> (index * 8U));
result.Bytes[index + 8] = static_cast<std::uint8_t>(second >> (index * 8U));
}
result.Bytes[6] = static_cast<std::uint8_t>((result.Bytes[6] & 0x0FU) | 0x50U);
result.Bytes[8] = static_cast<std::uint8_t>((result.Bytes[8] & 0x3FU) | 0x80U);
return result;
}
static void NormalizeSceneDocumentIdentity(MetaCoreSceneDocument& document) {
if (!document.SceneGuid.IsValid()) document.SceneGuid = BuildLegacyDocumentGuid("scene:", document.Name);
std::unordered_map<MetaCoreId, MetaCoreAssetGuid> stableIds;
for (auto& object : document.GameObjects) {
object.SceneGuid = document.SceneGuid;
if (!object.StableObjectGuid.IsValid())
object.StableObjectGuid = MetaCoreBuildDeterministicGuid(document.SceneGuid, object.Id);
stableIds[object.Id] = object.StableObjectGuid;
}
for (auto& object : document.GameObjects) {
if (object.PhysicsConstraint) {
auto& reference = object.PhysicsConstraint->TargetObjectReference;
if (!reference.IsValid()) {
if (const auto target = stableIds.find(object.PhysicsConstraint->TargetObjectId); target != stableIds.end()) {
reference.SceneGuid = document.SceneGuid;
reference.StableObjectGuid = target->second;
reference.Strength = MetaCoreObjectReferenceStrength::Hard;
}
} else if (!reference.SceneGuid.IsValid()) reference.SceneGuid = document.SceneGuid;
}
if (object.TimelineDirector) {
for (auto& binding : object.TimelineDirector->Bindings) {
if (!binding.ObjectReference.IsValid()) {
if (const auto target = stableIds.find(binding.ObjectId); target != stableIds.end()) {
binding.ObjectReference.SceneGuid = document.SceneGuid;
binding.ObjectReference.StableObjectGuid = target->second;
}
} else if (!binding.ObjectReference.SceneGuid.IsValid()) binding.ObjectReference.SceneGuid = document.SceneGuid;
}
}
}
document.SchemaVersion = GMetaCoreSceneSchemaVersion;
}
static void NormalizePrefabDocumentIdentity(MetaCorePrefabDocument& document) {
if (!document.PrefabGuid.IsValid()) document.PrefabGuid = BuildLegacyDocumentGuid("prefab:", document.Name);
for (auto& object : document.GameObjects) {
if (!object.StableObjectGuid.IsValid())
object.StableObjectGuid = MetaCoreBuildDeterministicGuid(document.PrefabGuid, object.Id);
}
document.SchemaVersion = GMetaCorePrefabSchemaVersion;
}
bool MetaCoreSceneSerializer::SaveSceneToJson(
const std::filesystem::path& absolutePath,
const MetaCoreSceneDocument& sceneDocument,
const MetaCoreTypeRegistry& registry
) {
try {
MetaCoreSceneDocument normalized = sceneDocument;
NormalizeSceneDocumentIdentity(normalized);
json sceneJson;
sceneJson["SchemaVersion"] = normalized.SchemaVersion;
sceneJson["SceneGuid"] = GuidToString(normalized.SceneGuid);
// 验证 MetaCoreSceneDocument 反射注册
const MetaCoreStructDescriptor* sceneDesc = registry.FindStruct<MetaCoreSceneDocument>();
@ -620,26 +770,55 @@ bool MetaCoreSceneSerializer::SaveSceneToJson(
for (const auto& field : sceneDesc->Fields) {
if (field.Name == "Name") {
sceneJson["SceneName"] = sceneDocument.Name;
sceneJson["SceneName"] = normalized.Name;
} else if (field.Name == "Environment") {
sceneJson["Environment"] = SceneEnvironmentToJson(sceneDocument.Environment);
sceneJson["Environment"] = SceneEnvironmentToJson(normalized.Environment);
} else if (field.Name == "Selection") {
const MetaCoreStructDescriptor* selDesc = registry.FindStruct<MetaCoreEditorSelectionSnapshot>();
if (selDesc) {
json selJson = json::object();
for (const auto& selField : selDesc->Fields) {
if (selField.Name == "SelectedObjectIds") selJson["SelectedObjectIds"] = sceneDocument.Selection.SelectedObjectIds;
else if (selField.Name == "ActiveObjectId") selJson["ActiveObjectId"] = sceneDocument.Selection.ActiveObjectId;
else if (selField.Name == "SelectionAnchorId") selJson["SelectionAnchorId"] = sceneDocument.Selection.SelectionAnchorId;
if (selField.Name == "SelectedObjectIds") selJson["SelectedObjectIds"] = normalized.Selection.SelectedObjectIds;
else if (selField.Name == "ActiveObjectId") selJson["ActiveObjectId"] = normalized.Selection.ActiveObjectId;
else if (selField.Name == "SelectionAnchorId") selJson["SelectionAnchorId"] = normalized.Selection.SelectionAnchorId;
}
sceneJson["Selection"] = selJson;
}
} else if (field.Name == "GameObjects") {
json gameObjectsArray = json::array();
for (const auto& obj : sceneDocument.GameObjects) {
for (const auto& obj : normalized.GameObjects) {
gameObjectsArray.push_back(GameObjectDataToJson(obj, registry));
}
sceneJson["GameObjects"] = gameObjectsArray;
} else if (field.Name == "SubScenes") {
sceneJson["SubScenes"] = json::array();
for (const auto& subScene : normalized.SubScenes) {
sceneJson["SubScenes"].push_back({
{"SceneGuid", GuidToString(subScene.SceneGuid)},
{"ScenePath", subScene.ScenePath.generic_string()},
{"LoadPolicy", static_cast<int>(subScene.LoadPolicy)},
{"BoundsCenter", Vec3ToJson(subScene.BoundsCenter)},
{"BoundsExtent", Vec3ToJson(subScene.BoundsExtent)},
{"CellCoordinate", Vec3ToJson(subScene.CellCoordinate)},
{"PreloadDistance", subScene.PreloadDistance},
{"UnloadDistance", subScene.UnloadDistance},
{"Priority", subScene.Priority}
});
}
} else if (field.Name == "PrefabInstances") {
sceneJson["PrefabInstances"] = json::array();
for (const auto& instance : normalized.PrefabInstances) {
json overrides = json::array();
for (const auto& entry : instance.Overrides) overrides.push_back(PrefabOverrideToJson(entry));
sceneJson["PrefabInstances"].push_back({
{"InstanceGuid", GuidToString(instance.InstanceGuid)},
{"PrefabAssetGuid", GuidToString(instance.PrefabAssetGuid)},
{"ParentObjectGuid", GuidToString(instance.ParentObjectGuid)},
{"Overrides", std::move(overrides)}
});
}
} else if (field.Name == "ComponentVersions") {
sceneJson["ComponentVersions"] = SchemaVersionsToJson(normalized.ComponentVersions);
}
}
@ -672,6 +851,10 @@ std::optional<MetaCoreSceneDocument> MetaCoreSceneSerializer::LoadSceneFromJson(
file >> sceneJson;
MetaCoreSceneDocument sceneDocument;
const std::uint32_t sourceVersion = sceneJson.value("SchemaVersion", 1U);
if (sourceVersion > GMetaCoreSceneSchemaVersion) return std::nullopt;
sceneDocument.SchemaVersion = sourceVersion;
if (sceneJson.contains("SceneGuid")) sceneDocument.SceneGuid = StringToGuid(sceneJson["SceneGuid"].get<std::string>());
// 验证 MetaCoreSceneDocument 反射注册
const MetaCoreStructDescriptor* sceneDesc = registry.FindStruct<MetaCoreSceneDocument>();
@ -703,9 +886,35 @@ std::optional<MetaCoreSceneDocument> MetaCoreSceneSerializer::LoadSceneFromJson(
for (const auto& objJson : sceneJson["GameObjects"]) {
sceneDocument.GameObjects.push_back(JsonToGameObjectData(objJson, registry));
}
} else if (field.Name == "SubScenes" && sceneJson.contains("SubScenes") && sceneJson["SubScenes"].is_array()) {
for (const auto& value : sceneJson["SubScenes"]) {
MetaCoreSubSceneDocument subScene;
subScene.SceneGuid = StringToGuid(value.value("SceneGuid", std::string{}));
subScene.ScenePath = value.value("ScenePath", std::string{});
subScene.LoadPolicy = static_cast<MetaCoreSubSceneLoadPolicy>(value.value("LoadPolicy", 0));
if (value.contains("BoundsCenter")) subScene.BoundsCenter = JsonToVec3(value["BoundsCenter"]);
if (value.contains("BoundsExtent")) subScene.BoundsExtent = JsonToVec3(value["BoundsExtent"]);
if (value.contains("CellCoordinate")) subScene.CellCoordinate = JsonToVec3(value["CellCoordinate"]);
subScene.PreloadDistance = value.value("PreloadDistance", 100.0F);
subScene.UnloadDistance = value.value("UnloadDistance", 125.0F);
subScene.Priority = value.value("Priority", 0);
sceneDocument.SubScenes.push_back(std::move(subScene));
}
} else if (field.Name == "PrefabInstances" && sceneJson.contains("PrefabInstances") && sceneJson["PrefabInstances"].is_array()) {
for (const auto& value : sceneJson["PrefabInstances"]) {
MetaCorePrefabInstanceDocument instance;
instance.InstanceGuid = StringToGuid(value.value("InstanceGuid", std::string{}));
instance.PrefabAssetGuid = StringToGuid(value.value("PrefabAssetGuid", std::string{}));
instance.ParentObjectGuid = StringToGuid(value.value("ParentObjectGuid", std::string{}));
if (value.contains("Overrides")) for (const auto& entry : value["Overrides"]) instance.Overrides.push_back(JsonToPrefabOverride(entry));
sceneDocument.PrefabInstances.push_back(std::move(instance));
}
} else if (field.Name == "ComponentVersions" && sceneJson.contains("ComponentVersions")) {
sceneDocument.ComponentVersions = JsonToSchemaVersions(sceneJson["ComponentVersions"]);
}
}
NormalizeSceneDocumentIdentity(sceneDocument);
return sceneDocument;
} catch (const std::exception& e) {
std::cerr << "LoadSceneFromJson: 发生异常 - " << e.what() << std::endl;
@ -720,13 +929,32 @@ bool MetaCoreSceneSerializer::SavePrefabToJson(
const MetaCoreTypeRegistry& registry
) {
try {
MetaCorePrefabDocument normalized = prefabDocument;
NormalizePrefabDocumentIdentity(normalized);
json prefabJson;
prefabJson["PrefabName"] = prefabDocument.Name;
prefabJson["SchemaVersion"] = normalized.SchemaVersion;
prefabJson["PrefabGuid"] = GuidToString(normalized.PrefabGuid);
prefabJson["BasePrefabGuid"] = GuidToString(normalized.BasePrefabGuid);
prefabJson["PrefabName"] = normalized.Name;
json gameObjectsArray = json::array();
for (const auto& obj : prefabDocument.GameObjects) {
for (const auto& obj : normalized.GameObjects) {
gameObjectsArray.push_back(GameObjectDataToJson(obj, registry));
}
prefabJson["GameObjects"] = gameObjectsArray;
prefabJson["Overrides"] = json::array();
for (const auto& value : normalized.Overrides) prefabJson["Overrides"].push_back(PrefabOverrideToJson(value));
prefabJson["NestedPrefabs"] = json::array();
for (const auto& nested : normalized.NestedPrefabs) {
json overrides = json::array();
for (const auto& value : nested.Overrides) overrides.push_back(PrefabOverrideToJson(value));
prefabJson["NestedPrefabs"].push_back({
{"MountNodeGuid", GuidToString(nested.MountNodeGuid)},
{"PrefabAssetGuid", GuidToString(nested.PrefabAssetGuid)},
{"InstanceGuid", GuidToString(nested.InstanceGuid)},
{"Overrides", std::move(overrides)}
});
}
prefabJson["ComponentVersions"] = SchemaVersionsToJson(normalized.ComponentVersions);
std::ofstream file(absolutePath);
if (!file.is_open()) {
@ -752,6 +980,11 @@ std::optional<MetaCorePrefabDocument> MetaCoreSceneSerializer::LoadPrefabFromJso
file >> prefabJson;
MetaCorePrefabDocument doc;
const std::uint32_t sourceVersion = prefabJson.value("SchemaVersion", 1U);
if (sourceVersion > GMetaCorePrefabSchemaVersion) return std::nullopt;
doc.SchemaVersion = sourceVersion;
doc.PrefabGuid = StringToGuid(prefabJson.value("PrefabGuid", std::string{}));
doc.BasePrefabGuid = StringToGuid(prefabJson.value("BasePrefabGuid", std::string{}));
if (prefabJson.contains("PrefabName")) {
doc.Name = prefabJson["PrefabName"].get<std::string>();
}
@ -760,6 +993,17 @@ std::optional<MetaCorePrefabDocument> MetaCoreSceneSerializer::LoadPrefabFromJso
doc.GameObjects.push_back(JsonToGameObjectData(objJson, registry));
}
}
if (prefabJson.contains("Overrides")) for (const auto& value : prefabJson["Overrides"]) doc.Overrides.push_back(JsonToPrefabOverride(value));
if (prefabJson.contains("NestedPrefabs")) for (const auto& value : prefabJson["NestedPrefabs"]) {
MetaCoreNestedPrefabDocument nested;
nested.MountNodeGuid = StringToGuid(value.value("MountNodeGuid", std::string{}));
nested.PrefabAssetGuid = StringToGuid(value.value("PrefabAssetGuid", std::string{}));
nested.InstanceGuid = StringToGuid(value.value("InstanceGuid", std::string{}));
if (value.contains("Overrides")) for (const auto& entry : value["Overrides"]) nested.Overrides.push_back(JsonToPrefabOverride(entry));
doc.NestedPrefabs.push_back(std::move(nested));
}
if (prefabJson.contains("ComponentVersions")) doc.ComponentVersions = JsonToSchemaVersions(prefabJson["ComponentVersions"]);
NormalizePrefabDocumentIdentity(doc);
return doc;
} catch (...) {
return std::nullopt;

View File

@ -0,0 +1,565 @@
#include "MetaCoreScene/MetaCoreSceneWorld.h"
#include <algorithm>
#include <cmath>
#include <thread>
namespace MetaCore {
namespace {
void HashBytes(std::uint64_t& hash, const std::uint8_t* bytes, std::size_t count) {
for (std::size_t index = 0; index < count; ++index) {
hash ^= bytes[index];
hash *= 1099511628211ULL;
}
}
float DistanceToBounds(const glm::vec3& point, const glm::vec3& center, const glm::vec3& extent) {
const glm::vec3 delta = glm::abs(point - center) - glm::max(extent, glm::vec3(0.0F));
const glm::vec3 outside = glm::max(delta, glm::vec3(0.0F));
return glm::length(outside);
}
void RemapLegacyObjectReferences(MetaCoreGameObjectData& object, const std::unordered_map<MetaCoreId, MetaCoreId>& ids) {
if (const auto parent = ids.find(object.ParentId); parent != ids.end()) object.ParentId = parent->second;
else object.ParentId = 0;
if (object.PhysicsConstraint) {
if (const auto target = ids.find(object.PhysicsConstraint->TargetObjectId); target != ids.end())
object.PhysicsConstraint->TargetObjectId = target->second;
else object.PhysicsConstraint->TargetObjectId = 0;
}
if (object.TimelineDirector) {
for (auto& binding : object.TimelineDirector->Bindings) {
if (const auto target = ids.find(binding.ObjectId); target != ids.end()) binding.ObjectId = target->second;
else binding.ObjectId = 0;
}
}
}
} // namespace
struct MetaCoreSceneWorld::PendingLoad {
std::uint64_t RequestId = 0;
MetaCoreAssetGuid SceneGuid{};
MetaCoreSceneLoadMode Mode = MetaCoreSceneLoadMode::Additive;
std::int32_t Priority = 0;
bool Cancelled = false;
std::future<std::optional<MetaCoreSceneDocument>> Future{};
};
MetaCoreAssetGuid MetaCoreBuildDeterministicGuid(const MetaCoreAssetGuid& namespaceGuid, const MetaCoreAssetGuid& localGuid) {
std::uint64_t first = 1469598103934665603ULL;
HashBytes(first, namespaceGuid.Bytes.data(), namespaceGuid.Bytes.size());
HashBytes(first, localGuid.Bytes.data(), localGuid.Bytes.size());
std::uint64_t second = 1099511628211ULL ^ first;
HashBytes(second, localGuid.Bytes.data(), localGuid.Bytes.size());
HashBytes(second, namespaceGuid.Bytes.data(), namespaceGuid.Bytes.size());
MetaCoreAssetGuid result;
for (std::size_t index = 0; index < 8; ++index) {
result.Bytes[index] = static_cast<std::uint8_t>((first >> (index * 8U)) & 0xFFU);
result.Bytes[index + 8] = static_cast<std::uint8_t>((second >> (index * 8U)) & 0xFFU);
}
result.Bytes[6] = static_cast<std::uint8_t>((result.Bytes[6] & 0x0FU) | 0x50U);
result.Bytes[8] = static_cast<std::uint8_t>((result.Bytes[8] & 0x3FU) | 0x80U);
return result;
}
MetaCoreAssetGuid MetaCoreBuildDeterministicGuid(const MetaCoreAssetGuid& namespaceGuid, std::uint64_t localValue) {
MetaCoreAssetGuid local;
for (std::size_t index = 0; index < 8; ++index)
local.Bytes[index] = static_cast<std::uint8_t>((localValue >> (index * 8U)) & 0xFFU);
return MetaCoreBuildDeterministicGuid(namespaceGuid, local);
}
std::size_t MetaCoreSceneWorld::ObjectKeyHasher::operator()(const ObjectKey& value) const noexcept {
MetaCoreAssetGuidHasher hasher;
return hasher(value.SceneGuid) ^ (hasher(value.ObjectGuid) + 0x9e3779b97f4a7c15ULL);
}
MetaCoreSceneWorld::MetaCoreSceneWorld(SceneLoader loader, MetaCoreSceneWorldSettings settings)
: Loader_(std::move(loader)), Settings_(settings), OwnedScene_(std::make_unique<MetaCoreScene>()), Scene_(OwnedScene_.get()) {}
MetaCoreSceneWorld::MetaCoreSceneWorld(MetaCoreScene& scene, SceneLoader loader, MetaCoreSceneWorldSettings settings)
: Loader_(std::move(loader)), Settings_(settings), Scene_(&scene) {}
MetaCoreSceneWorld::~MetaCoreSceneWorld() = default;
void MetaCoreSceneWorld::SetSceneLoader(SceneLoader loader) { Loader_ = std::move(loader); }
std::uint64_t MetaCoreSceneWorld::RequestLoad(
const MetaCoreAssetGuid& sceneGuid,
MetaCoreSceneLoadMode mode,
std::int32_t priority
) {
for (const auto& pending : PendingLoads_) {
if (!pending->Cancelled && pending->SceneGuid == sceneGuid) return pending->RequestId;
}
const std::uint64_t requestId = NextRequestId_++;
MetaCoreSceneLoadStatus status{requestId, sceneGuid, MetaCoreSceneLoadState::Loading, {}};
if (!sceneGuid.IsValid() || !Loader_) {
status.State = MetaCoreSceneLoadState::Failed;
status.Error = !sceneGuid.IsValid() ? "Scene GUID is invalid" : "Scene loader is not configured";
LoadStatuses_[requestId] = std::move(status);
return requestId;
}
if (IsSceneLoaded(sceneGuid)) {
status.State = MetaCoreSceneLoadState::Loaded;
LoadStatuses_[requestId] = std::move(status);
return requestId;
}
auto pending = std::make_unique<PendingLoad>();
pending->RequestId = requestId;
pending->SceneGuid = sceneGuid;
pending->Mode = mode;
pending->Priority = priority;
SceneLoader loader = Loader_;
pending->Future = std::async(std::launch::async, [loader = std::move(loader), sceneGuid] {
return loader(sceneGuid);
});
PendingLoads_.push_back(std::move(pending));
LoadStatuses_[requestId] = std::move(status);
return requestId;
}
bool MetaCoreSceneWorld::CancelLoad(std::uint64_t requestId) {
const auto iterator = std::find_if(PendingLoads_.begin(), PendingLoads_.end(), [&](const auto& pending) {
return pending->RequestId == requestId;
});
if (iterator == PendingLoads_.end()) return false;
(*iterator)->Cancelled = true;
auto& status = LoadStatuses_[requestId];
status.State = MetaCoreSceneLoadState::Cancelled;
status.Error = "Load request cancelled";
return true;
}
void MetaCoreSceneWorld::Tick() {
std::stable_sort(PendingLoads_.begin(), PendingLoads_.end(), [](const auto& lhs, const auto& rhs) {
return lhs->Priority > rhs->Priority;
});
for (auto iterator = PendingLoads_.begin(); iterator != PendingLoads_.end();) {
auto& pending = **iterator;
if (pending.Future.wait_for(std::chrono::milliseconds(0)) != std::future_status::ready) {
++iterator;
continue;
}
std::optional<MetaCoreSceneDocument> document = pending.Future.get();
auto& status = LoadStatuses_[pending.RequestId];
if (pending.Cancelled) {
status.State = MetaCoreSceneLoadState::Cancelled;
} else if (!document) {
status.State = MetaCoreSceneLoadState::Failed;
status.Error = "Scene loader returned no document";
} else {
if (!document->SceneGuid.IsValid()) document->SceneGuid = pending.SceneGuid;
std::string error;
if (CommitDocument(std::move(*document), pending.Mode, &error)) status.State = MetaCoreSceneLoadState::Loaded;
else {
status.State = MetaCoreSceneLoadState::Failed;
status.Error = std::move(error);
}
}
iterator = PendingLoads_.erase(iterator);
}
}
bool MetaCoreSceneWorld::LoadDocumentNow(MetaCoreSceneDocument document, MetaCoreSceneLoadMode mode, std::string* error) {
return CommitDocument(std::move(document), mode, error);
}
bool MetaCoreSceneWorld::CommitDocument(MetaCoreSceneDocument document, MetaCoreSceneLoadMode mode, std::string* error) {
if (document.SchemaVersion > GMetaCoreSceneSchemaVersion) {
if (error) *error = "Scene schema is newer than this engine";
return false;
}
if (!document.SceneGuid.IsValid()) {
if (error) *error = "Scene document has no stable GUID";
return false;
}
if (IsSceneLoaded(document.SceneGuid)) {
if (error) *error = "Scene is already loaded";
return false;
}
if (mode == MetaCoreSceneLoadMode::Replace) {
std::vector<MetaCoreAssetGuid> loaded;
loaded.reserve(LoadedScenes_.size());
for (const auto& [guid, scene] : LoadedScenes_) {
(void)scene;
loaded.push_back(guid);
}
for (const auto& guid : loaded) (void)UnloadScene(guid, true);
if (LoadedScenes_.empty() && !Scene_->GetGameObjects().empty()) {
Scene_->RestoreSnapshot(MetaCoreSceneSnapshot{});
}
}
std::unordered_map<MetaCoreId, MetaCoreId> ids;
for (const auto& object : document.GameObjects) ids.emplace(object.Id, MetaCoreIdGenerator::Generate());
LoadedScene record;
record.Source = document;
record.Source.SchemaVersion = GMetaCoreSceneSchemaVersion;
record.LoadMode = mode;
MetaCoreSceneSnapshot combined = Scene_->CaptureSnapshot();
if (LoadedScenes_.empty()) combined.Environment = document.Environment;
for (auto object : document.GameObjects) {
const MetaCoreId serializedId = object.Id;
object.Id = ids.at(serializedId);
object.SceneGuid = document.SceneGuid;
if (!object.StableObjectGuid.IsValid())
object.StableObjectGuid = MetaCoreBuildDeterministicGuid(document.SceneGuid, serializedId);
RemapLegacyObjectReferences(object, ids);
record.RuntimeObjectIds.push_back(object.Id);
record.RuntimeToSerializedId.emplace(object.Id, serializedId);
combined.GameObjects.push_back(std::move(object));
}
Scene_->RestoreSnapshot(combined);
const MetaCoreAssetGuid sceneGuid = document.SceneGuid;
LoadedScenes_.emplace(sceneGuid, std::move(record));
if (!ActiveSceneGuid_.IsValid() || mode == MetaCoreSceneLoadMode::Replace) ActiveSceneGuid_ = sceneGuid;
RebuildObjectIndex();
for (auto object : Scene_->GetGameObjects()) {
if (object.HasComponent<MetaCorePhysicsConstraintComponent>()) {
auto& constraint = object.GetComponent<MetaCorePhysicsConstraintComponent>();
if (constraint.TargetObjectReference.IsValid()) {
const auto target = ResolveObject(constraint.TargetObjectReference);
constraint.TargetObjectId = target ? target.GetId() : 0;
}
}
if (object.HasComponent<MetaCoreTimelineDirectorComponent>()) {
for (auto& binding : object.GetComponent<MetaCoreTimelineDirectorComponent>().Bindings) {
if (binding.ObjectReference.IsValid()) {
const auto target = ResolveObject(binding.ObjectReference);
binding.ObjectId = target ? target.GetId() : 0;
}
}
}
}
RebuildHardResidency();
Publish({MetaCoreSceneLifecycleEventType::AfterLoad, sceneGuid, LoadedScenes_.at(sceneGuid).RuntimeObjectIds});
for (const auto& subScene : document.SubScenes) {
if (subScene.LoadPolicy == MetaCoreSubSceneLoadPolicy::AlwaysLoaded && !IsSceneLoaded(subScene.SceneGuid))
(void)RequestLoad(subScene.SceneGuid, MetaCoreSceneLoadMode::SubScene, subScene.Priority);
}
return true;
}
bool MetaCoreSceneWorld::UnloadScene(const MetaCoreAssetGuid& sceneGuid, bool force) {
auto iterator = LoadedScenes_.find(sceneGuid);
if (iterator == LoadedScenes_.end()) return false;
if (!force && HardResidentScenes_.contains(sceneGuid)) return false;
const auto objectIds = iterator->second.RuntimeObjectIds;
Publish({MetaCoreSceneLifecycleEventType::BeforeUnload, sceneGuid, objectIds});
(void)Scene_->DeleteGameObjects(objectIds);
LoadedScenes_.erase(iterator);
HardResidentScenes_.erase(sceneGuid);
if (ActiveSceneGuid_ == sceneGuid)
ActiveSceneGuid_ = LoadedScenes_.empty() ? MetaCoreAssetGuid{} : LoadedScenes_.begin()->first;
RebuildObjectIndex();
RebuildHardResidency();
Publish({MetaCoreSceneLifecycleEventType::AfterUnload, sceneGuid, objectIds});
return true;
}
bool MetaCoreSceneWorld::SetActiveScene(const MetaCoreAssetGuid& sceneGuid) {
if (!IsSceneLoaded(sceneGuid)) return false;
ActiveSceneGuid_ = sceneGuid;
return true;
}
bool MetaCoreSceneWorld::IsSceneLoaded(const MetaCoreAssetGuid& sceneGuid) const {
return LoadedScenes_.contains(sceneGuid);
}
std::vector<MetaCoreLoadedSceneInfo> MetaCoreSceneWorld::GetLoadedScenes() const {
std::vector<MetaCoreLoadedSceneInfo> result;
result.reserve(LoadedScenes_.size());
for (const auto& [guid, scene] : LoadedScenes_) {
result.push_back({guid, scene.Source.Name, scene.LoadMode, guid == ActiveSceneGuid_, scene.Dirty,
HardResidentScenes_.contains(guid), scene.RuntimeObjectIds});
}
std::sort(result.begin(), result.end(), [](const auto& lhs, const auto& rhs) {
if (lhs.Active != rhs.Active) return lhs.Active;
return lhs.Name < rhs.Name;
});
return result;
}
std::optional<MetaCoreSceneLoadStatus> MetaCoreSceneWorld::GetLoadStatus(std::uint64_t requestId) const {
const auto iterator = LoadStatuses_.find(requestId);
return iterator == LoadStatuses_.end() ? std::nullopt : std::optional(iterator->second);
}
MetaCoreGameObject MetaCoreSceneWorld::ResolveObject(const MetaCoreObjectReference& reference) {
if (!reference.IsValid()) return {};
MetaCoreAssetGuid sceneGuid = reference.SceneGuid.IsValid() ? reference.SceneGuid : ActiveSceneGuid_;
const auto iterator = ObjectIndex_.find({sceneGuid, reference.StableObjectGuid});
if (iterator != ObjectIndex_.end()) return Scene_->FindGameObject(iterator->second);
if (reference.Strength == MetaCoreObjectReferenceStrength::Hard && sceneGuid.IsValid()) {
HardResidentScenes_.insert(sceneGuid);
if (!IsSceneLoaded(sceneGuid)) (void)RequestLoad(sceneGuid, MetaCoreSceneLoadMode::SubScene);
}
return {};
}
MetaCoreGameObject MetaCoreSceneWorld::ResolveObject(const MetaCoreObjectReference& reference) const {
if (!reference.IsValid()) return {};
const MetaCoreAssetGuid sceneGuid = reference.SceneGuid.IsValid() ? reference.SceneGuid : ActiveSceneGuid_;
const auto iterator = ObjectIndex_.find({sceneGuid, reference.StableObjectGuid});
return iterator == ObjectIndex_.end() ? MetaCoreGameObject{} : Scene_->FindGameObject(iterator->second);
}
MetaCoreObjectReference MetaCoreSceneWorld::BuildObjectReference(
MetaCoreId runtimeObjectId,
MetaCoreObjectReferenceStrength strength
) const {
const MetaCoreGameObject object = Scene_->FindGameObject(runtimeObjectId);
if (!object || !object.HasComponent<MetaCoreSceneObjectIdentityComponent>()) return {};
const auto& identity = object.GetComponent<MetaCoreSceneObjectIdentityComponent>();
return {identity.SceneGuid, identity.StableObjectGuid, strength};
}
bool MetaCoreSceneWorld::MoveSubtreeToScene(MetaCoreId rootObjectId, const MetaCoreAssetGuid& targetSceneGuid) {
if (!IsSceneLoaded(targetSceneGuid)) return false;
const auto ids = Scene_->GetSubtreeObjectIds(rootObjectId);
if (ids.empty()) return false;
MetaCoreAssetGuid sourceSceneGuid{};
for (MetaCoreId id : ids) {
auto object = Scene_->FindGameObject(id);
if (!object) return false;
if (!object.HasComponent<MetaCoreSceneObjectIdentityComponent>())
object.AddComponent<MetaCoreSceneObjectIdentityComponent>();
if (!sourceSceneGuid.IsValid())
sourceSceneGuid = object.GetComponent<MetaCoreSceneObjectIdentityComponent>().SceneGuid;
object.GetComponent<MetaCoreSceneObjectIdentityComponent>().SceneGuid = targetSceneGuid;
}
for (auto& [guid, scene] : LoadedScenes_) {
for (MetaCoreId id : ids) std::erase(scene.RuntimeObjectIds, id);
if (guid == sourceSceneGuid || guid == targetSceneGuid) scene.Dirty = true;
}
auto& target = LoadedScenes_.at(targetSceneGuid);
target.RuntimeObjectIds.insert(target.RuntimeObjectIds.end(), ids.begin(), ids.end());
RebuildObjectIndex();
Scene_->IncrementRevision();
return true;
}
std::optional<MetaCoreSceneDocument> MetaCoreSceneWorld::CaptureSceneDocument(const MetaCoreAssetGuid& sceneGuid) const {
const auto loaded = LoadedScenes_.find(sceneGuid);
if (loaded == LoadedScenes_.end()) return std::nullopt;
MetaCoreSceneDocument result = loaded->second.Source;
result.SchemaVersion = GMetaCoreSceneSchemaVersion;
result.SceneGuid = sceneGuid;
result.GameObjects.clear();
result.PrefabInstances.clear();
const auto snapshot = Scene_->CaptureSnapshot();
std::unordered_map<MetaCoreId, MetaCoreId> runtimeToSerialized = loaded->second.RuntimeToSerializedId;
std::unordered_set<MetaCoreId> usedSerializedIds;
for (const auto& [runtimeId, serializedId] : runtimeToSerialized) {
(void)runtimeId;
usedSerializedIds.insert(serializedId);
}
MetaCoreId nextSerializedId = 1;
for (const auto& object : snapshot.GameObjects) {
if (object.SceneGuid != sceneGuid || runtimeToSerialized.contains(object.Id)) continue;
while (usedSerializedIds.contains(nextSerializedId)) ++nextSerializedId;
runtimeToSerialized.emplace(object.Id, nextSerializedId);
usedSerializedIds.insert(nextSerializedId++);
}
for (auto object : snapshot.GameObjects) {
if (object.SceneGuid != sceneGuid) continue;
if (object.PrefabInstance &&
object.PrefabInstance->PrefabInstanceRootId == object.Id &&
object.PrefabInstance->InstanceGuid.IsValid()) {
MetaCorePrefabInstanceDocument instance;
instance.InstanceGuid = object.PrefabInstance->InstanceGuid;
instance.PrefabAssetGuid = object.PrefabInstance->PrefabAssetGuid;
if (object.ParentId != 0) {
const auto parent = std::find_if(snapshot.GameObjects.begin(), snapshot.GameObjects.end(),
[&](const auto& candidate) { return candidate.Id == object.ParentId; });
if (parent != snapshot.GameObjects.end()) instance.ParentObjectGuid = parent->StableObjectGuid;
}
result.PrefabInstances.push_back(std::move(instance));
}
object.Id = runtimeToSerialized.at(object.Id);
if (object.ParentId != 0) {
const auto parent = runtimeToSerialized.find(object.ParentId);
object.ParentId = parent == runtimeToSerialized.end() ? 0 : parent->second;
}
result.GameObjects.push_back(std::move(object));
}
return result;
}
MetaCoreSceneWorldSnapshot MetaCoreSceneWorld::CaptureWorldSnapshot() const {
MetaCoreSceneWorldSnapshot snapshot;
snapshot.CompositeScene = Scene_->CaptureSnapshot();
snapshot.ActiveSceneGuid = ActiveSceneGuid_;
snapshot.Scenes.reserve(LoadedScenes_.size());
for (const auto& [sceneGuid, loaded] : LoadedScenes_) {
if (auto document = CaptureSceneDocument(sceneGuid))
snapshot.Scenes.push_back({std::move(*document), loaded.LoadMode, loaded.Dirty});
}
return snapshot;
}
void MetaCoreSceneWorld::RestoreWorldSnapshot(const MetaCoreSceneWorldSnapshot& snapshot) {
for (auto& pending : PendingLoads_) {
pending->Cancelled = true;
auto& status = LoadStatuses_[pending->RequestId];
status.State = MetaCoreSceneLoadState::Cancelled;
status.Error = "Load request cancelled by world restore";
}
Scene_->RestoreSnapshot(snapshot.CompositeScene);
LoadedScenes_.clear();
for (const auto& entry : snapshot.Scenes) {
LoadedScene loaded;
loaded.Source = entry.Document;
loaded.LoadMode = entry.LoadMode;
loaded.Dirty = entry.Dirty;
std::unordered_map<MetaCoreAssetGuid, MetaCoreId, MetaCoreAssetGuidHasher> serializedIds;
for (const auto& object : entry.Document.GameObjects)
serializedIds[object.StableObjectGuid] = object.Id;
for (const auto object : Scene_->GetGameObjects()) {
if (!object.HasComponent<MetaCoreSceneObjectIdentityComponent>()) continue;
const auto& identity = object.GetComponent<MetaCoreSceneObjectIdentityComponent>();
if (identity.SceneGuid != entry.Document.SceneGuid) continue;
loaded.RuntimeObjectIds.push_back(object.GetId());
if (const auto serialized = serializedIds.find(identity.StableObjectGuid); serialized != serializedIds.end())
loaded.RuntimeToSerializedId.emplace(object.GetId(), serialized->second);
}
LoadedScenes_.emplace(entry.Document.SceneGuid, std::move(loaded));
}
ActiveSceneGuid_ = LoadedScenes_.contains(snapshot.ActiveSceneGuid)
? snapshot.ActiveSceneGuid
: (LoadedScenes_.empty() ? MetaCoreAssetGuid{} : LoadedScenes_.begin()->first);
RebuildObjectIndex();
RebuildHardResidency();
}
std::vector<std::string> MetaCoreSceneWorld::ValidateDependencies(const MetaCoreSceneDocument& root) const {
std::vector<std::string> issues;
std::unordered_set<MetaCoreAssetGuid, MetaCoreAssetGuidHasher> visited;
std::unordered_set<MetaCoreAssetGuid, MetaCoreAssetGuidHasher> active;
std::function<void(const MetaCoreSceneDocument&)> visit = [&](const MetaCoreSceneDocument& document) {
if (active.contains(document.SceneGuid)) {
issues.push_back("Scene dependency cycle at " + document.SceneGuid.ToString());
return;
}
if (!visited.insert(document.SceneGuid).second) return;
active.insert(document.SceneGuid);
std::unordered_set<MetaCoreAssetGuid, MetaCoreAssetGuidHasher> localDependencies;
std::vector<MetaCoreAssetGuid> dependencies;
for (const auto& subScene : document.SubScenes) {
if (!subScene.SceneGuid.IsValid()) {
issues.push_back("SubScene has no GUID: " + subScene.ScenePath.generic_string());
continue;
}
if (!localDependencies.insert(subScene.SceneGuid).second)
issues.push_back("Duplicate SubScene dependency: " + subScene.SceneGuid.ToString());
dependencies.push_back(subScene.SceneGuid);
if (subScene.LoadPolicy == MetaCoreSubSceneLoadPolicy::Distance &&
(subScene.PreloadDistance < 0.0F || subScene.UnloadDistance < subScene.PreloadDistance))
issues.push_back("Distance SubScene has invalid preload/unload thresholds: " + subScene.SceneGuid.ToString());
}
for (const auto& object : document.GameObjects) {
if (object.PhysicsConstraint) {
const auto& reference = object.PhysicsConstraint->TargetObjectReference;
if (reference.IsValid() && reference.Strength == MetaCoreObjectReferenceStrength::Hard)
dependencies.push_back(reference.SceneGuid);
}
if (object.TimelineDirector) for (const auto& binding : object.TimelineDirector->Bindings) {
if (binding.ObjectReference.IsValid() &&
binding.ObjectReference.Strength == MetaCoreObjectReferenceStrength::Hard)
dependencies.push_back(binding.ObjectReference.SceneGuid);
}
}
for (const auto& dependency : dependencies) {
if (active.contains(dependency)) {
issues.push_back("Scene dependency cycle: " + document.SceneGuid.ToString() + " -> " + dependency.ToString());
continue;
}
if (visited.contains(dependency) || !Loader_) continue;
auto child = Loader_(dependency);
if (!child) issues.push_back("Missing scene dependency: " + dependency.ToString());
else visit(*child);
}
active.erase(document.SceneGuid);
};
visit(root);
return issues;
}
void MetaCoreSceneWorld::UpdateStreaming(const std::vector<glm::vec3>& sourcePositions) {
std::vector<MetaCoreSubSceneDocument> subScenes;
for (const auto& [guid, scene] : LoadedScenes_) {
(void)guid;
subScenes.insert(subScenes.end(), scene.Source.SubScenes.begin(), scene.Source.SubScenes.end());
}
for (const auto& subScene : subScenes) {
if (!subScene.SceneGuid.IsValid() || subScene.LoadPolicy == MetaCoreSubSceneLoadPolicy::Manual) continue;
bool near = subScene.LoadPolicy == MetaCoreSubSceneLoadPolicy::AlwaysLoaded;
bool far = !sourcePositions.empty();
for (const auto& source : sourcePositions) {
const float distance = DistanceToBounds(source, subScene.BoundsCenter, subScene.BoundsExtent);
near = near || distance <= subScene.PreloadDistance;
far = far && distance > subScene.UnloadDistance;
}
if (near && !IsSceneLoaded(subScene.SceneGuid))
(void)RequestLoad(subScene.SceneGuid, MetaCoreSceneLoadMode::SubScene, subScene.Priority);
else if (far && IsSceneLoaded(subScene.SceneGuid) && !HardResidentScenes_.contains(subScene.SceneGuid))
(void)UnloadScene(subScene.SceneGuid);
}
}
std::uint64_t MetaCoreSceneWorld::AddLifecycleListener(LifecycleListener listener) {
const std::uint64_t id = NextListenerId_++;
Listeners_.emplace(id, std::move(listener));
return id;
}
void MetaCoreSceneWorld::RemoveLifecycleListener(std::uint64_t listenerId) { Listeners_.erase(listenerId); }
void MetaCoreSceneWorld::Publish(MetaCoreSceneLifecycleEvent event) {
for (const auto& [id, listener] : Listeners_) {
(void)id;
if (listener) listener(event);
}
}
void MetaCoreSceneWorld::RebuildObjectIndex() {
ObjectIndex_.clear();
for (const auto object : Scene_->GetGameObjects()) {
if (!object.HasComponent<MetaCoreSceneObjectIdentityComponent>()) continue;
const auto& identity = object.GetComponent<MetaCoreSceneObjectIdentityComponent>();
if (identity.SceneGuid.IsValid() && identity.StableObjectGuid.IsValid())
ObjectIndex_[{identity.SceneGuid, identity.StableObjectGuid}] = object.GetId();
}
}
void MetaCoreSceneWorld::RebuildHardResidency() {
HardResidentScenes_.clear();
for (const auto object : Scene_->GetGameObjects()) {
MetaCoreAssetGuid sourceSceneGuid{};
if (object.HasComponent<MetaCoreSceneObjectIdentityComponent>())
sourceSceneGuid = object.GetComponent<MetaCoreSceneObjectIdentityComponent>().SceneGuid;
if (object.HasComponent<MetaCorePhysicsConstraintComponent>()) {
const auto& reference = object.GetComponent<MetaCorePhysicsConstraintComponent>().TargetObjectReference;
if (reference.IsValid() && reference.Strength == MetaCoreObjectReferenceStrength::Hard &&
reference.SceneGuid != sourceSceneGuid)
HardResidentScenes_.insert(reference.SceneGuid);
}
if (object.HasComponent<MetaCoreTimelineDirectorComponent>()) {
for (const auto& binding : object.GetComponent<MetaCoreTimelineDirectorComponent>().Bindings) {
if (binding.ObjectReference.IsValid() &&
binding.ObjectReference.Strength == MetaCoreObjectReferenceStrength::Hard &&
binding.ObjectReference.SceneGuid != sourceSceneGuid)
HardResidentScenes_.insert(binding.ObjectReference.SceneGuid);
}
}
}
}
} // namespace MetaCore

View File

@ -4,6 +4,7 @@
#include "MetaCoreFoundation/MetaCoreId.h"
#include "MetaCoreFoundation/MetaCoreReflection.h"
#include "MetaCoreRendering/MetaCoreRendering.h"
#include "MetaCoreScene/MetaCoreObjectReference.h"
#include <glm/vec3.hpp>
#include <glm/gtc/quaternion.hpp>
@ -309,6 +310,8 @@ struct MetaCoreTimelineBinding {
std::string Slot{};
MC_PROPERTY()
MetaCoreId ObjectId = 0;
MC_PROPERTY()
MetaCoreObjectReference ObjectReference{};
};
MC_STRUCT()
@ -467,6 +470,8 @@ struct MetaCorePhysicsConstraintComponent {
MC_PROPERTY()
MetaCoreId TargetObjectId = 0;
MC_PROPERTY()
MetaCoreObjectReference TargetObjectReference{};
MC_PROPERTY()
glm::vec3 Anchor{0.0F};
MC_PROPERTY()
glm::vec3 TargetAnchor{0.0F};

View File

@ -25,6 +25,25 @@ struct MetaCorePrefabInstanceMetadata {
MC_PROPERTY()
MetaCoreId PrefabInstanceRootId = 0;
MC_PROPERTY()
MetaCoreAssetGuid InstanceGuid{};
MC_PROPERTY()
MetaCoreAssetGuid PrefabNodeGuid{};
};
// Runtime ownership metadata. StableObjectGuid is persisted through
// MetaCoreGameObjectData while RuntimeId remains process-local.
MC_STRUCT()
struct MetaCoreSceneObjectIdentityComponent {
MC_GENERATED_BODY()
MC_PROPERTY()
MetaCoreAssetGuid SceneGuid{};
MC_PROPERTY()
MetaCoreAssetGuid StableObjectGuid{};
};
// Component structs for basic properties
@ -116,6 +135,10 @@ struct MetaCoreGameObjectData {
MC_PROPERTY()
MetaCoreId ParentId = 0;
MC_PROPERTY()
MetaCoreAssetGuid SceneGuid{};
MC_PROPERTY()
MetaCoreAssetGuid StableObjectGuid{};
MC_PROPERTY()
std::string Name;
MC_PROPERTY()
MetaCoreTransformComponent Transform{};

View File

@ -0,0 +1,30 @@
#pragma once
#include "MetaCoreFoundation/MetaCoreAssetGuid.h"
#include "MetaCoreFoundation/MetaCoreReflection.h"
namespace MetaCore {
MC_ENUM()
enum class MetaCoreObjectReferenceStrength {
Soft = 0,
Hard
};
MC_STRUCT()
struct MetaCoreObjectReference {
MC_GENERATED_BODY()
MC_PROPERTY()
MetaCoreAssetGuid SceneGuid{};
MC_PROPERTY()
MetaCoreAssetGuid StableObjectGuid{};
MC_PROPERTY()
MetaCoreObjectReferenceStrength Strength = MetaCoreObjectReferenceStrength::Soft;
[[nodiscard]] bool IsValid() const { return StableObjectGuid.IsValid(); }
};
} // namespace MetaCore

View File

@ -0,0 +1,64 @@
#pragma once
#include "MetaCoreScene/MetaCoreSceneDocument.h"
#include <functional>
#include <optional>
#include <unordered_map>
namespace MetaCore {
struct MetaCoreResolvedPrefab {
MetaCorePrefabDocument Document{};
std::vector<MetaCorePrefabOverrideDocument> OrphanOverrides{};
std::vector<MetaCoreAssetGuid> DependencyChain{};
};
class MetaCorePrefabOverrideRegistry {
public:
using PropertyHandler = std::function<bool(MetaCoreGameObjectData&, std::string_view, std::string_view)>;
MetaCorePrefabOverrideRegistry();
void RegisterPropertyHandler(std::string componentTypeId, PropertyHandler handler);
[[nodiscard]] bool Apply(
MetaCorePrefabDocument& document,
const MetaCorePrefabOverrideDocument& overrideValue
) const;
private:
std::unordered_map<std::string, PropertyHandler> PropertyHandlers_{};
};
class MetaCorePrefabResolver {
public:
using PrefabLoader = std::function<std::optional<MetaCorePrefabDocument>(const MetaCoreAssetGuid&)>;
explicit MetaCorePrefabResolver(
PrefabLoader loader,
MetaCorePrefabOverrideRegistry registry = MetaCorePrefabOverrideRegistry{}
);
[[nodiscard]] std::optional<MetaCoreResolvedPrefab> Resolve(
const MetaCoreAssetGuid& prefabGuid,
std::string* error = nullptr
) const;
[[nodiscard]] std::optional<std::vector<MetaCoreGameObjectData>> Instantiate(
const MetaCoreAssetGuid& prefabGuid,
const MetaCoreAssetGuid& instanceGuid,
const std::vector<MetaCorePrefabOverrideDocument>& instanceOverrides = {},
std::string* error = nullptr
) const;
[[nodiscard]] std::vector<std::string> ValidateDependencies(const MetaCoreAssetGuid& prefabGuid) const;
private:
[[nodiscard]] std::optional<MetaCoreResolvedPrefab> ResolveInternal(
const MetaCoreAssetGuid& prefabGuid,
std::vector<MetaCoreAssetGuid>& stack,
std::string* error
) const;
PrefabLoader Loader_{};
MetaCorePrefabOverrideRegistry OverrideRegistry_{};
};
} // namespace MetaCore

View File

@ -5,6 +5,7 @@
#include "MetaCoreFoundation/MetaCoreAssetGuid.h"
#include "MetaCoreFoundation/MetaCoreAssetTypes.h"
#include "MetaCoreScene/MetaCoreGameObject.h"
#include "MetaCoreScene/MetaCoreObjectReference.h"
#include <glm/vec3.hpp>
#include <filesystem>
@ -13,6 +14,125 @@
namespace MetaCore {
inline constexpr std::uint32_t GMetaCoreSceneSchemaVersion = 2U;
inline constexpr std::uint32_t GMetaCorePrefabSchemaVersion = 2U;
MC_ENUM()
enum class MetaCoreSubSceneLoadPolicy {
AlwaysLoaded = 0,
Manual,
Distance
};
MC_STRUCT()
struct MetaCoreSubSceneDocument {
MC_GENERATED_BODY()
MC_PROPERTY()
MetaCoreAssetGuid SceneGuid{};
MC_PROPERTY()
std::filesystem::path ScenePath{};
MC_PROPERTY()
MetaCoreSubSceneLoadPolicy LoadPolicy = MetaCoreSubSceneLoadPolicy::AlwaysLoaded;
MC_PROPERTY()
glm::vec3 BoundsCenter{0.0F};
MC_PROPERTY()
glm::vec3 BoundsExtent{0.0F};
MC_PROPERTY()
glm::vec3 CellCoordinate{0.0F};
MC_PROPERTY()
float PreloadDistance = 100.0F;
MC_PROPERTY()
float UnloadDistance = 125.0F;
MC_PROPERTY()
std::int32_t Priority = 0;
};
MC_STRUCT()
struct MetaCoreSchemaVersionEntry {
MC_GENERATED_BODY()
MC_PROPERTY()
std::string TypeId{};
MC_PROPERTY()
std::uint32_t Version = 1U;
};
MC_ENUM()
enum class MetaCorePrefabOverrideOperation {
SetProperty = 0,
AddComponent,
RemoveComponent,
AddChild,
RemoveChild,
Reparent
};
MC_STRUCT()
struct MetaCorePrefabOverrideDocument {
MC_GENERATED_BODY()
MC_PROPERTY()
MetaCoreAssetGuid TargetNodeGuid{};
MC_PROPERTY()
std::string ComponentTypeId{};
MC_PROPERTY()
std::string PropertyPath{};
MC_PROPERTY()
MetaCorePrefabOverrideOperation Operation = MetaCorePrefabOverrideOperation::SetProperty;
// Canonical JSON value. Keeping the value self-describing allows component
// serializers to migrate it without coupling prefab documents to C++ layout.
MC_PROPERTY()
std::string ValueJson{};
};
MC_STRUCT()
struct MetaCoreNestedPrefabDocument {
MC_GENERATED_BODY()
MC_PROPERTY()
MetaCoreAssetGuid MountNodeGuid{};
MC_PROPERTY()
MetaCoreAssetGuid PrefabAssetGuid{};
MC_PROPERTY()
MetaCoreAssetGuid InstanceGuid{};
MC_PROPERTY()
std::vector<MetaCorePrefabOverrideDocument> Overrides{};
};
MC_STRUCT()
struct MetaCorePrefabInstanceDocument {
MC_GENERATED_BODY()
MC_PROPERTY()
MetaCoreAssetGuid InstanceGuid{};
MC_PROPERTY()
MetaCoreAssetGuid PrefabAssetGuid{};
MC_PROPERTY()
MetaCoreAssetGuid ParentObjectGuid{};
MC_PROPERTY()
std::vector<MetaCorePrefabOverrideDocument> Overrides{};
};
MC_STRUCT()
struct MetaCoreEditorSelectionSnapshot {
MC_GENERATED_BODY()
@ -69,6 +189,12 @@ MC_STRUCT()
struct MetaCoreSceneDocument {
MC_GENERATED_BODY()
MC_PROPERTY()
std::uint32_t SchemaVersion = GMetaCoreSceneSchemaVersion;
MC_PROPERTY()
MetaCoreAssetGuid SceneGuid{};
MC_PROPERTY()
std::string Name{};
@ -78,6 +204,15 @@ struct MetaCoreSceneDocument {
MC_PROPERTY()
std::vector<MetaCoreGameObjectData> GameObjects{};
MC_PROPERTY()
std::vector<MetaCoreSubSceneDocument> SubScenes{};
MC_PROPERTY()
std::vector<MetaCorePrefabInstanceDocument> PrefabInstances{};
MC_PROPERTY()
std::vector<MetaCoreSchemaVersionEntry> ComponentVersions{};
MC_PROPERTY()
MetaCoreEditorSelectionSnapshot Selection{};
};
@ -86,11 +221,29 @@ MC_STRUCT()
struct MetaCorePrefabDocument {
MC_GENERATED_BODY()
MC_PROPERTY()
std::uint32_t SchemaVersion = GMetaCorePrefabSchemaVersion;
MC_PROPERTY()
MetaCoreAssetGuid PrefabGuid{};
MC_PROPERTY()
MetaCoreAssetGuid BasePrefabGuid{};
MC_PROPERTY()
std::string Name{};
MC_PROPERTY()
std::vector<MetaCoreGameObjectData> GameObjects{};
MC_PROPERTY()
std::vector<MetaCoreNestedPrefabDocument> NestedPrefabs{};
MC_PROPERTY()
std::vector<MetaCorePrefabOverrideDocument> Overrides{};
MC_PROPERTY()
std::vector<MetaCoreSchemaVersionEntry> ComponentVersions{};
};
MC_ENUM()

View File

@ -0,0 +1,175 @@
#pragma once
#include "MetaCoreScene/MetaCoreScene.h"
#include <chrono>
#include <functional>
#include <future>
#include <memory>
#include <optional>
#include <unordered_map>
#include <unordered_set>
namespace MetaCore {
enum class MetaCoreSceneLoadMode {
Replace = 0,
Additive,
SubScene
};
enum class MetaCoreSceneLoadState {
Unloaded = 0,
Loading,
Loaded,
Failed,
Cancelled
};
enum class MetaCoreSceneLifecycleEventType {
BeforeUnload = 0,
AfterUnload,
AfterLoad
};
struct MetaCoreSceneLifecycleEvent {
MetaCoreSceneLifecycleEventType Type = MetaCoreSceneLifecycleEventType::AfterLoad;
MetaCoreAssetGuid SceneGuid{};
std::vector<MetaCoreId> ObjectIds{};
};
struct MetaCoreSceneLoadStatus {
std::uint64_t RequestId = 0;
MetaCoreAssetGuid SceneGuid{};
MetaCoreSceneLoadState State = MetaCoreSceneLoadState::Unloaded;
std::string Error{};
};
struct MetaCoreSceneWorldSettings {
double MainThreadCommitBudgetMilliseconds = 2.0;
std::size_t MemoryBudgetBytes = 512U * 1024U * 1024U;
};
struct MetaCoreLoadedSceneInfo {
MetaCoreAssetGuid SceneGuid{};
std::string Name{};
MetaCoreSceneLoadMode LoadMode = MetaCoreSceneLoadMode::Additive;
bool Active = false;
bool Dirty = false;
bool HardResident = false;
std::vector<MetaCoreId> RuntimeObjectIds{};
};
struct MetaCoreSceneWorldSnapshotEntry {
MetaCoreSceneDocument Document{};
MetaCoreSceneLoadMode LoadMode = MetaCoreSceneLoadMode::Additive;
bool Dirty = false;
};
struct MetaCoreSceneWorldSnapshot {
MetaCoreSceneSnapshot CompositeScene{};
MetaCoreAssetGuid ActiveSceneGuid{};
std::vector<MetaCoreSceneWorldSnapshotEntry> Scenes{};
};
class MetaCoreSceneWorld {
public:
using SceneLoader = std::function<std::optional<MetaCoreSceneDocument>(const MetaCoreAssetGuid&)>;
using LifecycleListener = std::function<void(const MetaCoreSceneLifecycleEvent&)>;
explicit MetaCoreSceneWorld(SceneLoader loader = {}, MetaCoreSceneWorldSettings settings = {});
MetaCoreSceneWorld(MetaCoreScene& scene, SceneLoader loader = {}, MetaCoreSceneWorldSettings settings = {});
~MetaCoreSceneWorld();
MetaCoreSceneWorld(const MetaCoreSceneWorld&) = delete;
MetaCoreSceneWorld& operator=(const MetaCoreSceneWorld&) = delete;
[[nodiscard]] MetaCoreScene& GetScene() { return *Scene_; }
[[nodiscard]] const MetaCoreScene& GetScene() const { return *Scene_; }
void SetSceneLoader(SceneLoader loader);
[[nodiscard]] std::uint64_t RequestLoad(
const MetaCoreAssetGuid& sceneGuid,
MetaCoreSceneLoadMode mode = MetaCoreSceneLoadMode::Additive,
std::int32_t priority = 0
);
[[nodiscard]] bool CancelLoad(std::uint64_t requestId);
void Tick();
void UpdateStreaming(const std::vector<glm::vec3>& sourcePositions);
[[nodiscard]] bool LoadDocumentNow(
MetaCoreSceneDocument document,
MetaCoreSceneLoadMode mode = MetaCoreSceneLoadMode::Additive,
std::string* error = nullptr
);
[[nodiscard]] bool UnloadScene(const MetaCoreAssetGuid& sceneGuid, bool force = false);
[[nodiscard]] bool SetActiveScene(const MetaCoreAssetGuid& sceneGuid);
[[nodiscard]] MetaCoreAssetGuid GetActiveSceneGuid() const { return ActiveSceneGuid_; }
[[nodiscard]] bool IsSceneLoaded(const MetaCoreAssetGuid& sceneGuid) const;
[[nodiscard]] std::vector<MetaCoreLoadedSceneInfo> GetLoadedScenes() const;
[[nodiscard]] std::optional<MetaCoreSceneLoadStatus> GetLoadStatus(std::uint64_t requestId) const;
[[nodiscard]] MetaCoreGameObject ResolveObject(const MetaCoreObjectReference& reference);
[[nodiscard]] MetaCoreGameObject ResolveObject(const MetaCoreObjectReference& reference) const;
[[nodiscard]] MetaCoreObjectReference BuildObjectReference(
MetaCoreId runtimeObjectId,
MetaCoreObjectReferenceStrength strength = MetaCoreObjectReferenceStrength::Soft
) const;
[[nodiscard]] bool MoveSubtreeToScene(MetaCoreId rootObjectId, const MetaCoreAssetGuid& targetSceneGuid);
[[nodiscard]] std::optional<MetaCoreSceneDocument> CaptureSceneDocument(const MetaCoreAssetGuid& sceneGuid) const;
[[nodiscard]] MetaCoreSceneWorldSnapshot CaptureWorldSnapshot() const;
void RestoreWorldSnapshot(const MetaCoreSceneWorldSnapshot& snapshot);
[[nodiscard]] std::vector<std::string> ValidateDependencies(const MetaCoreSceneDocument& root) const;
std::uint64_t AddLifecycleListener(LifecycleListener listener);
void RemoveLifecycleListener(std::uint64_t listenerId);
private:
struct ObjectKey {
MetaCoreAssetGuid SceneGuid{};
MetaCoreAssetGuid ObjectGuid{};
bool operator==(const ObjectKey&) const = default;
};
struct ObjectKeyHasher {
std::size_t operator()(const ObjectKey& value) const noexcept;
};
struct LoadedScene {
MetaCoreSceneDocument Source{};
MetaCoreSceneLoadMode LoadMode = MetaCoreSceneLoadMode::Additive;
std::vector<MetaCoreId> RuntimeObjectIds{};
std::unordered_map<MetaCoreId, MetaCoreId> RuntimeToSerializedId{};
bool Dirty = false;
};
struct PendingLoad;
[[nodiscard]] bool CommitDocument(MetaCoreSceneDocument document, MetaCoreSceneLoadMode mode, std::string* error);
void Publish(MetaCoreSceneLifecycleEvent event);
void RebuildObjectIndex();
void RebuildHardResidency();
SceneLoader Loader_{};
MetaCoreSceneWorldSettings Settings_{};
std::unique_ptr<MetaCoreScene> OwnedScene_{};
MetaCoreScene* Scene_ = nullptr;
MetaCoreAssetGuid ActiveSceneGuid_{};
std::unordered_map<MetaCoreAssetGuid, LoadedScene, MetaCoreAssetGuidHasher> LoadedScenes_{};
std::unordered_map<ObjectKey, MetaCoreId, ObjectKeyHasher> ObjectIndex_{};
std::unordered_set<MetaCoreAssetGuid, MetaCoreAssetGuidHasher> HardResidentScenes_{};
std::vector<std::unique_ptr<PendingLoad>> PendingLoads_{};
std::unordered_map<std::uint64_t, MetaCoreSceneLoadStatus> LoadStatuses_{};
std::unordered_map<std::uint64_t, LifecycleListener> Listeners_{};
std::uint64_t NextRequestId_ = 1;
std::uint64_t NextListenerId_ = 1;
};
[[nodiscard]] MetaCoreAssetGuid MetaCoreBuildDeterministicGuid(
const MetaCoreAssetGuid& namespaceGuid,
const MetaCoreAssetGuid& localGuid
);
[[nodiscard]] MetaCoreAssetGuid MetaCoreBuildDeterministicGuid(
const MetaCoreAssetGuid& namespaceGuid,
std::uint64_t localValue
);
} // namespace MetaCore

View File

@ -359,14 +359,25 @@ R8.1R8.5 已落地的工程边界:
### 10. Scene 与 Prefab
需要补齐
已完成
- [ ] 多场景编辑、Additive Load、SubScene 和异步流式加载。
- [ ] 大场景分区、按需加载和场景依赖检查。
- [ ] 嵌套 Prefab、Prefab Variant 和实例同步。
- [ ] 细粒度 Override 记录、可视化、Apply 和 Revert。
- [ ] 跨场景对象引用规则。
- [ ] Scene、Component、Prefab 的格式版本迁移机制。
- [x] 多场景编辑、Additive Load、SubScene 和异步流式加载。
- [x] 大场景分区、按需加载和场景依赖检查。
- [x] 嵌套 Prefab、Prefab Variant 和实例同步。
- [x] 细粒度 Override 记录、可视化、Apply 和 Revert。
- [x] 跨场景对象引用规则。
- [x] Scene、Component、Prefab 的格式版本迁移机制。
实现与验收基线2026-07-27
- 新增 `MetaCoreSceneWorld`,在一个组合 ECS 上管理 Replace/Additive/SubScene、活动 Scene、Scene Set 快照、异步请求去重/取消、优先级、距离迟滞、Hard 驻留、生命周期事件和限定对象引用Play/Stop 会恢复完整 Scene Set、活动 Scene、对象 ID、选择与 Dirty 状态。
- Scene/Prefab Schema 升级到版本 2持久化 Scene/Prefab/对象/Prefab 节点 GUID、SubScene Cell 元数据、组件版本、Prefab Base/嵌套关系和属性/组件/结构 Override。旧文档缺失 GUID 时使用确定性迁移高版本文档拒绝加载Cooked Package 同步写入 `CustomVersions` 与依赖表。
- Prefab 解析顺序固定为 Base → Variant → 嵌套 Prefab → Scene Instance Override循环与缺失依赖拒绝解析孤立 Override 可诊断,相同 Instance GUID 重复展开得到相同稳定对象 GUID。Override 注册表覆盖属性、组件增删、子对象增删和重挂接,编辑器继续提供字段级 Apply/Revert 与 Override 标记。
- Editor Hierarchy 按 Scene 分组并提供 Set Active、Unload、Save All 和 Additive Open新对象归属活动 Scene跨 Scene 直接父子关系被拒绝,跨场景移动通过 World 事务完成。根 Scene Environment 不被活动 Scene 覆盖。
- Cook 从启动 Scene 遍历 SubScene、Hard Scene 引用和 Prefab Base/Variant/嵌套依赖,缺失或循环在打包阶段失败;运行时清单记录 Scene GUID、包路径、Hash 和依赖Player 与 Editor 共用 `MetaCoreSceneWorld` 的 AlwaysLoaded/Distance 流式规则。
- 新增 `MetaCoreSceneTests``MetaCoreSceneBenchmark`。Linux x86_64 Development 全量 CTest **14/14** 通过10,000 对象基准实测 load **35.3637.10 ms**、capture p95 **8.9912.17 ms**、100 节点 Prefab 展开 p95 **0.0270.031 ms**,持续门禁按签收上界约 120% 设置为 **45 ms / 15 ms / 0.04 ms**
首版边界保持为显式 SubScene 加可选网格 Cell不做编辑器自动世界切分正式关闭平台为 Linux x86_64Windows 仅保留可编译接口。
### 11. 编辑器生产力与插件边界

View File

@ -52,6 +52,12 @@ int main() {
delivery.BuildId = "test-build";
delivery.StartupScene = "payload.bin";
delivery.ScriptModules.push_back("Project/Scripts/TestScripts.so");
MetaCore::MetaCoreRuntimeDeliveryDocument::SceneEntry sceneEntry;
sceneEntry.SceneGuid = MetaCore::MetaCoreAssetGuid::Generate();
sceneEntry.PackagePath = "Content/Scenes/root.mcscene";
sceneEntry.SourceHash = 42;
sceneEntry.Dependencies.push_back(MetaCore::MetaCoreAssetGuid::Generate());
delivery.ScenePackages.push_back(sceneEntry);
WriteText(package / delivery.ScriptModules.front(), "script");
Expect(MetaCore::MetaCoreWriteRuntimeDeliveryDocument(package / "Project" / "MetaCore.runtimeproject", delivery), "write runtime delivery");
MetaCore::MetaCoreReleaseManifestDocument manifest;
@ -67,6 +73,10 @@ int main() {
Expect(MetaCore::MetaCoreValidateReleasePackage(package).empty(), "valid package accepted");
const auto readDelivery = MetaCore::MetaCoreReadRuntimeDeliveryDocument(package / "Project" / "MetaCore.runtimeproject");
Expect(readDelivery.has_value() && readDelivery->ScriptModules == delivery.ScriptModules, "runtime delivery preserves script module paths");
Expect(readDelivery && readDelivery->ScenePackages.size() == 1 &&
readDelivery->ScenePackages[0].SceneGuid == sceneEntry.SceneGuid &&
readDelivery->ScenePackages[0].Dependencies == sceneEntry.Dependencies,
"runtime delivery preserves scene dependency manifest");
WriteText(package / "payload.bin", "tampered");
const auto corruptIssues = MetaCore::MetaCoreValidateReleasePackage(package);
Expect(!corruptIssues.empty(), "corrupt package rejected");

View File

@ -0,0 +1,81 @@
#include "MetaCoreScene/MetaCorePrefab.h"
#include "MetaCoreScene/MetaCoreSceneWorld.h"
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <iostream>
#include <vector>
namespace {
double MillisecondsSince(std::chrono::steady_clock::time_point begin) {
return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - begin).count();
}
} // namespace
int main() {
using namespace MetaCore;
MetaCoreSceneDocument largeScene;
largeScene.SceneGuid = MetaCoreAssetGuid::Generate();
largeScene.Name = "SceneBenchmark";
constexpr std::size_t objectCount = 10000;
largeScene.GameObjects.reserve(objectCount);
for (std::size_t index = 0; index < objectCount; ++index) {
MetaCoreGameObjectData object;
object.Id = index + 1;
object.ParentId = index == 0 ? 0 : (index / 10) + 1;
if (object.ParentId == object.Id) object.ParentId = 0;
object.SceneGuid = largeScene.SceneGuid;
object.StableObjectGuid = MetaCoreBuildDeterministicGuid(largeScene.SceneGuid, object.Id);
object.Name = "Object_" + std::to_string(index);
largeScene.GameObjects.push_back(std::move(object));
}
MetaCoreSceneWorld world;
const auto loadBegin = std::chrono::steady_clock::now();
if (!world.LoadDocumentNow(largeScene, MetaCoreSceneLoadMode::Replace)) return EXIT_FAILURE;
const double loadMilliseconds = MillisecondsSince(loadBegin);
std::vector<double> captureSamples;
captureSamples.reserve(40);
for (int sample = 0; sample < 40; ++sample) {
const auto begin = std::chrono::steady_clock::now();
if (!world.CaptureSceneDocument(largeScene.SceneGuid)) return EXIT_FAILURE;
captureSamples.push_back(MillisecondsSince(begin));
}
std::sort(captureSamples.begin(), captureSamples.end());
const double captureP95 = captureSamples[static_cast<std::size_t>(captureSamples.size() * 0.95)];
MetaCorePrefabDocument prefab;
prefab.PrefabGuid = MetaCoreAssetGuid::Generate();
prefab.Name = "BenchmarkPrefab";
prefab.GameObjects.assign(largeScene.GameObjects.begin(), largeScene.GameObjects.begin() + 100);
MetaCorePrefabResolver resolver([&](const MetaCoreAssetGuid& guid) -> std::optional<MetaCorePrefabDocument> {
return guid == prefab.PrefabGuid ? std::optional(prefab) : std::nullopt;
});
std::vector<double> prefabSamples;
prefabSamples.reserve(100);
for (int sample = 0; sample < 100; ++sample) {
const auto begin = std::chrono::steady_clock::now();
if (!resolver.Instantiate(prefab.PrefabGuid, MetaCoreAssetGuid::Generate())) return EXIT_FAILURE;
prefabSamples.push_back(MillisecondsSince(begin));
}
std::sort(prefabSamples.begin(), prefabSamples.end());
const double prefabP95 = prefabSamples[static_cast<std::size_t>(prefabSamples.size() * 0.95)];
std::cout << "SceneBenchmark objects=" << objectCount
<< " load_ms=" << loadMilliseconds
<< " capture_p95_ms=" << captureP95
<< " prefab100_p95_ms=" << prefabP95 << '\n';
// Linux x86_64 signed baseline (2026-07-27): load 37.10 ms,
// capture p95 12.17 ms, prefab(100 nodes) p95 0.031 ms. Gates are
// rounded up from 120% to avoid sub-microsecond timer noise.
if (loadMilliseconds > 45.0 || captureP95 > 15.0 || prefabP95 > 0.04) {
std::cerr << "Scene/Prefab benchmark exceeded Linux x86_64 regression gate\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

View File

@ -0,0 +1,306 @@
#include "MetaCoreScene/MetaCorePrefab.h"
#include "MetaCoreScene/MetaCoreScenePackage.h"
#include "MetaCoreScene/MetaCoreSceneSerializer.h"
#include "MetaCoreScene/MetaCoreSceneWorld.h"
#include "MetaCoreFoundation/MetaCorePackage.h"
#include <chrono>
#include <filesystem>
#include <iostream>
#include <thread>
#include <unordered_map>
namespace {
using namespace MetaCore;
void Expect(bool condition, const char* message) {
if (!condition) {
std::cerr << "FAILED: " << message << '\n';
std::exit(1);
}
}
MetaCoreSceneDocument MakeScene(const char* name, std::size_t objectCount = 1) {
MetaCoreSceneDocument document;
document.SceneGuid = MetaCoreAssetGuid::Generate();
document.Name = name;
for (std::size_t index = 0; index < objectCount; ++index) {
MetaCoreGameObjectData object;
object.Id = index + 1;
object.ParentId = index == 0 ? 0 : index;
object.SceneGuid = document.SceneGuid;
object.StableObjectGuid = MetaCoreBuildDeterministicGuid(document.SceneGuid, object.Id);
object.Name = std::string(name) + std::to_string(index);
document.GameObjects.push_back(std::move(object));
}
return document;
}
void TickUntilSettled(MetaCoreSceneWorld& world, std::uint64_t request) {
for (int iteration = 0; iteration < 1000; ++iteration) {
world.Tick();
const auto status = world.GetLoadStatus(request);
if (status && status->State != MetaCoreSceneLoadState::Loading) return;
std::this_thread::yield();
}
Expect(false, "asynchronous scene request did not settle");
}
void TestSceneJsonAndPackageVersions() {
const auto root = std::filesystem::temp_directory_path() / "MetaCoreSceneTests";
std::filesystem::create_directories(root);
const auto jsonPath = root / "Versioned.mcscene.json";
const auto packagePath = root / "Versioned.mcscene";
auto document = MakeScene("Versioned", 2);
MetaCoreSubSceneDocument sub;
sub.SceneGuid = MetaCoreAssetGuid::Generate();
sub.ScenePath = "Scenes/Cell.mcscene.json";
sub.LoadPolicy = MetaCoreSubSceneLoadPolicy::Distance;
sub.PreloadDistance = 25.0F;
sub.UnloadDistance = 30.0F;
document.SubScenes.push_back(sub);
document.ComponentVersions.push_back({"Transform", 2});
const auto registry = MetaCoreBuildScenePackageTypeRegistry();
Expect(MetaCoreSceneSerializer::SaveSceneToJson(jsonPath, document, registry), "versioned scene JSON save");
const auto jsonLoaded = MetaCoreSceneSerializer::LoadSceneFromJson(jsonPath, registry);
Expect(jsonLoaded && jsonLoaded->SchemaVersion == GMetaCoreSceneSchemaVersion, "scene JSON version round trip");
Expect(jsonLoaded->SceneGuid == document.SceneGuid, "scene GUID round trip");
Expect(jsonLoaded->GameObjects[1].StableObjectGuid == document.GameObjects[1].StableObjectGuid, "stable object GUID round trip");
Expect(jsonLoaded->SubScenes.size() == 1 && jsonLoaded->SubScenes[0].SceneGuid == sub.SceneGuid, "SubScene JSON round trip");
MetaCorePrefabDocument prefab;
prefab.Name = "VersionedPrefab";
prefab.PrefabGuid = MetaCoreAssetGuid::Generate();
prefab.GameObjects = document.GameObjects;
prefab.GameObjects[0].SceneGuid = {};
prefab.GameObjects[1].SceneGuid = {};
prefab.Overrides.push_back({
prefab.GameObjects[0].StableObjectGuid, "Name", "Value",
MetaCorePrefabOverrideOperation::SetProperty, "\"Renamed\""
});
const auto prefabPath = root / "Versioned.mcprefab.json";
Expect(MetaCoreSceneSerializer::SavePrefabToJson(prefabPath, prefab, registry), "versioned prefab JSON save");
const auto prefabLoaded = MetaCoreSceneSerializer::LoadPrefabFromJson(prefabPath, registry);
Expect(prefabLoaded && prefabLoaded->SchemaVersion == GMetaCorePrefabSchemaVersion,
"prefab JSON version round trip");
Expect(prefabLoaded->PrefabGuid == prefab.PrefabGuid && prefabLoaded->Overrides.size() == 1,
"prefab GUID and override round trip");
MetaCoreSceneDocument legacy;
legacy.Name = "LegacyDeterministic";
legacy.GameObjects.push_back(MetaCoreGameObjectData{});
legacy.GameObjects[0].Id = 42;
const auto legacyFirstPath = root / "LegacyFirst.mcscene.json";
const auto legacySecondPath = root / "LegacySecond.mcscene.json";
Expect(MetaCoreSceneSerializer::SaveSceneToJson(legacyFirstPath, legacy, registry), "legacy scene first migration save");
Expect(MetaCoreSceneSerializer::SaveSceneToJson(legacySecondPath, legacy, registry), "legacy scene second migration save");
const auto legacyFirst = MetaCoreSceneSerializer::LoadSceneFromJson(legacyFirstPath, registry);
const auto legacySecond = MetaCoreSceneSerializer::LoadSceneFromJson(legacySecondPath, registry);
Expect(legacyFirst && legacySecond && legacyFirst->SceneGuid == legacySecond->SceneGuid,
"legacy scene GUID migration is deterministic");
Expect(legacyFirst->GameObjects[0].StableObjectGuid == legacySecond->GameObjects[0].StableObjectGuid,
"legacy object GUID migration is deterministic");
Expect(MetaCoreWriteScenePackage(packagePath, document), "versioned scene package save");
const auto packageLoaded = MetaCoreReadScenePackage(packagePath);
Expect(packageLoaded && packageLoaded->SceneGuid == document.SceneGuid, "scene package round trip");
const auto package = MetaCoreReadPackageFile(packagePath, registry);
Expect(package && package->Header.PackageGuid == document.SceneGuid, "scene package uses scene GUID");
Expect(package->Dependencies.size() == 1 && package->Dependencies[0].AssetGuid == sub.SceneGuid, "scene package dependency table");
Expect(!package->CustomVersions.empty() && package->CustomVersions[0].Key == "MetaCore.Scene", "scene package custom version");
}
void TestAdditiveWorldAndQualifiedReferences() {
auto first = MakeScene("Root", 2);
auto second = MakeScene("Cell", 1);
first.GameObjects[0].PhysicsConstraint = MetaCorePhysicsConstraintComponent{};
first.GameObjects[0].PhysicsConstraint->TargetObjectReference = {
second.SceneGuid,
second.GameObjects[0].StableObjectGuid,
MetaCoreObjectReferenceStrength::Hard
};
std::unordered_map<MetaCoreAssetGuid, MetaCoreSceneDocument, MetaCoreAssetGuidHasher> documents;
documents.emplace(second.SceneGuid, second);
MetaCoreSceneWorld world([&](const MetaCoreAssetGuid& guid) -> std::optional<MetaCoreSceneDocument> {
const auto iterator = documents.find(guid);
return iterator == documents.end() ? std::nullopt : std::optional(iterator->second);
});
Expect(world.LoadDocumentNow(first, MetaCoreSceneLoadMode::Replace), "replace load root scene");
const auto sourceRoot = world.ResolveObject({
first.SceneGuid, first.GameObjects[0].StableObjectGuid, MetaCoreObjectReferenceStrength::Soft
});
Expect(sourceRoot, "source root resolves before additive load");
Expect(world.GetLoadedScenes().size() == 1 && world.GetScene().GetGameObjects().size() == 2, "root scene object count");
const auto requestStatusesBefore = world.GetLoadedScenes();
(void)requestStatusesBefore;
for (int iteration = 0; iteration < 1000 && !world.IsSceneLoaded(second.SceneGuid); ++iteration) {
world.Tick();
std::this_thread::yield();
}
Expect(world.IsSceneLoaded(second.SceneGuid), "hard reference loads target scene");
Expect(world.GetScene().GetGameObjects().size() == 3, "additive objects share composite ECS");
Expect(!world.UnloadScene(second.SceneGuid), "hard referenced scene cannot unload");
const auto targetReference = MetaCoreObjectReference{
second.SceneGuid,
second.GameObjects[0].StableObjectGuid,
MetaCoreObjectReferenceStrength::Soft
};
const auto target = world.ResolveObject(targetReference);
Expect(target, "qualified object reference resolves");
const auto rebuilt = world.BuildObjectReference(target.GetId());
Expect(rebuilt.SceneGuid == second.SceneGuid && rebuilt.StableObjectGuid == targetReference.StableObjectGuid,
"runtime object builds stable qualified reference");
const auto captured = world.CaptureSceneDocument(first.SceneGuid);
Expect(captured && captured->GameObjects.size() == 2, "capture filters one loaded scene");
Expect(world.SetActiveScene(second.SceneGuid) && world.GetActiveSceneGuid() == second.SceneGuid, "active additive scene");
Expect(world.MoveSubtreeToScene(sourceRoot.GetId(), second.SceneGuid), "move subtree across scenes");
const auto movedCapture = world.CaptureSceneDocument(second.SceneGuid);
Expect(movedCapture && movedCapture->GameObjects.size() == 3, "move subtree updates target scene ownership");
const auto worldSnapshot = world.CaptureWorldSnapshot();
Expect(world.UnloadScene(first.SceneGuid, true), "hard reference source can force unload");
Expect(world.UnloadScene(second.SceneGuid), "hard residency releases after reference source unloads");
world.RestoreWorldSnapshot(worldSnapshot);
Expect(world.GetLoadedScenes().size() == 2 && world.GetScene().GetGameObjects().size() == 3,
"world snapshot restores complete scene set");
Expect(world.GetActiveSceneGuid() == second.SceneGuid, "world snapshot restores active scene");
}
void TestAsynchronousLoadCancelAndStreaming() {
auto root = MakeScene("StreamingRoot");
auto cell = MakeScene("StreamingCell");
MetaCoreSubSceneDocument sub;
sub.SceneGuid = cell.SceneGuid;
sub.LoadPolicy = MetaCoreSubSceneLoadPolicy::Distance;
sub.BoundsCenter = {100.0F, 0.0F, 0.0F};
sub.BoundsExtent = {5.0F, 5.0F, 5.0F};
sub.PreloadDistance = 10.0F;
sub.UnloadDistance = 20.0F;
root.SubScenes.push_back(sub);
MetaCoreSceneWorld world([cell](const MetaCoreAssetGuid& guid) -> std::optional<MetaCoreSceneDocument> {
return guid == cell.SceneGuid ? std::optional(cell) : std::nullopt;
});
Expect(world.LoadDocumentNow(root, MetaCoreSceneLoadMode::Replace), "streaming root load");
world.UpdateStreaming({glm::vec3(90.0F, 0.0F, 0.0F)});
for (int iteration = 0; iteration < 1000 && !world.IsSceneLoaded(cell.SceneGuid); ++iteration) {
world.Tick();
std::this_thread::yield();
}
Expect(world.IsSceneLoaded(cell.SceneGuid), "distance cell loads near threshold");
world.UpdateStreaming({glm::vec3(111.0F, 0.0F, 0.0F)});
Expect(world.IsSceneLoaded(cell.SceneGuid), "distance hysteresis keeps cell before unload threshold");
world.UpdateStreaming({glm::vec3(130.0F, 0.0F, 0.0F)});
Expect(!world.IsSceneLoaded(cell.SceneGuid), "distance cell unloads beyond threshold");
const auto request = world.RequestLoad(cell.SceneGuid);
const auto duplicateRequest = world.RequestLoad(cell.SceneGuid);
Expect(duplicateRequest == request, "duplicate asynchronous scene requests are coalesced");
Expect(world.CancelLoad(request), "pending asynchronous load cancels");
TickUntilSettled(world, request);
Expect(world.GetLoadStatus(request)->State == MetaCoreSceneLoadState::Cancelled, "cancelled request remains cancelled");
}
void TestSceneDependencyValidation() {
auto root = MakeScene("DependencyRoot");
auto child = MakeScene("DependencyChild");
root.SubScenes.push_back({child.SceneGuid});
child.SubScenes.push_back({root.SceneGuid});
std::unordered_map<MetaCoreAssetGuid, MetaCoreSceneDocument, MetaCoreAssetGuidHasher> documents{
{root.SceneGuid, root}, {child.SceneGuid, child}
};
MetaCoreSceneWorld world([&](const MetaCoreAssetGuid& guid) -> std::optional<MetaCoreSceneDocument> {
const auto iterator = documents.find(guid);
return iterator == documents.end() ? std::nullopt : std::optional(iterator->second);
});
const auto issues = world.ValidateDependencies(root);
Expect(std::any_of(issues.begin(), issues.end(), [](const std::string& issue) {
return issue.find("cycle") != std::string::npos;
}), "recursive SubScene cycle is rejected");
}
void TestPrefabVariantNestedAndOverrides() {
MetaCorePrefabDocument base;
base.PrefabGuid = MetaCoreAssetGuid::Generate();
base.Name = "Base";
MetaCoreGameObjectData root;
root.Id = 1;
root.StableObjectGuid = MetaCoreBuildDeterministicGuid(base.PrefabGuid, 1);
root.Name = "BaseRoot";
base.GameObjects.push_back(root);
MetaCorePrefabDocument nested;
nested.PrefabGuid = MetaCoreAssetGuid::Generate();
nested.Name = "Nested";
MetaCoreGameObjectData nestedRoot;
nestedRoot.Id = 1;
nestedRoot.StableObjectGuid = MetaCoreBuildDeterministicGuid(nested.PrefabGuid, 1);
nestedRoot.Name = "NestedRoot";
nested.GameObjects.push_back(nestedRoot);
MetaCorePrefabDocument variant;
variant.PrefabGuid = MetaCoreAssetGuid::Generate();
variant.BasePrefabGuid = base.PrefabGuid;
variant.Name = "Variant";
variant.Overrides.push_back({
root.StableObjectGuid, "Transform", "Position", MetaCorePrefabOverrideOperation::SetProperty, "[1,2,3]"
});
MetaCoreNestedPrefabDocument nestedInstance;
nestedInstance.MountNodeGuid = root.StableObjectGuid;
nestedInstance.PrefabAssetGuid = nested.PrefabGuid;
nestedInstance.InstanceGuid = MetaCoreAssetGuid::Generate();
variant.NestedPrefabs.push_back(nestedInstance);
std::unordered_map<MetaCoreAssetGuid, MetaCorePrefabDocument, MetaCoreAssetGuidHasher> prefabs{
{base.PrefabGuid, base}, {nested.PrefabGuid, nested}, {variant.PrefabGuid, variant}
};
MetaCorePrefabResolver resolver([&](const MetaCoreAssetGuid& guid) -> std::optional<MetaCorePrefabDocument> {
const auto iterator = prefabs.find(guid);
return iterator == prefabs.end() ? std::nullopt : std::optional(iterator->second);
});
const auto resolved = resolver.Resolve(variant.PrefabGuid);
Expect(resolved && resolved->Document.GameObjects.size() == 2, "variant resolves base and nested prefab");
Expect(resolved->Document.GameObjects[0].Transform.Position == glm::vec3(1.0F, 2.0F, 3.0F), "variant property override");
Expect(resolved->Document.GameObjects[1].ParentId == resolved->Document.GameObjects[0].Id, "nested prefab mounts below target node");
const auto instanceGuid = MetaCoreAssetGuid::Generate();
const auto firstInstance = resolver.Instantiate(variant.PrefabGuid, instanceGuid);
const auto secondInstance = resolver.Instantiate(variant.PrefabGuid, instanceGuid);
Expect(firstInstance && secondInstance && firstInstance->size() == secondInstance->size(), "prefab instantiate succeeds");
for (std::size_t index = 0; index < firstInstance->size(); ++index)
Expect((*firstInstance)[index].StableObjectGuid == (*secondInstance)[index].StableObjectGuid,
"same instance GUID yields stable prefab object GUIDs");
const std::vector<MetaCorePrefabOverrideDocument> structuralOverrides{
{root.StableObjectGuid, "Camera", "", MetaCorePrefabOverrideOperation::AddComponent, "{}"},
{root.StableObjectGuid, "", "", MetaCorePrefabOverrideOperation::AddChild,
R"({"Name":"InstanceChild","Position":[4,5,6]})"}
};
const auto structuralInstance = resolver.Instantiate(
variant.PrefabGuid, MetaCoreAssetGuid::Generate(), structuralOverrides
);
Expect(structuralInstance && structuralInstance->size() == 3,
"instance structural override adds a child");
Expect((*structuralInstance)[0].Camera.has_value(), "instance component override adds a component");
prefabs[base.PrefabGuid].BasePrefabGuid = variant.PrefabGuid;
Expect(!resolver.Resolve(variant.PrefabGuid), "prefab inheritance cycle rejected");
}
} // namespace
int main() {
TestSceneJsonAndPackageVersions();
TestAdditiveWorldAndQualifiedReferences();
TestAsynchronousLoadCancelAndStreaming();
TestSceneDependencyValidation();
TestPrefabVariantNestedAndOverrides();
std::cout << "MetaCoreSceneTests passed\n";
return 0;
}

View File

@ -25,6 +25,7 @@
#include "MetaCoreScene/MetaCoreTransformUtils.h"
#include "MetaCoreFoundation/MetaCoreHash.h"
#include "../Source/MetaCoreEditor/Private/MetaCoreGltfImporter.h"
#include "../Source/MetaCoreEditor/Private/MetaCoreEditorCameraController.h"
#include <nlohmann/json.hpp>
#include <cstdlib>
@ -1429,6 +1430,10 @@ void MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot() {
for (const auto& renderable : snapshot.Renderables) {
if (renderable.ObjectId == root.GetId()) {
MetaCoreExpect(renderable.Visible == true, "RenderSync 根对象 Visible 应为 true");
MetaCoreExpect(
MetaCore::MetaCoreUsesNativeGltfVisibility(renderable),
"导入 glTF 应跳过 ECS 单位代理包围盒,交由 gltfio 使用真实几何包围盒剔除"
);
MetaCoreExpectVec3Near(renderable.BaseColor, glm::vec3(0.5F, 0.6F, 0.7F), "RenderSync 根对象 BaseColor 应正确");
MetaCoreExpect(std::abs(renderable.Metallic - 0.35F) < 0.0001F, "RenderSync 根对象 Metallic 应正确");
MetaCoreExpect(std::abs(renderable.Roughness - 0.45F) < 0.0001F, "RenderSync 根对象 Roughness 应正确");
@ -1442,6 +1447,10 @@ void MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot() {
MetaCoreExpect(renderable.EmissiveTexturePath == "Assets/Textures/SyncEmissive.png", "RenderSync 根对象自发光贴图路径应正确");
MetaCoreExpect(renderable.AoTexturePath == "Assets/Textures/SyncAo.png", "RenderSync 根对象 AO 贴图路径应正确");
} else if (renderable.ObjectId == child.GetId()) {
MetaCoreExpect(
!MetaCore::MetaCoreUsesNativeGltfVisibility(renderable),
"普通内置网格仍应使用引擎可见性系统"
);
MetaCoreExpect(renderable.Visible == false, "RenderSync 子对象 Visible 应为 false");
MetaCoreExpectVec3Near(renderable.BaseColor, glm::vec3(0.1F, 0.2F, 0.3F), "RenderSync 子对象 BaseColor 应正确");
}
@ -1770,8 +1779,8 @@ void MetaCoreTestViewportPickingUsesMeshTrianglesBeforeHierarchyDepth() {
shellMesh.IndexCount = 6;
shellMesh.PayloadIndex = 1;
shellMesh.PayloadSize = makePlanePayload(2.0F).size();
shellMesh.BoundingBoxMin = glm::vec3(-2.0F, -2.0F, 0.0F);
shellMesh.BoundingBoxMax = glm::vec3(2.0F, 2.0F, 0.0F);
shellMesh.BoundingBoxMin = glm::vec3(-2000.0F, -100.0F, -1000.0F);
shellMesh.BoundingBoxMax = glm::vec3(2000.0F, 100.0F, 1000.0F);
document.GeneratedMeshAssets.push_back(shellMesh);
MetaCore::MetaCoreMeshAssetDocument interiorMesh;
@ -1841,6 +1850,12 @@ void MetaCoreTestViewportPickingUsesMeshTrianglesBeforeHierarchyDepth() {
);
MetaCoreExpect(pickedObjectId == shellObject.GetId(), "三角面拾取应选择可见的外壳,而不是层级更深的内部部件");
editorContext.SetSelectedObjectId(shellObject.GetId());
interactionService.FocusSelectedObject(editorContext);
const auto focusedView = editorContext.GetCameraController().BuildSceneView();
const float focusedDistance = glm::length(focusedView.CameraPosition - focusedView.CameraTarget);
MetaCoreExpect(focusedDistance > 5000.0F, "模型聚焦应采用资源真实包围盒,而不是根节点单位 Scale");
MetaCoreExpect(focusedView.FarClip > focusedDistance + 2000.0F, "模型聚焦应扩展 Far Clip 覆盖完整资源包围盒");
coreServicesModule->Shutdown(moduleRegistry);
moduleRegistry.ShutdownServices();
@ -5493,6 +5508,16 @@ void MetaCoreTestParticleRenderableGrowthAndRemoval() {
} // namespace
void MetaCoreTestLargeBoundsEditorCameraFraming() {
MetaCore::MetaCoreEditorCameraController camera;
camera.FocusBounds(glm::vec3(625.0F, -510.0F, 46.0F), 1600.0F);
const MetaCore::MetaCoreSceneView view = camera.BuildSceneView();
const float distance = glm::length(view.CameraPosition - view.CameraTarget);
MetaCoreExpect(distance > 5000.0F, "大模型聚焦距离不应被 100 单位上限截断");
MetaCoreExpect(view.FarClip > distance + 1600.0F, "大模型应完整位于动态 Far Clip 内");
MetaCoreExpect(view.NearClip >= 0.01F && view.NearClip <= 1.0F, "动态 Near Clip 应保持有效范围");
}
int main() {
std::setvbuf(stdout, nullptr, _IONBF, 0);
std::setvbuf(stderr, nullptr, _IONBF, 0);
@ -5587,6 +5612,8 @@ int main() {
MetaCoreTestCameraComponentJsonRoundTripAndLegacyDefaults();
std::cout << "[RUN] MetaCoreTestCameraSelectionAndProjectionUtilities..." << std::endl;
MetaCoreTestCameraSelectionAndProjectionUtilities();
std::cout << "[RUN] MetaCoreTestLargeBoundsEditorCameraFraming..." << std::endl;
MetaCoreTestLargeBoundsEditorCameraFraming();
std::cout << "[RUN] MetaCoreTestViewportPickingPrefersModelChildNode..." << std::endl;
MetaCoreTestViewportPickingPrefersModelChildNode();
std::cout << "[RUN] MetaCoreTestViewportPickingUsesMeshTrianglesBeforeHierarchyDepth..." << std::endl;