From 84446d00601fdbeba737b7249a9b39291373033d Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Tue, 9 Jun 2026 15:24:38 +0800 Subject: [PATCH] =?UTF-8?q?=E8=84=9A=E6=9C=AC=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MetaCoreBuiltinCoreServicesModule.cpp | 929 ++++++++++++++++++ .../Private/MetaCoreEditorApp.cpp | 76 +- .../Private/MetaCoreEditorContext.cpp | 26 + .../Private/MetaCoreEditorServices.cpp | 32 + .../MetaCoreEditor/MetaCoreEditorServices.h | 51 + .../Private/MetaCoreFilamentSceneBridge.cpp | 231 ++++- .../Private/MetaCoreImGuiHelper.cpp | 115 ++- .../MetaCoreRender/MetaCoreImGuiHelper.h | 1 + .../MetaCoreScene/Private/MetaCoreScene.cpp | 7 + .../Private/MetaCoreSceneSerializer.cpp | 39 + .../Public/MetaCoreScene/MetaCoreComponents.h | 22 + .../Public/MetaCoreScene/MetaCoreGameObject.h | 2 + tests/MetaCoreSmokeTests.cpp | 416 ++++++++ 13 files changed, 1900 insertions(+), 47 deletions(-) diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp index 4f6f33b..5133f0b 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp @@ -6,6 +6,7 @@ #include "MetaCoreGltfImporter.h" #include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" #include "MetaCoreFoundation/MetaCoreHash.h" +#include "MetaCoreFoundation/MetaCoreId.h" #include "MetaCoreFoundation/MetaCoreAssetGuid.h" #include "MetaCoreFoundation/MetaCoreReflection.h" #include "MetaCoreFoundation/MetaCoreProject.h" @@ -39,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -55,6 +57,54 @@ namespace MetaCore { namespace { +[[nodiscard]] const char* MetaCoreDebugPlayModeStateName(MetaCorePlayModeState state) { + switch (state) { + case MetaCorePlayModeState::Edit: return "Edit"; + case MetaCorePlayModeState::Playing: return "Playing"; + case MetaCorePlayModeState::Paused: return "Paused"; + default: return "Unknown"; + } +} + +void MetaCoreDebugLog(const char* category, const std::string& message) { + std::cerr << "[MetaCoreDebug][" << category << "] " << message << std::endl; +} + +[[nodiscard]] std::string MetaCoreDebugSceneSummary(MetaCoreEditorContext& editorContext) { + const auto objects = editorContext.GetScene().GetGameObjects(); + std::size_t scriptComponentCount = 0; + std::size_t scriptInstanceCount = 0; + for (const MetaCoreGameObject& object : objects) { + if (!object.HasComponent()) { + continue; + } + ++scriptComponentCount; + scriptInstanceCount += object.GetComponent().Instances.size(); + } + + std::ostringstream stream; + stream + << "revision=" << editorContext.GetScene().GetRevision() + << " objects=" << objects.size() + << " scriptComponents=" << scriptComponentCount + << " scriptInstances=" << scriptInstanceCount; + return stream.str(); +} + +[[nodiscard]] std::string MetaCoreDebugScriptIdentity( + const MetaCoreGameObject& gameObject, + const MetaCoreScriptInstanceData& instance +) { + std::ostringstream stream; + stream + << "objectId=" << (gameObject ? gameObject.GetId() : 0) + << " objectName=\"" << (gameObject ? gameObject.GetName() : std::string("")) << "\"" + << " instanceId=" << instance.InstanceId + << " type=\"" << instance.ScriptTypeId << "\"" + << " enabled=" << (instance.Enabled ? "true" : "false"); + return stream.str(); +} + void MetaCoreWriteGuidArray(nlohmann::json& json, const char* key, const std::vector& guids) { if (guids.empty()) { return; @@ -241,6 +291,135 @@ void MetaCoreCopyStringToBuffer(const std::string& value, char* buffer, std::siz std::snprintf(buffer, bufferSize, "%s", value.c_str()); } +[[nodiscard]] nlohmann::json MetaCoreParseScriptFieldsJson(std::string_view fieldsJson) { + if (fieldsJson.empty()) { + return nlohmann::json::object(); + } + + try { + nlohmann::json parsed = nlohmann::json::parse(std::string(fieldsJson)); + if (parsed.is_object()) { + return parsed; + } + } catch (...) { + } + return nlohmann::json::object(); +} + +[[nodiscard]] nlohmann::json MetaCoreScriptFieldDefaultJson(const MetaCoreScriptFieldDefinition& field) { + switch (field.Type) { + case MetaCoreScriptFieldType::Bool: + return field.BoolDefault; + case MetaCoreScriptFieldType::Float: + return field.FloatDefault; + case MetaCoreScriptFieldType::Vec3: + return nlohmann::json::array({field.Vec3Default.x, field.Vec3Default.y, field.Vec3Default.z}); + case MetaCoreScriptFieldType::String: + return field.StringDefault; + } + return nullptr; +} + +[[nodiscard]] std::string MetaCoreBuildDefaultScriptFieldsJson(const MetaCoreScriptDefinition& definition) { + nlohmann::json fields = nlohmann::json::object(); + for (const MetaCoreScriptFieldDefinition& field : definition.Fields) { + if (!field.Name.empty()) { + fields[field.Name] = MetaCoreScriptFieldDefaultJson(field); + } + } + return fields.dump(); +} + +[[nodiscard]] std::string MetaCoreEnsureScriptFieldsJsonDefaults( + const MetaCoreScriptDefinition& definition, + std::string_view fieldsJson +) { + nlohmann::json fields = MetaCoreParseScriptFieldsJson(fieldsJson); + for (const MetaCoreScriptFieldDefinition& field : definition.Fields) { + if (!field.Name.empty() && !fields.contains(field.Name)) { + fields[field.Name] = MetaCoreScriptFieldDefaultJson(field); + } + } + return fields.dump(); +} + +[[nodiscard]] bool MetaCoreReadScriptBoolField( + const nlohmann::json& fields, + std::string_view fieldName, + bool fallback +) { + const auto iterator = fields.find(std::string(fieldName)); + if (iterator == fields.end()) { + return fallback; + } + if (iterator->is_boolean()) { + return iterator->get(); + } + if (iterator->is_number()) { + return iterator->get() != 0.0F; + } + return fallback; +} + +[[nodiscard]] float MetaCoreReadScriptFloatField( + const nlohmann::json& fields, + std::string_view fieldName, + float fallback +) { + const auto iterator = fields.find(std::string(fieldName)); + if (iterator != fields.end() && iterator->is_number()) { + return iterator->get(); + } + return fallback; +} + +[[nodiscard]] glm::vec3 MetaCoreReadScriptVec3Field( + const nlohmann::json& fields, + std::string_view fieldName, + const glm::vec3& fallback +) { + const auto iterator = fields.find(std::string(fieldName)); + if (iterator == fields.end()) { + return fallback; + } + if (iterator->is_array() && iterator->size() >= 3) { + return glm::vec3( + (*iterator)[0].is_number() ? (*iterator)[0].get() : fallback.x, + (*iterator)[1].is_number() ? (*iterator)[1].get() : fallback.y, + (*iterator)[2].is_number() ? (*iterator)[2].get() : fallback.z + ); + } + if (iterator->is_object()) { + const auto readAxis = [&](const char* axis, float axisFallback) { + const auto axisIterator = iterator->find(axis); + return axisIterator != iterator->end() && axisIterator->is_number() + ? axisIterator->get() + : axisFallback; + }; + return glm::vec3(readAxis("x", fallback.x), readAxis("y", fallback.y), readAxis("z", fallback.z)); + } + return fallback; +} + +[[nodiscard]] std::string MetaCoreReadScriptStringField( + const nlohmann::json& fields, + std::string_view fieldName, + const std::string& fallback +) { + const auto iterator = fields.find(std::string(fieldName)); + if (iterator != fields.end() && iterator->is_string()) { + return iterator->get(); + } + return fallback; +} + +[[nodiscard]] std::uint64_t MetaCoreEnsureScriptInstanceId(MetaCoreScriptInstanceData& instance) { + if (instance.InstanceId == 0) { + instance.InstanceId = MetaCoreIdGenerator::Generate(); + } + return instance.InstanceId; +} + [[nodiscard]] bool MetaCoreEditAssetGuidField( MetaCoreEditorContext& editorContext, const char* label, @@ -1737,6 +1916,171 @@ void MetaCoreDrawRotatorComponentInspector(MetaCoreEditorContext& editorContext, } } +void MetaCoreDrawScriptComponentInspector(MetaCoreEditorContext& editorContext, MetaCoreGameObject& gameObject) { + if (!gameObject.HasComponent()) { + return; + } + + MetaCoreScriptComponent& scriptComponent = gameObject.GetComponent(); + const auto scriptRegistry = editorContext.GetModuleRegistry().ResolveService(); + if (scriptRegistry == nullptr) { + ImGui::TextDisabled("脚本注册表不可用"); + return; + } + + const std::vector scriptDefinitions = scriptRegistry->GetScriptDefinitions(); + if (scriptDefinitions.empty()) { + ImGui::TextDisabled("当前没有已注册脚本类型"); + } else if (ImGui::BeginCombo("添加脚本", "选择脚本")) { + for (const MetaCoreScriptDefinition& definition : scriptDefinitions) { + const std::string label = definition.DisplayName.empty() ? definition.TypeId : definition.DisplayName; + if (ImGui::Selectable(label.c_str())) { + (void)editorContext.ExecuteSnapshotCommand("添加脚本", [&]() { + MetaCoreScriptInstanceData instance; + instance.InstanceId = MetaCoreIdGenerator::Generate(); + instance.ScriptTypeId = definition.TypeId; + instance.Enabled = true; + instance.FieldsJson = scriptRegistry->BuildDefaultFieldsJson(definition.TypeId); + scriptComponent.Instances.push_back(std::move(instance)); + editorContext.GetScene().IncrementRevision(); + return true; + }); + } + } + ImGui::EndCombo(); + } + + if (scriptComponent.Instances.empty()) { + ImGui::TextDisabled("尚未挂载脚本"); + return; + } + + for (std::size_t instanceIndex = 0; instanceIndex < scriptComponent.Instances.size();) { + MetaCoreScriptInstanceData& instance = scriptComponent.Instances[instanceIndex]; + const std::uint64_t instanceId = MetaCoreEnsureScriptInstanceId(instance); + const MetaCoreScriptDefinition* definition = scriptRegistry->FindScript(instance.ScriptTypeId); + const std::string title = definition != nullptr + ? (definition->DisplayName.empty() ? definition->TypeId : definition->DisplayName) + : std::string("脚本类型缺失"); + const std::string pushId = std::to_string(instanceId); + + ImGui::Separator(); + ImGui::PushID(pushId.c_str()); + const bool open = ImGui::CollapsingHeader(title.c_str(), ImGuiTreeNodeFlags_DefaultOpen); + ImGui::SameLine(); + if (ImGui::SmallButton("移除")) { + const std::size_t removeIndex = instanceIndex; + (void)editorContext.ExecuteSnapshotCommand("移除脚本", [&]() { + if (removeIndex >= scriptComponent.Instances.size()) { + return false; + } + scriptComponent.Instances.erase(scriptComponent.Instances.begin() + static_cast(removeIndex)); + editorContext.GetScene().IncrementRevision(); + return true; + }); + ImGui::PopID(); + continue; + } + + if (open) { + ImGui::TextDisabled("TypeId: %s", instance.ScriptTypeId.empty() ? "" : instance.ScriptTypeId.c_str()); + + bool enabled = instance.Enabled; + ImGui::Checkbox("启用", &enabled); + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", false); + if (ImGui::IsItemEdited()) { + instance.Enabled = enabled; + editorContext.GetScene().IncrementRevision(); + } + + if (definition == nullptr) { + ImGui::TextColored(ImVec4(0.95F, 0.55F, 0.20F, 1.0F), "脚本类型缺失,播放时会跳过该实例"); + } else { + const std::string ensuredFieldsJson = + scriptRegistry->EnsureFieldsJsonDefaults(instance.ScriptTypeId, instance.FieldsJson); + if (ensuredFieldsJson != instance.FieldsJson) { + instance.FieldsJson = ensuredFieldsJson; + } + + if (ImGui::SmallButton("重置字段")) { + (void)editorContext.ExecuteSnapshotCommand("重置脚本字段", [&]() { + instance.Enabled = true; + instance.FieldsJson = scriptRegistry->BuildDefaultFieldsJson(instance.ScriptTypeId); + editorContext.GetScene().IncrementRevision(); + return true; + }); + } + + nlohmann::json fields = MetaCoreParseScriptFieldsJson(instance.FieldsJson); + for (const MetaCoreScriptFieldDefinition& field : definition->Fields) { + if (field.Name.empty()) { + continue; + } + + const std::string labelPrefix = field.DisplayName.empty() ? field.Name : field.DisplayName; + const std::string label = labelPrefix + "##" + field.Name; + bool fieldChanged = false; + switch (field.Type) { + case MetaCoreScriptFieldType::Bool: { + bool value = MetaCoreReadScriptBoolField(fields, field.Name, field.BoolDefault); + ImGui::Checkbox(label.c_str(), &value); + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", false); + if (ImGui::IsItemEdited()) { + fields[field.Name] = value; + fieldChanged = true; + } + break; + } + case MetaCoreScriptFieldType::Float: { + float value = MetaCoreReadScriptFloatField(fields, field.Name, field.FloatDefault); + ImGui::DragFloat(label.c_str(), &value, 0.1F); + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", true); + if (ImGui::IsItemEdited()) { + fields[field.Name] = value; + fieldChanged = true; + } + break; + } + case MetaCoreScriptFieldType::Vec3: { + glm::vec3 value = MetaCoreReadScriptVec3Field(fields, field.Name, field.Vec3Default); + ImGui::DragFloat3(label.c_str(), glm::value_ptr(value), 0.05F); + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", true); + if (ImGui::IsItemEdited()) { + fields[field.Name] = nlohmann::json::array({value.x, value.y, value.z}); + fieldChanged = true; + } + break; + } + case MetaCoreScriptFieldType::String: { + std::array buffer{}; + MetaCoreCopyStringToBuffer( + MetaCoreReadScriptStringField(fields, field.Name, field.StringDefault), + buffer.data(), + buffer.size() + ); + ImGui::InputText(label.c_str(), buffer.data(), buffer.size()); + MetaCoreTrackComponentInspectorEdit(editorContext, "修改检查器属性", true); + if (ImGui::IsItemEdited()) { + fields[field.Name] = std::string(buffer.data()); + fieldChanged = true; + } + break; + } + } + + if (fieldChanged) { + instance.FieldsJson = fields.dump(); + editorContext.GetScene().IncrementRevision(); + } + } + } + } + + ImGui::PopID(); + ++instanceIndex; + } +} + void MetaCoreDrawMeshRendererComponentInspector(MetaCoreEditorContext& editorContext, MetaCoreGameObject& gameObject) { if (!gameObject.HasComponent()) { return; @@ -5917,6 +6261,51 @@ public: }, MetaCoreDrawRotatorComponentInspector }); + Descriptors_.push_back(MetaCoreComponentDescriptor{ + "Script", + "脚本", + [](const MetaCoreGameObject& gameObject) { return gameObject.HasComponent(); }, + [](MetaCoreGameObject& gameObject) { + if (gameObject.HasComponent()) { + return false; + } + gameObject.AddComponent(MetaCoreScriptComponent{}); + return true; + }, + [](MetaCoreGameObject& gameObject) { + if (!gameObject.HasComponent()) { + return false; + } + gameObject.RemoveComponent(); + return true; + }, + [](MetaCoreGameObject& gameObject) { + if (!gameObject.HasComponent()) { + return false; + } + gameObject.GetComponent() = MetaCoreScriptComponent{}; + return true; + }, + [](const MetaCoreGameObject& gameObject, const MetaCoreTypeRegistry& registry) -> std::optional> { + if (!gameObject.HasComponent()) { + return std::nullopt; + } + return MetaCoreSerializeComponentValue(gameObject.GetComponent(), registry); + }, + [](MetaCoreGameObject& gameObject, std::span payload, const MetaCoreTypeRegistry& registry) { + MetaCoreScriptComponent component{}; + if (!MetaCoreDeserializeComponentValue(payload, component, registry)) { + return false; + } + if (gameObject.HasComponent()) { + gameObject.GetComponent() = component; + } else { + gameObject.AddComponent(component); + } + return true; + }, + MetaCoreDrawScriptComponentInspector + }); Descriptors_.push_back(MetaCoreComponentDescriptor{ "MeshRenderer", "Mesh Renderer", @@ -6031,6 +6420,462 @@ private: std::vector CopiedComponentPayload_{}; }; +class MetaCoreRotatorNativeScript final : public MetaCoreIScriptBehaviour { +public: + void Update(MetaCoreScriptExecutionContext& context, double deltaSeconds) override { + if (deltaSeconds <= 0.0 || + !context.Instance.Enabled || + !context.GameObject.HasComponent()) { + return; + } + + const nlohmann::json fields = MetaCoreParseScriptFieldsJson(context.Instance.FieldsJson); + if (!MetaCoreReadScriptBoolField(fields, "Enabled", true)) { + return; + } + + const float degreesPerSecond = MetaCoreReadScriptFloatField(fields, "DegreesPerSecond", 90.0F); + if (MetaCoreNearlyEqual(degreesPerSecond, 0.0F)) { + return; + } + + context.GameObject.GetComponent().RotationEulerDegrees.y += + degreesPerSecond * static_cast(deltaSeconds); + context.EditorContext.GetScene().IncrementRevision(); + } +}; + +class MetaCoreMoverNativeScript final : public MetaCoreIScriptBehaviour { +public: + void Update(MetaCoreScriptExecutionContext& context, double deltaSeconds) override { + if (deltaSeconds <= 0.0 || + !context.Instance.Enabled || + !context.GameObject.HasComponent()) { + return; + } + + const nlohmann::json fields = MetaCoreParseScriptFieldsJson(context.Instance.FieldsJson); + if (!MetaCoreReadScriptBoolField(fields, "Enabled", true)) { + return; + } + + const glm::vec3 velocity = MetaCoreReadScriptVec3Field(fields, "Velocity", glm::vec3(0.0F, 1.0F, 0.0F)); + if (MetaCoreNearlyEqualVec3(velocity, glm::vec3(0.0F))) { + return; + } + + context.GameObject.GetComponent().Position += + velocity * static_cast(deltaSeconds); + context.EditorContext.GetScene().IncrementRevision(); + } +}; + +class MetaCoreBuiltinScriptRegistry final : public MetaCoreIScriptRegistry { +public: + [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.ScriptRegistry"; } + + void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { + (void)moduleRegistry; + Definitions_.clear(); + + MetaCoreScriptDefinition rotator; + rotator.TypeId = "MetaCore.Script.Rotator"; + rotator.DisplayName = "旋转脚本"; + rotator.Fields = { + MetaCoreScriptFieldDefinition{ + "Enabled", + "启用", + MetaCoreScriptFieldType::Bool, + true, + 0.0F, + glm::vec3(0.0F), + {} + }, + MetaCoreScriptFieldDefinition{ + "DegreesPerSecond", + "角速度 Y (度/秒)", + MetaCoreScriptFieldType::Float, + false, + 90.0F, + glm::vec3(0.0F), + {} + } + }; + rotator.Factory = []() { return std::make_unique(); }; + RegisterScript(std::move(rotator)); + + MetaCoreScriptDefinition mover; + mover.TypeId = "MetaCore.Script.Mover"; + mover.DisplayName = "移动脚本"; + mover.Fields = { + MetaCoreScriptFieldDefinition{ + "Enabled", + "启用", + MetaCoreScriptFieldType::Bool, + true, + 0.0F, + glm::vec3(0.0F), + {} + }, + MetaCoreScriptFieldDefinition{ + "Velocity", + "速度", + MetaCoreScriptFieldType::Vec3, + false, + 0.0F, + glm::vec3(0.0F, 1.0F, 0.0F), + {} + } + }; + mover.Factory = []() { return std::make_unique(); }; + RegisterScript(std::move(mover)); + } + + void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { + (void)moduleRegistry; + Definitions_.clear(); + } + + void RegisterScript(MetaCoreScriptDefinition definition) override { + if (definition.TypeId.empty()) { + return; + } + + const auto iterator = std::find_if(Definitions_.begin(), Definitions_.end(), [&](const MetaCoreScriptDefinition& existing) { + return existing.TypeId == definition.TypeId; + }); + if (iterator != Definitions_.end()) { + *iterator = std::move(definition); + return; + } + Definitions_.push_back(std::move(definition)); + } + + [[nodiscard]] const MetaCoreScriptDefinition* FindScript(std::string_view typeId) const override { + const auto iterator = std::find_if(Definitions_.begin(), Definitions_.end(), [&](const MetaCoreScriptDefinition& definition) { + return definition.TypeId == typeId; + }); + return iterator == Definitions_.end() ? nullptr : &(*iterator); + } + + [[nodiscard]] std::vector GetScriptDefinitions() const override { + return Definitions_; + } + + [[nodiscard]] std::unique_ptr CreateBehaviour(std::string_view typeId) const override { + const MetaCoreScriptDefinition* definition = FindScript(typeId); + if (definition == nullptr || !definition->Factory) { + return nullptr; + } + return definition->Factory(); + } + + [[nodiscard]] std::string BuildDefaultFieldsJson(std::string_view typeId) const override { + const MetaCoreScriptDefinition* definition = FindScript(typeId); + return definition != nullptr ? MetaCoreBuildDefaultScriptFieldsJson(*definition) : std::string("{}"); + } + + [[nodiscard]] std::string EnsureFieldsJsonDefaults(std::string_view typeId, std::string_view fieldsJson) const override { + const MetaCoreScriptDefinition* definition = FindScript(typeId); + return definition != nullptr + ? MetaCoreEnsureScriptFieldsJsonDefaults(*definition, fieldsJson) + : MetaCoreParseScriptFieldsJson(fieldsJson).dump(); + } + +private: + std::vector Definitions_{}; +}; + +class MetaCoreScriptPlayModeComponentSystem final : public MetaCoreIPlayModeComponentSystem { +public: + [[nodiscard]] std::string GetSystemId() const override { return "MetaCore.PlayMode.Script"; } + + void OnPlayStart(MetaCoreEditorContext& editorContext) override { + MetaCoreDebugLog( + "ScriptSystem", + "OnPlayStart begin runtimeEntries=" + std::to_string(RuntimeEntries_.size()) + " " + + MetaCoreDebugSceneSummary(editorContext) + ); + RuntimeEntries_.clear(); + MissingScriptWarnings_.clear(); + DispatchScriptPhase(editorContext, 0.0, ScriptPhase::PlayStart); + MetaCoreDebugLog( + "ScriptSystem", + "OnPlayStart end runtimeEntries=" + std::to_string(RuntimeEntries_.size()) + " " + + MetaCoreDebugSceneSummary(editorContext) + ); + } + + void FixedUpdate(MetaCoreEditorContext& editorContext, double fixedDeltaSeconds) override { + DispatchScriptPhase(editorContext, fixedDeltaSeconds, ScriptPhase::FixedUpdate); + } + + void Update(MetaCoreEditorContext& editorContext, double deltaSeconds) override { + DispatchScriptPhase(editorContext, deltaSeconds, ScriptPhase::Update); + } + + void LateUpdate(MetaCoreEditorContext& editorContext, double deltaSeconds) override { + DispatchScriptPhase(editorContext, deltaSeconds, ScriptPhase::LateUpdate); + } + + void OnPlayStop(MetaCoreEditorContext& editorContext) override { + MetaCoreDebugLog( + "ScriptSystem", + "OnPlayStop begin runtimeEntries=" + std::to_string(RuntimeEntries_.size()) + " " + + MetaCoreDebugSceneSummary(editorContext) + ); + for (RuntimeEntry& entry : RuntimeEntries_) { + if (entry.Behaviour == nullptr || !entry.Started) { + continue; + } + + MetaCoreGameObject gameObject = editorContext.GetScene().FindGameObject(entry.ObjectId); + MetaCoreScriptInstanceData* instance = FindScriptInstance(gameObject, entry.InstanceId); + if (gameObject && instance != nullptr) { + MetaCoreDebugLog( + "ScriptSystem", + "OnPlayStop dispatch " + MetaCoreDebugScriptIdentity(gameObject, *instance) + ); + MetaCoreScriptExecutionContext scriptContext{editorContext, gameObject, *instance}; + entry.Behaviour->OnPlayStop(scriptContext); + } else { + std::ostringstream stream; + stream + << "OnPlayStop skip missing target objectId=" << entry.ObjectId + << " instanceId=" << entry.InstanceId + << " type=\"" << entry.ScriptTypeId << "\""; + MetaCoreDebugLog("ScriptSystem", stream.str()); + } + } + RuntimeEntries_.clear(); + MissingScriptWarnings_.clear(); + MetaCoreDebugLog( + "ScriptSystem", + "OnPlayStop end runtimeEntries=" + std::to_string(RuntimeEntries_.size()) + " " + + MetaCoreDebugSceneSummary(editorContext) + ); + } + +private: + enum class ScriptPhase { + PlayStart, + FixedUpdate, + Update, + LateUpdate + }; + + struct RuntimeEntry { + MetaCoreId ObjectId = 0; + std::uint64_t InstanceId = 0; + std::string ScriptTypeId{}; + std::unique_ptr Behaviour{}; + bool Started = false; + }; + + [[nodiscard]] static MetaCoreScriptInstanceData* FindScriptInstance(MetaCoreGameObject& gameObject, std::uint64_t instanceId) { + if (!gameObject || !gameObject.HasComponent()) { + return nullptr; + } + + MetaCoreScriptComponent& scriptComponent = gameObject.GetComponent(); + const auto iterator = std::find_if(scriptComponent.Instances.begin(), scriptComponent.Instances.end(), [&](MetaCoreScriptInstanceData& instance) { + return instance.InstanceId == instanceId; + }); + return iterator == scriptComponent.Instances.end() ? nullptr : &(*iterator); + } + + [[nodiscard]] RuntimeEntry* FindRuntimeEntry(MetaCoreId objectId, std::uint64_t instanceId) { + const auto iterator = std::find_if(RuntimeEntries_.begin(), RuntimeEntries_.end(), [&](const RuntimeEntry& entry) { + return entry.ObjectId == objectId && entry.InstanceId == instanceId; + }); + return iterator == RuntimeEntries_.end() ? nullptr : &(*iterator); + } + + [[nodiscard]] RuntimeEntry* EnsureRuntimeEntry( + MetaCoreEditorContext& editorContext, + MetaCoreGameObject& gameObject, + MetaCoreScriptInstanceData& instance + ) { + const std::uint64_t instanceId = MetaCoreEnsureScriptInstanceId(instance); + if (instance.ScriptTypeId.empty()) { + WarnMissingScript(editorContext, ""); + return nullptr; + } + + if (RuntimeEntry* existingEntry = FindRuntimeEntry(gameObject.GetId(), instanceId); existingEntry != nullptr) { + if (existingEntry->ScriptTypeId == instance.ScriptTypeId) { + return existingEntry; + } + MetaCoreDebugLog( + "ScriptSystem", + "ReplaceRuntimeEntry " + MetaCoreDebugScriptIdentity(gameObject, instance) + + " oldType=\"" + existingEntry->ScriptTypeId + "\"" + ); + StopRuntimeEntry(editorContext, *existingEntry); + RuntimeEntries_.erase(std::remove_if(RuntimeEntries_.begin(), RuntimeEntries_.end(), [&](const RuntimeEntry& entry) { + return entry.ObjectId == gameObject.GetId() && entry.InstanceId == instanceId; + }), RuntimeEntries_.end()); + } + + const auto scriptRegistry = editorContext.GetModuleRegistry().ResolveService(); + if (scriptRegistry == nullptr || scriptRegistry->FindScript(instance.ScriptTypeId) == nullptr) { + WarnMissingScript(editorContext, instance.ScriptTypeId); + return nullptr; + } + + std::unique_ptr behaviour = scriptRegistry->CreateBehaviour(instance.ScriptTypeId); + if (behaviour == nullptr) { + WarnMissingScript(editorContext, instance.ScriptTypeId); + return nullptr; + } + + instance.FieldsJson = scriptRegistry->EnsureFieldsJsonDefaults(instance.ScriptTypeId, instance.FieldsJson); + + MetaCoreDebugLog( + "ScriptSystem", + "CreateRuntimeEntry begin " + MetaCoreDebugScriptIdentity(gameObject, instance) + + " fields=" + instance.FieldsJson + ); + RuntimeEntry entry; + entry.ObjectId = gameObject.GetId(); + entry.InstanceId = instanceId; + entry.ScriptTypeId = instance.ScriptTypeId; + entry.Behaviour = std::move(behaviour); + RuntimeEntries_.push_back(std::move(entry)); + + RuntimeEntry& createdEntry = RuntimeEntries_.back(); + MetaCoreScriptExecutionContext scriptContext{editorContext, gameObject, instance}; + createdEntry.Behaviour->OnPlayStart(scriptContext); + createdEntry.Started = true; + MetaCoreDebugLog( + "ScriptSystem", + "CreateRuntimeEntry end " + MetaCoreDebugScriptIdentity(gameObject, instance) + + " runtimeEntries=" + std::to_string(RuntimeEntries_.size()) + ); + return &createdEntry; + } + + void StopRuntimeEntry(MetaCoreEditorContext& editorContext, RuntimeEntry& entry) { + if (entry.Behaviour == nullptr || !entry.Started) { + std::ostringstream stream; + stream + << "StopRuntimeEntry skip objectId=" << entry.ObjectId + << " instanceId=" << entry.InstanceId + << " type=\"" << entry.ScriptTypeId << "\"" + << " started=" << (entry.Started ? "true" : "false"); + MetaCoreDebugLog("ScriptSystem", stream.str()); + return; + } + + MetaCoreGameObject gameObject = editorContext.GetScene().FindGameObject(entry.ObjectId); + MetaCoreScriptInstanceData* instance = FindScriptInstance(gameObject, entry.InstanceId); + if (gameObject && instance != nullptr) { + MetaCoreDebugLog( + "ScriptSystem", + "StopRuntimeEntry dispatch " + MetaCoreDebugScriptIdentity(gameObject, *instance) + ); + MetaCoreScriptExecutionContext scriptContext{editorContext, gameObject, *instance}; + entry.Behaviour->OnPlayStop(scriptContext); + } else { + std::ostringstream stream; + stream + << "StopRuntimeEntry missing target objectId=" << entry.ObjectId + << " instanceId=" << entry.InstanceId + << " type=\"" << entry.ScriptTypeId << "\""; + MetaCoreDebugLog("ScriptSystem", stream.str()); + } + entry.Started = false; + } + + void CleanupStaleRuntimeEntries(MetaCoreEditorContext& editorContext) { + auto iterator = RuntimeEntries_.begin(); + while (iterator != RuntimeEntries_.end()) { + MetaCoreGameObject gameObject = editorContext.GetScene().FindGameObject(iterator->ObjectId); + MetaCoreScriptInstanceData* instance = FindScriptInstance(gameObject, iterator->InstanceId); + const bool stale = + !gameObject || + instance == nullptr || + !instance->Enabled || + instance->ScriptTypeId != iterator->ScriptTypeId; + if (stale) { + std::ostringstream stream; + stream + << "CleanupStaleRuntimeEntry objectId=" << iterator->ObjectId + << " instanceId=" << iterator->InstanceId + << " type=\"" << iterator->ScriptTypeId << "\"" + << " hasObject=" << (gameObject ? "true" : "false") + << " hasInstance=" << (instance != nullptr ? "true" : "false"); + if (instance != nullptr) { + stream + << " enabled=" << (instance->Enabled ? "true" : "false") + << " currentType=\"" << instance->ScriptTypeId << "\""; + } + MetaCoreDebugLog("ScriptSystem", stream.str()); + StopRuntimeEntry(editorContext, *iterator); + iterator = RuntimeEntries_.erase(iterator); + } else { + ++iterator; + } + } + } + + void DispatchScriptPhase(MetaCoreEditorContext& editorContext, double deltaSeconds, ScriptPhase phase) { + CleanupStaleRuntimeEntries(editorContext); + + for (MetaCoreGameObject gameObject : editorContext.GetScene().GetGameObjects()) { + if (!gameObject.HasComponent()) { + continue; + } + + MetaCoreScriptComponent& scriptComponent = gameObject.GetComponent(); + for (MetaCoreScriptInstanceData& instance : scriptComponent.Instances) { + if (!instance.Enabled) { + continue; + } + + RuntimeEntry* entry = EnsureRuntimeEntry(editorContext, gameObject, instance); + if (entry == nullptr || entry->Behaviour == nullptr) { + continue; + } + + if (phase == ScriptPhase::PlayStart) { + continue; + } + + MetaCoreScriptExecutionContext scriptContext{editorContext, gameObject, instance}; + switch (phase) { + case ScriptPhase::FixedUpdate: + entry->Behaviour->FixedUpdate(scriptContext, deltaSeconds); + break; + case ScriptPhase::Update: + entry->Behaviour->Update(scriptContext, deltaSeconds); + break; + case ScriptPhase::LateUpdate: + entry->Behaviour->LateUpdate(scriptContext, deltaSeconds); + break; + case ScriptPhase::PlayStart: + break; + } + } + } + } + + void WarnMissingScript(MetaCoreEditorContext& editorContext, std::string_view scriptTypeId) { + const std::string warningKey(scriptTypeId); + if (!MissingScriptWarnings_.insert(warningKey).second) { + return; + } + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Warning, + "Script", + "脚本类型缺失,已跳过: " + warningKey + ); + } + + std::vector RuntimeEntries_{}; + std::unordered_set MissingScriptWarnings_{}; +}; + class MetaCoreRotatorPlayModeComponentSystem final : public MetaCoreIPlayModeComponentSystem { public: [[nodiscard]] std::string GetSystemId() const override { return "MetaCore.PlayMode.Rotator"; } @@ -6071,6 +6916,7 @@ public: (void)moduleRegistry; Systems_.clear(); RegisterSystem(std::make_shared()); + RegisterSystem(std::make_shared()); } void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { @@ -6115,10 +6961,19 @@ public: void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { ComponentSystemRegistry_ = moduleRegistry.ResolveService(); + MetaCoreDebugLog( + "PlayMode", + "Startup componentSystemRegistry=" + std::string(ComponentSystemRegistry_ != nullptr ? "valid" : "null") + ); } void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { (void)moduleRegistry; + MetaCoreDebugLog( + "PlayMode", + "Shutdown state=" + std::string(MetaCoreDebugPlayModeStateName(State_)) + + " commandSuppressed=" + (CommandRecordingSuppressed_ ? std::string("true") : std::string("false")) + ); ComponentSystemRegistry_.reset(); State_ = MetaCorePlayModeState::Edit; HasPrePlaySnapshot_ = false; @@ -6129,41 +6984,80 @@ public: [[nodiscard]] bool EnterPlayMode(MetaCoreEditorContext& editorContext) override { if (State_ != MetaCorePlayModeState::Edit) { + MetaCoreDebugLog( + "PlayMode", + "Enter rejected state=" + std::string(MetaCoreDebugPlayModeStateName(State_)) + + " " + MetaCoreDebugSceneSummary(editorContext) + ); return false; } + MetaCoreDebugLog("PlayMode", "Enter begin " + MetaCoreDebugSceneSummary(editorContext)); PrePlaySnapshot_ = editorContext.CaptureStateSnapshot(); HasPrePlaySnapshot_ = true; + MetaCoreDebugLog( + "PlayMode", + "Enter captured prePlaySnapshot objects=" + + std::to_string(PrePlaySnapshot_.SceneSnapshot.GameObjects.size()) + ); ElapsedTimeSeconds_ = 0.0; FixedTimeAccumulatorSeconds_ = 0.0; State_ = MetaCorePlayModeState::Playing; + MetaCoreDebugLog( + "PlayMode", + "Enter state set state=" + std::string(MetaCoreDebugPlayModeStateName(State_)) + ); editorContext.BeginCommandRecordingSuppression(); CommandRecordingSuppressed_ = true; + MetaCoreDebugLog("PlayMode", "Enter command recording suppressed"); DispatchOnPlayStart(editorContext); + MetaCoreDebugLog("PlayMode", "Enter after OnPlayStart " + MetaCoreDebugSceneSummary(editorContext)); RefreshSceneDirtyState(editorContext); editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "PlayMode", "已进入播放模式"); + MetaCoreDebugLog("PlayMode", "Enter end " + MetaCoreDebugSceneSummary(editorContext)); return true; } [[nodiscard]] bool ExitPlayMode(MetaCoreEditorContext& editorContext) override { if (State_ == MetaCorePlayModeState::Edit) { + MetaCoreDebugLog("PlayMode", "Exit rejected already Edit " + MetaCoreDebugSceneSummary(editorContext)); return false; } + MetaCoreDebugLog( + "PlayMode", + "Exit begin state=" + std::string(MetaCoreDebugPlayModeStateName(State_)) + + " elapsed=" + std::to_string(ElapsedTimeSeconds_) + " " + + MetaCoreDebugSceneSummary(editorContext) + ); DispatchOnPlayStop(editorContext); + MetaCoreDebugLog("PlayMode", "Exit after OnPlayStop " + MetaCoreDebugSceneSummary(editorContext)); State_ = MetaCorePlayModeState::Edit; + MetaCoreDebugLog("PlayMode", "Exit state set Edit"); if (CommandRecordingSuppressed_) { + MetaCoreDebugLog("PlayMode", "Exit ending command recording suppression"); editorContext.EndCommandRecordingSuppression(); CommandRecordingSuppressed_ = false; + MetaCoreDebugLog("PlayMode", "Exit command recording suppression ended"); } if (HasPrePlaySnapshot_) { + MetaCoreDebugLog("PlayMode", "Exit capturing current snapshot"); const MetaCoreEditorStateSnapshot currentSnapshot = editorContext.CaptureStateSnapshot(); + MetaCoreDebugLog( + "PlayMode", + "Exit committing undo preObjects=" + + std::to_string(PrePlaySnapshot_.SceneSnapshot.GameObjects.size()) + + " currentObjects=" + + std::to_string(currentSnapshot.SceneSnapshot.GameObjects.size()) + ); (void)editorContext.CommitStateTransition("播放模式改动", PrePlaySnapshot_, currentSnapshot); + MetaCoreDebugLog("PlayMode", "Exit undo committed " + MetaCoreDebugSceneSummary(editorContext)); } else { + MetaCoreDebugLog("PlayMode", "Exit no prePlaySnapshot, refresh dirty state"); RefreshSceneDirtyState(editorContext); } @@ -6171,38 +7065,65 @@ public: ElapsedTimeSeconds_ = 0.0; FixedTimeAccumulatorSeconds_ = 0.0; editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "PlayMode", "已停止播放模式,播放期改动已保留"); + MetaCoreDebugLog("PlayMode", "Exit end " + MetaCoreDebugSceneSummary(editorContext)); return true; } [[nodiscard]] bool Pause() override { if (State_ != MetaCorePlayModeState::Playing) { + MetaCoreDebugLog("PlayMode", "Pause rejected state=" + std::string(MetaCoreDebugPlayModeStateName(State_))); return false; } + MetaCoreDebugLog( + "PlayMode", + "Pause begin elapsed=" + std::to_string(ElapsedTimeSeconds_) + + " accumulator=" + std::to_string(FixedTimeAccumulatorSeconds_) + ); State_ = MetaCorePlayModeState::Paused; + MetaCoreDebugLog("PlayMode", "Pause end state=Paused"); return true; } [[nodiscard]] bool Resume() override { if (State_ != MetaCorePlayModeState::Paused) { + MetaCoreDebugLog("PlayMode", "Resume rejected state=" + std::string(MetaCoreDebugPlayModeStateName(State_))); return false; } + MetaCoreDebugLog( + "PlayMode", + "Resume begin elapsed=" + std::to_string(ElapsedTimeSeconds_) + + " accumulator=" + std::to_string(FixedTimeAccumulatorSeconds_) + ); State_ = MetaCorePlayModeState::Playing; + MetaCoreDebugLog("PlayMode", "Resume end state=Playing"); return true; } [[nodiscard]] bool Step(MetaCoreEditorContext& editorContext, double deltaSeconds = 1.0 / 60.0) override { if (State_ != MetaCorePlayModeState::Paused) { + MetaCoreDebugLog( + "PlayMode", + "Step rejected state=" + std::string(MetaCoreDebugPlayModeStateName(State_)) + + " " + MetaCoreDebugSceneSummary(editorContext) + ); return false; } const double updateDeltaSeconds = NormalizeDeltaSeconds(deltaSeconds, FixedTimeStepSeconds_); + MetaCoreDebugLog( + "PlayMode", + "Step begin updateDelta=" + std::to_string(updateDeltaSeconds) + + " fixedDelta=" + std::to_string(FixedTimeStepSeconds_) + " " + + MetaCoreDebugSceneSummary(editorContext) + ); DispatchFixedUpdate(editorContext, FixedTimeStepSeconds_); DispatchUpdate(editorContext, updateDeltaSeconds); DispatchLateUpdate(editorContext, updateDeltaSeconds); ElapsedTimeSeconds_ += updateDeltaSeconds; RefreshSceneDirtyState(editorContext); + MetaCoreDebugLog("PlayMode", "Step end " + MetaCoreDebugSceneSummary(editorContext)); return true; } @@ -6265,7 +7186,9 @@ private: void DispatchOnPlayStart(MetaCoreEditorContext& editorContext) { for (const auto& system : GetSystems()) { if (system != nullptr) { + MetaCoreDebugLog("PlayMode", "Dispatch OnPlayStart system=" + system->GetSystemId()); system->OnPlayStart(editorContext); + MetaCoreDebugLog("PlayMode", "Dispatch OnPlayStart done system=" + system->GetSystemId()); } } } @@ -6297,7 +7220,9 @@ private: void DispatchOnPlayStop(MetaCoreEditorContext& editorContext) { for (const auto& system : GetSystems()) { if (system != nullptr) { + MetaCoreDebugLog("PlayMode", "Dispatch OnPlayStop system=" + system->GetSystemId()); system->OnPlayStop(editorContext); + MetaCoreDebugLog("PlayMode", "Dispatch OnPlayStop done system=" + system->GetSystemId()); } } } @@ -6437,6 +7362,7 @@ std::optional MetaCoreBuiltinPrefabService::CreatePrefabF if (gameObject.HasComponent()) data.Camera = gameObject.GetComponent(); if (gameObject.HasComponent()) data.Light = gameObject.GetComponent(); if (gameObject.HasComponent()) data.Rotator = gameObject.GetComponent(); + if (gameObject.HasComponent()) data.Script = gameObject.GetComponent(); if (gameObject.HasComponent()) data.MeshRenderer = gameObject.GetComponent(); prefabDataObjects.push_back(std::move(data)); } @@ -6499,6 +7425,7 @@ std::optional MetaCoreBuiltinPrefabService::InstantiatePrefabDocumen if (sourceObject.Camera.has_value()) clonedObject.AddComponent(*sourceObject.Camera); if (sourceObject.Light.has_value()) clonedObject.AddComponent(*sourceObject.Light); if (sourceObject.Rotator.has_value()) clonedObject.AddComponent(*sourceObject.Rotator); + if (sourceObject.Script.has_value()) clonedObject.AddComponent(*sourceObject.Script); if (sourceObject.MeshRenderer.has_value()) clonedObject.AddComponent(*sourceObject.MeshRenderer); clonedObject.AddComponent(MetaCorePrefabInstanceMetadata{ @@ -6610,6 +7537,7 @@ bool MetaCoreBuiltinPrefabService::ApplySelectedPrefabInstance(MetaCoreEditorCon if (gameObject.HasComponent()) data.Camera = gameObject.GetComponent(); if (gameObject.HasComponent()) data.Light = gameObject.GetComponent(); if (gameObject.HasComponent()) data.Rotator = gameObject.GetComponent(); + if (gameObject.HasComponent()) data.Script = gameObject.GetComponent(); if (gameObject.HasComponent()) data.MeshRenderer = gameObject.GetComponent(); prefabDataObjects.push_back(std::move(data)); @@ -7373,6 +8301,7 @@ public: moduleRegistry.RegisterService(std::make_shared()); moduleRegistry.RegisterService(std::make_shared()); moduleRegistry.RegisterService(std::make_shared()); + moduleRegistry.RegisterService(std::make_shared()); moduleRegistry.RegisterService(std::make_shared()); moduleRegistry.RegisterService(std::make_shared()); moduleRegistry.RegisterService(std::make_shared()); diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp index efac278..5794f46 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp @@ -133,6 +133,15 @@ void MetaCorePushGhostButtonStyle() { } } +[[nodiscard]] const char* MetaCoreDebugPlayModeStateName(MetaCorePlayModeState state) { + switch (state) { + case MetaCorePlayModeState::Edit: return "Edit"; + case MetaCorePlayModeState::Playing: return "Playing"; + case MetaCorePlayModeState::Paused: return "Paused"; + default: return "Unknown"; + } +} + [[nodiscard]] bool MetaCoreToolbarToggle(const char* label, bool selected, ImVec2 size = ImVec2(0.0F, 24.0F)) { if (selected) { MetaCorePushFlatButtonStyle(GInfernuxAccent); @@ -215,9 +224,23 @@ void MetaCoreDrawInfernuxToolbar(MetaCoreEditorContext& editorContext, const ImV ImGui::BeginDisabled(); } if (ImGui::Button(isPlaying ? "停止" : "播放", ImVec2(58.0F, 24.0F))) { + const MetaCorePlayModeState beforeState = playModeService->GetState(); + std::cerr + << "[MetaCoreDebug][Toolbar] button=" + << (isPlaying ? "Stop" : "Play") + << " before=" << MetaCoreDebugPlayModeStateName(beforeState) + << " sceneRevision=" << editorContext.GetScene().GetRevision() + << std::endl; const bool ok = isPlaying ? playModeService->ExitPlayMode(editorContext) : playModeService->EnterPlayMode(editorContext); + std::cerr + << "[MetaCoreDebug][Toolbar] button=" + << (isPlaying ? "Stop" : "Play") + << " ok=" << (ok ? "true" : "false") + << " after=" << MetaCoreDebugPlayModeStateName(playModeService->GetState()) + << " sceneRevision=" << editorContext.GetScene().GetRevision() + << std::endl; if (!ok) { editorContext.AddConsoleMessage(MetaCoreLogLevel::Warning, "PlayMode", "播放模式状态切换失败。"); } @@ -233,7 +256,21 @@ void MetaCoreDrawInfernuxToolbar(MetaCoreEditorContext& editorContext, const ImV ImGui::BeginDisabled(); } if (ImGui::Button(isPaused ? "继续" : "暂停", ImVec2(64.0F, 24.0F))) { + const MetaCorePlayModeState beforeState = playModeService->GetState(); + std::cerr + << "[MetaCoreDebug][Toolbar] button=" + << (isPaused ? "Resume" : "Pause") + << " before=" << MetaCoreDebugPlayModeStateName(beforeState) + << " sceneRevision=" << editorContext.GetScene().GetRevision() + << std::endl; const bool ok = isPaused ? playModeService->Resume() : playModeService->Pause(); + std::cerr + << "[MetaCoreDebug][Toolbar] button=" + << (isPaused ? "Resume" : "Pause") + << " ok=" << (ok ? "true" : "false") + << " after=" << MetaCoreDebugPlayModeStateName(playModeService->GetState()) + << " sceneRevision=" << editorContext.GetScene().GetRevision() + << std::endl; if (!ok) { editorContext.AddConsoleMessage(MetaCoreLogLevel::Warning, "PlayMode", "暂停状态切换失败。"); } @@ -249,7 +286,18 @@ void MetaCoreDrawInfernuxToolbar(MetaCoreEditorContext& editorContext, const ImV ImGui::BeginDisabled(); } if (ImGui::Button("单步", ImVec2(54.0F, 24.0F))) { - if (!playModeService->Step(editorContext)) { + std::cerr + << "[MetaCoreDebug][Toolbar] button=Step before=" + << MetaCoreDebugPlayModeStateName(playModeService->GetState()) + << " sceneRevision=" << editorContext.GetScene().GetRevision() + << std::endl; + const bool ok = playModeService->Step(editorContext); + std::cerr + << "[MetaCoreDebug][Toolbar] button=Step ok=" << (ok ? "true" : "false") + << " after=" << MetaCoreDebugPlayModeStateName(playModeService->GetState()) + << " sceneRevision=" << editorContext.GetScene().GetRevision() + << std::endl; + if (!ok) { editorContext.AddConsoleMessage(MetaCoreLogLevel::Warning, "PlayMode", "单步只在暂停状态下可用。"); } } @@ -1136,12 +1184,9 @@ int MetaCoreEditorApp::Run() { ImGui::Render(); - // 1. 收集并更新场景数据(但不在这里渲染) - MetaCoreSceneView sceneView = EditorContext_->GetCameraController().BuildSceneView(); - sceneView.SelectedObjectId = EditorContext_->GetSelectedObjectId(); - ViewportRenderer_.RenderSceneToViewport(Scene_, sceneView); - - // 2. 统一在一个 Frame 内渲染离屏 3D 和上屏 UI + // 视口面板在 DrawEditorFrame() 中完成本帧场景同步并把当前离屏纹理写入 ImGui。 + // 这里仅提交一次 Filament 渲染,避免状态按钮切换后同一帧二次 Resize/Sync + // 让 ImGui draw data 采样到已释放的 FTexture。 ViewportRenderer_.RenderAll(); Window_.EndFrame(); @@ -1422,6 +1467,23 @@ void MetaCoreEditorApp::DrawEditorFrame() { // 7. 绘制画面 (应用 UV 翻转:Filament Y轴起点在底部,ImGui 在顶部) void* texPtr = ViewportRenderer_.GetFilamentTexturePointer(); + { + static void* lastDebugTexturePointer = nullptr; + static MetaCorePlayModeState lastDebugPlayModeState = MetaCorePlayModeState::Edit; + const auto playModeService = ModuleRegistry_.ResolveService(); + const MetaCorePlayModeState currentPlayModeState = + playModeService != nullptr ? playModeService->GetState() : MetaCorePlayModeState::Edit; + if (texPtr != lastDebugTexturePointer || currentPlayModeState != lastDebugPlayModeState) { + std::cerr + << "[MetaCoreDebug][Viewport] ImGuiTexture pointer=" << texPtr + << " playState=" << MetaCoreDebugPlayModeStateName(currentPlayModeState) + << " viewport=" << viewportSize.x << "x" << viewportSize.y + << " sceneRevision=" << Scene_.GetRevision() + << std::endl; + lastDebugTexturePointer = texPtr; + lastDebugPlayModeState = currentPlayModeState; + } + } if (texPtr) { ImGui::Image(texPtr, viewportSize, ImVec2(0, 1), ImVec2(1, 0)); } diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp index 3738e48..3bc3f8e 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp @@ -84,6 +84,25 @@ private: MetaCoreNearlyEqualVec3(lhs.Scale, rhs.Scale); } +[[nodiscard]] bool MetaCoreScriptComponentEqual(const MetaCoreScriptComponent& lhs, const MetaCoreScriptComponent& rhs) { + if (lhs.Instances.size() != rhs.Instances.size()) { + return false; + } + + for (std::size_t index = 0; index < lhs.Instances.size(); ++index) { + const MetaCoreScriptInstanceData& lhsInstance = lhs.Instances[index]; + const MetaCoreScriptInstanceData& rhsInstance = rhs.Instances[index]; + if (lhsInstance.InstanceId != rhsInstance.InstanceId || + lhsInstance.ScriptTypeId != rhsInstance.ScriptTypeId || + lhsInstance.Enabled != rhsInstance.Enabled || + lhsInstance.FieldsJson != rhsInstance.FieldsJson) { + return false; + } + } + + return true; +} + [[nodiscard]] bool MetaCoreGameObjectEqual(const MetaCoreGameObjectData& lhs, const MetaCoreGameObjectData& rhs) { if (lhs.Id != rhs.Id || lhs.ParentId != rhs.ParentId || lhs.Name != rhs.Name || !MetaCoreTransformEqual(lhs.Transform, rhs.Transform)) { return false; @@ -134,6 +153,13 @@ private: } } + if (lhs.Script.has_value() != rhs.Script.has_value()) { + return false; + } + if (lhs.Script.has_value() && !MetaCoreScriptComponentEqual(*lhs.Script, *rhs.Script)) { + return false; + } + return true; } diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorServices.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorServices.cpp index f68cdee..34210ac 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorServices.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorServices.cpp @@ -49,6 +49,38 @@ void MetaCoreIEditorService::Shutdown(MetaCoreEditorModuleRegistry& moduleRegist (void)moduleRegistry; } +void MetaCoreIScriptBehaviour::OnPlayStart(MetaCoreScriptExecutionContext& context) { + (void)context; +} + +void MetaCoreIScriptBehaviour::FixedUpdate( + MetaCoreScriptExecutionContext& context, + double fixedDeltaSeconds +) { + (void)context; + (void)fixedDeltaSeconds; +} + +void MetaCoreIScriptBehaviour::Update( + MetaCoreScriptExecutionContext& context, + double deltaSeconds +) { + (void)context; + (void)deltaSeconds; +} + +void MetaCoreIScriptBehaviour::LateUpdate( + MetaCoreScriptExecutionContext& context, + double deltaSeconds +) { + (void)context; + (void)deltaSeconds; +} + +void MetaCoreIScriptBehaviour::OnPlayStop(MetaCoreScriptExecutionContext& context) { + (void)context; +} + void MetaCoreIPlayModeComponentSystem::OnPlayStart(MetaCoreEditorContext& editorContext) { (void)editorContext; } diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h index 125a47d..a7dfe24 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h @@ -306,6 +306,57 @@ public: [[nodiscard]] virtual bool PasteComponent(std::string_view typeId, MetaCoreGameObject& gameObject) const = 0; }; +enum class MetaCoreScriptFieldType { + Bool = 0, + Float, + Vec3, + String +}; + +struct MetaCoreScriptFieldDefinition { + std::string Name{}; + std::string DisplayName{}; + MetaCoreScriptFieldType Type = MetaCoreScriptFieldType::Float; + bool BoolDefault = false; + float FloatDefault = 0.0F; + glm::vec3 Vec3Default{0.0F, 0.0F, 0.0F}; + std::string StringDefault{}; +}; + +struct MetaCoreScriptExecutionContext { + MetaCoreEditorContext& EditorContext; + MetaCoreGameObject GameObject; + MetaCoreScriptInstanceData& Instance; +}; + +class MetaCoreIScriptBehaviour { +public: + virtual ~MetaCoreIScriptBehaviour() = default; + + virtual void OnPlayStart(MetaCoreScriptExecutionContext& context); + virtual void FixedUpdate(MetaCoreScriptExecutionContext& context, double fixedDeltaSeconds); + virtual void Update(MetaCoreScriptExecutionContext& context, double deltaSeconds); + virtual void LateUpdate(MetaCoreScriptExecutionContext& context, double deltaSeconds); + virtual void OnPlayStop(MetaCoreScriptExecutionContext& context); +}; + +struct MetaCoreScriptDefinition { + std::string TypeId{}; + std::string DisplayName{}; + std::vector Fields{}; + std::function()> Factory{}; +}; + +class MetaCoreIScriptRegistry : public MetaCoreIEditorService { +public: + virtual void RegisterScript(MetaCoreScriptDefinition definition) = 0; + [[nodiscard]] virtual const MetaCoreScriptDefinition* FindScript(std::string_view typeId) const = 0; + [[nodiscard]] virtual std::vector GetScriptDefinitions() const = 0; + [[nodiscard]] virtual std::unique_ptr CreateBehaviour(std::string_view typeId) const = 0; + [[nodiscard]] virtual std::string BuildDefaultFieldsJson(std::string_view typeId) const = 0; + [[nodiscard]] virtual std::string EnsureFieldsJsonDefaults(std::string_view typeId, std::string_view fieldsJson) const = 0; +}; + enum class MetaCorePlayModeState { Edit = 0, Playing, diff --git a/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp b/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp index c85a2e3..e399426 100644 --- a/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp @@ -49,6 +49,7 @@ #include #include #include +#include #include #include @@ -99,6 +100,17 @@ filament::Engine::Backend MetaCoreResolveFilamentBackend() { return filament::Engine::Backend::DEFAULT; } +template +[[nodiscard]] std::string MetaCoreDebugPointer(T* pointer) { + std::ostringstream stream; + stream << static_cast(pointer); + return stream.str(); +} + +void MetaCoreFilamentDebugLog(const std::string& message) { + std::cerr << "[MetaCoreDebug][Filament] " << message << std::endl; +} + } // namespace class MetaCoreFilamentSceneBridge::MetaCoreFilamentSceneBridgeImpl { @@ -189,6 +201,8 @@ public: .texture(filament::RenderTarget::AttachmentPoint::COLOR, FilamentTexture_) .texture(filament::RenderTarget::AttachmentPoint::DEPTH, DepthTexture_) .build(*Engine_); + ++RenderTargetGeneration_; + DebugRenderTargetState("Initialize offscreen render target"); // 4. 设置到 View View_->setRenderTarget(RenderTarget_); @@ -227,14 +241,24 @@ public: } if (Engine_) { std::cout << "FilamentSceneBridge: Shutting down..." << std::endl; + DebugRenderTargetState("Shutdown begin"); // 1. 销毁所有加载的资产,切断其内部 Renderable 对克隆材质实例的占用引用 std::cout << "[DEBUG] LoadedAssets_ size: " << LoadedAssets_.size() << std::endl; for (auto& [id, asset] : LoadedAssets_) { std::cout << "[DEBUG] Destroying asset for ID: " << id << ", asset pointer: " << asset << std::endl; + MetaCoreFilamentDebugLog( + "Shutdown destroyAsset begin hostRootId=" + std::to_string(id) + + " asset=" + MetaCoreDebugPointer(asset) + ); if (asset) { + Engine_->flushAndWait(); AssetLoader_->destroyAsset(asset); } + MetaCoreFilamentDebugLog( + "Shutdown destroyAsset end hostRootId=" + std::to_string(id) + + " asset=" + MetaCoreDebugPointer(asset) + ); std::cout << "[DEBUG] Destroyed asset for ID: " << id << std::endl; } LoadedAssets_.clear(); @@ -270,6 +294,11 @@ public: // 3. 销毁所有克隆的材质实例,由于引用的实体都已被销毁,此时可安全释放 for (auto& [id, mats] : AssetDuplicatedMaterials_) { + MetaCoreFilamentDebugLog( + "Shutdown destroy duplicated materials hostRootId=" + std::to_string(id) + + " count=" + std::to_string(mats.size()) + ); + Engine_->flushAndWait(); for (auto* mat : mats) { if (mat) { Engine_->destroy(mat); @@ -301,19 +330,30 @@ public: MaterialProvider_ = nullptr; } + DestroyDeferredRenderTargetResources(true); + // 销毁离屏渲染资源 if (RenderTarget_) { + DebugRenderTargetState("Shutdown destroy current render target begin"); + Engine_->flushAndWait(); Engine_->destroy(RenderTarget_); RenderTarget_ = nullptr; } if (FilamentTexture_) { + MetaCoreFilamentDebugLog( + "Shutdown destroy current colorTexture=" + MetaCoreDebugPointer(FilamentTexture_) + ); Engine_->destroy(FilamentTexture_); FilamentTexture_ = nullptr; } if (DepthTexture_) { + MetaCoreFilamentDebugLog( + "Shutdown destroy current depthTexture=" + MetaCoreDebugPointer(DepthTexture_) + ); Engine_->destroy(DepthTexture_); DepthTexture_ = nullptr; } + DebugRenderTargetState("Shutdown after current render target destroy"); if (GLTextureId_) { glDeleteTextures(1, &GLTextureId_); GLTextureId_ = 0; @@ -530,6 +570,18 @@ public: if (!activeHostRootIds.contains(hostRootId) || invalidated) { std::cout << "FilamentSceneBridge: Unloading deleted asset: " << hostRootId << std::endl; + const size_t assetEntityCount = asset != nullptr ? asset->getEntityCount() : 0; + const auto materialIt = AssetDuplicatedMaterials_.find(hostRootId); + const size_t duplicatedMaterialCount = + materialIt != AssetDuplicatedMaterials_.end() ? materialIt->second.size() : 0; + MetaCoreFilamentDebugLog( + "Unload begin hostRootId=" + std::to_string(hostRootId) + + " asset=" + MetaCoreDebugPointer(asset) + + " active=" + (activeHostRootIds.contains(hostRootId) ? std::string("true") : std::string("false")) + + " invalidated=" + (invalidated ? std::string("true") : std::string("false")) + + " entities=" + std::to_string(assetEntityCount) + + " duplicatedMaterials=" + std::to_string(duplicatedMaterialCount) + ); // 从场景中移除该资产关联的所有实体 if (Scene_ && asset) { @@ -555,9 +607,27 @@ public: } } + // 等待后台渲染线程消费完上一帧命令,再销毁 GLTF 资产持有的纹理/材质。 + // 脚本播放会每帧推进 Transform,暂停/热更新等操作可能和渲染同步落在相邻帧, + // 不等待会让 OpenGL descriptor 绑定已释放的 FTexture。 + if (Engine_) { + MetaCoreFilamentDebugLog( + "Unload flush before destroyAsset hostRootId=" + std::to_string(hostRootId) + ); + Engine_->flushAndWait(); + } + // 销毁资产本身 if (asset) { + MetaCoreFilamentDebugLog( + "Unload destroyAsset begin hostRootId=" + std::to_string(hostRootId) + + " asset=" + MetaCoreDebugPointer(asset) + ); AssetLoader_->destroyAsset(asset); + MetaCoreFilamentDebugLog( + "Unload destroyAsset end hostRootId=" + std::to_string(hostRootId) + + " asset=" + MetaCoreDebugPointer(asset) + ); } // 从 ObjectToFilamentEntity_ 和 ObjectWorldMatrices_ 中清除所有指向该 asset 的映射 @@ -577,17 +647,32 @@ public: // 销毁克隆的材质 auto matIt = AssetDuplicatedMaterials_.find(hostRootId); if (matIt != AssetDuplicatedMaterials_.end()) { + MetaCoreFilamentDebugLog( + "Unload destroy duplicated materials begin hostRootId=" + std::to_string(hostRootId) + + " count=" + std::to_string(matIt->second.size()) + ); + if (Engine_) { + Engine_->flushAndWait(); + } for (auto* mat : matIt->second) { if (mat) { Engine_->destroy(mat); } } + MetaCoreFilamentDebugLog( + "Unload destroy duplicated materials end hostRootId=" + std::to_string(hostRootId) + ); AssetDuplicatedMaterials_.erase(matIt); } // 从 LoadedAssets_ 中移除 HostRootSourceModelGuids_.erase(hostRootId); it = LoadedAssets_.erase(it); + MetaCoreFilamentDebugLog( + "Unload end hostRootId=" + std::to_string(hostRootId) + + " loadedAssets=" + std::to_string(LoadedAssets_.size()) + + " objectMappings=" + std::to_string(ObjectToFilamentEntity_.size()) + ); } else { ++it; } @@ -630,6 +715,12 @@ public: // 加载新模型,以顶级根宿主 hostRootId 进行注册存储 std::cout << "FilamentSceneBridge: Loading GLTF: " << absolutePath << " (Host: " << hostRootName << ")" << std::endl; + MetaCoreFilamentDebugLog( + "Load begin hostRootId=" + std::to_string(hostRootId) + + " host=\"" + hostRootName + "\"" + + " modelPath=\"" + modelPath + "\"" + + " loadedAssets=" + std::to_string(LoadedAssets_.size()) + ); std::ifstream file(absolutePath, std::ios::binary); if (!file.is_open()) { @@ -648,6 +739,11 @@ public: std::cerr << "Failed to create asset!" << std::endl; continue; } + MetaCoreFilamentDebugLog( + "Load createAsset hostRootId=" + std::to_string(hostRootId) + + " asset=" + MetaCoreDebugPointer(asset) + + " entities=" + std::to_string(asset->getEntityCount()) + ); // 加载贴图等资源 filament::gltfio::ResourceLoader resourceLoader({ Engine_ }); @@ -692,6 +788,10 @@ public: } } } + MetaCoreFilamentDebugLog( + "Load duplicated materials hostRootId=" + std::to_string(hostRootId) + + " count=" + std::to_string(dupList.size()) + ); } if (scene != nullptr) { @@ -975,6 +1075,23 @@ public: return; } + ++RenderFrameIndex_; + if (!PendingRenderTargetResources_.empty()) { + DebugRenderTargetState("RenderAll before beginFrame with pending destroy"); + } + + // ImGui draw data -> Filament renderable 的同步会修改 Renderable、VertexBuffer、 + // MaterialInstance 参数和 descriptor 绑定。必须在 beginFrame/render pass 之前完成, + // 否则 Filament 会警告 descriptor set 在 begin/endRenderPass 中被修改,并可能在 + // 后续句柄校验中触发 FTexture use-after-free/Postcondition 崩溃。 + const bool hasUiPass = UIView_ != nullptr && ImGuiHelper_ != nullptr; + if (hasUiPass) { + ImGuiIO& io = ImGui::GetIO(); + ImGuiHelper_->setDisplaySize(io.DisplaySize.x, io.DisplaySize.y); + UIView_->setViewport({0, 0, static_cast(io.DisplaySize.x), static_cast(io.DisplaySize.y)}); + ImGuiHelper_->processImGuiCommands(ImGui::GetDrawData(), io); + } + if (Renderer_->beginFrame(SwapChain_)) { if (RenderTarget_) { // 1. 渲染 3D 离屏视口 (输出到 RenderTarget) @@ -985,18 +1102,11 @@ public: Renderer_->render(View_); - if (UIView_ && ImGuiHelper_) { + if (hasUiPass) { // 2. 准备并渲染 UI 视口 (输出到 SwapChain) - ImGuiIO& io = ImGui::GetIO(); - ImGuiHelper_->setDisplaySize(io.DisplaySize.x, io.DisplaySize.y); - UIView_->setViewport({0, 0, static_cast(io.DisplaySize.x), static_cast(io.DisplaySize.y)}); - options.clearColor = { 0.11f, 0.12f, 0.14f, 1.0f }; // 灰色 options.clear = true; Renderer_->setClearOptions(options); - - ImGuiHelper_->processImGuiCommands(ImGui::GetDrawData(), io); - Renderer_->render(UIView_); } } else { @@ -1011,11 +1121,9 @@ public: Renderer_->endFrame(); - // 在帧结束、指令提交后,安全地销毁上一帧遗留的旧纹理 - for (auto* tex : OldTextures_) { - Engine_->destroy(tex); - } - OldTextures_.clear(); + DestroyDeferredRenderTargetResources(true); + } else if (!PendingRenderTargetResources_.empty()) { + DebugRenderTargetState("RenderAll beginFrame failed with pending destroy"); } } @@ -1027,6 +1135,10 @@ public: if (width <= 0 || height <= 0) return; if (!RenderTarget_) { + MetaCoreFilamentDebugLog( + "Resize without render target width=" + std::to_string(width) + + " height=" + std::to_string(height) + ); View_->setViewport({0, 0, static_cast(width), static_cast(height)}); return; } @@ -1039,13 +1151,15 @@ public: return; } - if (RenderTarget_) Engine_->destroy(RenderTarget_); - if (DepthTexture_) Engine_->destroy(DepthTexture_); - - // 【关键修复】:推迟销毁旧纹理,避免当前帧 ImGui 的 DrawList 还在引用它导致崩溃。 - if (FilamentTexture_) { - OldTextures_.push_back(FilamentTexture_); - } + MetaCoreFilamentDebugLog( + "Resize recreate begin width=" + std::to_string(width) + + " height=" + std::to_string(height) + + " oldWidth=" + (FilamentTexture_ != nullptr ? std::to_string(FilamentTexture_->getWidth()) : std::string("null")) + + " oldHeight=" + (FilamentTexture_ != nullptr ? std::to_string(FilamentTexture_->getHeight()) : std::string("null")) + ); + DebugRenderTargetState("Resize before queue current resources"); + QueueCurrentRenderTargetResourcesForDestroy(); + DestroyDeferredRenderTargetResources(true); FilamentTexture_ = filament::Texture::Builder() .width(static_cast(width)) @@ -1065,15 +1179,67 @@ public: .texture(filament::RenderTarget::AttachmentPoint::COLOR, FilamentTexture_) .texture(filament::RenderTarget::AttachmentPoint::DEPTH, DepthTexture_) .build(*Engine_); + ++RenderTargetGeneration_; View_->setRenderTarget(RenderTarget_); View_->setViewport({0, 0, static_cast(width), static_cast(height)}); + DebugRenderTargetState("Resize after recreate resources"); } void* GetFilamentTexturePointer() const { return FilamentTexture_; } + void QueueCurrentRenderTargetResourcesForDestroy() { + if (RenderTarget_ == nullptr && FilamentTexture_ == nullptr && DepthTexture_ == nullptr) { + return; + } + + PendingRenderTargetResources_.push_back(PendingRenderTargetResource{ + RenderTarget_, + FilamentTexture_, + DepthTexture_, + RenderTargetGeneration_ + }); + DebugRenderTargetState("Queue current render target resources for destroy"); + RenderTarget_ = nullptr; + FilamentTexture_ = nullptr; + DepthTexture_ = nullptr; + } + + void DestroyDeferredRenderTargetResources(bool waitForBackend) { + if (PendingRenderTargetResources_.empty() || Engine_ == nullptr) { + return; + } + + if (waitForBackend) { + DebugRenderTargetState("Destroy deferred resources flush begin"); + Engine_->flushAndWait(); + DebugRenderTargetState("Destroy deferred resources flush end"); + } + + for (PendingRenderTargetResource& resource : PendingRenderTargetResources_) { + std::ostringstream stream; + stream + << "Destroy deferred resource generation=" << resource.Generation + << " renderTarget=" << MetaCoreDebugPointer(resource.RenderTarget) + << " colorTexture=" << MetaCoreDebugPointer(resource.ColorTexture) + << " depthTexture=" << MetaCoreDebugPointer(resource.DepthTexture); + MetaCoreFilamentDebugLog(stream.str()); + if (resource.RenderTarget != nullptr) { + Engine_->destroy(resource.RenderTarget); + } + if (resource.ColorTexture != nullptr) { + Engine_->destroy(resource.ColorTexture); + } + if (resource.DepthTexture != nullptr) { + Engine_->destroy(resource.DepthTexture); + } + } + PendingRenderTargetResources_.clear(); + DebugRenderTargetState("Destroy deferred resources end"); + } + bool TryGetObjectWorldMatrix(MetaCoreId objectId, glm::mat4& worldMatrix) const { const auto worldIt = ObjectWorldMatrices_.find(objectId); if (worldIt == ObjectWorldMatrices_.end()) { @@ -1338,6 +1504,21 @@ private: } } + void DebugRenderTargetState(const char* event) const { + std::ostringstream stream; + stream + << event + << " frame=" << RenderFrameIndex_ + << " generation=" << RenderTargetGeneration_ + << " renderTarget=" << MetaCoreDebugPointer(RenderTarget_) + << " colorTexture=" << MetaCoreDebugPointer(FilamentTexture_) + << " depthTexture=" << MetaCoreDebugPointer(DepthTexture_) + << " pending=" << PendingRenderTargetResources_.size() + << " loadedAssets=" << LoadedAssets_.size() + << " objectMappings=" << ObjectToFilamentEntity_.size(); + MetaCoreFilamentDebugLog(stream.str()); + } + filament::Engine* Engine_ = nullptr; filament::SwapChain* SwapChain_ = nullptr; filament::Renderer* Renderer_ = nullptr; @@ -1364,10 +1545,18 @@ private: MetaCoreImGuiHelper* ImGuiHelper_ = nullptr; GLuint GLTextureId_ = 0; + struct PendingRenderTargetResource { + filament::RenderTarget* RenderTarget = nullptr; + filament::Texture* ColorTexture = nullptr; + filament::Texture* DepthTexture = nullptr; + std::uint64_t Generation = 0; + }; filament::Texture* FilamentTexture_ = nullptr; filament::Texture* DepthTexture_ = nullptr; filament::RenderTarget* RenderTarget_ = nullptr; - std::vector OldTextures_; + std::vector PendingRenderTargetResources_; + std::uint64_t RenderFrameIndex_ = 0; + std::uint64_t RenderTargetGeneration_ = 0; utils::Entity Light_; std::filesystem::path ProjectRootPath_{}; diff --git a/Source/MetaCoreRender/Private/MetaCoreImGuiHelper.cpp b/Source/MetaCoreRender/Private/MetaCoreImGuiHelper.cpp index 46510ee..0e1a2c3 100644 --- a/Source/MetaCoreRender/Private/MetaCoreImGuiHelper.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreImGuiHelper.cpp @@ -90,7 +90,9 @@ MetaCoreImGuiHelper::MetaCoreImGuiHelper(Engine* engine, filament::View* view, c void MetaCoreImGuiHelper::createAtlasTexture(Engine* engine) { if (mTexture) { + mImGuiTextures.erase(mTexture); engine->destroy(mTexture); + mTexture = nullptr; } ImGuiIO& io = ImGui::GetIO(); @@ -132,25 +134,51 @@ MetaCoreImGuiHelper::~MetaCoreImGuiHelper() { ImGui::SetCurrentContext(mImGuiContext); ImGui::GetIO().BackendFlags &= ~ImGuiBackendFlags_RendererHasTextures; - mEngine->destroy(mScene); - mEngine->destroy(mRenderable); + auto& rcm = mEngine->getRenderableManager(); + if (rcm.getInstance(mRenderable)) { + rcm.destroy(mRenderable); + } + mRenderableBuilt = false; + if (mScene) { + mScene->remove(mRenderable); + } mEngine->destroyCameraComponent(mCameraEntity); for (auto& mi : mMaterial2dInstances) { - mEngine->destroy(mi); + if (mi != nullptr) { + mEngine->destroy(mi); + } + } + if (mMaterial2d != nullptr) { + mEngine->destroy(mMaterial2d); + } + if (mTexture != nullptr) { + mImGuiTextures.erase(mTexture); + mEngine->destroy(mTexture); + mTexture = nullptr; } - mEngine->destroy(mMaterial2d); - mEngine->destroy(mTexture); for (auto& vb : mVertexBuffers) { - mEngine->destroy(vb); + if (vb != nullptr) { + mEngine->destroy(vb); + } } for (auto& ib : mIndexBuffers) { - mEngine->destroy(ib); + if (ib != nullptr) { + mEngine->destroy(ib); + } } for (auto itextures: mImGuiTextures) { - mEngine->destroy(itextures); + if (itextures != nullptr) { + mEngine->destroy(itextures); + } + } + mImGuiTextures.clear(); + + if (mScene != nullptr) { + mEngine->destroy(mScene); + mScene = nullptr; } EntityManager& em = utils::EntityManager::get(); @@ -218,13 +246,25 @@ void MetaCoreImGuiHelper::processImGuiCommands(ImDrawData* commands, const ImGui size_t nPrims = 0; for (int cmdListIndex = 0; cmdListIndex < commands->CmdListsCount; cmdListIndex++) { const ImDrawList* cmds = commands->CmdLists[cmdListIndex]; - nPrims += cmds->CmdBuffer.size(); + for (const auto& pcmd : cmds->CmdBuffer) { + if (pcmd.UserCallback == nullptr && pcmd.ElemCount > 0) { + ++nPrims; + } + } + } + + if (rcm.getInstance(mRenderable)) { + rcm.destroy(mRenderable); + } + mRenderableBuilt = false; + + if (nPrims == 0) { + return; } auto rbuilder = RenderableManager::Builder(nPrims); rbuilder.boundingBox({{ 0, 0, 0 }, { 10000, 10000, 10000 }}).culling(false); - rcm.destroy(mRenderable); int bufferIndex = 0; int primIndex = 0; int material2dIndex = 0; @@ -239,6 +279,9 @@ void MetaCoreImGuiHelper::processImGuiCommands(ImDrawData* commands, const ImGui if (pcmd.UserCallback) { pcmd.UserCallback(cmds, &pcmd); } else { + if (pcmd.ElemCount == 0) { + continue; + } auto texture = (Texture const*)pcmd.GetTexID(); MaterialInstance* materialInstance; @@ -279,8 +322,9 @@ void MetaCoreImGuiHelper::processImGuiCommands(ImDrawData* commands, const ImGui } bufferIndex++; } - if (commands->CmdListsCount > 0) { + if (primIndex > 0) { rbuilder.build(*mEngine, mRenderable); + mRenderableBuilt = true; } } @@ -370,9 +414,19 @@ void MetaCoreImGuiHelper::updateTexture(ImTextureData* textureData) { } if (textureData->Status == ImTextureStatus_WantCreate) { - if (mTexture != nullptr) { - mEngine->destroy(mTexture); - mTexture = nullptr; + auto* oldTexture = static_cast(textureData->BackendUserData); + if (oldTexture == nullptr && textureData->TexID != ImTextureID_Invalid) { + oldTexture = reinterpret_cast(static_cast(textureData->TexID)); + } + if (oldTexture != nullptr) { + const bool isFontTexture = oldTexture == mTexture; + const bool isOwnedTexture = isFontTexture || mImGuiTextures.erase(oldTexture) > 0; + if (isOwnedTexture) { + mEngine->destroy(oldTexture); + } + if (isFontTexture) { + mTexture = nullptr; + } } Texture::PixelBufferDescriptor pb( @@ -381,7 +435,7 @@ void MetaCoreImGuiHelper::updateTexture(ImTextureData* textureData) { Texture::Format::RGBA, Texture::Type::UBYTE); - mTexture = Texture::Builder() + Texture* createdTexture = Texture::Builder() .width(static_cast(textureData->Width)) .height(static_cast(textureData->Height)) .levels(static_cast(1)) @@ -389,9 +443,25 @@ void MetaCoreImGuiHelper::updateTexture(ImTextureData* textureData) { .sampler(Texture::Sampler::SAMPLER_2D) .build(*mEngine); - mTexture->setImage(*mEngine, 0, std::move(pb)); - textureData->BackendUserData = mTexture; - textureData->SetTexID(static_cast(reinterpret_cast(mTexture))); + createdTexture->setImage(*mEngine, 0, std::move(pb)); + Texture* texture = createdTexture; +#if IMGUI_VERSION_NUM >= 19200 + const bool isFontAtlasTexture = + ImGui::GetIO().Fonts != nullptr && + ImGui::GetIO().Fonts->TexData == textureData; + if (isFontAtlasTexture) { + mTexture = createdTexture; + if (mMaterial2d) { + mMaterial2d->setDefaultParameter("albedo", mTexture, mSampler); + } + } else { + mImGuiTextures.insert(createdTexture); + } +#else + mTexture = createdTexture; +#endif + textureData->BackendUserData = texture; + textureData->SetTexID(static_cast(reinterpret_cast(texture))); textureData->SetStatus(ImTextureStatus_OK); return; } @@ -432,9 +502,16 @@ void MetaCoreImGuiHelper::updateTexture(ImTextureData* textureData) { if (textureData->Status == ImTextureStatus_WantDestroy && textureData->UnusedFrames > 0) { auto* texture = static_cast(textureData->BackendUserData); - if (texture != nullptr && texture != mTexture) { + const bool isFontTexture = texture != nullptr && texture == mTexture; + const bool isOwnedTexture = + texture != nullptr && + (isFontTexture || mImGuiTextures.erase(texture) > 0); + if (isOwnedTexture) { mEngine->destroy(texture); } + if (isFontTexture) { + mTexture = nullptr; + } textureData->BackendUserData = nullptr; textureData->SetTexID(ImTextureID_Invalid); textureData->SetStatus(ImTextureStatus_Destroyed); diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreImGuiHelper.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreImGuiHelper.h index ef7bc8d..8d75257 100644 --- a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreImGuiHelper.h +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreImGuiHelper.h @@ -66,6 +66,7 @@ private: filament::TextureSampler mSampler; bool mFlipVertical = false; bool mOwnsImGuiContext = false; + bool mRenderableBuilt = false; std::filesystem::path mSettingsPath; std::unordered_set mImGuiTextures; }; diff --git a/Source/MetaCoreScene/Private/MetaCoreScene.cpp b/Source/MetaCoreScene/Private/MetaCoreScene.cpp index 2479ce5..b3d4697 100644 --- a/Source/MetaCoreScene/Private/MetaCoreScene.cpp +++ b/Source/MetaCoreScene/Private/MetaCoreScene.cpp @@ -271,6 +271,9 @@ std::vector MetaCoreScene::DuplicateGameObjects(const std::vector()) clonedObject.AddComponent(sourceObject.GetComponent()); + + if (sourceObject.HasComponent()) + clonedObject.AddComponent(sourceObject.GetComponent()); if (sourceObject.HasComponent()) clonedObject.AddComponent(sourceObject.GetComponent()); @@ -421,6 +424,8 @@ MetaCoreSceneSnapshot MetaCoreScene::CaptureSnapshot() const { data.Light = obj.GetComponent(); if (obj.HasComponent()) data.Rotator = obj.GetComponent(); + if (obj.HasComponent()) + data.Script = obj.GetComponent(); if (obj.HasComponent()) data.PrefabInstance = obj.GetComponent(); if (obj.HasComponent()) @@ -449,6 +454,8 @@ void MetaCoreScene::RestoreSnapshot(const MetaCoreSceneSnapshot& snapshot) { obj.AddComponent(data.Light.value()); if (data.Rotator.has_value()) obj.AddComponent(data.Rotator.value()); + if (data.Script.has_value()) + obj.AddComponent(data.Script.value()); if (data.PrefabInstance.has_value()) obj.AddComponent(data.PrefabInstance.value()); if (data.ModelRootTag.has_value()) diff --git a/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp b/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp index 667503d..5093367 100644 --- a/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp +++ b/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp @@ -119,6 +119,19 @@ static json GameObjectDataToJson(const MetaCoreGameObjectData& obj, const MetaCo } objJson["Rotator"] = rotatorJson; } + } else if (objField.Name == "Script" && obj.Script.has_value()) { + json scriptJson = json::object(); + json instancesJson = json::array(); + for (const MetaCoreScriptInstanceData& instance : obj.Script->Instances) { + json instanceJson = json::object(); + instanceJson["InstanceId"] = instance.InstanceId; + instanceJson["ScriptTypeId"] = instance.ScriptTypeId; + instanceJson["Enabled"] = instance.Enabled; + instanceJson["FieldsJson"] = instance.FieldsJson; + instancesJson.push_back(std::move(instanceJson)); + } + scriptJson["Instances"] = std::move(instancesJson); + objJson["Script"] = std::move(scriptJson); } else if (objField.Name == "Camera" && obj.Camera.has_value()) { const MetaCoreStructDescriptor* camDesc = registry.FindStruct(); if (camDesc) { @@ -238,6 +251,32 @@ static MetaCoreGameObjectData JsonToGameObjectData(const json& objJson, const Me } obj.Rotator = rotator; } + } else if (objField.Name == "Script" && objJson.contains("Script")) { + const auto& scriptJson = objJson["Script"]; + MetaCoreScriptComponent script; + if (scriptJson.contains("Instances") && scriptJson["Instances"].is_array()) { + for (const auto& instanceJson : scriptJson["Instances"]) { + MetaCoreScriptInstanceData instance; + if (instanceJson.contains("InstanceId")) { + instance.InstanceId = instanceJson["InstanceId"].get(); + } + if (instanceJson.contains("ScriptTypeId")) { + instance.ScriptTypeId = instanceJson["ScriptTypeId"].get(); + } + if (instanceJson.contains("Enabled")) { + instance.Enabled = instanceJson["Enabled"].get(); + } + if (instanceJson.contains("FieldsJson")) { + instance.FieldsJson = instanceJson["FieldsJson"].get(); + } + if (!instance.ScriptTypeId.empty()) { + script.Instances.push_back(std::move(instance)); + } + } + } + if (!script.Instances.empty()) { + obj.Script = std::move(script); + } } else if (objField.Name == "Camera" && objJson.contains("Camera")) { const auto& camJson = objJson["Camera"]; const MetaCoreStructDescriptor* camDesc = registry.FindStruct(); diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h index 0b00b42..4bd6e04 100644 --- a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h @@ -142,6 +142,28 @@ struct MetaCoreRotatorComponent { float DegreesPerSecond = 90.0F; }; +MC_STRUCT() +struct MetaCoreScriptInstanceData { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::uint64_t InstanceId = 0; + MC_PROPERTY() + std::string ScriptTypeId{}; + MC_PROPERTY() + bool Enabled = true; + MC_PROPERTY() + std::string FieldsJson = "{}"; +}; + +MC_STRUCT() +struct MetaCoreScriptComponent { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::vector Instances{}; +}; + MC_STRUCT() struct MetaCoreModelRootTag { MC_GENERATED_BODY() diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h index eb00bd3..163243f 100644 --- a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h @@ -131,6 +131,8 @@ struct MetaCoreGameObjectData { std::optional ModelRootTag; MC_PROPERTY() std::optional Rotator; + MC_PROPERTY() + std::optional Script; }; } // namespace MetaCore diff --git a/tests/MetaCoreSmokeTests.cpp b/tests/MetaCoreSmokeTests.cpp index ef0f200..f2be623 100644 --- a/tests/MetaCoreSmokeTests.cpp +++ b/tests/MetaCoreSmokeTests.cpp @@ -32,8 +32,10 @@ #include #include #include +#include #include #include +#include #if !defined(_WIN32) #include @@ -184,6 +186,43 @@ public: double LastLateDeltaSeconds = 0.0; }; +struct MetaCoreScriptLifecycleCounters { + int StartCount = 0; + int FixedUpdateCount = 0; + int UpdateCount = 0; + int LateUpdateCount = 0; + int StopCount = 0; +}; + +class MetaCoreCountingNativeScript final : public MetaCore::MetaCoreIScriptBehaviour { +public: + explicit MetaCoreCountingNativeScript(std::shared_ptr counters) + : Counters_(std::move(counters)) {} + + void OnPlayStart(MetaCore::MetaCoreScriptExecutionContext&) override { + ++Counters_->StartCount; + } + + void FixedUpdate(MetaCore::MetaCoreScriptExecutionContext&, double) override { + ++Counters_->FixedUpdateCount; + } + + void Update(MetaCore::MetaCoreScriptExecutionContext&, double) override { + ++Counters_->UpdateCount; + } + + void LateUpdate(MetaCore::MetaCoreScriptExecutionContext&, double) override { + ++Counters_->LateUpdateCount; + } + + void OnPlayStop(MetaCore::MetaCoreScriptExecutionContext&) override { + ++Counters_->StopCount; + } + +private: + std::shared_ptr Counters_{}; +}; + void MetaCoreTestSceneEditApi() { MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); const std::size_t originalCount = scene.GetGameObjects().size(); @@ -303,6 +342,7 @@ void MetaCoreTestBuiltinModuleComposition() { editorViewsModule->Shutdown(moduleRegistry); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); } void MetaCoreTestLegacyBinarySceneStartupLoadCompatibility() { @@ -573,32 +613,38 @@ void MetaCoreTestComponentRegistryDescriptors() { const auto* cameraDescriptor = componentRegistry->FindDescriptor("Camera"); const auto* lightDescriptor = componentRegistry->FindDescriptor("Light"); const auto* rotatorDescriptor = componentRegistry->FindDescriptor("Rotator"); + const auto* scriptDescriptor = componentRegistry->FindDescriptor("Script"); const auto* meshRendererDescriptor = componentRegistry->FindDescriptor("MeshRenderer"); std::cout << "[CHECK] transformDescriptor: " << transformDescriptor << std::endl; std::cout << "[CHECK] cameraDescriptor: " << cameraDescriptor << std::endl; std::cout << "[CHECK] lightDescriptor: " << lightDescriptor << std::endl; std::cout << "[CHECK] rotatorDescriptor: " << rotatorDescriptor << std::endl; + std::cout << "[CHECK] scriptDescriptor: " << scriptDescriptor << std::endl; std::cout << "[CHECK] meshRendererDescriptor: " << meshRendererDescriptor << std::endl; if (transformDescriptor) std::cout << "[CHECK] transform DrawInspector: " << static_cast(transformDescriptor->DrawInspector) << std::endl; if (cameraDescriptor) std::cout << "[CHECK] camera DrawInspector: " << static_cast(cameraDescriptor->DrawInspector) << std::endl; if (lightDescriptor) std::cout << "[CHECK] light DrawInspector: " << static_cast(lightDescriptor->DrawInspector) << std::endl; if (rotatorDescriptor) std::cout << "[CHECK] rotator DrawInspector: " << static_cast(rotatorDescriptor->DrawInspector) << std::endl; + if (scriptDescriptor) std::cout << "[CHECK] script DrawInspector: " << static_cast(scriptDescriptor->DrawInspector) << std::endl; if (meshRendererDescriptor) std::cout << "[CHECK] meshRenderer DrawInspector: " << static_cast(meshRendererDescriptor->DrawInspector) << std::endl; MetaCoreExpect(transformDescriptor != nullptr, "Transform descriptor 应存在"); MetaCoreExpect(cameraDescriptor != nullptr, "Camera descriptor 应存在"); MetaCoreExpect(lightDescriptor != nullptr, "Light descriptor 应存在"); MetaCoreExpect(rotatorDescriptor != nullptr, "Rotator descriptor 应存在"); + MetaCoreExpect(scriptDescriptor != nullptr, "Script descriptor 应存在"); MetaCoreExpect(meshRendererDescriptor != nullptr, "MeshRenderer descriptor 应存在"); MetaCoreExpect(!transformDescriptor->DrawInspector, "Transform 应继续由独立 InspectorDrawer 处理"); MetaCoreExpect(static_cast(cameraDescriptor->DrawInspector), "Camera descriptor 应提供 drawer"); MetaCoreExpect(static_cast(lightDescriptor->DrawInspector), "Light descriptor 应提供 drawer"); MetaCoreExpect(static_cast(rotatorDescriptor->DrawInspector), "Rotator descriptor 应提供 drawer"); + MetaCoreExpect(static_cast(scriptDescriptor->DrawInspector), "Script descriptor 应提供 drawer"); MetaCoreExpect(static_cast(meshRendererDescriptor->DrawInspector), "MeshRenderer descriptor 应提供 drawer"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); } void MetaCoreTestPlayModeStateMachineAndLifecycle() { @@ -770,6 +816,320 @@ void MetaCoreTestPlayModeRotatorRetainsChangesAsSingleUndoStep() { moduleRegistry.ShutdownServices(); } +void MetaCoreTestNativeScriptRegistryAndDefaults() { + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + const auto scriptRegistry = moduleRegistry.ResolveService(); + MetaCoreExpect(scriptRegistry != nullptr, "应解析到 ScriptRegistry"); + + const auto* rotatorDefinition = scriptRegistry->FindScript("MetaCore.Script.Rotator"); + const auto* moverDefinition = scriptRegistry->FindScript("MetaCore.Script.Mover"); + MetaCoreExpect(rotatorDefinition != nullptr, "应注册内置 RotatorScript"); + MetaCoreExpect(moverDefinition != nullptr, "应注册内置 MoverScript"); + MetaCoreExpect(scriptRegistry->CreateBehaviour("MetaCore.Script.Rotator") != nullptr, "RotatorScript 应能创建运行时对象"); + + const nlohmann::json rotatorDefaults = nlohmann::json::parse(scriptRegistry->BuildDefaultFieldsJson("MetaCore.Script.Rotator")); + MetaCoreExpect(rotatorDefaults.contains("Enabled") && rotatorDefaults["Enabled"].get(), "RotatorScript 默认应启用"); + MetaCoreExpect(rotatorDefaults.contains("DegreesPerSecond"), "RotatorScript 默认字段应包含角速度"); + + const nlohmann::json moverEnsured = + nlohmann::json::parse(scriptRegistry->EnsureFieldsJsonDefaults("MetaCore.Script.Mover", "{}")); + MetaCoreExpect(moverEnsured.contains("Enabled"), "MoverScript 应补齐 Enabled 默认字段"); + MetaCoreExpect(moverEnsured.contains("Velocity"), "MoverScript 应补齐 Velocity 默认字段"); + + MetaCore::MetaCoreScriptDefinition replacement; + replacement.TypeId = "MetaCore.Script.Mover"; + replacement.DisplayName = "替换移动脚本"; + replacement.Fields = { + MetaCore::MetaCoreScriptFieldDefinition{ + "Label", + "标签", + MetaCore::MetaCoreScriptFieldType::String, + false, + 0.0F, + glm::vec3(0.0F), + "demo" + } + }; + replacement.Factory = []() { return std::make_unique(std::make_shared()); }; + scriptRegistry->RegisterScript(std::move(replacement)); + const auto* replacedDefinition = scriptRegistry->FindScript("MetaCore.Script.Mover"); + MetaCoreExpect(replacedDefinition != nullptr && replacedDefinition->DisplayName == "替换移动脚本", "重复 TypeId 注册应替换旧定义"); + MetaCoreExpect(scriptRegistry->CreateBehaviour("MetaCore.Script.Mover") != nullptr, "替换后的脚本应能创建运行时对象"); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); +} + +void MetaCoreTestScriptComponentSerializationSnapshotAndJson() { + MetaCore::MetaCoreTypeRegistry registry; + MetaCore::MetaCoreRegisterFoundationGeneratedTypes(registry); + MetaCore::MetaCoreRegisterSceneGeneratedTypes(registry); + + MetaCore::MetaCoreScriptComponent component; + component.Instances.push_back(MetaCore::MetaCoreScriptInstanceData{ + 1001, + "MetaCore.Script.Rotator", + true, + "{\"Enabled\":true,\"DegreesPerSecond\":45.0}" + }); + + const auto serializedComponent = MetaCore::MetaCoreSerializeToBytes(component, registry); + MetaCoreExpect(serializedComponent.has_value(), "Script 组件应可二进制序列化"); + MetaCore::MetaCoreScriptComponent roundTripComponent; + MetaCoreExpect( + MetaCore::MetaCoreDeserializeFromBytes(*serializedComponent, roundTripComponent, registry), + "Script 组件应可二进制反序列化" + ); + MetaCoreExpect(roundTripComponent.Instances.size() == 1, "Script 组件 round-trip 后实例数量应保留"); + MetaCoreExpect(roundTripComponent.Instances.front().ScriptTypeId == "MetaCore.Script.Rotator", "Script TypeId 应保留"); + MetaCoreExpect(roundTripComponent.Instances.front().FieldsJson == component.Instances.front().FieldsJson, "Script FieldsJson 应保留"); + + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreGameObject object = scene.CreateGameObject("ScriptObject"); + object.AddComponent(component); + const MetaCore::MetaCoreId objectId = object.GetId(); + + const MetaCore::MetaCoreSceneSnapshot snapshot = scene.CaptureSnapshot(); + MetaCoreExpect(snapshot.GameObjects.size() == 1, "快照应包含脚本对象"); + MetaCoreExpect(snapshot.GameObjects.front().Script.has_value(), "快照应保存 Script 组件"); + + scene.RestoreSnapshot(MetaCore::MetaCoreSceneSnapshot{}); + MetaCoreExpect(!scene.FindGameObject(objectId), "恢复空快照后对象应不存在"); + scene.RestoreSnapshot(snapshot); + MetaCore::MetaCoreGameObject restoredObject = scene.FindGameObject(objectId); + MetaCoreExpect(restoredObject && restoredObject.HasComponent(), "恢复快照后 Script 组件应保留"); + MetaCoreExpect( + restoredObject.GetComponent().Instances.front().InstanceId == 1001, + "恢复快照后脚本 InstanceId 应保留" + ); + + const std::filesystem::path tempDirectory = + std::filesystem::temp_directory_path() / "MetaCoreScriptSerialization"; + std::filesystem::remove_all(tempDirectory); + std::filesystem::create_directories(tempDirectory); + + MetaCore::MetaCoreSceneDocument sceneDocument; + sceneDocument.Name = "ScriptScene"; + sceneDocument.GameObjects = scene.CaptureSnapshot().GameObjects; + const std::filesystem::path scenePath = tempDirectory / "ScriptScene.mcscene.json"; + MetaCoreExpect( + MetaCore::MetaCoreSceneSerializer::SaveSceneToJson(scenePath, sceneDocument, registry), + "包含 Script 的场景 JSON 应可保存" + ); + const auto loadedSceneDocument = MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson(scenePath, registry); + MetaCoreExpect(loadedSceneDocument.has_value(), "包含 Script 的场景 JSON 应可读取"); + MetaCoreExpect(!loadedSceneDocument->GameObjects.empty(), "读取后场景对象应保留"); + MetaCoreExpect(loadedSceneDocument->GameObjects.front().Script.has_value(), "读取后 Script 组件应保留"); + MetaCoreExpect( + loadedSceneDocument->GameObjects.front().Script->Instances.front().FieldsJson == component.Instances.front().FieldsJson, + "读取后 Script FieldsJson 应保留" + ); + + const std::filesystem::path oldScenePath = tempDirectory / "OldSceneNoScript.mcscene.json"; + { + std::ofstream oldSceneFile(oldScenePath, std::ios::trunc); + oldSceneFile << "{\n" + << " \"SceneName\": \"OldScene\",\n" + << " \"GameObjects\": [\n" + << " {\"Id\": 42, \"ParentId\": 0, \"Name\": \"OldObject\", \"Transform\": {" + << "\"Position\": [0, 0, 0], \"RotationEulerDegrees\": [0, 0, 0], \"Scale\": [1, 1, 1]}}\n" + << " ]\n" + << "}\n"; + } + const auto oldSceneDocument = MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson(oldScenePath, registry); + MetaCoreExpect(oldSceneDocument.has_value(), "旧场景缺少 Script 字段时仍应可读取"); + MetaCoreExpect(!oldSceneDocument->GameObjects.front().Script.has_value(), "旧场景缺少 Script 字段时不应生成脚本组件"); + + std::filesystem::remove_all(tempDirectory); +} + +void MetaCoreTestNativeScriptLifecycle() { + _putenv_s("METACORE_PROJECT_PATH", ""); + + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreGameObject object = scene.CreateGameObject("ScriptLifecycleObject"); + auto& scriptComponent = object.AddComponent(); + scriptComponent.Instances.push_back(MetaCore::MetaCoreScriptInstanceData{ + 2001, + "MetaCore.Tests.CountingScript", + true, + "{}" + }); + + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + const auto counters = std::make_shared(); + const auto scriptRegistry = moduleRegistry.ResolveService(); + MetaCoreExpect(scriptRegistry != nullptr, "应解析到 ScriptRegistry"); + MetaCore::MetaCoreScriptDefinition countingDefinition; + countingDefinition.TypeId = "MetaCore.Tests.CountingScript"; + countingDefinition.DisplayName = "计数脚本"; + countingDefinition.Factory = [counters]() { return std::make_unique(counters); }; + scriptRegistry->RegisterScript(std::move(countingDefinition)); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + const auto playMode = moduleRegistry.ResolveService(); + MetaCoreExpect(playMode != nullptr, "应解析到 PlayModeService"); + MetaCoreExpect(playMode->EnterPlayMode(editorContext), "脚本生命周期测试应能进入播放模式"); + MetaCoreExpect(counters->StartCount == 1, "脚本 OnPlayStart 应只调用一次"); + + playMode->Tick(editorContext, 1.0 / 60.0); + MetaCoreExpect(counters->FixedUpdateCount == 1, "脚本播放 Tick 应执行一次 FixedUpdate"); + MetaCoreExpect(counters->UpdateCount == 1, "脚本播放 Tick 应执行一次 Update"); + MetaCoreExpect(counters->LateUpdateCount == 1, "脚本播放 Tick 应执行一次 LateUpdate"); + + MetaCoreExpect(playMode->Pause(), "脚本生命周期测试应能暂停"); + playMode->Tick(editorContext, 1.0 / 60.0); + MetaCoreExpect(counters->FixedUpdateCount == 1, "暂停时脚本 FixedUpdate 不应继续调用"); + MetaCoreExpect(counters->UpdateCount == 1, "暂停时脚本 Update 不应继续调用"); + MetaCoreExpect(counters->LateUpdateCount == 1, "暂停时脚本 LateUpdate 不应继续调用"); + + MetaCoreExpect(playMode->Step(editorContext), "暂停时脚本应能单步"); + MetaCoreExpect(counters->FixedUpdateCount == 2, "脚本单步应执行一次 FixedUpdate"); + MetaCoreExpect(counters->UpdateCount == 2, "脚本单步应执行一次 Update"); + MetaCoreExpect(counters->LateUpdateCount == 2, "脚本单步应执行一次 LateUpdate"); + + MetaCoreExpect(playMode->ExitPlayMode(editorContext), "脚本生命周期测试应能退出播放模式"); + MetaCoreExpect(counters->StopCount == 1, "脚本 OnPlayStop 应调用一次并清理运行时实例"); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); +} + +void MetaCoreTestNativeScriptBehavioursRetainChangesAsSingleUndoStep() { + _putenv_s("METACORE_PROJECT_PATH", ""); + + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreGameObject object = scene.CreateGameObject("ScriptActor"); + auto& scriptComponent = object.AddComponent(); + scriptComponent.Instances.push_back(MetaCore::MetaCoreScriptInstanceData{ + 3001, + "MetaCore.Script.Rotator", + true, + "{\"Enabled\":true,\"DegreesPerSecond\":60.0}" + }); + scriptComponent.Instances.push_back(MetaCore::MetaCoreScriptInstanceData{ + 3002, + "MetaCore.Script.Mover", + true, + "{\"Enabled\":true,\"Velocity\":[2.0,0.0,0.0]}" + }); + const MetaCore::MetaCoreId objectId = object.GetId(); + + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + const auto playMode = moduleRegistry.ResolveService(); + const auto scenePersistence = moduleRegistry.ResolveService(); + MetaCoreExpect(playMode != nullptr, "应解析到 PlayModeService"); + MetaCoreExpect(scenePersistence != nullptr, "应解析到 ScenePersistenceService"); + MetaCoreExpect(editorContext.GetCommandService().GetUndoCount() == 0, "脚本播放前 Undo 栈应为空"); + + MetaCoreExpect(playMode->EnterPlayMode(editorContext), "脚本行为测试应能进入播放模式"); + for (int frameIndex = 0; frameIndex < 5; ++frameIndex) { + playMode->Tick(editorContext, 0.1); + } + + MetaCore::MetaCoreGameObject updatedObject = scene.FindGameObject(objectId); + MetaCoreExpectVec3Near( + updatedObject.GetComponent().RotationEulerDegrees, + glm::vec3(0.0F, 30.0F, 0.0F), + "RotatorScript 播放 0.5 秒后应旋转 30 度" + ); + MetaCoreExpectVec3Near( + updatedObject.GetComponent().Position, + glm::vec3(1.0F, 0.0F, 0.0F), + "MoverScript 播放 0.5 秒后应移动 1 个单位" + ); + + MetaCoreExpect(playMode->Pause(), "脚本行为测试应能暂停"); + playMode->Tick(editorContext, 0.5); + updatedObject = scene.FindGameObject(objectId); + MetaCoreExpectVec3Near( + updatedObject.GetComponent().RotationEulerDegrees, + glm::vec3(0.0F, 30.0F, 0.0F), + "暂停期间 RotatorScript 不应继续旋转" + ); + MetaCoreExpectVec3Near( + updatedObject.GetComponent().Position, + glm::vec3(1.0F, 0.0F, 0.0F), + "暂停期间 MoverScript 不应继续移动" + ); + + MetaCoreExpect(playMode->Step(editorContext), "暂停后脚本行为应能单步"); + updatedObject = scene.FindGameObject(objectId); + MetaCoreExpectVec3Near( + updatedObject.GetComponent().RotationEulerDegrees, + glm::vec3(0.0F, 31.0F, 0.0F), + "RotatorScript 单步应按固定步长旋转" + ); + MetaCoreExpectVec3Near( + updatedObject.GetComponent().Position, + glm::vec3(1.0F + (2.0F / 60.0F), 0.0F, 0.0F), + "MoverScript 单步应按固定步长移动" + ); + + MetaCoreExpect(playMode->ExitPlayMode(editorContext), "脚本行为测试应能退出播放模式"); + MetaCoreExpect(editorContext.GetCommandService().GetUndoCount() == 1, "脚本播放期差异应合并为一个 Undo 步"); + MetaCoreExpect(scenePersistence->IsSceneDirty(), "脚本播放结果保留后场景应标记为脏"); + updatedObject = scene.FindGameObject(objectId); + MetaCoreExpect(updatedObject.HasComponent(), "停止后 Script 组件应保留"); + MetaCoreExpectVec3Near( + updatedObject.GetComponent().RotationEulerDegrees, + glm::vec3(0.0F, 31.0F, 0.0F), + "停止后 RotatorScript 结果应保留" + ); + + MetaCoreExpect(editorContext.UndoCommand(), "Undo 应能回到脚本播放前快照"); + updatedObject = scene.FindGameObject(objectId); + MetaCoreExpectVec3Near( + updatedObject.GetComponent().RotationEulerDegrees, + glm::vec3(0.0F, 0.0F, 0.0F), + "Undo 后 RotatorScript 旋转应回到播放前" + ); + MetaCoreExpectVec3Near( + updatedObject.GetComponent().Position, + glm::vec3(0.0F, 0.0F, 0.0F), + "Undo 后 MoverScript 位移应回到播放前" + ); + MetaCoreExpect(updatedObject.HasComponent(), "Undo 后 Script 组件应保留"); + MetaCoreExpect(updatedObject.GetComponent().Instances.size() == 2, "Undo 后脚本实例列表应保留"); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); +} + void MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot() { MetaCore::MetaCoreScene scene; @@ -1855,6 +2215,13 @@ void MetaCoreTestPrefabWorkflow() { MetaCore::MetaCoreGameObject root = scene.CreateGameObject("PrefabRoot"); root.AddComponent(); + auto& rootScript = root.AddComponent(); + rootScript.Instances.push_back(MetaCore::MetaCoreScriptInstanceData{ + 4101, + "MetaCore.Script.Mover", + true, + "{\"Enabled\":true,\"Velocity\":[1.0,2.0,3.0]}" + }); root.GetComponent().Position = glm::vec3(1.0F, 2.0F, 3.0F); const MetaCore::MetaCoreId rootId = root.GetId(); MetaCore::MetaCoreGameObject child = scene.CreateGameObject("PrefabChild", rootId); @@ -1887,6 +2254,11 @@ void MetaCoreTestPrefabWorkflow() { MetaCore::MetaCoreGameObject instantiatedRoot = scene.FindGameObject(*instantiatedRootId); MetaCoreExpect(instantiatedRoot, "实例化后应能找到根对象"); MetaCoreExpect(instantiatedRoot.HasComponent(), "实例根应带有 prefab 元数据"); + MetaCoreExpect(instantiatedRoot.HasComponent(), "实例根应保留 Script 组件"); + MetaCoreExpect( + instantiatedRoot.GetComponent().Instances.size() == 1, + "Prefab 实例应保留脚本实例" + ); instantiatedRoot.GetComponent().Position = glm::vec3(8.0F, 9.0F, 10.0F); editorContext.SelectOnly(instantiatedRoot.GetId()); @@ -1895,6 +2267,11 @@ void MetaCoreTestPrefabWorkflow() { const auto appliedPrefabDocument = MetaCore::MetaCoreSceneSerializer::LoadPrefabFromJson(tempProjectRoot / prefabPath, reflectionRegistry->GetTypeRegistry()); MetaCoreExpect(appliedPrefabDocument.has_value(), "Apply 后 prefab 应能被 JSON 解析"); MetaCoreExpect(!appliedPrefabDocument->GameObjects.empty(), "Apply 后 prefab 应保留对象数据"); + MetaCoreExpect(appliedPrefabDocument->GameObjects.front().Script.has_value(), "Apply 后 prefab 资源应保留 Script 组件"); + MetaCoreExpect( + appliedPrefabDocument->GameObjects.front().Script->Instances.front().ScriptTypeId == "MetaCore.Script.Mover", + "Apply 后 prefab 资源应保留脚本 TypeId" + ); MetaCoreExpectVec3Near( appliedPrefabDocument->GameObjects.front().Transform.Position, glm::vec3(8.0F, 9.0F, 10.0F), @@ -1905,6 +2282,7 @@ void MetaCoreTestPrefabWorkflow() { MetaCoreExpect(secondInstanceRootId.has_value(), "应用后应仍能再次实例化 prefab"); MetaCore::MetaCoreGameObject secondInstanceRoot = scene.FindGameObject(*secondInstanceRootId); MetaCoreExpect(secondInstanceRoot, "第二个实例应存在"); + MetaCoreExpect(secondInstanceRoot.HasComponent(), "第二个实例应保留 Script 组件"); MetaCoreExpectVec3Near(secondInstanceRoot.GetComponent().Position, glm::vec3(8.0F, 9.0F, 10.0F), "Apply 后新实例应继承修改过的 transform"); secondInstanceRoot.GetComponent().Position = glm::vec3(-1.0F, -2.0F, -3.0F); @@ -2067,8 +2445,10 @@ void MetaCoreTestComponentRegistryOperations() { const auto* cameraDescriptor = componentRegistry->FindDescriptor("Camera"); const auto* lightDescriptor = componentRegistry->FindDescriptor("Light"); + const auto* scriptDescriptor = componentRegistry->FindDescriptor("Script"); MetaCoreExpect(cameraDescriptor != nullptr, "应能找到 Camera 描述符"); MetaCoreExpect(lightDescriptor != nullptr, "应能找到 Light 描述符"); + MetaCoreExpect(scriptDescriptor != nullptr, "应能找到 Script 描述符"); MetaCoreExpect(!cameraDescriptor->HasComponent(object), "初始时不应有 Camera"); const bool addedCamera = editorContext.ExecuteSnapshotCommand("添加 Camera", [&]() { @@ -2083,6 +2463,18 @@ void MetaCoreTestComponentRegistryOperations() { MetaCoreExpect(addedLight, "应能通过注册表添加 Light"); MetaCoreExpect(object.HasComponent(), "添加后对象应拥有 Light"); + const bool addedScript = editorContext.ExecuteSnapshotCommand("添加 Script", [&]() { + return scriptDescriptor->AddComponent(object); + }); + MetaCoreExpect(addedScript, "应能通过注册表添加 Script"); + MetaCoreExpect(object.HasComponent(), "添加后对象应拥有 Script"); + object.GetComponent().Instances.push_back(MetaCore::MetaCoreScriptInstanceData{ + 5101, + "MetaCore.Script.Rotator", + true, + "{\"Enabled\":true,\"DegreesPerSecond\":120.0}" + }); + const bool removedCamera = editorContext.ExecuteSnapshotCommand("移除 Camera", [&]() { return cameraDescriptor->RemoveComponent(object); }); @@ -2114,10 +2506,26 @@ void MetaCoreTestComponentRegistryOperations() { MetaCoreExpect(pasteTarget.GetComponent().IsPrimary, "粘贴后 IsPrimary 应一致"); MetaCoreExpect(std::abs(secondPasteTarget.GetComponent().FieldOfViewDegrees - 77.0F) <= 0.0001F, "第二个对象粘贴后 FOV 应一致"); + MetaCoreExpect(componentRegistry->CopyComponent("Script", object), "应能复制 Script 组件值"); + MetaCoreExpect(componentRegistry->CanPasteComponent("Script"), "复制后应可粘贴 Script"); + MetaCoreExpect(componentRegistry->PasteComponent("Script", pasteTarget), "应能粘贴 Script 组件值"); + MetaCoreExpect(pasteTarget.HasComponent(), "粘贴后目标对象应拥有 Script"); + MetaCoreExpect( + pasteTarget.GetComponent().Instances.size() == 1, + "粘贴后 Script 实例数量应一致" + ); + MetaCoreExpect( + pasteTarget.GetComponent().Instances.front().ScriptTypeId == "MetaCore.Script.Rotator", + "粘贴后 Script TypeId 应一致" + ); + MetaCoreExpect(cameraDescriptor->ResetComponent != nullptr, "Camera descriptor 应提供 Reset"); pasteTarget.GetComponent().FieldOfViewDegrees = 10.0F; MetaCoreExpect(cameraDescriptor->ResetComponent(pasteTarget), "应能重置 Camera"); MetaCoreExpect(std::abs(pasteTarget.GetComponent().FieldOfViewDegrees - 60.0F) <= 0.0001F, "重置后 Camera 应恢复默认值"); + MetaCoreExpect(scriptDescriptor->ResetComponent != nullptr, "Script descriptor 应提供 Reset"); + MetaCoreExpect(scriptDescriptor->ResetComponent(pasteTarget), "应能重置 Script"); + MetaCoreExpect(pasteTarget.GetComponent().Instances.empty(), "重置后 Script 实例列表应清空"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); @@ -3597,6 +4005,14 @@ int main() { MetaCoreTestPlayModeStateMachineAndLifecycle(); std::cout << "[RUN] MetaCoreTestPlayModeRotatorRetainsChangesAsSingleUndoStep..." << std::endl; MetaCoreTestPlayModeRotatorRetainsChangesAsSingleUndoStep(); + std::cout << "[RUN] MetaCoreTestNativeScriptRegistryAndDefaults..." << std::endl; + MetaCoreTestNativeScriptRegistryAndDefaults(); + std::cout << "[RUN] MetaCoreTestScriptComponentSerializationSnapshotAndJson..." << std::endl; + MetaCoreTestScriptComponentSerializationSnapshotAndJson(); + std::cout << "[RUN] MetaCoreTestNativeScriptLifecycle..." << std::endl; + MetaCoreTestNativeScriptLifecycle(); + std::cout << "[RUN] MetaCoreTestNativeScriptBehavioursRetainChangesAsSingleUndoStep..." << std::endl; + MetaCoreTestNativeScriptBehavioursRetainChangesAsSingleUndoStep(); std::cout << "[RUN] MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot..." << std::endl; MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot(); std::cout << "[RUN] MetaCoreTestJsonSceneSaveCurrentSceneUsesMcsceneJson..." << std::endl;