From 2626070af7751cc60aac659fd6e3849d6367cbec Mon Sep 17 00:00:00 2001 From: Rowland <975945824@qq.com> Date: Thu, 16 Jul 2026 10:26:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=98=A0=E5=B0=84=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Apps/MetaCorePlayer/main.cpp | 48 +++++- CMakeLists.txt | 49 +++++- SandboxProject/Runtime/Input.mcruntime | Bin 0 -> 3777 bytes .../Private/MetaCoreBuildPipeline.cpp | 17 +++ .../MetaCoreBuiltinCoreServicesModule.cpp | 130 +++++++++++++++- .../Private/MetaCoreBuiltinEditorModule.cpp | 58 ++++++++ .../Private/MetaCoreEditorApp.cpp | 10 +- .../Private/MetaCoreEditorContext.cpp | 9 +- .../MetaCoreEditor/MetaCoreEditorContext.h | 3 + .../MetaCoreGeneratedReflection.h | 1 + .../Private/MetaCoreInput.cpp | 39 +++++ .../Private/MetaCoreWindow.cpp | 50 +++++++ .../Private/MetaCoreWindowGlfw.cpp | 67 +++++++++ .../Public/MetaCorePlatform/MetaCoreInput.h | 36 +++++ .../MetaCoreEditorViewportRenderer.cpp | 4 + .../Private/MetaCoreFilamentSceneBridge.cpp | 58 ++++++++ .../MetaCoreEditorViewportRenderer.h | 1 + .../MetaCoreFilamentSceneBridge.h | 9 ++ .../MetaCoreRuntimeDataProject.h | 3 + .../Private/MetaCoreInputMap.cpp | 78 ++++++++++ .../Private/MetaCoreRuntimeInput.cpp | 124 ++++++++++++++++ .../Private/MetaCoreRuntimeInteraction.cpp | 140 ++++++++++++++++++ .../MetaCoreRuntimeInput/MetaCoreInputMap.h | 99 +++++++++++++ .../MetaCoreRuntimeInput.h | 67 +++++++++ .../MetaCoreRuntimeInteraction.h | 50 +++++++ .../Private/MetaCoreRuntimeUiSystem.cpp | 45 +++--- .../MetaCoreRuntimeUiSystem.h | 2 + .../MetaCoreScene/Private/MetaCoreScene.cpp | 6 + .../Private/MetaCoreSceneSerializer.cpp | 13 ++ .../Public/MetaCoreScene/MetaCoreComponents.h | 19 +++ .../Public/MetaCoreScene/MetaCoreGameObject.h | 2 + .../Private/MetaCoreScripting.cpp | 44 ++++-- .../MetaCoreScripting/MetaCoreScriptAbi.h | 13 +- .../MetaCoreScripting/MetaCoreScripting.h | 5 +- ...etacore-mature-engine-gap-and-work-plan.md | 14 +- tests/MetaCoreRuntimeInputTests.cpp | 113 ++++++++++++++ 36 files changed, 1376 insertions(+), 50 deletions(-) create mode 100644 SandboxProject/Runtime/Input.mcruntime create mode 100644 Source/MetaCoreRuntimeInput/Private/MetaCoreInputMap.cpp create mode 100644 Source/MetaCoreRuntimeInput/Private/MetaCoreRuntimeInput.cpp create mode 100644 Source/MetaCoreRuntimeInput/Private/MetaCoreRuntimeInteraction.cpp create mode 100644 Source/MetaCoreRuntimeInput/Public/MetaCoreRuntimeInput/MetaCoreInputMap.h create mode 100644 Source/MetaCoreRuntimeInput/Public/MetaCoreRuntimeInput/MetaCoreRuntimeInput.h create mode 100644 Source/MetaCoreRuntimeInput/Public/MetaCoreRuntimeInput/MetaCoreRuntimeInteraction.h create mode 100644 tests/MetaCoreRuntimeInputTests.cpp diff --git a/Apps/MetaCorePlayer/main.cpp b/Apps/MetaCorePlayer/main.cpp index 570eb14..a57aff5 100644 --- a/Apps/MetaCorePlayer/main.cpp +++ b/Apps/MetaCorePlayer/main.cpp @@ -10,6 +10,8 @@ #include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" #include "MetaCoreRuntimeData/MetaCoreRuntimeDataSource.h" #include "MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h" +#include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h" +#include "MetaCoreRuntimeInput/MetaCoreRuntimeInteraction.h" #include "MetaCoreScene/MetaCoreScenePackage.h" #include "MetaCoreScene/MetaCoreScene.h" #include "MetaCoreScripting/MetaCoreScripting.h" @@ -77,6 +79,7 @@ MetaCore::MetaCoreSceneView MetaCoreBuildPlayerSceneView() { document.BindingsPath = std::filesystem::path("Runtime") / "Bindings.mcruntime"; document.DiagnosticsPath = std::filesystem::path("Runtime") / "Diagnostics.mcruntimestate"; document.UiManifestPath = std::filesystem::path("Runtime") / "Ui.mcruntime"; + document.InputMapPath = std::filesystem::path("Runtime") / "Input.mcruntime"; return document; } @@ -345,6 +348,7 @@ int main(int argc, char* argv[]) { viewportRenderer.SetProjectRootPath(projectRoot); MetaCore::MetaCoreTypeRegistry typeRegistry; MetaCore::MetaCoreRegisterRuntimeDataGeneratedTypes(typeRegistry); + MetaCore::MetaCoreRegisterRuntimeInputGeneratedTypes(typeRegistry); const auto loadedRuntimeProjectDocument = MetaCore::MetaCoreReadRuntimeProjectDocument( deliveryDocument ? packageRoot / deliveryDocument->RuntimeConfig : projectRoot / "Runtime" / "ProjectRuntime.mcruntimecfg", typeRegistry @@ -412,12 +416,22 @@ int main(int argc, char* argv[]) { } } if (deliveryDocument && missingRequiredScript) return 2; + const auto loadedInputMap = runtimeProjectDocument.InputMapPath.empty() ? std::optional{} : + MetaCore::MetaCoreReadInputMap((projectRoot / runtimeProjectDocument.InputMapPath).lexically_normal(), typeRegistry); + MetaCore::MetaCoreRuntimeInput runtimeInput; + runtimeInput.SetInputMap(loadedInputMap.value_or(MetaCore::MetaCoreInputMapDocument{})); + const auto projectDocument = MetaCore::MetaCoreReadProjectFile(projectPath); + const std::string inputProjectName = projectDocument ? projectDocument->Name : projectRoot.filename().string(); + const auto inputOverridePath = MetaCore::MetaCoreGetInputOverridePath(inputProjectName); + (void)runtimeInput.LoadOverrides(inputOverridePath); + for (const auto& warning : runtimeInput.GetWarnings()) errors << "MetaCorePlayer Input: " << warning << '\n'; + MetaCore::MetaCoreScriptRuntime scriptRuntime( scene, scriptRegistry, [&output, &errors](std::uint32_t level, std::string_view category, std::string_view message) { std::ostream& stream = level >= 2U ? errors : output; stream << "MetaCorePlayer Script[" << category << "]: " << message << '\n'; - } + }, &runtimeInput ); scriptRuntime.Start(); @@ -493,12 +507,42 @@ int main(int argc, char* argv[]) { ? runtimeAdapter->GetStatus().State : MetaCore::MetaCoreRuntimeDataSourceState::Disconnected; std::unordered_map lastReportedBindingIssueKeys; + MetaCore::MetaCoreRuntimeInteraction runtimeInteraction; + const std::uint64_t inputSubscription = runtimeInput.Subscribe([&scriptRuntime](const MetaCore::MetaCoreInputActionEvent& event) { + MetaCoreScriptValueV1 phase{}; phase.Type = MetaCoreScriptAbiValue_Int; phase.IntValue = static_cast(event.Phase); + scriptRuntime.Publish("MetaCore.Input.Action/" + event.ActionId, {phase}); + }); + runtimeUi.SetEventCallback([&scriptRuntime](const MetaCore::MetaCoreRuntimeUiEvent& event) { + const auto textValue = [](const std::string& text) { + MetaCoreScriptValueV1 value{}; value.Type = MetaCoreScriptAbiValue_String; + std::snprintf(value.Text, sizeof(value.Text), "%s", text.c_str()); return value; + }; + scriptRuntime.Publish("MetaCore.UI", { + textValue(event.DocumentId), textValue(event.ElementId), textValue(event.Action), + textValue(event.Type), textValue(event.Value) + }); + }); + runtimeInteraction.SetEventCallback([&scriptRuntime](const MetaCore::MetaCorePointerEvent& event) { + MetaCoreScriptValueV1 type{}; type.Type = MetaCoreScriptAbiValue_Int; type.IntValue = static_cast(event.Type); + MetaCoreScriptValueV1 button{}; button.Type = MetaCoreScriptAbiValue_Int; button.IntValue = event.Button; + MetaCoreScriptValueV1 screen{}; screen.Type = MetaCoreScriptAbiValue_Vec3; screen.Vec3[0] = event.ScreenPosition.x; screen.Vec3[1] = event.ScreenPosition.y; + MetaCoreScriptValueV1 delta{}; delta.Type = MetaCoreScriptAbiValue_Vec3; delta.Vec3[0] = event.ScreenDelta.x; delta.Vec3[1] = event.ScreenDelta.y; + MetaCoreScriptValueV1 world{}; world.Type = MetaCoreScriptAbiValue_Vec3; world.Vec3[0] = event.WorldPosition.x; world.Vec3[1] = event.WorldPosition.y; world.Vec3[2] = event.WorldPosition.z; + MetaCoreScriptValueV1 target{}; target.Type = MetaCoreScriptAbiValue_GameObjectRef; + target.ObjectValue = {scriptRuntime.GetWorldGeneration(), event.TargetObjectId}; + scriptRuntime.PublishToObject(event.TargetObjectId, "MetaCore.Interaction.Pointer", {type, button, screen, delta, world, target}); + }); std::uint64_t diagnosticsWriteFrame = 0; std::uint64_t renderedFrames = 0U; while (!window.ShouldClose()) { window.BeginFrame(); const double frameDeltaSeconds = window.GetDeltaSeconds(); + const MetaCore::MetaCoreInputConsumption uiConsumption = runtimeUi.ProcessInput(); + runtimeInput.Update(window.GetInput(), uiConsumption); + const auto [inputWidth, inputHeight] = window.GetWindowSize(); + const MetaCore::MetaCoreViewportRect inputViewport{0.0F, 0.0F, static_cast(inputWidth), static_cast(inputHeight)}; + runtimeInteraction.Update(scene, viewportRenderer, window.GetInput(), inputViewport, uiConsumption.Mouse); scriptRuntime.FixedUpdate(1.0 / 60.0); scriptRuntime.Update(frameDeltaSeconds); const auto [windowWidth, windowHeight] = window.GetWindowSize(); @@ -568,6 +612,8 @@ int main(int argc, char* argv[]) { } scriptRuntime.Stop(); + runtimeInput.Unsubscribe(inputSubscription); + (void)runtimeInput.SaveOverrides(inputOverridePath); runtimeUi.Shutdown(); viewportRenderer.Shutdown(); renderDevice.Shutdown(); diff --git a/CMakeLists.txt b/CMakeLists.txt index e1f0fb3..855bbe0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -427,12 +427,36 @@ if(METACORE_ENABLE_PLATFORM) target_link_libraries(MetaCorePlatform PUBLIC glfw) endif() - target_compile_options(MetaCorePlatform PRIVATE ${METACORE_COMMON_WARNINGS}) +target_compile_options(MetaCorePlatform PRIVATE ${METACORE_COMMON_WARNINGS}) if(WIN32) target_compile_definitions(MetaCorePlatform PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX) + target_link_libraries(MetaCorePlatform PRIVATE xinput9_1_0) endif() endif() +set(METACORE_RUNTIME_INPUT_HEADERS + Source/MetaCoreRuntimeInput/Public/MetaCoreRuntimeInput/MetaCoreInputMap.h + Source/MetaCoreRuntimeInput/Public/MetaCoreRuntimeInput/MetaCoreRuntimeInput.h +) +set(METACORE_RUNTIME_INPUT_SOURCES + Source/MetaCoreRuntimeInput/Private/MetaCoreInputMap.cpp + Source/MetaCoreRuntimeInput/Private/MetaCoreRuntimeInput.cpp +) +metacore_generate_reflection( + RuntimeInput + MetaCoreRegisterRuntimeInputGeneratedTypes + METACORE_RUNTIME_INPUT_GENERATED_SOURCE + Source/MetaCoreRuntimeInput/Public/MetaCoreRuntimeInput/MetaCoreInputMap.h +) +add_library(MetaCoreRuntimeInput STATIC + ${METACORE_RUNTIME_INPUT_HEADERS} + ${METACORE_RUNTIME_INPUT_SOURCES} + ${METACORE_RUNTIME_INPUT_GENERATED_SOURCE} +) +target_include_directories(MetaCoreRuntimeInput PUBLIC Source/MetaCoreRuntimeInput/Public PRIVATE third_party) +target_link_libraries(MetaCoreRuntimeInput PUBLIC MetaCoreFoundation MetaCorePlatform) +target_compile_options(MetaCoreRuntimeInput PRIVATE ${METACORE_COMMON_WARNINGS}) + set(METACORE_SCENE_HEADERS Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h @@ -487,7 +511,7 @@ target_include_directories(MetaCoreScripting PUBLIC Source/MetaCoreScripting/Public PRIVATE third_party ) -target_link_libraries(MetaCoreScripting PUBLIC MetaCoreFoundation MetaCoreScene glm::glm) +target_link_libraries(MetaCoreScripting PUBLIC MetaCoreFoundation MetaCoreScene MetaCoreRuntimeInput glm::glm) if(UNIX AND NOT APPLE) target_link_libraries(MetaCoreScripting PRIVATE dl) endif() @@ -504,7 +528,7 @@ target_include_directories(MetaCoreDelivery PRIVATE third_party ) target_link_libraries(MetaCoreDelivery - PUBLIC MetaCoreFoundation MetaCoreScene MetaCoreScripting + PUBLIC MetaCoreFoundation MetaCoreScene MetaCoreScripting MetaCoreRuntimeInput MetaCoreRuntimeData ) target_compile_options(MetaCoreDelivery PRIVATE ${METACORE_COMMON_WARNINGS}) if(crashpad_FOUND) @@ -566,6 +590,14 @@ metacore_use_filament(MetaCoreRender) target_compile_options(MetaCoreRender PRIVATE ${METACORE_COMMON_WARNINGS}) + +add_library(MetaCoreRuntimeInteraction STATIC + Source/MetaCoreRuntimeInput/Public/MetaCoreRuntimeInput/MetaCoreRuntimeInteraction.h + Source/MetaCoreRuntimeInput/Private/MetaCoreRuntimeInteraction.cpp +) +target_include_directories(MetaCoreRuntimeInteraction PUBLIC Source/MetaCoreRuntimeInput/Public) +target_link_libraries(MetaCoreRuntimeInteraction PUBLIC MetaCoreRuntimeInput MetaCoreRender MetaCoreScene) +target_compile_options(MetaCoreRuntimeInteraction PRIVATE ${METACORE_COMMON_WARNINGS}) endif() if(METACORE_ENABLE_RUNTIME_DATA) @@ -618,6 +650,7 @@ target_link_libraries(MetaCoreRuntimeConfigTool PRIVATE MetaCoreFoundation MetaCoreRuntimeData + MetaCoreRuntimeInput MetaCoreScene MetaCoreDelivery ) @@ -714,7 +747,10 @@ target_link_libraries(MetaCoreEditor MetaCoreFoundation MetaCorePlatform MetaCoreRender + MetaCoreRuntimeInteraction MetaCoreRuntimeData + MetaCoreRuntimeInput + MetaCoreRuntimeUi MetaCoreScene MetaCoreScripting MetaCoreDelivery @@ -784,6 +820,7 @@ target_link_libraries(MetaCoreRuntimeUi MetaCorePlatform MetaCoreRender MetaCoreRuntimeData + MetaCoreRuntimeInput RmlUi::RmlUi PNG::PNG unofficial::brotli::brotlidec @@ -802,6 +839,7 @@ target_link_libraries(MetaCorePlayer MetaCoreFoundation MetaCorePlatform MetaCoreRender + MetaCoreRuntimeInteraction MetaCoreRuntimeData MetaCoreScene MetaCoreRuntimeUi @@ -833,6 +871,11 @@ if(METACORE_BUILD_TESTS) target_compile_definitions(MetaCoreScriptingTests PRIVATE METACORE_SOURCE_ROOT="${CMAKE_SOURCE_DIR}") add_test(NAME MetaCoreScriptingTests COMMAND MetaCoreScriptingTests) + add_executable(MetaCoreRuntimeInputTests tests/MetaCoreRuntimeInputTests.cpp Source/MetaCoreRender/Private/MetaCoreGlibcCompat.cpp) + target_link_libraries(MetaCoreRuntimeInputTests PRIVATE MetaCoreRuntimeInput MetaCoreRuntimeInteraction MetaCorePlatform MetaCoreScene) + target_compile_options(MetaCoreRuntimeInputTests PRIVATE ${METACORE_COMMON_WARNINGS}) + add_test(NAME MetaCoreRuntimeInputTests COMMAND MetaCoreRuntimeInputTests) + add_executable(MetaCoreSmokeTests tests/MetaCoreSmokeTests.cpp Source/MetaCoreRender/Private/MetaCoreGlibcCompat.cpp diff --git a/SandboxProject/Runtime/Input.mcruntime b/SandboxProject/Runtime/Input.mcruntime new file mode 100644 index 0000000000000000000000000000000000000000..ec3f351c48bd05214fbeb8fa8599be921b05c85c GIT binary patch literal 3777 zcmd5uog*-VzW#B^%`yO&@1eT z+B4MS%n%645=ud}giJh>8Gruf;p{k$bKU%Injef$JW@i2*ODh^$IK+Ex!Oq7PZV95 zQGx&T-1Huzj9m0ON1gA2Q#9JsfbN7qJwe(y5xK2{IiE;0d)UE0LM}e%P;_X#S8$KbP8K$05(>J z0SlmzfsFwm%IhNZL^x2tap4YlG=oCycJk<;UekMp3r}z!n5?b{%=k(m$BL8fk1QH+ zU0fe1wG+KG_EXLdWvC zg(-NnK{QniHtW=ab!{c8aOJYm0@iGTC1qewO>AZ;0+;W+Z5#oq4aE8sN=x=usW333@#ozPz0^K~oIfWOA!RdDKHHz@9 z5vRrU^_V*>Ltpn_(bq15w+4OLj7aJ0%jn!>##Rx2E<(5lfmBT?RV+f^w2;bV88&Dj zot4BRH zXS%R)n6$&T7VdB(JYEiTDVG3rH7?5lU4u~yKIlw*qSX&*S7zX3XNZ^dOCkFKV-MbZ z+>oV{NXTgJlJ7Y5&QZvEKKJTzgZJ6k*HbUl4b3*xIN-biAm))x~ yl~56};WvLJqg#^|&0S#fJAOfRuntimeDirectory / "ProjectRuntime.mcruntimecfg"; + const auto runtimeProject = MetaCoreReadRuntimeProjectDocument(runtimeProjectPath, inputRegistry); + if (!runtimeProject) return fail(MetaCoreBuildErrorCode::CookFailed, "Runtime project configuration is missing or unreadable", runtimeProjectPath); + if (runtimeProject->InputMapPath.empty() || !IsSafeRelativePath(runtimeProject->InputMapPath)) + return fail(MetaCoreBuildErrorCode::UnsafePath, "Runtime input map path is empty or unsafe", runtimeProjectPath); + const auto inputMapPath = request.ProjectRoot / runtimeProject->InputMapPath; + const auto inputMap = MetaCoreReadInputMap(inputMapPath, inputRegistry); + if (!inputMap) return fail(MetaCoreBuildErrorCode::CookFailed, "Runtime input map is missing or unreadable", inputMapPath); + const auto inputIssues = MetaCoreValidateInputMap(*inputMap); + if (std::any_of(inputIssues.begin(), inputIssues.end(), [](const auto& issue) { return issue.Error; })) + return fail(MetaCoreBuildErrorCode::CookFailed, "Runtime input map validation failed", inputMapPath); std::filesystem::path startupSource = profile->StartupScene.empty() ? project->StartupScenePath : profile->StartupScene; if (!IsSafeRelativePath(startupSource)) return fail(MetaCoreBuildErrorCode::UnsafePath, "Startup scene path is unsafe", startupSource); const auto absoluteStartup = request.ProjectRoot / startupSource; diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp index b386709..07d54f4 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp @@ -11,6 +11,12 @@ #include "MetaCoreFoundation/MetaCoreReflection.h" #include "MetaCoreFoundation/MetaCoreProject.h" #include "MetaCorePlatform/MetaCoreInput.h" +#include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h" +#include "MetaCoreRuntimeInput/MetaCoreRuntimeInteraction.h" +#include "MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h" +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" +#include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" +#include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" #include "MetaCoreFoundation/MetaCoreAssetRegistry.h" #include "MetaCoreFoundation/MetaCoreAssetDependencyGraph.h" #include "MetaCoreFoundation/MetaCoreRuntimeAssetRegistry.h" @@ -3580,6 +3586,20 @@ void MetaCoreDrawUiRendererComponentInspector(MetaCoreEditorContext& editorConte ImGui::TextDisabled("2D 使用屏幕空间层级;3D 使用对象位置/旋转、世界尺寸和每单位像素。"); } +void MetaCoreDrawInteractableComponentInspector(MetaCoreEditorContext& editorContext, MetaCoreGameObject& gameObject) { + if (!gameObject.HasComponent()) return; + auto& component = gameObject.GetComponent(); + bool changed = ImGui::Checkbox("启用", &component.Enabled); + bool hover = (component.EventMask & MetaCoreInteraction_Hover) != 0; + bool click = (component.EventMask & MetaCoreInteraction_Click) != 0; + bool drag = (component.EventMask & MetaCoreInteraction_Drag) != 0; + if (ImGui::Checkbox("悬停事件", &hover)) { component.EventMask = hover ? component.EventMask | MetaCoreInteraction_Hover : component.EventMask & ~MetaCoreInteraction_Hover; changed = true; } + if (ImGui::Checkbox("点击事件", &click)) { component.EventMask = click ? component.EventMask | MetaCoreInteraction_Click : component.EventMask & ~MetaCoreInteraction_Click; changed = true; } + if (ImGui::Checkbox("拖拽事件", &drag)) { component.EventMask = drag ? component.EventMask | MetaCoreInteraction_Drag : component.EventMask & ~MetaCoreInteraction_Drag; changed = true; } + changed |= ImGui::DragFloat("拖拽阈值(像素)", &component.DragThresholdPixels, 0.25F, 0.0F, 100.0F); + if (changed) editorContext.GetScene().IncrementRevision(); +} + void MetaCoreDrawMeshRendererComponentInspector(MetaCoreEditorContext& editorContext, MetaCoreGameObject& gameObject) { if (!gameObject.HasComponent()) { return; @@ -6388,6 +6408,14 @@ bool MetaCoreBuiltinAssetDatabaseService::CreateProject(const std::filesystem::p return false; } + MetaCoreTypeRegistry registry; + MetaCoreRegisterRuntimeInputGeneratedTypes(registry); + const std::filesystem::path inputMapPath = *normalizedRoot / "Runtime" / "Input.mcruntime"; + if (!std::filesystem::exists(inputMapPath) && + !MetaCoreWriteInputMap(inputMapPath, MetaCoreBuildDefaultInputMap(), registry)) { + return false; + } + return OpenProject(*normalizedRoot); } @@ -9986,6 +10014,16 @@ public: }, MetaCoreDrawMeshRendererComponentInspector }); + Descriptors_.push_back(MetaCoreComponentDescriptor{ + "Interactable", "可交互对象", + [](const MetaCoreGameObject& object) { return object.HasComponent(); }, + [](MetaCoreGameObject& object) { if (object.HasComponent()) return false; object.AddComponent(MetaCoreInteractableComponent{}); return true; }, + [](MetaCoreGameObject& object) { if (!object.HasComponent()) return false; object.RemoveComponent(); return true; }, + [](MetaCoreGameObject& object) { if (!object.HasComponent()) return false; object.GetComponent() = {}; return true; }, + [](const MetaCoreGameObject& object, const MetaCoreTypeRegistry& registry) -> std::optional> { if (!object.HasComponent()) return std::nullopt; return MetaCoreSerializeComponentValue(object.GetComponent(), registry); }, + [](MetaCoreGameObject& object, std::span payload, const MetaCoreTypeRegistry& registry) { MetaCoreInteractableComponent value{}; if (!MetaCoreDeserializeComponentValue(payload, value, registry)) return false; if (object.HasComponent()) object.GetComponent() = value; else object.AddComponent(value); return true; }, + MetaCoreDrawInteractableComponentInspector + }); } void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { @@ -10341,8 +10379,60 @@ private: std::filesystem::path ActiveProjectRoot_{}; }; +class MetaCoreInputPlayModeComponentSystem final : public MetaCoreIPlayModeComponentSystem { +public: + [[nodiscard]] std::string GetSystemId() const override { return "MetaCore.PlayMode.Input"; } + void OnPlayStart(MetaCoreEditorContext& editorContext) override { + MetaCoreTypeRegistry registry; MetaCoreRegisterRuntimeDataGeneratedTypes(registry); MetaCoreRegisterRuntimeInputGeneratedTypes(registry); + const auto assets = editorContext.GetModuleRegistry().ResolveService(); + const auto root = assets && assets->HasProject() ? assets->GetProjectDescriptor().RootPath : std::filesystem::current_path(); + const auto runtimeProject = MetaCoreReadRuntimeProjectDocument(root / "Runtime" / "ProjectRuntime.mcruntimecfg", registry) + .value_or(MetaCoreRuntimeProjectDocument{}); + const auto path = root / (runtimeProject.InputMapPath.empty() ? std::filesystem::path("Runtime/Input.mcruntime") : std::filesystem::path(runtimeProject.InputMapPath)); + Runtime_.SetInputMap(MetaCoreReadInputMap(path, registry).value_or(MetaCoreInputMapDocument{})); + OverridePath_ = MetaCoreGetInputOverridePath(assets && assets->HasProject() ? assets->GetProjectDescriptor().Name : root.filename().string()); + (void)Runtime_.LoadOverrides(OverridePath_); + for (const auto& warning : Runtime_.GetWarnings()) editorContext.AddConsoleMessage(MetaCoreLogLevel::Warning, "Input", warning); + const auto manifestPath = root / (runtimeProject.UiManifestPath.empty() ? std::filesystem::path("Runtime/Ui.mcruntime") : std::filesystem::path(runtimeProject.UiManifestPath)); + if (const auto manifest = MetaCoreReadRuntimeUiManifest(manifestPath, registry); manifest.has_value() && + RuntimeUi_.Initialize(editorContext.GetWindow(), root, root / "Ui", *manifest)) { + RuntimeUi_.AttachToViewportRenderer(editorContext.GetViewportRenderer()); + RuntimeUiActive_ = true; + } + } + void Update(MetaCoreEditorContext& editorContext, double deltaSeconds) override { + const auto& viewport = editorContext.GetSceneViewportState(); + const bool focused = editorContext.GetWorkspacePage() == MetaCoreEditorWorkspacePage::Game && + editorContext.HasGameViewInputFocus() && editorContext.GetInput().IsWindowFocused(); + if (focused) { + const MetaCoreInputConsumption consumption = RuntimeUiActive_ ? RuntimeUi_.ProcessInput() : MetaCoreInputConsumption{}; + Runtime_.Update(editorContext.GetInput(), consumption); + Interaction_.Update(editorContext.GetScene(), editorContext.GetViewportRenderer(), editorContext.GetInput(), + MetaCoreViewportRect{viewport.Left, viewport.Top, viewport.Width, viewport.Height}, consumption.Mouse); + if (RuntimeUiActive_) RuntimeUi_.Update(static_cast(deltaSeconds)); + } else { + Runtime_.CancelAll(); + Interaction_.Cancel(); + } + } + void OnPlayStop(MetaCoreEditorContext& editorContext) override { + Runtime_.CancelAll(); Interaction_.Cancel(); if (!OverridePath_.empty()) (void)Runtime_.SaveOverrides(OverridePath_); RuntimeUi_.Shutdown(); RuntimeUiActive_ = false; + editorContext.SetGameViewInputFocus(false); + } + [[nodiscard]] MetaCoreRuntimeInput& GetRuntime() { return Runtime_; } + [[nodiscard]] MetaCoreRuntimeInteraction& GetInteraction() { return Interaction_; } + [[nodiscard]] MetaCoreRuntimeUiSystem& GetRuntimeUi() { return RuntimeUi_; } +private: + MetaCoreRuntimeInput Runtime_{}; + MetaCoreRuntimeInteraction Interaction_{}; + MetaCoreRuntimeUiSystem RuntimeUi_{}; + bool RuntimeUiActive_ = false; + std::filesystem::path OverridePath_{}; +}; + class MetaCoreScriptPlayModeComponentSystem final : public MetaCoreIPlayModeComponentSystem { public: + explicit MetaCoreScriptPlayModeComponentSystem(std::shared_ptr input) : Input_(std::move(input)) {} [[nodiscard]] std::string GetSystemId() const override { return "MetaCore.PlayMode.Script"; } void OnPlayStart(MetaCoreEditorContext& editorContext) override { @@ -10353,9 +10443,35 @@ public: [&editorContext](std::uint32_t level, std::string_view category, std::string_view message) { const MetaCoreLogLevel mapped = level >= 2U ? MetaCoreLogLevel::Error : level == 1U ? MetaCoreLogLevel::Warning : MetaCoreLogLevel::Info; editorContext.AddConsoleMessage(mapped, std::string(category), std::string(message)); - } + }, Input_ ? &Input_->GetRuntime() : nullptr ); Runtime_->Start(); + if (Input_) { + InputSubscription_ = Input_->GetRuntime().Subscribe([this](const MetaCoreInputActionEvent& event) { + if (!Runtime_) return; + MetaCoreScriptValueV1 phase{}; phase.Type = MetaCoreScriptAbiValue_Int; phase.IntValue = static_cast(event.Phase); + Runtime_->Publish("MetaCore.Input.Action/" + event.ActionId, {phase}); + }); + Input_->GetInteraction().SetEventCallback([this](const MetaCorePointerEvent& event) { + if (!Runtime_) return; + MetaCoreScriptValueV1 type{}; type.Type = MetaCoreScriptAbiValue_Int; type.IntValue = static_cast(event.Type); + MetaCoreScriptValueV1 button{}; button.Type = MetaCoreScriptAbiValue_Int; button.IntValue = event.Button; + MetaCoreScriptValueV1 screen{}; screen.Type = MetaCoreScriptAbiValue_Vec3; screen.Vec3[0] = event.ScreenPosition.x; screen.Vec3[1] = event.ScreenPosition.y; + MetaCoreScriptValueV1 delta{}; delta.Type = MetaCoreScriptAbiValue_Vec3; delta.Vec3[0] = event.ScreenDelta.x; delta.Vec3[1] = event.ScreenDelta.y; + MetaCoreScriptValueV1 world{}; world.Type = MetaCoreScriptAbiValue_Vec3; world.Vec3[0] = event.WorldPosition.x; world.Vec3[1] = event.WorldPosition.y; world.Vec3[2] = event.WorldPosition.z; + MetaCoreScriptValueV1 target{}; target.Type = MetaCoreScriptAbiValue_GameObjectRef; target.ObjectValue = {Runtime_->GetWorldGeneration(), event.TargetObjectId}; + Runtime_->PublishToObject(event.TargetObjectId, "MetaCore.Interaction.Pointer", {type, button, screen, delta, world, target}); + }); + Input_->GetRuntimeUi().SetEventCallback([this](const MetaCoreRuntimeUiEvent& event) { + if (!Runtime_) return; + const auto textValue = [](const std::string& text) { + MetaCoreScriptValueV1 value{}; value.Type = MetaCoreScriptAbiValue_String; + std::snprintf(value.Text, sizeof(value.Text), "%s", text.c_str()); return value; + }; + Runtime_->Publish("MetaCore.UI", {textValue(event.DocumentId), textValue(event.ElementId), + textValue(event.Action), textValue(event.Type), textValue(event.Value)}); + }); + } } void FixedUpdate(MetaCoreEditorContext&, double fixedDeltaSeconds) override { @@ -10371,12 +10487,20 @@ public: } void OnPlayStop(MetaCoreEditorContext&) override { + if (Input_) { + Input_->GetRuntime().Unsubscribe(InputSubscription_); + Input_->GetInteraction().SetEventCallback({}); + Input_->GetRuntimeUi().SetEventCallback({}); + InputSubscription_ = 0; + } if (Runtime_ != nullptr) Runtime_->Stop(); Runtime_.reset(); } private: + std::shared_ptr Input_{}; std::unique_ptr Runtime_{}; + std::uint64_t InputSubscription_ = 0; }; class MetaCoreRotatorPlayModeComponentSystem final : public MetaCoreIPlayModeComponentSystem { @@ -10497,9 +10621,11 @@ public: void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { (void)moduleRegistry; Systems_.clear(); + auto input = std::make_shared(); + RegisterSystem(input); RegisterSystem(std::make_shared()); RegisterSystem(std::make_shared()); - RegisterSystem(std::make_shared()); + RegisterSystem(std::make_shared(std::move(input))); } void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp index cb92b6e..daa6804 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp @@ -17,6 +17,7 @@ #include "MetaCoreFoundation/MetaCorePackage.h" #include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" #include "MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h" +#include "MetaCoreRuntimeInput/MetaCoreInputMap.h" #include "MetaCorePlatform/MetaCoreWindow.h" #include "MetaCoreFoundation/MetaCoreAssetRegistry.h" #include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" @@ -109,6 +110,7 @@ static std::unordered_map Model MetaCoreTypeRegistry registry; MetaCoreRegisterFoundationGeneratedTypes(registry); MetaCoreRegisterRuntimeDataGeneratedTypes(registry); + MetaCoreRegisterRuntimeInputGeneratedTypes(registry); return registry; } @@ -3421,6 +3423,61 @@ private: int AddRmlIndex_ = 0; }; +class MetaCoreInputMapPanelProvider final : public MetaCoreIEditorPanelProvider { +public: + std::string GetPanelId() const override { return "InputMap"; } + std::string GetPanelTitle() const override { return "输入映射"; } + bool IsOpenByDefault() const override { return false; } + + void DrawPanel(MetaCoreEditorContext& editorContext) override { + const auto assets = editorContext.GetModuleRegistry().ResolveService(); + if (!assets || !assets->HasProject()) { ImGui::TextDisabled("没有打开项目。"); return; } + const auto root = assets->GetProjectDescriptor().RootPath; + if (LoadedRoot_ != root) { MetaCoreTypeRegistry registry = MetaCoreBuildRuntimePanelTypeRegistry(); const auto runtimeProject = MetaCoreReadRuntimeProjectDocument(root / "Runtime" / "ProjectRuntime.mcruntimecfg", registry).value_or(MetaCoreRuntimeProjectDocument{}); MapPath_ = root / (runtimeProject.InputMapPath.empty() ? std::filesystem::path("Runtime/Input.mcruntime") : std::filesystem::path(runtimeProject.InputMapPath)); Map_ = MetaCoreReadInputMap(MapPath_, registry).value_or(MetaCoreBuildDefaultInputMap()); LoadedRoot_ = root; Dirty_ = false; } + const auto& path = MapPath_; + if (!ListeningBinding_.empty()) { + for (const auto& event : editorContext.GetInput().GetEvents()) if (event.Type == MetaCoreInputEventType::Control && std::abs(event.Value) > 0.5F) { + const auto it = std::find_if(Map_.Bindings.begin(), Map_.Bindings.end(), [&](const auto& b){ return b.BindingId == ListeningBinding_; }); + if (it != Map_.Bindings.end()) { it->ControlPath = event.ControlPath; Dirty_ = true; } + ListeningBinding_.clear(); break; + } + } + if (ImGui::Button("保存")) { + const auto issues = MetaCoreValidateInputMap(Map_); + const bool invalid = std::any_of(issues.begin(), issues.end(), [](const auto& value){ return value.Error; }); + if (!invalid) { MetaCoreTypeRegistry registry = MetaCoreBuildRuntimePanelTypeRegistry(); Dirty_ = !MetaCoreWriteInputMap(path, Map_, registry); } + for (const auto& issue : issues) editorContext.AddConsoleMessage(issue.Error ? MetaCoreLogLevel::Error : MetaCoreLogLevel::Warning, "InputMap", issue.Scope + ": " + issue.Message); + } + ImGui::SameLine(); if (ImGui::Button("恢复默认")) { Map_ = MetaCoreBuildDefaultInputMap(); Dirty_ = true; } + ImGui::SameLine(); ImGui::TextDisabled("%s", Dirty_ ? "未保存" : "已保存"); + auto editString = [&](const char* label, std::string& value) { std::array buffer{}; std::snprintf(buffer.data(), buffer.size(), "%s", value.c_str()); if (ImGui::InputText(label, buffer.data(), buffer.size())) { value = buffer.data(); Dirty_ = true; } }; + if (ImGui::CollapsingHeader("Actions", ImGuiTreeNodeFlags_DefaultOpen)) { + for (std::size_t i = 0; i < Map_.Actions.size();) { ImGui::PushID(static_cast(i)); editString("ID", Map_.Actions[i].Id); editString("显示名", Map_.Actions[i].DisplayName); ImGui::SameLine(); if (ImGui::SmallButton("删除")) { Map_.Actions.erase(Map_.Actions.begin() + static_cast(i)); Dirty_ = true; ImGui::PopID(); continue; } ImGui::PopID(); ++i; } + if (ImGui::Button("添加 Action")) { const auto id = "Action" + std::to_string(Map_.Actions.size() + 1); Map_.Actions.push_back({id, id}); Dirty_ = true; } + } + if (ImGui::CollapsingHeader("Axes", ImGuiTreeNodeFlags_DefaultOpen)) { + for (std::size_t i = 0; i < Map_.Axes.size();) { auto& axis = Map_.Axes[i]; ImGui::PushID(1000 + static_cast(i)); editString("ID", axis.Id); Dirty_ |= ImGui::DragFloat("死区", &axis.DeadZone, 0.01F, 0.0F, 0.99F); Dirty_ |= ImGui::DragFloat("灵敏度", &axis.Sensitivity, 0.05F, 0.0F, 10.0F); float range[2]{axis.Minimum, axis.Maximum}; if (ImGui::DragFloat2("范围", range, 0.05F, -100.0F, 100.0F)) { axis.Minimum = range[0]; axis.Maximum = range[1]; Dirty_ = true; } ImGui::SameLine(); if (ImGui::SmallButton("删除")) { Map_.Axes.erase(Map_.Axes.begin() + static_cast(i)); Dirty_ = true; ImGui::PopID(); continue; } ImGui::PopID(); ++i; } + if (ImGui::Button("添加 Axis")) { const auto id = "Axis" + std::to_string(Map_.Axes.size() + 1); Map_.Axes.push_back({id, id, 0.0F, 1.0F, -1.0F, 1.0F}); Dirty_ = true; } + } + if (ImGui::CollapsingHeader("Bindings", ImGuiTreeNodeFlags_DefaultOpen)) { + for (std::size_t i = 0; i < Map_.Bindings.size();) { auto& binding = Map_.Bindings[i]; ImGui::PushID(2000 + static_cast(i)); editString("Binding ID", binding.BindingId); int targetKind = static_cast(binding.Target); if (ImGui::Combo("目标类型", &targetKind, "Action\0Axis\0")) { binding.Target = static_cast(targetKind); Dirty_ = true; } editString("目标", binding.TargetId); editString("控制路径", binding.ControlPath); Dirty_ |= ImGui::DragFloat("倍率", &binding.Scale, 0.05F, -10.0F, 10.0F); std::string modifiers; for (const auto& modifier : binding.Modifiers) { if (!modifiers.empty()) modifiers += ","; modifiers += modifier; } std::array modifierBuffer{}; std::snprintf(modifierBuffer.data(), modifierBuffer.size(), "%s", modifiers.c_str()); if (ImGui::InputText("修饰键(逗号分隔)", modifierBuffer.data(), modifierBuffer.size())) { binding.Modifiers.clear(); std::stringstream stream(modifierBuffer.data()); for (std::string item; std::getline(stream, item, ',');) if (!item.empty()) binding.Modifiers.push_back(item); Dirty_ = true; } if (ImGui::Button(ListeningBinding_ == binding.BindingId ? "等待输入..." : "监听下一个控制")) ListeningBinding_ = binding.BindingId; ImGui::SameLine(); if (ImGui::SmallButton("删除")) { const std::string id = binding.BindingId; Map_.Bindings.erase(Map_.Bindings.begin() + static_cast(i)); for (auto& context : Map_.Contexts) std::erase(context.BindingIds, id); Dirty_ = true; ImGui::PopID(); continue; } ImGui::Separator(); ImGui::PopID(); ++i; } + if (ImGui::Button("添加 Binding")) { const auto id = "binding." + std::to_string(Map_.Bindings.size() + 1); const std::string target = Map_.Actions.empty() ? std::string{} : Map_.Actions.front().Id; Map_.Bindings.push_back({id, target, MetaCoreInputBindingTarget::Action, "/Space", 1.0F, {}}); if (!Map_.Contexts.empty()) Map_.Contexts.front().BindingIds.push_back(id); Dirty_ = true; } + } + if (ImGui::CollapsingHeader("Contexts", ImGuiTreeNodeFlags_DefaultOpen)) { + for (std::size_t i = 0; i < Map_.Contexts.size();) { auto& context = Map_.Contexts[i]; ImGui::PushID(3000 + static_cast(i)); editString("ID", context.Id); Dirty_ |= ImGui::InputInt("优先级", &context.Priority); Dirty_ |= ImGui::Checkbox("默认启用", &context.EnabledByDefault); Dirty_ |= ImGui::Checkbox("消费低优先级", &context.ConsumeLowerPriority); if (ImGui::TreeNode("Bindings")) { for (const auto& binding : Map_.Bindings) { bool included = std::find(context.BindingIds.begin(), context.BindingIds.end(), binding.BindingId) != context.BindingIds.end(); if (ImGui::Checkbox(binding.BindingId.c_str(), &included)) { if (included) context.BindingIds.push_back(binding.BindingId); else std::erase(context.BindingIds, binding.BindingId); Dirty_ = true; } } ImGui::TreePop(); } if (ImGui::SmallButton("删除 Context")) { Map_.Contexts.erase(Map_.Contexts.begin() + static_cast(i)); Dirty_ = true; ImGui::PopID(); continue; } ImGui::Separator(); ImGui::PopID(); ++i; } + if (ImGui::Button("添加 Context")) { const auto id = "Context" + std::to_string(Map_.Contexts.size() + 1); Map_.Contexts.push_back({id, 0, true, false, {}}); Dirty_ = true; } + } + for (std::size_t i = 0; i < Map_.Bindings.size(); ++i) for (std::size_t j = i + 1; j < Map_.Bindings.size(); ++j) if (Map_.Bindings[i].ControlPath == Map_.Bindings[j].ControlPath && Map_.Bindings[i].Modifiers == Map_.Bindings[j].Modifiers) { ImGui::TextColored(ImVec4(1.0F, 0.65F, 0.2F, 1.0F), "冲突: %s / %s", Map_.Bindings[i].BindingId.c_str(), Map_.Bindings[j].BindingId.c_str()); } + ImGui::Separator(); ImGui::Text("鼠标增量: %.1f, %.1f 手柄: %s", editorContext.GetInput().GetCursorDelta().x, editorContext.GetInput().GetCursorDelta().y, editorContext.GetInput().GetGamepadState().Connected ? "已连接" : "未连接"); + } +private: + std::filesystem::path LoadedRoot_{}; + std::filesystem::path MapPath_{}; + MetaCoreInputMapDocument Map_{}; + std::string ListeningBinding_{}; + bool Dirty_ = false; +}; + class MetaCoreRmlUiVisualEditorPanelProvider final : public MetaCoreIEditorPanelProvider { public: std::string GetPanelId() const override { return "RmlUiVisualEditor"; } @@ -9878,6 +9935,7 @@ public: moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); + moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterService(std::make_shared()); // The central Scene / Game viewport is not a dockable provider, but it keeps diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp index ffd60cc..9764a95 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp @@ -2764,8 +2764,16 @@ void MetaCoreEditorApp::DrawEditorFrame() { mousePosition.x <= viewportState.Left + viewportState.Width && mousePosition.y >= viewportState.Top && mousePosition.y <= viewportState.Top + viewportState.Height; - viewportState.Focused = viewportState.Hovered && !ImGui::GetIO().WantCaptureMouse; const bool isGameView = activeWorkspacePage == MetaCoreEditorWorkspacePage::Game; + if (!isGameView || !EditorContext_->GetInput().IsWindowFocused() || + EditorContext_->GetInput().WasControlPressed("/Escape")) { + EditorContext_->SetGameViewInputFocus(false); + } else if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + EditorContext_->SetGameViewInputFocus(viewportState.Hovered && !ImGui::GetIO().WantCaptureMouse); + } + viewportState.Focused = isGameView + ? EditorContext_->HasGameViewInputFocus() + : viewportState.Hovered && !ImGui::GetIO().WantCaptureMouse; const MetaCoreSceneView preInteractionSceneView = EditorContext_->GetCameraController().BuildSceneView(); const bool uiPreviewHoveredBeforeCamera = !isGameView && diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp index c5fd35f..51d88ac 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp @@ -8,6 +8,7 @@ #include "MetaCoreEditorCameraController.h" #include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" #include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" +#include "MetaCoreRuntimeInput/MetaCoreInputMap.h" #include "MetaCorePlatform/MetaCoreWindow.h" #include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" #include "MetaCoreRender/MetaCoreRenderDevice.h" @@ -249,6 +250,7 @@ private: MetaCoreTypeRegistry registry; MetaCoreRegisterFoundationGeneratedTypes(registry); MetaCoreRegisterRuntimeDataGeneratedTypes(registry); + MetaCoreRegisterRuntimeInputGeneratedTypes(registry); return registry; } @@ -259,6 +261,7 @@ private: document.BindingsPath = std::filesystem::path("Runtime") / "Bindings.mcruntime"; document.DiagnosticsPath = std::filesystem::path("Runtime") / "Diagnostics.mcruntimestate"; document.UiManifestPath = std::filesystem::path("Runtime") / "Ui.mcruntime"; + document.InputMapPath = std::filesystem::path("Runtime") / "Input.mcruntime"; return document; } @@ -296,6 +299,8 @@ MetaCoreEditorModuleRegistry& MetaCoreEditorContext::GetModuleRegistry() { retur const MetaCoreEditorModuleRegistry& MetaCoreEditorContext::GetModuleRegistry() const { return ModuleRegistry_; } MetaCoreSceneViewportState& MetaCoreEditorContext::GetSceneViewportState() { return SceneViewportState_; } const MetaCoreSceneViewportState& MetaCoreEditorContext::GetSceneViewportState() const { return SceneViewportState_; } +bool MetaCoreEditorContext::HasGameViewInputFocus() const { return GameViewInputFocused_; } +void MetaCoreEditorContext::SetGameViewInputFocus(bool focused) { GameViewInputFocused_ = focused; } MetaCoreEditorCameraController& MetaCoreEditorContext::GetCameraController() { return *CameraController_; } const MetaCoreEditorCameraController& MetaCoreEditorContext::GetCameraController() const { return *CameraController_; } @@ -694,8 +699,10 @@ bool MetaCoreEditorContext::SaveRuntimeDataConfig() { RuntimeUiManifestDocument_, runtimeRegistry ); + const std::filesystem::path inputMapPath = runtimeDirectory / "Input.mcruntime"; + const bool wroteInputMap = std::filesystem::exists(inputMapPath) || MetaCoreWriteInputMap(inputMapPath, MetaCoreBuildDefaultInputMap(), runtimeRegistry); - if (!wroteProjectRuntime || !wroteSources || !wroteBindings || !wroteUiManifest) { + if (!wroteProjectRuntime || !wroteSources || !wroteBindings || !wroteUiManifest || !wroteInputMap) { AddConsoleMessage(MetaCoreLogLevel::Error, "RuntimeData", "保存 Runtime 配置失败"); return false; } diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h index 0d1e4fe..42d52c0 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h @@ -195,6 +195,8 @@ public: [[nodiscard]] bool HasDockLayoutBuilt() const; [[nodiscard]] MetaCoreEditorWorkspacePage GetWorkspacePage() const; void SetWorkspacePage(MetaCoreEditorWorkspacePage page); + [[nodiscard]] bool HasGameViewInputFocus() const; + void SetGameViewInputFocus(bool focused); private: void NormalizeSelection(); @@ -228,6 +230,7 @@ private: MetaCoreRuntimeUiManifest RuntimeUiManifestDocument_{}; MetaCoreSelectedAssetState SelectedAsset_{}; std::filesystem::path SelectedProjectDirectory_{"Assets"}; + bool GameViewInputFocused_ = false; std::string SelectedAssetSubId_{}; MetaCoreEditorWorkspacePage WorkspacePage_ = MetaCoreEditorWorkspacePage::Scene; }; diff --git a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreGeneratedReflection.h b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreGeneratedReflection.h index 00b9e0e..3d252af 100644 --- a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreGeneratedReflection.h +++ b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreGeneratedReflection.h @@ -8,5 +8,6 @@ void MetaCoreRegisterFoundationGeneratedTypes(MetaCoreTypeRegistry& registry); void MetaCoreRegisterSceneGeneratedTypes(MetaCoreTypeRegistry& registry); void MetaCoreRegisterEditorGeneratedTypes(MetaCoreTypeRegistry& registry); void MetaCoreRegisterRuntimeDataGeneratedTypes(MetaCoreTypeRegistry& registry); +void MetaCoreRegisterRuntimeInputGeneratedTypes(MetaCoreTypeRegistry& registry); } // namespace MetaCore diff --git a/Source/MetaCorePlatform/Private/MetaCoreInput.cpp b/Source/MetaCorePlatform/Private/MetaCoreInput.cpp index 0940f39..d000e56 100644 --- a/Source/MetaCorePlatform/Private/MetaCoreInput.cpp +++ b/Source/MetaCorePlatform/Private/MetaCoreInput.cpp @@ -1,5 +1,7 @@ #include "MetaCorePlatform/MetaCoreInput.h" +#include + namespace MetaCore { void MetaCoreInput::BeginFrame() { @@ -10,6 +12,7 @@ void MetaCoreInput::BeginFrame() { MouseWheelDelta_ = 0.0F; KeyEvents_.clear(); TextInput_.clear(); + Events_.clear(); } void MetaCoreInput::SetKeyState(MetaCoreInputKey key, bool isDown) { @@ -35,8 +38,24 @@ void MetaCoreInput::AddKeyEvent(MetaCoreKeyEvent event) { void MetaCoreInput::AddTextInput(char32_t codePoint) { TextInput_.push_back(codePoint); + Events_.push_back({MetaCoreInputEventType::Text, {}, 0.0F, codePoint}); } +void MetaCoreInput::SetControlValue(std::string controlPath, float value) { + const auto iterator = ControlValues_.find(controlPath); + const float previous = iterator == ControlValues_.end() ? 0.0F : iterator->second; + ControlValues_[controlPath] = value; + if (previous != value) Events_.push_back({MetaCoreInputEventType::Control, std::move(controlPath), value, 0}); +} + +void MetaCoreInput::SetWindowFocused(bool focused) { + if (WindowFocused_ == focused) return; + WindowFocused_ = focused; + Events_.push_back({MetaCoreInputEventType::Focus, {}, focused ? 1.0F : 0.0F, 0}); +} + +void MetaCoreInput::SetGamepadState(const MetaCoreGamepadState& state) { Gamepad_ = state; } + bool MetaCoreInput::IsKeyDown(MetaCoreInputKey key) const { return CurrentKeyStates_[static_cast(key)]; } @@ -84,4 +103,24 @@ std::vector MetaCoreInput::ConsumeTextInput() { return text; } +const std::vector& MetaCoreInput::GetKeyEvents() const { return KeyEvents_; } +const std::vector& MetaCoreInput::GetTextInput() const { return TextInput_; } +const std::vector& MetaCoreInput::GetEvents() const { return Events_; } +float MetaCoreInput::GetControlValue(std::string_view controlPath) const { + const auto iterator = ControlValues_.find(std::string(controlPath)); + return iterator == ControlValues_.end() ? 0.0F : iterator->second; +} +bool MetaCoreInput::WasControlPressed(std::string_view controlPath) const { + return std::any_of(Events_.begin(), Events_.end(), [controlPath](const MetaCoreInputEvent& event) { + return event.Type == MetaCoreInputEventType::Control && event.ControlPath == controlPath && event.Value > 0.5F; + }); +} +bool MetaCoreInput::WasControlReleased(std::string_view controlPath) const { + return std::any_of(Events_.begin(), Events_.end(), [controlPath](const MetaCoreInputEvent& event) { + return event.Type == MetaCoreInputEventType::Control && event.ControlPath == controlPath && event.Value <= 0.5F; + }); +} +bool MetaCoreInput::IsWindowFocused() const { return WindowFocused_; } +const MetaCoreGamepadState& MetaCoreInput::GetGamepadState() const { return Gamepad_; } + } // namespace MetaCore diff --git a/Source/MetaCorePlatform/Private/MetaCoreWindow.cpp b/Source/MetaCorePlatform/Private/MetaCoreWindow.cpp index 1bae96c..cadfc55 100644 --- a/Source/MetaCorePlatform/Private/MetaCoreWindow.cpp +++ b/Source/MetaCorePlatform/Private/MetaCoreWindow.cpp @@ -5,6 +5,7 @@ #endif #include #include +#include #include #include @@ -18,6 +19,7 @@ #include #include #include +#include namespace MetaCore { @@ -158,6 +160,30 @@ int MetaCoreMapVirtualKeyToRuntimeKey(WPARAM virtualKey) { } } +std::string MetaCoreWin32KeyControlPath(WPARAM key) { + if (key >= 'A' && key <= 'Z') return "/" + std::string(1, static_cast(key)); + if (key >= '0' && key <= '9') return "/" + std::string(1, static_cast(key)); + if (key >= VK_F1 && key <= VK_F24) return "/F" + std::to_string(key - VK_F1 + 1); + if (key >= VK_NUMPAD0 && key <= VK_NUMPAD9) return "/Numpad" + std::to_string(key - VK_NUMPAD0); + switch (key) { + case VK_SPACE: return "/Space"; case VK_ESCAPE: return "/Escape"; + case VK_RETURN: return "/Enter"; case VK_TAB: return "/Tab"; + case VK_LEFT: return "/Left"; case VK_RIGHT: return "/Right"; + case VK_UP: return "/Up"; case VK_DOWN: return "/Down"; + case VK_LSHIFT: return "/LeftShift"; case VK_RSHIFT: return "/RightShift"; + case VK_LCONTROL: return "/LeftControl"; case VK_RCONTROL: return "/RightControl"; + case VK_LMENU: return "/LeftAlt"; case VK_RMENU: return "/RightAlt"; + case VK_BACK: return "/Backspace"; case VK_DELETE: return "/Delete"; + case VK_INSERT: return "/Insert"; case VK_HOME: return "/Home"; case VK_END: return "/End"; + case VK_PRIOR: return "/PageUp"; case VK_NEXT: return "/PageDown"; case VK_CAPITAL: return "/CapsLock"; + case VK_OEM_MINUS: return "/Minus"; case VK_OEM_PLUS: return "/Equal"; + case VK_OEM_4: return "/LeftBracket"; case VK_OEM_6: return "/RightBracket"; case VK_OEM_5: return "/Backslash"; + case VK_OEM_1: return "/Semicolon"; case VK_OEM_7: return "/Quote"; + case VK_OEM_COMMA: return "/Comma"; case VK_OEM_PERIOD: return "/Period"; case VK_OEM_2: return "/Slash"; case VK_OEM_3: return "/Backquote"; + default: return {}; + } +} + std::unordered_map& MetaCoreGetWindowMap() { static std::unordered_map windowMap; return windowMap; @@ -213,6 +239,8 @@ LRESULT CALLBACK MetaCoreWindowSubclassProc(HWND hwnd, UINT msg, WPARAM wparam, msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN, MetaCoreGetWin32InputModifiers() }); + const std::string path = MetaCoreWin32KeyControlPath(wparam); + if (!path.empty()) owner.Input.SetControlValue(path, (msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN) ? 1.0F : 0.0F); } } else if (msg == WM_CHAR) { if (wparam >= 0x20 && wparam != 0x7F) { @@ -390,6 +418,28 @@ void MetaCoreWindow::BeginFrame() { if (GetCursorPos(&cursorPosition) != FALSE && ScreenToClient(Impl_->NativeHandle, &cursorPosition) != FALSE) { Impl_->Input.SetCursorPosition(glm::vec2(static_cast(cursorPosition.x), static_cast(cursorPosition.y))); } + Impl_->Input.SetControlValue("/delta/x", Impl_->Input.GetCursorDelta().x); + Impl_->Input.SetControlValue("/delta/y", Impl_->Input.GetCursorDelta().y); + Impl_->Input.SetControlValue("/wheel", Impl_->Input.GetMouseWheelDelta()); + Impl_->Input.SetControlValue("/leftButton", Impl_->Input.IsMouseButtonDown(MetaCoreMouseButton::Left) ? 1.0F : 0.0F); + Impl_->Input.SetControlValue("/rightButton", Impl_->Input.IsMouseButtonDown(MetaCoreMouseButton::Right) ? 1.0F : 0.0F); + Impl_->Input.SetControlValue("/middleButton", Impl_->Input.IsMouseButtonDown(MetaCoreMouseButton::Middle) ? 1.0F : 0.0F); + Impl_->Input.SetWindowFocused(GetForegroundWindow() == Impl_->NativeHandle); + + XINPUT_STATE xinput{}; + MetaCoreGamepadState gamepad; + gamepad.Connected = XInputGetState(0, &xinput) == ERROR_SUCCESS; + if (gamepad.Connected) { + const auto normalizeStick = [](SHORT value) { return value < 0 ? static_cast(value) / 32768.0F : static_cast(value) / 32767.0F; }; + gamepad.Axes = {normalizeStick(xinput.Gamepad.sThumbLX), normalizeStick(xinput.Gamepad.sThumbLY), normalizeStick(xinput.Gamepad.sThumbRX), normalizeStick(xinput.Gamepad.sThumbRY), static_cast(xinput.Gamepad.bLeftTrigger) / 255.0F, static_cast(xinput.Gamepad.bRightTrigger) / 255.0F}; + static constexpr std::array masks{XINPUT_GAMEPAD_A, XINPUT_GAMEPAD_B, XINPUT_GAMEPAD_X, XINPUT_GAMEPAD_Y, XINPUT_GAMEPAD_LEFT_SHOULDER, XINPUT_GAMEPAD_RIGHT_SHOULDER, XINPUT_GAMEPAD_BACK, XINPUT_GAMEPAD_START, 0, XINPUT_GAMEPAD_LEFT_THUMB, XINPUT_GAMEPAD_RIGHT_THUMB, XINPUT_GAMEPAD_DPAD_UP, XINPUT_GAMEPAD_DPAD_RIGHT, XINPUT_GAMEPAD_DPAD_DOWN, XINPUT_GAMEPAD_DPAD_LEFT}; + for (std::size_t i = 0; i < masks.size(); ++i) gamepad.Buttons[i] = masks[i] != 0 && (xinput.Gamepad.wButtons & masks[i]) != 0; + } + Impl_->Input.SetGamepadState(gamepad); + static constexpr std::array axisPaths{"leftStick/x", "leftStick/y", "rightStick/x", "rightStick/y", "leftTrigger", "rightTrigger"}; + for (std::size_t i = 0; i < axisPaths.size(); ++i) Impl_->Input.SetControlValue(std::string("/") + axisPaths[i], gamepad.Connected ? gamepad.Axes[i] : 0.0F); + static constexpr std::array buttonPaths{"buttonSouth", "buttonEast", "buttonWest", "buttonNorth", "leftBumper", "rightBumper", "Back", "Start", "Guide", "leftStickPress", "rightStickPress", "dpadUp", "dpadRight", "dpadDown", "dpadLeft"}; + for (std::size_t i = 0; i < buttonPaths.size(); ++i) Impl_->Input.SetControlValue(std::string("/") + buttonPaths[i], gamepad.Connected && gamepad.Buttons[i] ? 1.0F : 0.0F); } void MetaCoreWindow::EndFrame() { diff --git a/Source/MetaCorePlatform/Private/MetaCoreWindowGlfw.cpp b/Source/MetaCorePlatform/Private/MetaCoreWindowGlfw.cpp index 467207a..c2b2164 100644 --- a/Source/MetaCorePlatform/Private/MetaCoreWindowGlfw.cpp +++ b/Source/MetaCorePlatform/Private/MetaCoreWindowGlfw.cpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace MetaCore { @@ -30,6 +31,49 @@ bool MetaCoreIsGlfwMouseButtonDown(GLFWwindow* window, int button) { return glfwGetMouseButton(window, button) == GLFW_PRESS; } +std::string MetaCoreGlfwKeyControlPath(int key) { + if (key >= GLFW_KEY_A && key <= GLFW_KEY_Z) return "/" + std::string(1, static_cast('A' + key - GLFW_KEY_A)); + if (key >= GLFW_KEY_0 && key <= GLFW_KEY_9) return "/" + std::string(1, static_cast('0' + key - GLFW_KEY_0)); + if (key >= GLFW_KEY_F1 && key <= GLFW_KEY_F25) return "/F" + std::to_string(key - GLFW_KEY_F1 + 1); + if (key >= GLFW_KEY_KP_0 && key <= GLFW_KEY_KP_9) return "/Numpad" + std::to_string(key - GLFW_KEY_KP_0); + switch (key) { + case GLFW_KEY_SPACE: return "/Space"; + case GLFW_KEY_ESCAPE: return "/Escape"; + case GLFW_KEY_ENTER: return "/Enter"; + case GLFW_KEY_TAB: return "/Tab"; + case GLFW_KEY_LEFT: return "/Left"; + case GLFW_KEY_RIGHT: return "/Right"; + case GLFW_KEY_UP: return "/Up"; + case GLFW_KEY_DOWN: return "/Down"; + case GLFW_KEY_LEFT_SHIFT: return "/LeftShift"; + case GLFW_KEY_RIGHT_SHIFT: return "/RightShift"; + case GLFW_KEY_LEFT_CONTROL: return "/LeftControl"; + case GLFW_KEY_RIGHT_CONTROL: return "/RightControl"; + case GLFW_KEY_LEFT_ALT: return "/LeftAlt"; + case GLFW_KEY_RIGHT_ALT: return "/RightAlt"; + case GLFW_KEY_BACKSPACE: return "/Backspace"; + case GLFW_KEY_DELETE: return "/Delete"; + case GLFW_KEY_INSERT: return "/Insert"; + case GLFW_KEY_HOME: return "/Home"; + case GLFW_KEY_END: return "/End"; + case GLFW_KEY_PAGE_UP: return "/PageUp"; + case GLFW_KEY_PAGE_DOWN: return "/PageDown"; + case GLFW_KEY_CAPS_LOCK: return "/CapsLock"; + case GLFW_KEY_MINUS: return "/Minus"; + case GLFW_KEY_EQUAL: return "/Equal"; + case GLFW_KEY_LEFT_BRACKET: return "/LeftBracket"; + case GLFW_KEY_RIGHT_BRACKET: return "/RightBracket"; + case GLFW_KEY_BACKSLASH: return "/Backslash"; + case GLFW_KEY_SEMICOLON: return "/Semicolon"; + case GLFW_KEY_APOSTROPHE: return "/Quote"; + case GLFW_KEY_COMMA: return "/Comma"; + case GLFW_KEY_PERIOD: return "/Period"; + case GLFW_KEY_SLASH: return "/Slash"; + case GLFW_KEY_GRAVE_ACCENT: return "/Backquote"; + default: return {}; + } +} + class MetaCoreGlfwContext { public: static bool Retain() { @@ -101,6 +145,8 @@ void MetaCoreGlfwKeyCallback(GLFWwindow* window, int key, int, int action, int m if (auto* owner = MetaCoreGetWindowImpl(window); owner != nullptr) { if (action == GLFW_PRESS || action == GLFW_REPEAT || action == GLFW_RELEASE) { owner->Input.AddKeyEvent({key, action != GLFW_RELEASE, modifiers}); + const std::string path = MetaCoreGlfwKeyControlPath(key); + if (!path.empty()) owner->Input.SetControlValue(path, action == GLFW_RELEASE ? 0.0F : 1.0F); } } } @@ -234,6 +280,27 @@ void MetaCoreWindow::BeginFrame() { double cursorY = 0.0; glfwGetCursorPos(window, &cursorX, &cursorY); Impl_->Input.SetCursorPosition(glm::vec2(static_cast(cursorX), static_cast(cursorY))); + Impl_->Input.SetControlValue("/delta/x", Impl_->Input.GetCursorDelta().x); + Impl_->Input.SetControlValue("/delta/y", Impl_->Input.GetCursorDelta().y); + Impl_->Input.SetControlValue("/wheel", Impl_->Input.GetMouseWheelDelta()); + Impl_->Input.SetControlValue("/leftButton", Impl_->Input.IsMouseButtonDown(MetaCoreMouseButton::Left) ? 1.0F : 0.0F); + Impl_->Input.SetControlValue("/rightButton", Impl_->Input.IsMouseButtonDown(MetaCoreMouseButton::Right) ? 1.0F : 0.0F); + Impl_->Input.SetControlValue("/middleButton", Impl_->Input.IsMouseButtonDown(MetaCoreMouseButton::Middle) ? 1.0F : 0.0F); + Impl_->Input.SetWindowFocused(glfwGetWindowAttrib(window, GLFW_FOCUSED) == GLFW_TRUE); + + MetaCoreGamepadState gamepad; + GLFWgamepadstate glfwGamepad{}; + gamepad.Connected = glfwJoystickIsGamepad(GLFW_JOYSTICK_1) == GLFW_TRUE && + glfwGetGamepadState(GLFW_JOYSTICK_1, &glfwGamepad) == GLFW_TRUE; + if (gamepad.Connected) { + for (std::size_t i = 0; i < gamepad.Axes.size(); ++i) gamepad.Axes[i] = glfwGamepad.axes[i]; + for (std::size_t i = 0; i < gamepad.Buttons.size(); ++i) gamepad.Buttons[i] = glfwGamepad.buttons[i] == GLFW_PRESS; + } + Impl_->Input.SetGamepadState(gamepad); + static constexpr std::array axisPaths{"leftStick/x", "leftStick/y", "rightStick/x", "rightStick/y", "leftTrigger", "rightTrigger"}; + for (std::size_t i = 0; i < axisPaths.size(); ++i) Impl_->Input.SetControlValue(std::string("/") + axisPaths[i], gamepad.Connected ? gamepad.Axes[i] : 0.0F); + static constexpr std::array buttonPaths{"buttonSouth", "buttonEast", "buttonWest", "buttonNorth", "leftBumper", "rightBumper", "Back", "Start", "Guide", "leftStickPress", "rightStickPress", "dpadUp", "dpadRight", "dpadDown", "dpadLeft"}; + for (std::size_t i = 0; i < buttonPaths.size(); ++i) Impl_->Input.SetControlValue(std::string("/") + buttonPaths[i], gamepad.Connected && gamepad.Buttons[i] ? 1.0F : 0.0F); Impl_->CloseRequested = Impl_->CloseRequested || glfwWindowShouldClose(window) == GLFW_TRUE; } diff --git a/Source/MetaCorePlatform/Public/MetaCorePlatform/MetaCoreInput.h b/Source/MetaCorePlatform/Public/MetaCorePlatform/MetaCoreInput.h index ac9e6db..0310e5e 100644 --- a/Source/MetaCorePlatform/Public/MetaCorePlatform/MetaCoreInput.h +++ b/Source/MetaCorePlatform/Public/MetaCorePlatform/MetaCoreInput.h @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include namespace MetaCore { @@ -38,6 +40,25 @@ struct MetaCoreKeyEvent { int Modifiers = 0; }; +enum class MetaCoreInputEventType : std::uint8_t { + Control, + Text, + Focus +}; + +struct MetaCoreInputEvent { + MetaCoreInputEventType Type = MetaCoreInputEventType::Control; + std::string ControlPath{}; + float Value = 0.0F; + char32_t CodePoint = 0; +}; + +struct MetaCoreGamepadState { + bool Connected = false; + std::array Axes{}; + std::array Buttons{}; +}; + class MetaCoreInput { public: void BeginFrame(); @@ -47,6 +68,9 @@ public: void AddMouseWheelDelta(float delta); void AddKeyEvent(MetaCoreKeyEvent event); void AddTextInput(char32_t codePoint); + void SetControlValue(std::string controlPath, float value); + void SetWindowFocused(bool focused); + void SetGamepadState(const MetaCoreGamepadState& state); [[nodiscard]] bool IsKeyDown(MetaCoreInputKey key) const; [[nodiscard]] bool WasKeyPressed(MetaCoreInputKey key) const; @@ -58,6 +82,14 @@ public: [[nodiscard]] const glm::vec2& GetCursorDelta() const; [[nodiscard]] std::vector ConsumeKeyEvents(); [[nodiscard]] std::vector ConsumeTextInput(); + [[nodiscard]] const std::vector& GetKeyEvents() const; + [[nodiscard]] const std::vector& GetTextInput() const; + [[nodiscard]] const std::vector& GetEvents() const; + [[nodiscard]] float GetControlValue(std::string_view controlPath) const; + [[nodiscard]] bool WasControlPressed(std::string_view controlPath) const; + [[nodiscard]] bool WasControlReleased(std::string_view controlPath) const; + [[nodiscard]] bool IsWindowFocused() const; + [[nodiscard]] const MetaCoreGamepadState& GetGamepadState() const; private: static constexpr std::size_t KeyCount_ = static_cast(MetaCoreInputKey::Count); @@ -73,6 +105,10 @@ private: float MouseWheelDelta_ = 0.0F; std::vector KeyEvents_{}; std::vector TextInput_{}; + std::vector Events_{}; + std::unordered_map ControlValues_{}; + bool WindowFocused_ = true; + MetaCoreGamepadState Gamepad_{}; }; } // namespace MetaCore diff --git a/Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp b/Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp index db618e6..76cc4ff 100644 --- a/Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp @@ -122,6 +122,10 @@ void MetaCoreEditorViewportRenderer::RenderAll() { FilamentSceneBridge_.RenderAll(); } +bool MetaCoreEditorViewportRenderer::RequestPick(std::uint32_t x, std::uint32_t y, MetaCoreFilamentSceneBridge::PickCallback callback) { + return FilamentSceneBridge_.RequestPick(x, y, std::move(callback)); +} + void MetaCoreEditorViewportRenderer::SetProjectRootPath(const std::filesystem::path& projectRootPath) { FilamentSceneBridge_.SetProjectRootPath(projectRootPath); diff --git a/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp b/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp index 4d6feaa..454dc7c 100644 --- a/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include "MetaCoreScene/MetaCoreTransformUtils.h" #define GLM_ENABLE_EXPERIMENTAL @@ -621,7 +622,10 @@ public: GLTextureId_ = 0; } // 销毁 View + Engine_->flushAndWait(); + PickCallbacks_.clear(); Engine_->destroy(View_); + View_ = nullptr; if (EnvironmentView_) { Engine_->destroy(EnvironmentView_); EnvironmentView_ = nullptr; @@ -1898,9 +1902,11 @@ public: if (mappedAncestor) { shouldBeInScene = isObjectVisible(entityToObjectId[mappedAncestor]); + entityToObjectId[entity] = entityToObjectId[mappedAncestor]; } else { // 回退保护:如果没有找到,则与该资产对应的顶级根宿主保持状态一致 shouldBeInScene = isObjectVisible(hostRootId); + entityToObjectId[entity] = hostRootId; } } @@ -1918,6 +1924,7 @@ public: } } } + EntityToObjectId_ = std::move(entityToObjectId); // 3. 控制 ObjectToFilamentEntity_ 中所有非资产实体(如 pivotEntity 辅助实体)的可见性 for (const auto& [objectId, assetEntity] : ObjectToFilamentEntity_) { @@ -2429,6 +2436,43 @@ public: } } + bool RequestPick(std::uint32_t x, std::uint32_t y, MetaCoreFilamentSceneBridge::PickCallback callback) { + if (View_ == nullptr || Camera_ == nullptr || !callback) return false; + const auto viewport = View_->getViewport(); + if (x >= viewport.width || y >= viewport.height) return false; + View_->setTransparentPickingEnabled(true); + const std::uint64_t callbackId = NextPickCallbackId_++; + PickCallbacks_.emplace(callbackId, PendingPick{ + std::move(callback), Camera_->getProjectionMatrix(), Camera_->getModelMatrix(), viewport.width, viewport.height + }); + View_->pick(x, y, [this, callbackId](const filament::View::PickingQueryResult& result) { + const auto callbackIterator = PickCallbacks_.find(callbackId); + if (callbackIterator == PickCallbacks_.end()) return; + auto pending = std::move(callbackIterator->second); + PickCallbacks_.erase(callbackIterator); + MetaCoreFilamentSceneBridge::PickResult converted; + const auto iterator = EntityToObjectId_.find(result.renderable); + if (iterator != EntityToObjectId_.end()) converted.ObjectId = iterator->second; + converted.Depth = result.depth; + converted.FragmentCoordinates = {result.fragCoords.x, result.fragCoords.y, result.fragCoords.z}; + const filament::math::double4 clip{ + (static_cast(result.fragCoords.x) / static_cast(pending.ViewportWidth)) * 2.0 - 1.0, + (static_cast(result.fragCoords.y) / static_cast(pending.ViewportHeight)) * 2.0 - 1.0, + static_cast(result.fragCoords.z) * 2.0 - 1.0, + 1.0 + }; + filament::math::double4 viewPosition = inverse(pending.Projection) * clip; + if (std::abs(viewPosition.w) > std::numeric_limits::epsilon()) viewPosition /= viewPosition.w; + filament::math::double4 worldPosition = pending.Model * viewPosition; + if (std::abs(worldPosition.w) > std::numeric_limits::epsilon()) worldPosition /= worldPosition.w; + converted.WorldPosition = { + static_cast(worldPosition.x), static_cast(worldPosition.y), static_cast(worldPosition.z) + }; + pending.Callback(converted); + }); + return true; + } + uint32_t GetGLTextureId() const { return GLTextureId_; } @@ -3524,6 +3568,16 @@ private: std::unordered_map LastMaterialSyncDebugByObject_{}; std::unordered_map ObjectWorldMatrices_{}; std::unordered_map> ObjectToFilamentEntity_; + std::unordered_map EntityToObjectId_; + struct PendingPick { + MetaCoreFilamentSceneBridge::PickCallback Callback{}; + filament::math::mat4 Projection{}; + filament::math::mat4 Model{}; + std::uint32_t ViewportWidth = 1; + std::uint32_t ViewportHeight = 1; + }; + std::unordered_map PickCallbacks_; + std::uint64_t NextPickCallbackId_ = 1; std::unordered_map LoadedAssets_; std::unordered_map PendingResourceLoads_; std::unordered_map HostRootSourceModelGuids_; @@ -3609,6 +3663,10 @@ void MetaCoreFilamentSceneBridge::RenderAll() { Impl_->RenderAll(); } +bool MetaCoreFilamentSceneBridge::RequestPick(std::uint32_t x, std::uint32_t y, PickCallback callback) { + return Impl_->RequestPick(x, y, std::move(callback)); +} + uint32_t MetaCoreFilamentSceneBridge::GetGLTextureId() const { return Impl_->GetGLTextureId(); } diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreEditorViewportRenderer.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreEditorViewportRenderer.h index b3b88ce..2a86263 100644 --- a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreEditorViewportRenderer.h +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreEditorViewportRenderer.h @@ -22,6 +22,7 @@ public: void RenderSceneToViewport(MetaCoreScene& scene, const MetaCoreSceneView& sceneView, bool useScenePrimaryCamera = false); void RenderSnapshotToViewport(const MetaCoreSceneRenderSyncSnapshot& snapshot, MetaCoreScene* scene, const MetaCoreSceneView& sceneView); void RenderAll(); + [[nodiscard]] bool RequestPick(std::uint32_t x, std::uint32_t y, MetaCoreFilamentSceneBridge::PickCallback callback); void SetProjectRootPath(const std::filesystem::path& projectRootPath); void SetEditorGridVisible(bool visible); void SetRuntimeOverlayRenderCallback(MetaCoreFilamentSceneBridge::RuntimeOverlayRenderCallback callback); diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h index 6382b19..b7c1074 100644 --- a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h @@ -3,6 +3,7 @@ #include "MetaCoreFoundation/MetaCoreId.h" #include +#include #include #include @@ -27,6 +28,13 @@ struct MetaCoreSceneRenderSyncSnapshot; */ class MetaCoreFilamentSceneBridge { public: + struct PickResult { + MetaCoreId ObjectId = 0; + float Depth = 0.0F; + glm::vec3 FragmentCoordinates{0.0F}; + glm::vec3 WorldPosition{0.0F}; + }; + using PickCallback = std::function; using RuntimeOverlayRenderCallback = std::function +#include +#include +#include + +namespace MetaCore { + +std::vector MetaCoreValidateInputMap(const MetaCoreInputMapDocument& document) { + std::vector issues; + std::unordered_set actionIds, axisIds, bindingIds, contextIds; + for (const auto& value : document.Actions) { + if (value.Id.empty() || !actionIds.insert(value.Id).second) issues.push_back({true, value.Id, "Action ID 为空或重复"}); + } + for (const auto& value : document.Axes) { + if (value.Id.empty() || !axisIds.insert(value.Id).second) issues.push_back({true, value.Id, "Axis ID 为空或重复"}); + if (value.DeadZone < 0.0F || value.DeadZone >= 1.0F || value.Sensitivity < 0.0F || value.Minimum >= value.Maximum) + issues.push_back({true, value.Id, "Axis 死区、灵敏度或取值范围无效"}); + } + for (const auto& value : document.Bindings) { + if (value.BindingId.empty() || !bindingIds.insert(value.BindingId).second) issues.push_back({true, value.BindingId, "Binding ID 为空或重复"}); + if (value.ControlPath.empty() || value.ControlPath.front() != '<' || value.ControlPath.find("/", 1) == std::string::npos) issues.push_back({true, value.BindingId, "控制路径无效"}); + const bool targetExists = value.Target == MetaCoreInputBindingTarget::Action ? actionIds.contains(value.TargetId) : axisIds.contains(value.TargetId); + if (!targetExists) issues.push_back({true, value.BindingId, "Binding 目标不存在"}); + } + for (const auto& value : document.Contexts) { + if (value.Id.empty() || !contextIds.insert(value.Id).second) issues.push_back({true, value.Id, "Context ID 为空或重复"}); + for (const auto& binding : value.BindingIds) if (!bindingIds.contains(binding)) issues.push_back({true, value.Id, "Context 引用了不存在的 Binding: " + binding}); + } + return issues; +} + +bool MetaCoreWriteInputMap(const std::filesystem::path& path, const MetaCoreInputMapDocument& document, const MetaCoreTypeRegistry& registry) { + const auto bytes = MetaCoreSerializeToBytes(document, registry); + if (!bytes) return false; + std::error_code error; + std::filesystem::create_directories(path.parent_path(), error); + std::ofstream output(path, std::ios::binary | std::ios::trunc); + if (!output) return false; + output.write(reinterpret_cast(bytes->data()), static_cast(bytes->size())); + return output.good(); +} + +std::optional MetaCoreReadInputMap(const std::filesystem::path& path, const MetaCoreTypeRegistry& registry) { + std::ifstream input(path, std::ios::binary | std::ios::ate); + if (!input) return std::nullopt; + const auto size = static_cast(input.tellg()); input.seekg(0); + std::vector bytes(size); + if (size && !input.read(reinterpret_cast(bytes.data()), static_cast(size))) return std::nullopt; + MetaCoreInputMapDocument result; + if (!MetaCoreDeserializeFromBytes(std::span(bytes.data(), bytes.size()), result, registry)) return std::nullopt; + return result; +} + +MetaCoreInputMapDocument MetaCoreBuildDefaultInputMap() { + MetaCoreInputMapDocument map; + map.Actions = {{"Interact", "Interact"}, {"Cancel", "Cancel"}}; + map.Axes = {{"MoveX", "Move X", 0.15F, 1.0F, -1.0F, 1.0F}, {"MoveY", "Move Y", 0.15F, 1.0F, -1.0F, 1.0F}, {"LookX", "Look X", 0.0F, 1.0F, -1.0F, 1.0F}, {"LookY", "Look Y", 0.0F, 1.0F, -1.0F, 1.0F}}; + map.Bindings = { + {"game.interact.mouse", "Interact", MetaCoreInputBindingTarget::Action, "/leftButton", 1.0F, {}}, + {"game.cancel.escape", "Cancel", MetaCoreInputBindingTarget::Action, "/Escape", 1.0F, {}}, + {"game.move.left", "MoveX", MetaCoreInputBindingTarget::Axis, "/A", -1.0F, {}}, + {"game.move.right", "MoveX", MetaCoreInputBindingTarget::Axis, "/D", 1.0F, {}}, + {"game.move.back", "MoveY", MetaCoreInputBindingTarget::Axis, "/S", -1.0F, {}}, + {"game.move.forward", "MoveY", MetaCoreInputBindingTarget::Axis, "/W", 1.0F, {}}, + {"game.look.x", "LookX", MetaCoreInputBindingTarget::Axis, "/delta/x", 1.0F, {}}, + {"game.look.y", "LookY", MetaCoreInputBindingTarget::Axis, "/delta/y", 1.0F, {}}, + {"game.pad.move.x", "MoveX", MetaCoreInputBindingTarget::Axis, "/leftStick/x", 1.0F, {}}, + {"game.pad.move.y", "MoveY", MetaCoreInputBindingTarget::Axis, "/leftStick/y", 1.0F, {}} + }; + MetaCoreInputContextDefinition gameplay{"Gameplay", 0, true, false, {}}; + for (const auto& binding : map.Bindings) gameplay.BindingIds.push_back(binding.BindingId); + map.Contexts.push_back(std::move(gameplay)); + return map; +} + +} // namespace MetaCore diff --git a/Source/MetaCoreRuntimeInput/Private/MetaCoreRuntimeInput.cpp b/Source/MetaCoreRuntimeInput/Private/MetaCoreRuntimeInput.cpp new file mode 100644 index 0000000..6f5ba82 --- /dev/null +++ b/Source/MetaCoreRuntimeInput/Private/MetaCoreRuntimeInput.cpp @@ -0,0 +1,124 @@ +#include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h" + +#include "MetaCorePlatform/MetaCoreInput.h" +#include + +#include +#include +#include +#include +#include +#include + +namespace MetaCore { + +class MetaCoreRuntimeInput::Impl { +public: + MetaCoreInputMapDocument Map{}; + std::unordered_map Actions{}; + std::unordered_map Axes{}; + std::unordered_map Contexts{}; + std::vector ContextStack{}; + std::unordered_map Overrides{}; + std::unordered_map Subscribers{}; + std::vector Warnings{}; + std::uint64_t NextSubscription = 1; + + void Emit(std::string id, MetaCoreInputActionPhase phase) const { + MetaCoreInputActionEvent event{std::move(id), phase}; + for (const auto& [_, callback] : Subscribers) if (callback) callback(event); + } +}; + +MetaCoreRuntimeInput::MetaCoreRuntimeInput() : Impl_(std::make_unique()) {} +MetaCoreRuntimeInput::~MetaCoreRuntimeInput() = default; +void MetaCoreRuntimeInput::SetInputMap(MetaCoreInputMapDocument document) { + Impl_->Map = std::move(document); Impl_->Actions.clear(); Impl_->Axes.clear(); Impl_->Contexts.clear(); + for (const auto& action : Impl_->Map.Actions) Impl_->Actions[action.Id] = {}; + for (const auto& axis : Impl_->Map.Axes) Impl_->Axes[axis.Id] = 0.0F; + for (const auto& context : Impl_->Map.Contexts) Impl_->Contexts[context.Id] = context.EnabledByDefault; +} +const MetaCoreInputMapDocument& MetaCoreRuntimeInput::GetInputMap() const { return Impl_->Map; } + +void MetaCoreRuntimeInput::Update(const MetaCoreInput& input, const MetaCoreInputConsumption& consumed) { + if (!input.IsWindowFocused()) { CancelAll(); return; } + std::unordered_map nextActions; + std::unordered_map nextAxes; + std::unordered_set consumedPaths; + std::vector contexts; + for (const auto& context : Impl_->Map.Contexts) if (Impl_->Contexts[context.Id]) contexts.push_back(&context); + std::stable_sort(contexts.begin(), contexts.end(), [](auto* a, auto* b) { return a->Priority > b->Priority; }); + const auto bindingById = [&](std::string_view id) -> const MetaCoreInputBindingDefinition* { + const auto it = std::find_if(Impl_->Map.Bindings.begin(), Impl_->Map.Bindings.end(), [&](const auto& value) { return value.BindingId == id; }); + return it == Impl_->Map.Bindings.end() ? nullptr : &*it; + }; + for (const auto* context : contexts) { + for (const auto& bindingId : context->BindingIds) { + const auto* binding = bindingById(bindingId); if (!binding) continue; + const auto overrideIt = Impl_->Overrides.find(binding->BindingId); + const std::string& path = overrideIt == Impl_->Overrides.end() ? binding->ControlPath : overrideIt->second; + if (consumedPaths.contains(path)) continue; + if ((consumed.Keyboard && path.starts_with("")) || (consumed.Mouse && path.starts_with("")) || (consumed.Gamepad && path.starts_with(""))) continue; + bool modifiersHeld = true; + for (const auto& modifier : binding->Modifiers) modifiersHeld = modifiersHeld && std::abs(input.GetControlValue(modifier)) > 0.5F; + const float value = modifiersHeld ? input.GetControlValue(path) * binding->Scale : 0.0F; + if (binding->Target == MetaCoreInputBindingTarget::Action) nextActions[binding->TargetId] = nextActions[binding->TargetId] || std::abs(value) > 0.5F; + else nextAxes[binding->TargetId] += value; + if (context->ConsumeLowerPriority && std::abs(value) > 0.0001F) consumedPaths.insert(path); + } + } + for (const auto& definition : Impl_->Map.Actions) { + auto& state = Impl_->Actions[definition.Id]; const bool wasHeld = state.Held; const bool held = nextActions[definition.Id]; + state = {!wasHeld && held, held, wasHeld && !held}; + if (state.Pressed) Impl_->Emit(definition.Id, MetaCoreInputActionPhase::Started); + if (held) Impl_->Emit(definition.Id, MetaCoreInputActionPhase::Performed); + if (state.Released) Impl_->Emit(definition.Id, MetaCoreInputActionPhase::Canceled); + } + for (const auto& definition : Impl_->Map.Axes) { + float value = std::clamp(nextAxes[definition.Id], -1.0F, 1.0F); + if (std::abs(value) < definition.DeadZone) value = 0.0F; + Impl_->Axes[definition.Id] = std::clamp(value * definition.Sensitivity, definition.Minimum, definition.Maximum); + } +} + +void MetaCoreRuntimeInput::CancelAll() { + for (auto& [id, state] : Impl_->Actions) { if (state.Held) Impl_->Emit(id, MetaCoreInputActionPhase::Canceled); state = {false, false, state.Held}; } + for (auto& [_, value] : Impl_->Axes) value = 0.0F; +} +MetaCoreInputActionState MetaCoreRuntimeInput::GetAction(std::string_view id) const { const auto it = Impl_->Actions.find(std::string(id)); return it == Impl_->Actions.end() ? MetaCoreInputActionState{} : it->second; } +float MetaCoreRuntimeInput::GetAxis(std::string_view id) const { const auto it = Impl_->Axes.find(std::string(id)); return it == Impl_->Axes.end() ? 0.0F : it->second; } +bool MetaCoreRuntimeInput::SetContextEnabled(std::string_view id, bool enabled) { auto it = Impl_->Contexts.find(std::string(id)); if (it == Impl_->Contexts.end()) return false; it->second = enabled; return true; } +bool MetaCoreRuntimeInput::PushContext(std::string_view id) { if (!SetContextEnabled(id, true)) return false; Impl_->ContextStack.emplace_back(id); return true; } +bool MetaCoreRuntimeInput::PopContext(std::string_view id) { auto it = std::find(Impl_->ContextStack.rbegin(), Impl_->ContextStack.rend(), id); if (it == Impl_->ContextStack.rend()) return false; Impl_->ContextStack.erase(std::next(it).base()); return SetContextEnabled(id, false); } +std::uint64_t MetaCoreRuntimeInput::Subscribe(ActionCallback callback) { const auto id = Impl_->NextSubscription++; Impl_->Subscribers.emplace(id, std::move(callback)); return id; } +void MetaCoreRuntimeInput::Unsubscribe(std::uint64_t id) { Impl_->Subscribers.erase(id); } +bool MetaCoreRuntimeInput::Rebind(std::string_view id, std::string path) { if (path.empty() || path.front() != '<') return false; const auto it = std::find_if(Impl_->Map.Bindings.begin(), Impl_->Map.Bindings.end(), [&](const auto& b){ return b.BindingId == id; }); if (it == Impl_->Map.Bindings.end()) return false; Impl_->Overrides[std::string(id)] = std::move(path); return true; } +bool MetaCoreRuntimeInput::ResetBinding(std::string_view id) { return Impl_->Overrides.erase(std::string(id)) > 0; } +void MetaCoreRuntimeInput::ResetAllBindings() { Impl_->Overrides.clear(); } +bool MetaCoreRuntimeInput::LoadOverrides(const std::filesystem::path& path) { + Impl_->Warnings.clear(); std::ifstream input(path); if (!input) return true; + try { nlohmann::json json; input >> json; for (auto it = json.begin(); it != json.end(); ++it) if (!Rebind(it.key(), it.value().get())) Impl_->Warnings.push_back("忽略无效输入覆盖: " + it.key()); } + catch (const std::exception& error) { Impl_->Warnings.push_back(std::string("输入覆盖损坏,已使用默认绑定: ") + error.what()); Impl_->Overrides.clear(); return false; } + return true; +} +bool MetaCoreRuntimeInput::SaveOverrides(const std::filesystem::path& path) const { + std::error_code error; std::filesystem::create_directories(path.parent_path(), error); const auto temporary = path.string() + ".tmp"; + nlohmann::json json = Impl_->Overrides; { std::ofstream output(temporary, std::ios::trunc); if (!output) return false; output << json.dump(2); if (!output.good()) return false; } + std::filesystem::rename(temporary, path, error); if (!error) return true; + std::filesystem::remove(path, error); error.clear(); std::filesystem::rename(temporary, path, error); return !error; +} +const std::vector& MetaCoreRuntimeInput::GetWarnings() const { return Impl_->Warnings; } + +std::filesystem::path MetaCoreGetInputOverridePath(std::string_view projectName) { + std::string safe(projectName); for (char& value : safe) if (!std::isalnum(static_cast(value)) && value != '-' && value != '_') value = '_'; +#if defined(_WIN32) + const char* root = std::getenv("LOCALAPPDATA"); + return std::filesystem::path(root ? root : ".") / "MetaCore" / safe / "InputBindings.json"; +#else + const char* xdg = std::getenv("XDG_DATA_HOME"); const char* home = std::getenv("HOME"); + const std::filesystem::path root = xdg ? xdg : (home ? std::filesystem::path(home) / ".local" / "share" : std::filesystem::path(".")); + return root / "MetaCore" / safe / "InputBindings.json"; +#endif +} + +} // namespace MetaCore diff --git a/Source/MetaCoreRuntimeInput/Private/MetaCoreRuntimeInteraction.cpp b/Source/MetaCoreRuntimeInput/Private/MetaCoreRuntimeInteraction.cpp new file mode 100644 index 0000000..ec6470c --- /dev/null +++ b/Source/MetaCoreRuntimeInput/Private/MetaCoreRuntimeInteraction.cpp @@ -0,0 +1,140 @@ +#include "MetaCoreRuntimeInput/MetaCoreRuntimeInteraction.h" + +#include "MetaCorePlatform/MetaCoreInput.h" +#include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" +#include "MetaCoreRender/MetaCoreRenderTypes.h" +#include "MetaCoreScene/MetaCoreScene.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace MetaCore { + +class MetaCoreRuntimeInteraction::Impl { +public: + enum class QueryKind { Hover, Down, Up }; + struct Completed { std::uint64_t Sequence; QueryKind Kind; glm::vec2 Position; MetaCoreId Object; glm::vec3 World; std::uint64_t SceneRevision; std::uint64_t InteractionRevision; }; + struct AsyncState { + std::mutex Mutex{}; + std::vector CompletedQueries{}; + bool Alive = true; + }; + EventCallback Callback{}; + std::shared_ptr Async = std::make_shared(); + std::uint64_t NextSequence = 1; + std::uint64_t LatestHoverSequence = 0; + std::uint64_t InteractionRevision = 1; + std::deque PointerSequenceOrder{}; + std::unordered_map PointerResults{}; + MetaCoreId Hovered = 0; + MetaCoreId Captured = 0; + glm::vec2 PressPosition{0.0F}; + glm::vec2 PreviousPosition{0.0F}; + bool Dragging = false; + + void Emit(MetaCorePointerEventType type, MetaCoreId target, glm::vec2 position, glm::vec3 world = {}, std::uint32_t button = 0) { + if (Callback && target != 0) Callback({type, target, button, position, position - PreviousPosition, world}); + } + bool IsInteractive(MetaCoreScene& scene, MetaCoreId id, std::uint32_t mask) const { + auto object = scene.FindGameObject(id); + return object && object.HasComponent() && object.GetComponent().Enabled && + object.HasComponent() && object.GetComponent().Visible && + (object.GetComponent().EventMask & mask) != 0; + } +}; + +MetaCoreRuntimeInteraction::MetaCoreRuntimeInteraction() : Impl_(std::make_unique()) {} +MetaCoreRuntimeInteraction::~MetaCoreRuntimeInteraction() { + std::scoped_lock lock(Impl_->Async->Mutex); + Impl_->Async->Alive = false; + Impl_->Async->CompletedQueries.clear(); +} +void MetaCoreRuntimeInteraction::SetEventCallback(EventCallback callback) { Impl_->Callback = std::move(callback); } +void MetaCoreRuntimeInteraction::Cancel() { + if (Impl_->Captured) Impl_->Emit(MetaCorePointerEventType::Cancel, Impl_->Captured, Impl_->PreviousPosition); + if (Impl_->Hovered) Impl_->Emit(MetaCorePointerEventType::Leave, Impl_->Hovered, Impl_->PreviousPosition); + Impl_->Captured = Impl_->Hovered = 0; Impl_->Dragging = false; + ++Impl_->InteractionRevision; +} + +void MetaCoreRuntimeInteraction::Update(MetaCoreScene& scene, MetaCoreEditorViewportRenderer& renderer, const MetaCoreInput& input, const MetaCoreViewportRect& viewport, bool mouseConsumed) { + Update(scene, [&renderer](std::uint32_t x, std::uint32_t y, PickCallback callback) { + return renderer.RequestPick(x, y, [callback = std::move(callback)](const MetaCoreFilamentSceneBridge::PickResult& result) { + callback({result.ObjectId, result.WorldPosition}); + }); + }, input, viewport, mouseConsumed); +} + +void MetaCoreRuntimeInteraction::Update(MetaCoreScene& scene, const PickRequest& requestPick, const MetaCoreInput& input, const MetaCoreViewportRect& viewport, bool mouseConsumed) { + std::vector completed; + { std::scoped_lock lock(Impl_->Async->Mutex); completed.swap(Impl_->Async->CompletedQueries); } + std::sort(completed.begin(), completed.end(), [](const auto& a, const auto& b){ return a.Sequence < b.Sequence; }); + const auto processResult = [&](const Impl::Completed& result) { + if (result.SceneRevision != scene.GetRevision() || result.InteractionRevision != Impl_->InteractionRevision) return; + if (result.Kind == Impl::QueryKind::Hover) { + if (result.Sequence != Impl_->LatestHoverSequence) return; + const MetaCoreId next = Impl_->IsInteractive(scene, result.Object, MetaCoreInteraction_Hover) ? result.Object : 0; + if (next != Impl_->Hovered) { if (Impl_->Hovered) Impl_->Emit(MetaCorePointerEventType::Leave, Impl_->Hovered, result.Position); Impl_->Hovered = next; if (next) Impl_->Emit(MetaCorePointerEventType::Enter, next, result.Position, result.World); } + else if (next) Impl_->Emit(MetaCorePointerEventType::Move, next, result.Position, result.World); + } else if (result.Kind == Impl::QueryKind::Down) { + if (Impl_->IsInteractive(scene, result.Object, MetaCoreInteraction_Click | MetaCoreInteraction_Drag)) { + Impl_->Captured = result.Object; Impl_->PressPosition = result.Position; Impl_->Dragging = false; + Impl_->Emit(MetaCorePointerEventType::Down, result.Object, result.Position, result.World); + } + } else if (result.Kind == Impl::QueryKind::Up && Impl_->Captured) { + const MetaCoreId target = Impl_->Captured; + Impl_->Emit(MetaCorePointerEventType::Up, target, result.Position, result.World); + if (Impl_->Dragging) Impl_->Emit(MetaCorePointerEventType::DragEnd, target, result.Position, result.World); + else if (result.Object == target && Impl_->IsInteractive(scene, target, MetaCoreInteraction_Click)) Impl_->Emit(MetaCorePointerEventType::Click, target, result.Position, result.World); + Impl_->Captured = 0; Impl_->Dragging = false; + } + }; + for (const auto& result : completed) { + if (result.Kind == Impl::QueryKind::Hover) processResult(result); + else Impl_->PointerResults.insert_or_assign(result.Sequence, result); + } + while (!Impl_->PointerSequenceOrder.empty()) { + const auto iterator = Impl_->PointerResults.find(Impl_->PointerSequenceOrder.front()); + if (iterator == Impl_->PointerResults.end()) break; + processResult(iterator->second); + Impl_->PointerResults.erase(iterator); + Impl_->PointerSequenceOrder.pop_front(); + } + const glm::vec2 cursor = input.GetCursorPosition(); + if (Impl_->Captured && !Impl_->IsInteractive(scene, Impl_->Captured, MetaCoreInteraction_Click | MetaCoreInteraction_Drag)) Cancel(); + if (!input.IsWindowFocused() || mouseConsumed || cursor.x < viewport.Left || cursor.y < viewport.Top || cursor.x >= viewport.Left + viewport.Width || cursor.y >= viewport.Top + viewport.Height) { Cancel(); Impl_->PreviousPosition = cursor; return; } + if (Impl_->Captured && input.IsMouseButtonDown(MetaCoreMouseButton::Left)) { + auto object = scene.FindGameObject(Impl_->Captured); + const float threshold = object && object.HasComponent() ? object.GetComponent().DragThresholdPixels : 5.0F; + if (!Impl_->Dragging && glm::length(cursor - Impl_->PressPosition) >= threshold && Impl_->IsInteractive(scene, Impl_->Captured, MetaCoreInteraction_Drag)) { Impl_->Dragging = true; Impl_->Emit(MetaCorePointerEventType::DragStart, Impl_->Captured, cursor); } + if (Impl_->Dragging) Impl_->Emit(MetaCorePointerEventType::Drag, Impl_->Captured, cursor); + } + const auto submit = [&](Impl::QueryKind kind) { + const std::uint64_t sequence = Impl_->NextSequence++; const std::uint64_t revision = scene.GetRevision(); + if (kind == Impl::QueryKind::Hover) Impl_->LatestHoverSequence = sequence; + else Impl_->PointerSequenceOrder.push_back(sequence); + const std::uint64_t interactionRevision = Impl_->InteractionRevision; + const auto localX = static_cast(std::clamp(cursor.x - viewport.Left, 0.0F, viewport.Width - 1.0F)); + const auto localY = static_cast(std::clamp(viewport.Height - 1.0F - (cursor.y - viewport.Top), 0.0F, viewport.Height - 1.0F)); + const std::weak_ptr async = Impl_->Async; + const bool submitted = requestPick(localX, localY, [async, sequence, kind, cursor, revision, interactionRevision](const MetaCoreInteractionPickResult& result) { + const auto state = async.lock(); + if (!state) return; + std::scoped_lock lock(state->Mutex); + if (state->Alive) state->CompletedQueries.push_back({sequence, kind, cursor, result.ObjectId, result.WorldPosition, revision, interactionRevision}); + }); + if (!submitted && kind != Impl::QueryKind::Hover && !Impl_->PointerSequenceOrder.empty()) Impl_->PointerSequenceOrder.pop_back(); + }; + submit(Impl::QueryKind::Hover); + if (input.WasMouseButtonPressed(MetaCoreMouseButton::Left)) submit(Impl::QueryKind::Down); + if (input.WasMouseButtonReleased(MetaCoreMouseButton::Left)) submit(Impl::QueryKind::Up); + Impl_->PreviousPosition = cursor; +} + +} // namespace MetaCore diff --git a/Source/MetaCoreRuntimeInput/Public/MetaCoreRuntimeInput/MetaCoreInputMap.h b/Source/MetaCoreRuntimeInput/Public/MetaCoreRuntimeInput/MetaCoreInputMap.h new file mode 100644 index 0000000..4779ecf --- /dev/null +++ b/Source/MetaCoreRuntimeInput/Public/MetaCoreRuntimeInput/MetaCoreInputMap.h @@ -0,0 +1,99 @@ +#pragma once + +#include "MetaCoreFoundation/MetaCoreReflection.h" + +#include +#include +#include +#include +#include + +namespace MetaCore { + +enum class MetaCoreInputBindingTarget : std::uint32_t { Action = 0, Axis = 1 }; + +MC_STRUCT() +struct MetaCoreInputActionDefinition { + MC_GENERATED_BODY() + MC_PROPERTY() + std::string Id{}; + MC_PROPERTY() + std::string DisplayName{}; +}; + +MC_STRUCT() +struct MetaCoreInputAxisDefinition { + MC_GENERATED_BODY() + MC_PROPERTY() + std::string Id{}; + MC_PROPERTY() + std::string DisplayName{}; + MC_PROPERTY() + float DeadZone = 0.0F; + MC_PROPERTY() + float Sensitivity = 1.0F; + MC_PROPERTY() + float Minimum = -1.0F; + MC_PROPERTY() + float Maximum = 1.0F; +}; + +MC_STRUCT() +struct MetaCoreInputBindingDefinition { + MC_GENERATED_BODY() + MC_PROPERTY() + std::string BindingId{}; + MC_PROPERTY() + std::string TargetId{}; + MC_PROPERTY() + MetaCoreInputBindingTarget Target = MetaCoreInputBindingTarget::Action; + MC_PROPERTY() + std::string ControlPath{}; + MC_PROPERTY() + float Scale = 1.0F; + MC_PROPERTY() + std::vector Modifiers{}; +}; + +MC_STRUCT() +struct MetaCoreInputContextDefinition { + MC_GENERATED_BODY() + MC_PROPERTY() + std::string Id{}; + MC_PROPERTY() + std::int32_t Priority = 0; + MC_PROPERTY() + bool EnabledByDefault = true; + MC_PROPERTY() + bool ConsumeLowerPriority = false; + MC_PROPERTY() + std::vector BindingIds{}; +}; + +MC_STRUCT() +struct MetaCoreInputMapDocument { + MC_GENERATED_BODY() + MC_PROPERTY() + std::uint32_t Version = 1; + MC_PROPERTY() + std::vector Actions{}; + MC_PROPERTY() + std::vector Axes{}; + MC_PROPERTY() + std::vector Bindings{}; + MC_PROPERTY() + std::vector Contexts{}; +}; + +struct MetaCoreInputMapIssue { + bool Error = false; + std::string Scope{}; + std::string Message{}; +}; + +[[nodiscard]] std::vector MetaCoreValidateInputMap(const MetaCoreInputMapDocument& document); +[[nodiscard]] bool MetaCoreWriteInputMap(const std::filesystem::path& path, const MetaCoreInputMapDocument& document, const MetaCoreTypeRegistry& registry); +[[nodiscard]] std::optional MetaCoreReadInputMap(const std::filesystem::path& path, const MetaCoreTypeRegistry& registry); +[[nodiscard]] MetaCoreInputMapDocument MetaCoreBuildDefaultInputMap(); + +} // namespace MetaCore diff --git a/Source/MetaCoreRuntimeInput/Public/MetaCoreRuntimeInput/MetaCoreRuntimeInput.h b/Source/MetaCoreRuntimeInput/Public/MetaCoreRuntimeInput/MetaCoreRuntimeInput.h new file mode 100644 index 0000000..0529909 --- /dev/null +++ b/Source/MetaCoreRuntimeInput/Public/MetaCoreRuntimeInput/MetaCoreRuntimeInput.h @@ -0,0 +1,67 @@ +#pragma once + +#include "MetaCoreRuntimeInput/MetaCoreInputMap.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace MetaCore { + +class MetaCoreInput; + +enum class MetaCoreInputActionPhase : std::uint8_t { Started, Performed, Canceled }; + +struct MetaCoreInputActionState { + bool Pressed = false; + bool Held = false; + bool Released = false; +}; + +struct MetaCoreInputActionEvent { + std::string ActionId{}; + MetaCoreInputActionPhase Phase = MetaCoreInputActionPhase::Performed; +}; + +struct MetaCoreInputConsumption { + bool Keyboard = false; + bool Mouse = false; + bool Gamepad = false; +}; + +class MetaCoreRuntimeInput { +public: + using ActionCallback = std::function; + + MetaCoreRuntimeInput(); + ~MetaCoreRuntimeInput(); + void SetInputMap(MetaCoreInputMapDocument document); + [[nodiscard]] const MetaCoreInputMapDocument& GetInputMap() const; + void Update(const MetaCoreInput& input, const MetaCoreInputConsumption& consumed = {}); + void CancelAll(); + [[nodiscard]] MetaCoreInputActionState GetAction(std::string_view actionId) const; + [[nodiscard]] float GetAxis(std::string_view axisId) const; + [[nodiscard]] bool SetContextEnabled(std::string_view contextId, bool enabled); + [[nodiscard]] bool PushContext(std::string_view contextId); + [[nodiscard]] bool PopContext(std::string_view contextId); + [[nodiscard]] std::uint64_t Subscribe(ActionCallback callback); + void Unsubscribe(std::uint64_t subscriptionId); + [[nodiscard]] bool Rebind(std::string_view bindingId, std::string controlPath); + [[nodiscard]] bool ResetBinding(std::string_view bindingId); + void ResetAllBindings(); + [[nodiscard]] bool LoadOverrides(const std::filesystem::path& path); + [[nodiscard]] bool SaveOverrides(const std::filesystem::path& path) const; + [[nodiscard]] const std::vector& GetWarnings() const; + +private: + class Impl; + std::unique_ptr Impl_; +}; + +[[nodiscard]] std::filesystem::path MetaCoreGetInputOverridePath(std::string_view projectName); + +} // namespace MetaCore diff --git a/Source/MetaCoreRuntimeInput/Public/MetaCoreRuntimeInput/MetaCoreRuntimeInteraction.h b/Source/MetaCoreRuntimeInput/Public/MetaCoreRuntimeInput/MetaCoreRuntimeInteraction.h new file mode 100644 index 0000000..6c92272 --- /dev/null +++ b/Source/MetaCoreRuntimeInput/Public/MetaCoreRuntimeInput/MetaCoreRuntimeInteraction.h @@ -0,0 +1,50 @@ +#pragma once + +#include "MetaCoreFoundation/MetaCoreId.h" +#include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h" + +#include +#include +#include +#include +#include +#include + +namespace MetaCore { + +class MetaCoreInput; +class MetaCoreScene; +class MetaCoreEditorViewportRenderer; +struct MetaCoreViewportRect; + +enum class MetaCorePointerEventType : std::uint8_t { Enter, Move, Leave, Down, Up, Click, DragStart, Drag, DragEnd, Cancel }; +struct MetaCorePointerEvent { + MetaCorePointerEventType Type = MetaCorePointerEventType::Move; + MetaCoreId TargetObjectId = 0; + std::uint32_t Button = 0; + glm::vec2 ScreenPosition{0.0F}; + glm::vec2 ScreenDelta{0.0F}; + glm::vec3 WorldPosition{0.0F}; +}; +struct MetaCoreInteractionPickResult { + MetaCoreId ObjectId = 0; + glm::vec3 WorldPosition{0.0F}; +}; + +class MetaCoreRuntimeInteraction { +public: + using EventCallback = std::function; + using PickCallback = std::function; + using PickRequest = std::function; + MetaCoreRuntimeInteraction(); + ~MetaCoreRuntimeInteraction(); + void SetEventCallback(EventCallback callback); + void Update(MetaCoreScene& scene, MetaCoreEditorViewportRenderer& renderer, const MetaCoreInput& input, const MetaCoreViewportRect& viewport, bool mouseConsumed); + void Update(MetaCoreScene& scene, const PickRequest& requestPick, const MetaCoreInput& input, const MetaCoreViewportRect& viewport, bool mouseConsumed); + void Cancel(); +private: + class Impl; + std::unique_ptr Impl_; +}; + +} // namespace MetaCore diff --git a/Source/MetaCoreRuntimeUi/Private/MetaCoreRuntimeUiSystem.cpp b/Source/MetaCoreRuntimeUi/Private/MetaCoreRuntimeUiSystem.cpp index a3fc60b..1df6e2c 100644 --- a/Source/MetaCoreRuntimeUi/Private/MetaCoreRuntimeUiSystem.cpp +++ b/Source/MetaCoreRuntimeUi/Private/MetaCoreRuntimeUiSystem.cpp @@ -667,34 +667,33 @@ void MetaCoreRuntimeUiSystem::Update(float) { if (!Impl_->Initialized_ || Impl_->Context_ == nullptr || Impl_->Window_ == nullptr) return; const auto [width, height] = Impl_->Window_->GetFramebufferSize(); Impl_->Context_->SetDimensions({width, height}); - MetaCoreInput& input = Impl_->Window_->GetInput(); - if (Impl_->HasInputEnabledDocument()) { - const glm::vec2 cursor = input.GetCursorPosition(); - Impl_->Context_->ProcessMouseMove(static_cast(cursor.x), static_cast(cursor.y), 0); - for (int button = 0; button < 3; ++button) { - const auto mouseButton = static_cast(button); - if (input.WasMouseButtonPressed(mouseButton)) Impl_->Context_->ProcessMouseButtonDown(button, 0); - if (input.WasMouseButtonReleased(mouseButton)) Impl_->Context_->ProcessMouseButtonUp(button, 0); - } - if (std::abs(input.GetMouseWheelDelta()) > 0.001F) Impl_->Context_->ProcessMouseWheel(input.GetMouseWheelDelta(), 0); - for (const MetaCoreKeyEvent& event : input.ConsumeKeyEvents()) { - const Rml::Input::KeyIdentifier key = MetaCoreMapGlfwKeyToRml(event.Key); - const int modifiers = MetaCoreMapGlfwModifiersToRml(event.Modifiers); - if (event.Pressed) Impl_->Context_->ProcessKeyDown(key, modifiers); - else Impl_->Context_->ProcessKeyUp(key, modifiers); - } - for (const char32_t codePoint : input.ConsumeTextInput()) { - Impl_->Context_->ProcessTextInput(static_cast(codePoint)); - } - } else { - (void)input.ConsumeKeyEvents(); - (void)input.ConsumeTextInput(); - } Impl_->RenderInterface_.BeginFrame(); Impl_->Context_->Update(); Impl_->Context_->Render(); } +MetaCoreInputConsumption MetaCoreRuntimeUiSystem::ProcessInput() { + MetaCoreInputConsumption consumed; + if (!Impl_->Initialized_ || Impl_->Context_ == nullptr || Impl_->Window_ == nullptr || !Impl_->HasInputEnabledDocument()) return consumed; + MetaCoreInput& input = Impl_->Window_->GetInput(); + const glm::vec2 cursor = input.GetCursorPosition(); + consumed.Mouse = !Impl_->Context_->ProcessMouseMove(static_cast(cursor.x), static_cast(cursor.y), 0); + for (int button = 0; button < 3; ++button) { + const auto mouseButton = static_cast(button); + if (input.WasMouseButtonPressed(mouseButton)) consumed.Mouse = !Impl_->Context_->ProcessMouseButtonDown(button, 0) || consumed.Mouse; + if (input.WasMouseButtonReleased(mouseButton)) consumed.Mouse = !Impl_->Context_->ProcessMouseButtonUp(button, 0) || consumed.Mouse; + } + if (std::abs(input.GetMouseWheelDelta()) > 0.001F) consumed.Mouse = !Impl_->Context_->ProcessMouseWheel(input.GetMouseWheelDelta(), 0) || consumed.Mouse; + for (const MetaCoreKeyEvent& event : input.GetKeyEvents()) { + const Rml::Input::KeyIdentifier key = MetaCoreMapGlfwKeyToRml(event.Key); + const int modifiers = MetaCoreMapGlfwModifiersToRml(event.Modifiers); + const bool propagated = event.Pressed ? Impl_->Context_->ProcessKeyDown(key, modifiers) : Impl_->Context_->ProcessKeyUp(key, modifiers); + consumed.Keyboard = !propagated || consumed.Keyboard; + } + for (const char32_t codePoint : input.GetTextInput()) consumed.Keyboard = !Impl_->Context_->ProcessTextInput(static_cast(codePoint)) || consumed.Keyboard; + return consumed; +} + void MetaCoreRuntimeUiSystem::ApplyDataUpdates(const std::vector& updates) { if (!Impl_->Initialized_) return; for (const MetaCoreRuntimeDataUpdate& update : updates) Impl_->Values_[update.DataPointId] = update.Value; diff --git a/Source/MetaCoreRuntimeUi/Public/MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h b/Source/MetaCoreRuntimeUi/Public/MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h index 7022bb9..9c7ddf7 100644 --- a/Source/MetaCoreRuntimeUi/Public/MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h +++ b/Source/MetaCoreRuntimeUi/Public/MetaCoreRuntimeUi/MetaCoreRuntimeUiSystem.h @@ -2,6 +2,7 @@ #include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" #include "MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h" +#include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h" #include #include @@ -40,6 +41,7 @@ public: void AttachToViewportRenderer(MetaCoreEditorViewportRenderer& viewportRenderer); void Update(float deltaSeconds); + [[nodiscard]] MetaCoreInputConsumption ProcessInput(); void ApplyDataUpdates(const std::vector& updates); [[nodiscard]] bool SetDocumentVisible(const std::string& documentId, bool visible); [[nodiscard]] std::vector ConsumeEvents(); diff --git a/Source/MetaCoreScene/Private/MetaCoreScene.cpp b/Source/MetaCoreScene/Private/MetaCoreScene.cpp index 03adbf0..69ad2b9 100644 --- a/Source/MetaCoreScene/Private/MetaCoreScene.cpp +++ b/Source/MetaCoreScene/Private/MetaCoreScene.cpp @@ -293,6 +293,8 @@ 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()); @@ -459,6 +461,8 @@ MetaCoreSceneSnapshot MetaCoreScene::CaptureSnapshot() const { data.Script = obj.GetComponent(); if (obj.HasComponent()) data.UiRenderer = obj.GetComponent(); + if (obj.HasComponent()) + data.Interactable = obj.GetComponent(); if (obj.HasComponent()) data.PrefabInstance = obj.GetComponent(); if (obj.HasComponent()) @@ -494,6 +498,8 @@ void MetaCoreScene::RestoreSnapshot(const MetaCoreSceneSnapshot& snapshot) { obj.AddComponent(data.Script.value()); if (data.UiRenderer.has_value()) obj.AddComponent(data.UiRenderer.value()); + if (data.Interactable.has_value()) + obj.AddComponent(data.Interactable.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 77e1031..75cc551 100644 --- a/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp +++ b/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp @@ -193,6 +193,12 @@ static json GameObjectDataToJson(const MetaCoreGameObjectData& obj, const MetaCo } objJson["UiRenderer"] = std::move(uiJson); } + } else if (objField.Name == "Interactable" && obj.Interactable.has_value()) { + objJson["Interactable"] = { + {"Enabled", obj.Interactable->Enabled}, + {"EventMask", obj.Interactable->EventMask}, + {"DragThresholdPixels", obj.Interactable->DragThresholdPixels} + }; } else if (objField.Name == "Camera" && obj.Camera.has_value()) { const MetaCoreStructDescriptor* camDesc = registry.FindStruct(); if (camDesc) { @@ -379,6 +385,13 @@ static MetaCoreGameObjectData JsonToGameObjectData(const json& objJson, const Me } obj.UiRenderer = ui; } + } else if (objField.Name == "Interactable" && objJson.contains("Interactable")) { + const auto& value = objJson["Interactable"]; + MetaCoreInteractableComponent interactable; + if (value.contains("Enabled")) interactable.Enabled = value["Enabled"].get(); + if (value.contains("EventMask")) interactable.EventMask = value["EventMask"].get(); + if (value.contains("DragThresholdPixels")) interactable.DragThresholdPixels = value["DragThresholdPixels"].get(); + obj.Interactable = interactable; } 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 76f7ef2..1359218 100644 --- a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h @@ -245,6 +245,25 @@ struct MetaCoreUiRendererComponent { glm::vec3 WorldSize{1.92F, 1.08F, 0.0F}; }; +enum MetaCoreInteractionEventMask : std::uint32_t { + MetaCoreInteraction_Hover = 1U << 0U, + MetaCoreInteraction_Click = 1U << 1U, + MetaCoreInteraction_Drag = 1U << 2U, + MetaCoreInteraction_All = MetaCoreInteraction_Hover | MetaCoreInteraction_Click | MetaCoreInteraction_Drag +}; + +MC_STRUCT() +struct MetaCoreInteractableComponent { + MC_GENERATED_BODY() + + MC_PROPERTY() + bool Enabled = true; + MC_PROPERTY() + std::uint32_t EventMask = MetaCoreInteraction_All; + MC_PROPERTY() + float DragThresholdPixels = 5.0F; +}; + MC_STRUCT() struct MetaCoreModelRootTag { MC_GENERATED_BODY() diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h index adcc579..cf2ddb5 100644 --- a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreGameObject.h @@ -137,6 +137,8 @@ struct MetaCoreGameObjectData { std::optional Script; MC_PROPERTY() std::optional UiRenderer; + MC_PROPERTY() + std::optional Interactable; }; } // namespace MetaCore diff --git a/Source/MetaCoreScripting/Private/MetaCoreScripting.cpp b/Source/MetaCoreScripting/Private/MetaCoreScripting.cpp index 631fcc1..d6c7682 100644 --- a/Source/MetaCoreScripting/Private/MetaCoreScripting.cpp +++ b/Source/MetaCoreScripting/Private/MetaCoreScripting.cpp @@ -1,4 +1,5 @@ #include "MetaCoreScripting/MetaCoreScripting.h" +#include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h" #include "MetaCoreFoundation/MetaCoreHash.h" #include @@ -432,7 +433,7 @@ std::optional MetaCoreScriptModuleLoader::LoadCandid struct MetaCoreScriptRuntime::Impl { struct Entry { MetaCoreId ObjectId = 0; std::uint64_t InstanceId = 0; std::string TypeId; std::unique_ptr Behaviour; bool Started = false; bool Faulted = false; }; - struct Event { std::string Topic; std::vector Fields; }; + struct Event { MetaCoreId TargetObjectId = 0; std::string Topic; std::vector Fields; }; struct Subscription { std::uint64_t Id; MetaCoreId ObjectId; std::uint64_t InstanceId; std::string Topic; EventHandler Handler; }; struct Timer { std::uint64_t Id; MetaCoreId ObjectId; std::uint64_t InstanceId; double Remaining; double Interval; bool Unscaled; std::function Callback; }; @@ -447,9 +448,10 @@ struct MetaCoreScriptRuntime::Impl { std::uint64_t NextSubscription = 1; std::uint64_t NextTimer = 1; bool Running = false; + MetaCoreRuntimeInput* Input = nullptr; - Impl(MetaCoreScriptRuntime& owner, MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger) - : Owner(owner), Scene(scene), Registry(registry), Logger(std::move(logger)) {} + Impl(MetaCoreScriptRuntime& owner, MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger, MetaCoreRuntimeInput* input) + : Owner(owner), Scene(scene), Registry(registry), Logger(std::move(logger)), Input(input) {} void Log(std::uint32_t level, std::string_view category, std::string_view text) const { if (Logger) Logger(level, category, text); } Entry* FindEntry(MetaCoreId object, std::uint64_t instance) { @@ -529,7 +531,7 @@ struct MetaCoreScriptRuntime::Impl { for (const auto& event : current) { const auto subscriptions = Subscriptions; for (const auto& subscription : subscriptions) { - if (subscription.Topic == event.Topic && FindInstance(Scene.FindGameObject(subscription.ObjectId), subscription.InstanceId)) { + if (subscription.Topic == event.Topic && (event.TargetObjectId == 0 || event.TargetObjectId == subscription.ObjectId) && FindInstance(Scene.FindGameObject(subscription.ObjectId), subscription.InstanceId)) { try { subscription.Handler(event.Topic, event.Fields); } catch (...) { Log(2, "Script.Event", "Event handler threw an exception"); } } @@ -554,8 +556,8 @@ struct MetaCoreScriptRuntime::Impl { } }; -MetaCoreScriptRuntime::MetaCoreScriptRuntime(MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger) - : Impl_(std::make_unique(*this, scene, registry, std::move(logger))) {} +MetaCoreScriptRuntime::MetaCoreScriptRuntime(MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger, MetaCoreRuntimeInput* input) + : Impl_(std::make_unique(*this, scene, registry, std::move(logger), input)) {} MetaCoreScriptRuntime::~MetaCoreScriptRuntime() { Stop(); } void MetaCoreScriptRuntime::Start() { if (Impl_->Running) return; Impl_->Running = true; ++WorldGeneration_; for (auto o : Impl_->Scene.GetGameObjects()) if (o.HasComponent()) for (auto& i : o.GetComponent().Instances) if (i.Enabled) Impl_->Ensure(o, i); } void MetaCoreScriptRuntime::FixedUpdate(double delta) { if (!Impl_->Running) return; Time_.FixedDeltaSeconds = std::max(0.0, delta); Impl_->Dispatch(Impl::Phase::Fixed, Time_.FixedDeltaSeconds); } @@ -575,7 +577,9 @@ void MetaCoreScriptRuntime::SetTimeScale(double scale) { Time_.TimeScale = std:: std::size_t MetaCoreScriptRuntime::GetFaultedInstanceCount() const { return std::count_if(Impl_->Entries.begin(), Impl_->Entries.end(), [](const auto& e) { return e.Faulted; }); } std::uint64_t MetaCoreScriptRuntime::Subscribe(MetaCoreId object, std::uint64_t instance, std::string topic, EventHandler handler) { const auto id = Impl_->NextSubscription++; Impl_->Subscriptions.push_back({id, object, instance, std::move(topic), std::move(handler)}); return id; } void MetaCoreScriptRuntime::Unsubscribe(std::uint64_t id) { std::erase_if(Impl_->Subscriptions, [id](const auto& s) { return s.Id == id; }); } -void MetaCoreScriptRuntime::Publish(std::string topic, std::vector fields) { Impl_->Events.push_back({std::move(topic), std::move(fields)}); } +void MetaCoreScriptRuntime::Publish(std::string topic, std::vector fields) { Impl_->Events.push_back({0, std::move(topic), std::move(fields)}); } +void MetaCoreScriptRuntime::PublishToObject(MetaCoreId object, std::string topic, std::vector fields) { Impl_->Events.push_back({object, std::move(topic), std::move(fields)}); } +MetaCoreRuntimeInput* MetaCoreScriptRuntime::GetRuntimeInput() const { return Impl_->Input; } std::uint64_t MetaCoreScriptRuntime::ScheduleTimer(MetaCoreId object, std::uint64_t instance, double delay, double interval, bool unscaled, std::function callback) { const auto id = Impl_->NextTimer++; Impl_->Timers.push_back({id, object, instance, std::max(0.0, delay), std::max(0.0, interval), unscaled, std::move(callback)}); return id; } void MetaCoreScriptRuntime::CancelTimer(std::uint64_t id) { std::erase_if(Impl_->Timers, [id](const auto& t) { return t.Id == id; }); } bool MetaCoreScriptRuntime::IsHandleValid(MetaCoreScriptObjectHandleV1 handle) const { return handle.WorldGeneration == WorldGeneration_ && handle.ObjectId != 0 && static_cast(Impl_->Scene.FindGameObject(handle.ObjectId)); } @@ -638,7 +642,12 @@ std::uint64_t HSubscribe(void* c, MetaCoreScriptObjectHandleV1 h, std::uint64_t void HUnsubscribe(void* c, std::uint64_t id) noexcept { try { if (c) static_cast(c)->Unsubscribe(id); } catch (...) {} } std::uint64_t HSchedule(void* c, MetaCoreScriptObjectHandleV1 h, std::uint64_t instance, double delay, double interval, bool unscaled, MetaCoreScriptTimerCallbackV1 cb, void* user) noexcept { try { if (!c || !cb) return 0; auto* runtime = static_cast(c); return runtime->ScheduleTimer(h.ObjectId, instance, delay, interval, unscaled, [runtime, h, instance, cb, user] { MetaCoreScriptExecutionContextV1 context{&HostApi(), runtime, h, instance, "", "{}", {}}; if (const char* e = cb(user, &context); e) runtime->Log(2, "Script.Timer", e); }); } catch (...) { return 0; } } void HCancel(void* c, std::uint64_t id) noexcept { try { if (c) static_cast(c)->CancelTimer(id); } catch (...) {} } -const MetaCoreScriptHostApiV1& HostApi() { static const MetaCoreScriptHostApiV1 api{METACORE_SCRIPT_ABI_MAJOR, METACORE_SCRIPT_ABI_MINOR, METACORE_SCRIPT_HOST_CAPABILITIES, HValid, HHas, HGet, HSet, HGetField, HSetField, HLog, HPublish, HSubscribe, HUnsubscribe, HSchedule, HCancel}; return api; } +bool HInputAction(void* c, const char* id, MetaCoreScriptInputActionStateV1* out) noexcept { try { if (!c || !id || !out) return false; auto* input = static_cast(c)->GetRuntimeInput(); if (!input) return false; const auto state = input->GetAction(id); *out = {state.Pressed, state.Held, state.Released}; return true; } catch (...) { return false; } } +bool HInputAxis(void* c, const char* id, float* out) noexcept { try { if (!c || !id || !out) return false; auto* input = static_cast(c)->GetRuntimeInput(); if (!input) return false; *out = input->GetAxis(id); return true; } catch (...) { return false; } } +bool HInputContext(void* c, const char* id, bool enabled) noexcept { try { auto* input = c ? static_cast(c)->GetRuntimeInput() : nullptr; return input && id && input->SetContextEnabled(id, enabled); } catch (...) { return false; } } +bool HInputRebind(void* c, const char* id, const char* path) noexcept { try { auto* input = c ? static_cast(c)->GetRuntimeInput() : nullptr; return input && id && path && input->Rebind(id, path); } catch (...) { return false; } } +bool HInputReset(void* c, const char* id) noexcept { try { auto* input = c ? static_cast(c)->GetRuntimeInput() : nullptr; return input && id && input->ResetBinding(id); } catch (...) { return false; } } +const MetaCoreScriptHostApiV1& HostApi() { static const MetaCoreScriptHostApiV1 api{METACORE_SCRIPT_ABI_MAJOR, METACORE_SCRIPT_ABI_MINOR, METACORE_SCRIPT_HOST_CAPABILITIES, HValid, HHas, HGet, HSet, HGetField, HSetField, HLog, HPublish, HSubscribe, HUnsubscribe, HSchedule, HCancel, HInputAction, HInputAxis, HInputContext, HInputRebind, HInputReset}; return api; } } // namespace bool MetaCoreGenerateScriptProject(const std::filesystem::path& root, std::string_view projectName, const std::filesystem::path& sdkRoot, std::string* error) { @@ -650,7 +659,24 @@ bool MetaCoreGenerateScriptProject(const std::filesystem::path& root, std::strin if (ec) { if (error) *error = ec.message(); return false; } const std::string id = SafeIdentifier(projectName); const std::string cmake = "cmake_minimum_required(VERSION 3.20)\nproject(" + id + "Scripts LANGUAGES CXX)\nif(NOT CMAKE_SYSTEM_NAME STREQUAL \"Linux\" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)\n message(FATAL_ERROR \"MetaCore script v1 requires Linux x86_64\")\nendif()\nif(NOT CMAKE_CXX_COMPILER_ID MATCHES \"Clang\")\n message(FATAL_ERROR \"MetaCore script v1 requires Clang and libc++\")\nendif()\nset(CMAKE_CXX_STANDARD 20)\nset(CMAKE_POSITION_INDEPENDENT_CODE ON)\nfile(GLOB SCRIPT_SOURCES CONFIGURE_DEPENDS Source/*.cpp)\nadd_library(" + id + "Scripts SHARED ${SCRIPT_SOURCES})\ntarget_include_directories(" + id + "Scripts PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/SDK)\ntarget_compile_definitions(" + id + "Scripts PRIVATE METACORE_SCRIPT_MODULE_ID=\\\"" + id + "\\\" METACORE_SCRIPT_BUILD_ID=\\\"${METACORE_SCRIPT_BUILD_ID}\\\")\nset_target_properties(" + id + "Scripts PROPERTIES PREFIX \"\" OUTPUT_NAME \"" + id + "Scripts_${METACORE_SCRIPT_BUILD_ID}\" LIBRARY_OUTPUT_DIRECTORY \"${METACORE_SCRIPT_OUTPUT_DIR}\")\n"; - const std::string sdk = "#pragma once\n#include \n#include \n#include \n#include \nnamespace MetaCoreScriptSdk { inline std::vector& Registry(){ static std::vector value; return value; } struct Registrar { explicit Registrar(const MetaCoreScriptBehaviourDescriptorV1* value){ Registry().push_back(value); } }; template const char* Guard(F&& function) noexcept { try { function(); return nullptr; } catch(const std::exception& e) { static thread_local std::string error; error=e.what(); return error.c_str(); } catch(...) { return \"unknown C++ exception\"; } } }\n#define MC_REGISTER_SCRIPT(Name, Descriptor) static MetaCoreScriptSdk::Registrar Name##Registrar(&(Descriptor))\n"; + const std::string sdk = R"SDK(#pragma once +#include +#include +#include +#include +namespace MetaCoreScriptSdk { +inline std::vector& Registry(){ static std::vector value; return value; } +struct Registrar { explicit Registrar(const MetaCoreScriptBehaviourDescriptorV1* value){ Registry().push_back(value); } }; +template const char* Guard(F&& function) noexcept { try { function(); return nullptr; } catch(const std::exception& e) { static thread_local std::string error; error=e.what(); return error.c_str(); } catch(...) { return "unknown C++ exception"; } } +inline bool HasInput(const MetaCoreScriptExecutionContextV1& context) { return context.Host && (context.Host->Capabilities & METACORE_SCRIPT_CAP_INPUT) != 0; } +inline MetaCoreScriptInputActionStateV1 GetAction(const MetaCoreScriptExecutionContextV1& context, const char* actionId) { MetaCoreScriptInputActionStateV1 state{}; if(HasInput(context) && context.Host->GetInputAction) context.Host->GetInputAction(context.HostContext, actionId, &state); return state; } +inline float GetAxis(const MetaCoreScriptExecutionContextV1& context, const char* axisId) { float value=0.0f; if(HasInput(context) && context.Host->GetInputAxis) context.Host->GetInputAxis(context.HostContext, axisId, &value); return value; } +inline bool SetContextEnabled(const MetaCoreScriptExecutionContextV1& context, const char* contextId, bool enabled) { return HasInput(context) && context.Host->SetInputContextEnabled && context.Host->SetInputContextEnabled(context.HostContext, contextId, enabled); } +inline bool Rebind(const MetaCoreScriptExecutionContextV1& context, const char* bindingId, const char* controlPath) { return HasInput(context) && context.Host->RebindInput && context.Host->RebindInput(context.HostContext, bindingId, controlPath); } +inline bool ResetBinding(const MetaCoreScriptExecutionContextV1& context, const char* bindingId) { return HasInput(context) && context.Host->ResetInputBinding && context.Host->ResetInputBinding(context.HostContext, bindingId); } +} +#define MC_REGISTER_SCRIPT(Name, Descriptor) static MetaCoreScriptSdk::Registrar Name##Registrar(&(Descriptor)) +)SDK"; const std::string module = "#include \n#include \nextern \"C\" METACORE_SCRIPT_EXPORT bool MetaCoreLoadScriptModuleV1(const MetaCoreScriptHostApiV1* host, MetaCoreScriptModuleApiV1* out) noexcept { if(!host||!out||host->AbiMajor!=METACORE_SCRIPT_ABI_MAJOR) return false; static std::vector descriptors; if(descriptors.empty()) for(const auto* descriptor:MetaCoreScriptSdk::Registry()) descriptors.push_back(*descriptor); *out={METACORE_SCRIPT_ABI_MAJOR,METACORE_SCRIPT_ABI_MINOR,METACORE_SCRIPT_CAP_PROPERTIES|METACORE_SCRIPT_CAP_EVENTS|METACORE_SCRIPT_CAP_TIMERS,METACORE_SCRIPT_MODULE_ID,METACORE_SCRIPT_BUILD_ID,descriptors.size(),descriptors.data(),nullptr}; return true; }\n"; const std::string launch = "{\n \"version\": \"0.2.0\",\n \"configurations\": [\n {\"name\": \"Attach MetaCore Editor (LLDB)\", \"type\": \"lldb\", \"request\": \"attach\", \"pid\": \"${command:pickMyProcess}\"},\n {\"name\": \"Attach MetaCore Player (LLDB)\", \"type\": \"lldb\", \"request\": \"attach\", \"pid\": \"${command:pickMyProcess}\"}\n ]\n}\n"; if (!WriteIfMissing(scripts / "CMakeLists.txt", cmake) || diff --git a/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScriptAbi.h b/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScriptAbi.h index 206eb5a..defe5ac 100644 --- a/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScriptAbi.h +++ b/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScriptAbi.h @@ -12,12 +12,13 @@ extern "C" { inline constexpr std::uint32_t METACORE_SCRIPT_ABI_MAJOR = 1U; -inline constexpr std::uint32_t METACORE_SCRIPT_ABI_MINOR = 0U; +inline constexpr std::uint32_t METACORE_SCRIPT_ABI_MINOR = 1U; inline constexpr std::uint64_t METACORE_SCRIPT_CAP_PROPERTIES = 1ULL << 0U; inline constexpr std::uint64_t METACORE_SCRIPT_CAP_EVENTS = 1ULL << 1U; inline constexpr std::uint64_t METACORE_SCRIPT_CAP_TIMERS = 1ULL << 2U; +inline constexpr std::uint64_t METACORE_SCRIPT_CAP_INPUT = 1ULL << 3U; inline constexpr std::uint64_t METACORE_SCRIPT_HOST_CAPABILITIES = - METACORE_SCRIPT_CAP_PROPERTIES | METACORE_SCRIPT_CAP_EVENTS | METACORE_SCRIPT_CAP_TIMERS; + METACORE_SCRIPT_CAP_PROPERTIES | METACORE_SCRIPT_CAP_EVENTS | METACORE_SCRIPT_CAP_TIMERS | METACORE_SCRIPT_CAP_INPUT; enum MetaCoreScriptAbiValueType : std::uint32_t { MetaCoreScriptAbiValue_None = 0, @@ -54,6 +55,8 @@ struct MetaCoreScriptTimeV1 { double TimeScale; }; +struct MetaCoreScriptInputActionStateV1 { bool Pressed; bool Held; bool Released; }; + struct MetaCoreScriptExecutionContextV1; using MetaCoreScriptTimerCallbackV1 = const char* (*)(void*, const MetaCoreScriptExecutionContextV1*) noexcept; using MetaCoreScriptEventCallbackV1 = const char* (*)(void*, const MetaCoreScriptExecutionContextV1*, const char*, const MetaCoreScriptValueV1*, std::size_t) noexcept; @@ -74,6 +77,11 @@ struct MetaCoreScriptHostApiV1 { void (*UnsubscribeEvent)(void*, std::uint64_t) noexcept; std::uint64_t (*ScheduleTimer)(void*, MetaCoreScriptObjectHandleV1, std::uint64_t, double, double, bool, MetaCoreScriptTimerCallbackV1, void*) noexcept; void (*CancelTimer)(void*, std::uint64_t) noexcept; + bool (*GetInputAction)(void*, const char*, MetaCoreScriptInputActionStateV1*) noexcept; + bool (*GetInputAxis)(void*, const char*, float*) noexcept; + bool (*SetInputContextEnabled)(void*, const char*, bool) noexcept; + bool (*RebindInput)(void*, const char*, const char*) noexcept; + bool (*ResetInputBinding)(void*, const char*) noexcept; }; struct MetaCoreScriptExecutionContextV1 { @@ -127,4 +135,3 @@ struct MetaCoreScriptModuleApiV1 { using MetaCoreLoadScriptModuleV1Fn = bool (*)(const MetaCoreScriptHostApiV1*, MetaCoreScriptModuleApiV1*) noexcept; } // extern "C" - diff --git a/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScripting.h b/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScripting.h index 8b0d562..5454b0d 100644 --- a/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScripting.h +++ b/Source/MetaCoreScripting/Public/MetaCoreScripting/MetaCoreScripting.h @@ -49,6 +49,7 @@ struct MetaCoreScriptTime { }; class MetaCoreScriptRuntime; +class MetaCoreRuntimeInput; struct MetaCoreScriptExecutionContext { MetaCoreScene& Scene; @@ -115,7 +116,7 @@ public: class MetaCoreScriptRuntime { public: - MetaCoreScriptRuntime(MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger = {}); + MetaCoreScriptRuntime(MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger = {}, MetaCoreRuntimeInput* input = nullptr); ~MetaCoreScriptRuntime(); void Start(); @@ -132,6 +133,8 @@ public: std::uint64_t Subscribe(MetaCoreId objectId, std::uint64_t instanceId, std::string topic, EventHandler handler); void Unsubscribe(std::uint64_t subscriptionId); void Publish(std::string topic, std::vector fields = {}); + void PublishToObject(MetaCoreId objectId, std::string topic, std::vector fields = {}); + [[nodiscard]] MetaCoreRuntimeInput* GetRuntimeInput() const; std::uint64_t ScheduleTimer(MetaCoreId objectId, std::uint64_t instanceId, double delay, double interval, bool unscaled, std::function callback); void CancelTimer(std::uint64_t timerId); diff --git a/docs/designs/metacore-mature-engine-gap-and-work-plan.md b/docs/designs/metacore-mature-engine-gap-and-work-plan.md index 7ab962d..f93579a 100644 --- a/docs/designs/metacore-mature-engine-gap-and-work-plan.md +++ b/docs/designs/metacore-mature-engine-gap-and-work-plan.md @@ -178,12 +178,14 @@ MetaCore 已经超过单纯编辑器 Demo 阶段,当前具备: #### 需要做的事 -- [ ] 定义 Input Action、Axis、Binding 和 Input Context 数据模型。 -- [ ] 支持键盘、鼠标和基础手柄映射。 -- [ ] 建立编辑器输入、Game View 输入和 Player 输入的焦点规则。 -- [ ] 统一 RmlUi 与 3D 场景输入路由和事件消费顺序。 -- [ ] 为运行时对象提供点击、悬停、拖拽和射线命中事件。 -- [ ] 将输入配置纳入项目、Cook 和 Player 加载链路。 +- [x] 定义 Input Action、Axis、Binding 和 Input Context 数据模型。 +- [x] 支持键盘、鼠标和基础手柄映射。 +- [x] 建立编辑器输入、Game View 输入和 Player 输入的焦点规则。 +- [x] 统一 RmlUi 与 3D 场景输入路由和事件消费顺序。 +- [x] 为运行时对象提供点击、悬停、拖拽和射线命中事件。 +- [x] 将输入配置纳入项目、Cook 和 Player 加载链路。 + +实施结果:已新增 `MetaCoreRuntimeInput`/`MetaCoreRuntimeInteraction`,由平台逐帧输入快照经 RmlUi 消费后求值 Input Map,并通过 Filament GPU Picking 生成定向 3D 交互事件。Editor Play Mode 与独立 Player 共用 `Runtime/Input.mcruntime`、脚本输入 ABI 和用户重绑定覆盖;项目创建、Cook/Stage 与正式打包校验均已接入。模块测试覆盖输入生命周期、Context 消费、序列化/损坏回退、异步 Picking 乱序与 Click 路由,相关 Scripting、Delivery 和端到端 Smoke Tests 同步通过。 #### 验收标准 diff --git a/tests/MetaCoreRuntimeInputTests.cpp b/tests/MetaCoreRuntimeInputTests.cpp new file mode 100644 index 0000000..7b87c2d --- /dev/null +++ b/tests/MetaCoreRuntimeInputTests.cpp @@ -0,0 +1,113 @@ +#include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h" +#include "MetaCoreRuntimeInput/MetaCoreRuntimeInteraction.h" +#include "MetaCorePlatform/MetaCoreInput.h" +#include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" +#include "MetaCoreRender/MetaCoreRenderTypes.h" +#include "MetaCoreScene/MetaCoreScene.h" +#include "MetaCoreScene/MetaCoreComponents.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { +void Expect(bool value, const char* message) { if (!value) { std::cerr << message << '\n'; std::exit(1); } } +} + +int main() { + MetaCore::MetaCoreRuntimeInput runtime; + runtime.SetInputMap(MetaCore::MetaCoreBuildDefaultInputMap()); + MetaCore::MetaCoreInput input; + input.BeginFrame(); input.SetControlValue("/W", 1.0F); + runtime.Update(input); + Expect(std::abs(runtime.GetAxis("MoveY") - 1.0F) < 0.001F, "W should drive MoveY"); + input.BeginFrame(); input.SetControlValue("/leftButton", 1.0F); runtime.Update(input); + Expect(runtime.GetAction("Interact").Pressed && runtime.GetAction("Interact").Held, "action should start"); + input.BeginFrame(); runtime.Update(input); Expect(!runtime.GetAction("Interact").Pressed && runtime.GetAction("Interact").Held, "action should remain held"); + input.BeginFrame(); input.SetControlValue("/leftButton", 0.0F); runtime.Update(input); + Expect(runtime.GetAction("Interact").Released, "action should cancel"); + Expect(runtime.Rebind("game.move.forward", "/Up"), "rebind should succeed"); + input.BeginFrame(); input.SetControlValue("/W", 0.0F); input.SetControlValue("/Up", 1.0F); runtime.Update(input); + Expect(runtime.GetAxis("MoveY") > 0.9F, "override should be evaluated"); + runtime.CancelAll(); Expect(!runtime.GetAction("Interact").Held && runtime.GetAxis("MoveY") == 0.0F, "cancel should clear state"); + MetaCore::MetaCoreTypeRegistry registry; MetaCore::MetaCoreRegisterRuntimeInputGeneratedTypes(registry); + const auto mapPath = std::filesystem::temp_directory_path() / "MetaCoreInputMap.test"; + Expect(MetaCore::MetaCoreWriteInputMap(mapPath, MetaCore::MetaCoreBuildDefaultInputMap(), registry), "input map should serialize"); + const auto loaded = MetaCore::MetaCoreReadInputMap(mapPath, registry); + Expect(loaded && loaded->Bindings.size() == 10 && loaded->Contexts.size() == 1, "input map should round trip"); + const auto overridePath = std::filesystem::temp_directory_path() / "MetaCoreInputOverrides.test.json"; + Expect(runtime.SaveOverrides(overridePath), "overrides should save atomically"); + MetaCore::MetaCoreRuntimeInput loadedRuntime; loadedRuntime.SetInputMap(MetaCore::MetaCoreBuildDefaultInputMap()); + Expect(loadedRuntime.LoadOverrides(overridePath), "overrides should load"); + auto invalidMap = MetaCore::MetaCoreBuildDefaultInputMap(); + invalidMap.Bindings.push_back(invalidMap.Bindings.front()); + invalidMap.Bindings.back().ControlPath = "invalid"; + const auto invalidIssues = MetaCore::MetaCoreValidateInputMap(invalidMap); + Expect(std::count_if(invalidIssues.begin(), invalidIssues.end(), [](const auto& issue){ return issue.Error; }) >= 2, + "duplicate binding IDs and invalid paths should fail validation"); + { std::ofstream corrupted(overridePath, std::ios::trunc); corrupted << "{broken"; } + Expect(!loadedRuntime.LoadOverrides(overridePath) && !loadedRuntime.GetWarnings().empty(), "corrupt overrides should fall back with diagnostics"); + + MetaCore::MetaCoreInputMapDocument priorityMap; + priorityMap.Actions = {{"High", "High"}, {"Low", "Low"}}; + priorityMap.Bindings = {{"high", "High", MetaCore::MetaCoreInputBindingTarget::Action, "/Space", 1.0F, {}}, + {"low", "Low", MetaCore::MetaCoreInputBindingTarget::Action, "/Space", 1.0F, {}}}; + priorityMap.Contexts = {{"HighContext", 10, true, true, {"high"}}, {"LowContext", 0, true, false, {"low"}}}; + MetaCore::MetaCoreRuntimeInput priorityRuntime; priorityRuntime.SetInputMap(priorityMap); + MetaCore::MetaCoreInput priorityInput; priorityInput.BeginFrame(); priorityInput.SetControlValue("/Space", 1.0F); priorityRuntime.Update(priorityInput); + Expect(priorityRuntime.GetAction("High").Held && !priorityRuntime.GetAction("Low").Held, "higher context should consume the physical control"); + priorityInput.SetWindowFocused(false); priorityRuntime.Update(priorityInput); + Expect(priorityRuntime.GetAction("High").Released, "focus loss should cancel held actions"); + + MetaCore::MetaCoreScene scene; + auto interactiveObject = scene.CreateGameObject("Interactive"); + interactiveObject.AddComponent(); + interactiveObject.AddComponent(); + MetaCore::MetaCoreRuntimeInteraction interaction; + std::vector pointerEvents; + std::vector pickCallbacks; + interaction.SetEventCallback([&](const auto& event) { pointerEvents.push_back(event); }); + const MetaCore::MetaCoreRuntimeInteraction::PickRequest pickBackend = [&](std::uint32_t, std::uint32_t, auto callback) { + pickCallbacks.push_back(std::move(callback)); return true; + }; + const MetaCore::MetaCoreViewportRect viewport{0.0F, 0.0F, 100.0F, 100.0F}; + MetaCore::MetaCoreInput pointerInput; + pointerInput.BeginFrame(); pointerInput.SetCursorPosition({20.0F, 20.0F}); + interaction.Update(scene, pickBackend, pointerInput, viewport, false); + Expect(pickCallbacks.size() == 1, "hover should submit one pick"); + auto firstHover = std::move(pickCallbacks.back()); pickCallbacks.clear(); + pointerInput.BeginFrame(); pointerInput.SetCursorPosition({21.0F, 20.0F}); + interaction.Update(scene, pickBackend, pointerInput, viewport, false); + auto latestHover = std::move(pickCallbacks.back()); pickCallbacks.clear(); + latestHover({interactiveObject.GetId(), {1.0F, 2.0F, 3.0F}}); + firstHover({0, {}}); + pointerInput.BeginFrame(); interaction.Update(scene, pickBackend, pointerInput, viewport, false); + Expect(!pointerEvents.empty() && pointerEvents.back().Type == MetaCore::MetaCorePointerEventType::Enter, + "out-of-order hover should accept only the latest result"); + + pickCallbacks.clear(); pointerEvents.clear(); + pointerInput.BeginFrame(); pointerInput.SetMouseButtonState(MetaCore::MetaCoreMouseButton::Left, true); + interaction.Update(scene, pickBackend, pointerInput, viewport, false); + Expect(pickCallbacks.size() == 2, "press frame should preserve hover and down picks"); + auto down = std::move(pickCallbacks[1]); pickCallbacks.clear(); down({interactiveObject.GetId(), {}}); + pointerInput.BeginFrame(); interaction.Update(scene, pickBackend, pointerInput, viewport, false); + Expect(std::any_of(pointerEvents.begin(), pointerEvents.end(), [](const auto& e){ return e.Type == MetaCore::MetaCorePointerEventType::Down; }), "press result should capture target"); + pickCallbacks.clear(); + pointerInput.BeginFrame(); pointerInput.SetMouseButtonState(MetaCore::MetaCoreMouseButton::Left, false); + interaction.Update(scene, pickBackend, pointerInput, viewport, false); + Expect(pickCallbacks.size() == 2, "release frame should preserve hover and up picks"); + auto up = std::move(pickCallbacks[1]); pickCallbacks.clear(); up({interactiveObject.GetId(), {}}); + pointerInput.BeginFrame(); interaction.Update(scene, pickBackend, pointerInput, viewport, false); + Expect(std::any_of(pointerEvents.begin(), pointerEvents.end(), [](const auto& e){ return e.Type == MetaCore::MetaCorePointerEventType::Click; }), "same-target release should click"); + pickCallbacks.clear(); + pointerInput.BeginFrame(); interaction.Update(scene, pickBackend, pointerInput, viewport, true); + Expect(pickCallbacks.empty(), "UI-consumed mouse should not submit picking"); + + std::filesystem::remove(mapPath); std::filesystem::remove(overridePath); + std::cout << "MetaCoreRuntimeInputTests passed\n"; + return 0; +}