映射优化

This commit is contained in:
Rowland 2026-07-16 10:26:51 +08:00
parent 204ece778f
commit 2626070af7
36 changed files with 1376 additions and 50 deletions

View File

@ -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::MetaCoreInputMapDocument>{} :
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<std::string, std::string> 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<std::int64_t>(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<std::int64_t>(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<float>(inputWidth), static_cast<float>(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();

View File

@ -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

Binary file not shown.

View File

@ -3,6 +3,9 @@
#include "MetaCoreFoundation/MetaCoreHash.h"
#include "MetaCoreFoundation/MetaCorePackage.h"
#include "MetaCoreFoundation/MetaCoreProject.h"
#include "MetaCoreFoundation/MetaCoreGeneratedReflection.h"
#include "MetaCoreRuntimeInput/MetaCoreInputMap.h"
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h"
#include "MetaCoreScene/MetaCoreScenePackage.h"
#include "MetaCoreScene/MetaCoreSceneSerializer.h"
#include "MetaCoreScripting/MetaCoreScripting.h"
@ -613,6 +616,20 @@ MetaCoreBuildResult MetaCoreBuildPipeline::Run(const MetaCoreBuildRequest& reque
if (error) return fail(MetaCoreBuildErrorCode::StageFailed, "Failed to create staging directory", staging);
report(MetaCoreBuildStage::Cook, "Cooking startup scene and project content");
MetaCoreTypeRegistry inputRegistry;
MetaCoreRegisterRuntimeDataGeneratedTypes(inputRegistry);
MetaCoreRegisterRuntimeInputGeneratedTypes(inputRegistry);
const auto runtimeProjectPath = request.ProjectRoot / project->RuntimeDirectory / "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;

View File

@ -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<MetaCoreInteractableComponent>()) return;
auto& component = gameObject.GetComponent<MetaCoreInteractableComponent>();
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<MetaCoreMeshRendererComponent>()) {
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<MetaCoreInteractableComponent>(); },
[](MetaCoreGameObject& object) { if (object.HasComponent<MetaCoreInteractableComponent>()) return false; object.AddComponent<MetaCoreInteractableComponent>(MetaCoreInteractableComponent{}); return true; },
[](MetaCoreGameObject& object) { if (!object.HasComponent<MetaCoreInteractableComponent>()) return false; object.RemoveComponent<MetaCoreInteractableComponent>(); return true; },
[](MetaCoreGameObject& object) { if (!object.HasComponent<MetaCoreInteractableComponent>()) return false; object.GetComponent<MetaCoreInteractableComponent>() = {}; return true; },
[](const MetaCoreGameObject& object, const MetaCoreTypeRegistry& registry) -> std::optional<std::vector<std::byte>> { if (!object.HasComponent<MetaCoreInteractableComponent>()) return std::nullopt; return MetaCoreSerializeComponentValue(object.GetComponent<MetaCoreInteractableComponent>(), registry); },
[](MetaCoreGameObject& object, std::span<const std::byte> payload, const MetaCoreTypeRegistry& registry) { MetaCoreInteractableComponent value{}; if (!MetaCoreDeserializeComponentValue(payload, value, registry)) return false; if (object.HasComponent<MetaCoreInteractableComponent>()) object.GetComponent<MetaCoreInteractableComponent>() = value; else object.AddComponent<MetaCoreInteractableComponent>(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<MetaCoreIAssetDatabaseService>();
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<float>(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<MetaCoreInputPlayModeComponentSystem> 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<std::int64_t>(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<std::int64_t>(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<MetaCoreInputPlayModeComponentSystem> Input_{};
std::unique_ptr<MetaCoreScriptRuntime> 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<MetaCoreInputPlayModeComponentSystem>();
RegisterSystem(input);
RegisterSystem(std::make_shared<MetaCoreAnimationPlayModeComponentSystem>());
RegisterSystem(std::make_shared<MetaCoreRotatorPlayModeComponentSystem>());
RegisterSystem(std::make_shared<MetaCoreScriptPlayModeComponentSystem>());
RegisterSystem(std::make_shared<MetaCoreScriptPlayModeComponentSystem>(std::move(input)));
}
void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override {

View File

@ -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<std::string, MetaCoreModelAnimationPreviewState> 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<MetaCoreIAssetDatabaseService>();
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<char, 256> 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<int>(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<std::ptrdiff_t>(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<int>(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<std::ptrdiff_t>(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<int>(i)); editString("Binding ID", binding.BindingId); int targetKind = static_cast<int>(binding.Target); if (ImGui::Combo("目标类型", &targetKind, "Action\0Axis\0")) { binding.Target = static_cast<MetaCoreInputBindingTarget>(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<char, 512> 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<std::ptrdiff_t>(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, "<Keyboard>/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<int>(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<std::ptrdiff_t>(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<MetaCoreHierarchyPanelProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreScenePanelProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreRuntimeUiPanelProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreInputMapPanelProvider>());
moduleRegistry.RegisterPanelProvider(std::make_unique<MetaCoreRmlUiVisualEditorPanelProvider>());
moduleRegistry.RegisterService<MetaCoreIUiDesignerService>(std::make_shared<MetaCoreNativeUiDesignerService>());
// The central Scene / Game viewport is not a dockable provider, but it keeps

View File

@ -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("<Keyboard>/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 &&

View File

@ -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;
}

View File

@ -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;
};

View File

@ -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

View File

@ -1,5 +1,7 @@
#include "MetaCorePlatform/MetaCoreInput.h"
#include <algorithm>
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<std::size_t>(key)];
}
@ -84,4 +103,24 @@ std::vector<char32_t> MetaCoreInput::ConsumeTextInput() {
return text;
}
const std::vector<MetaCoreKeyEvent>& MetaCoreInput::GetKeyEvents() const { return KeyEvents_; }
const std::vector<char32_t>& MetaCoreInput::GetTextInput() const { return TextInput_; }
const std::vector<MetaCoreInputEvent>& 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

View File

@ -5,6 +5,7 @@
#endif
#include <windows.h>
#include <shellapi.h>
#include <xinput.h>
#include <chrono>
#include <cstdio>
@ -18,6 +19,7 @@
#include <memory>
#include <utility>
#include <vector>
#include <array>
namespace MetaCore {
@ -158,6 +160,30 @@ int MetaCoreMapVirtualKeyToRuntimeKey(WPARAM virtualKey) {
}
}
std::string MetaCoreWin32KeyControlPath(WPARAM key) {
if (key >= 'A' && key <= 'Z') return "<Keyboard>/" + std::string(1, static_cast<char>(key));
if (key >= '0' && key <= '9') return "<Keyboard>/" + std::string(1, static_cast<char>(key));
if (key >= VK_F1 && key <= VK_F24) return "<Keyboard>/F" + std::to_string(key - VK_F1 + 1);
if (key >= VK_NUMPAD0 && key <= VK_NUMPAD9) return "<Keyboard>/Numpad" + std::to_string(key - VK_NUMPAD0);
switch (key) {
case VK_SPACE: return "<Keyboard>/Space"; case VK_ESCAPE: return "<Keyboard>/Escape";
case VK_RETURN: return "<Keyboard>/Enter"; case VK_TAB: return "<Keyboard>/Tab";
case VK_LEFT: return "<Keyboard>/Left"; case VK_RIGHT: return "<Keyboard>/Right";
case VK_UP: return "<Keyboard>/Up"; case VK_DOWN: return "<Keyboard>/Down";
case VK_LSHIFT: return "<Keyboard>/LeftShift"; case VK_RSHIFT: return "<Keyboard>/RightShift";
case VK_LCONTROL: return "<Keyboard>/LeftControl"; case VK_RCONTROL: return "<Keyboard>/RightControl";
case VK_LMENU: return "<Keyboard>/LeftAlt"; case VK_RMENU: return "<Keyboard>/RightAlt";
case VK_BACK: return "<Keyboard>/Backspace"; case VK_DELETE: return "<Keyboard>/Delete";
case VK_INSERT: return "<Keyboard>/Insert"; case VK_HOME: return "<Keyboard>/Home"; case VK_END: return "<Keyboard>/End";
case VK_PRIOR: return "<Keyboard>/PageUp"; case VK_NEXT: return "<Keyboard>/PageDown"; case VK_CAPITAL: return "<Keyboard>/CapsLock";
case VK_OEM_MINUS: return "<Keyboard>/Minus"; case VK_OEM_PLUS: return "<Keyboard>/Equal";
case VK_OEM_4: return "<Keyboard>/LeftBracket"; case VK_OEM_6: return "<Keyboard>/RightBracket"; case VK_OEM_5: return "<Keyboard>/Backslash";
case VK_OEM_1: return "<Keyboard>/Semicolon"; case VK_OEM_7: return "<Keyboard>/Quote";
case VK_OEM_COMMA: return "<Keyboard>/Comma"; case VK_OEM_PERIOD: return "<Keyboard>/Period"; case VK_OEM_2: return "<Keyboard>/Slash"; case VK_OEM_3: return "<Keyboard>/Backquote";
default: return {};
}
}
std::unordered_map<HWND, class MetaCoreWindow::MetaCoreWindowImpl*>& MetaCoreGetWindowMap() {
static std::unordered_map<HWND, class MetaCoreWindow::MetaCoreWindowImpl*> 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<float>(cursorPosition.x), static_cast<float>(cursorPosition.y)));
}
Impl_->Input.SetControlValue("<Mouse>/delta/x", Impl_->Input.GetCursorDelta().x);
Impl_->Input.SetControlValue("<Mouse>/delta/y", Impl_->Input.GetCursorDelta().y);
Impl_->Input.SetControlValue("<Mouse>/wheel", Impl_->Input.GetMouseWheelDelta());
Impl_->Input.SetControlValue("<Mouse>/leftButton", Impl_->Input.IsMouseButtonDown(MetaCoreMouseButton::Left) ? 1.0F : 0.0F);
Impl_->Input.SetControlValue("<Mouse>/rightButton", Impl_->Input.IsMouseButtonDown(MetaCoreMouseButton::Right) ? 1.0F : 0.0F);
Impl_->Input.SetControlValue("<Mouse>/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<float>(value) / 32768.0F : static_cast<float>(value) / 32767.0F; };
gamepad.Axes = {normalizeStick(xinput.Gamepad.sThumbLX), normalizeStick(xinput.Gamepad.sThumbLY), normalizeStick(xinput.Gamepad.sThumbRX), normalizeStick(xinput.Gamepad.sThumbRY), static_cast<float>(xinput.Gamepad.bLeftTrigger) / 255.0F, static_cast<float>(xinput.Gamepad.bRightTrigger) / 255.0F};
static constexpr std::array<WORD, 15> 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<const char*, 6> 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("<Gamepad>/") + axisPaths[i], gamepad.Connected ? gamepad.Axes[i] : 0.0F);
static constexpr std::array<const char*, 15> 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("<Gamepad>/") + buttonPaths[i], gamepad.Connected && gamepad.Buttons[i] ? 1.0F : 0.0F);
}
void MetaCoreWindow::EndFrame() {

View File

@ -10,6 +10,7 @@
#include <string>
#include <utility>
#include <vector>
#include <array>
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 "<Keyboard>/" + std::string(1, static_cast<char>('A' + key - GLFW_KEY_A));
if (key >= GLFW_KEY_0 && key <= GLFW_KEY_9) return "<Keyboard>/" + std::string(1, static_cast<char>('0' + key - GLFW_KEY_0));
if (key >= GLFW_KEY_F1 && key <= GLFW_KEY_F25) return "<Keyboard>/F" + std::to_string(key - GLFW_KEY_F1 + 1);
if (key >= GLFW_KEY_KP_0 && key <= GLFW_KEY_KP_9) return "<Keyboard>/Numpad" + std::to_string(key - GLFW_KEY_KP_0);
switch (key) {
case GLFW_KEY_SPACE: return "<Keyboard>/Space";
case GLFW_KEY_ESCAPE: return "<Keyboard>/Escape";
case GLFW_KEY_ENTER: return "<Keyboard>/Enter";
case GLFW_KEY_TAB: return "<Keyboard>/Tab";
case GLFW_KEY_LEFT: return "<Keyboard>/Left";
case GLFW_KEY_RIGHT: return "<Keyboard>/Right";
case GLFW_KEY_UP: return "<Keyboard>/Up";
case GLFW_KEY_DOWN: return "<Keyboard>/Down";
case GLFW_KEY_LEFT_SHIFT: return "<Keyboard>/LeftShift";
case GLFW_KEY_RIGHT_SHIFT: return "<Keyboard>/RightShift";
case GLFW_KEY_LEFT_CONTROL: return "<Keyboard>/LeftControl";
case GLFW_KEY_RIGHT_CONTROL: return "<Keyboard>/RightControl";
case GLFW_KEY_LEFT_ALT: return "<Keyboard>/LeftAlt";
case GLFW_KEY_RIGHT_ALT: return "<Keyboard>/RightAlt";
case GLFW_KEY_BACKSPACE: return "<Keyboard>/Backspace";
case GLFW_KEY_DELETE: return "<Keyboard>/Delete";
case GLFW_KEY_INSERT: return "<Keyboard>/Insert";
case GLFW_KEY_HOME: return "<Keyboard>/Home";
case GLFW_KEY_END: return "<Keyboard>/End";
case GLFW_KEY_PAGE_UP: return "<Keyboard>/PageUp";
case GLFW_KEY_PAGE_DOWN: return "<Keyboard>/PageDown";
case GLFW_KEY_CAPS_LOCK: return "<Keyboard>/CapsLock";
case GLFW_KEY_MINUS: return "<Keyboard>/Minus";
case GLFW_KEY_EQUAL: return "<Keyboard>/Equal";
case GLFW_KEY_LEFT_BRACKET: return "<Keyboard>/LeftBracket";
case GLFW_KEY_RIGHT_BRACKET: return "<Keyboard>/RightBracket";
case GLFW_KEY_BACKSLASH: return "<Keyboard>/Backslash";
case GLFW_KEY_SEMICOLON: return "<Keyboard>/Semicolon";
case GLFW_KEY_APOSTROPHE: return "<Keyboard>/Quote";
case GLFW_KEY_COMMA: return "<Keyboard>/Comma";
case GLFW_KEY_PERIOD: return "<Keyboard>/Period";
case GLFW_KEY_SLASH: return "<Keyboard>/Slash";
case GLFW_KEY_GRAVE_ACCENT: return "<Keyboard>/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<float>(cursorX), static_cast<float>(cursorY)));
Impl_->Input.SetControlValue("<Mouse>/delta/x", Impl_->Input.GetCursorDelta().x);
Impl_->Input.SetControlValue("<Mouse>/delta/y", Impl_->Input.GetCursorDelta().y);
Impl_->Input.SetControlValue("<Mouse>/wheel", Impl_->Input.GetMouseWheelDelta());
Impl_->Input.SetControlValue("<Mouse>/leftButton", Impl_->Input.IsMouseButtonDown(MetaCoreMouseButton::Left) ? 1.0F : 0.0F);
Impl_->Input.SetControlValue("<Mouse>/rightButton", Impl_->Input.IsMouseButtonDown(MetaCoreMouseButton::Right) ? 1.0F : 0.0F);
Impl_->Input.SetControlValue("<Mouse>/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<const char*, 6> 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("<Gamepad>/") + axisPaths[i], gamepad.Connected ? gamepad.Axes[i] : 0.0F);
static constexpr std::array<const char*, 15> 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("<Gamepad>/") + buttonPaths[i], gamepad.Connected && gamepad.Buttons[i] ? 1.0F : 0.0F);
Impl_->CloseRequested = Impl_->CloseRequested || glfwWindowShouldClose(window) == GLFW_TRUE;
}

View File

@ -6,6 +6,8 @@
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
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<float, 6> Axes{};
std::array<bool, 15> 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<MetaCoreKeyEvent> ConsumeKeyEvents();
[[nodiscard]] std::vector<char32_t> ConsumeTextInput();
[[nodiscard]] const std::vector<MetaCoreKeyEvent>& GetKeyEvents() const;
[[nodiscard]] const std::vector<char32_t>& GetTextInput() const;
[[nodiscard]] const std::vector<MetaCoreInputEvent>& 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<std::size_t>(MetaCoreInputKey::Count);
@ -73,6 +105,10 @@ private:
float MouseWheelDelta_ = 0.0F;
std::vector<MetaCoreKeyEvent> KeyEvents_{};
std::vector<char32_t> TextInput_{};
std::vector<MetaCoreInputEvent> Events_{};
std::unordered_map<std::string, float> ControlValues_{};
bool WindowFocused_ = true;
MetaCoreGamepadState Gamepad_{};
};
} // namespace MetaCore

View File

@ -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);

View File

@ -44,6 +44,7 @@
#include <utils/Entity.h>
#include <utils/EntityManager.h>
#include <utils/NameComponentManager.h>
#include <math/mat4.h>
#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<double>(result.fragCoords.x) / static_cast<double>(pending.ViewportWidth)) * 2.0 - 1.0,
(static_cast<double>(result.fragCoords.y) / static_cast<double>(pending.ViewportHeight)) * 2.0 - 1.0,
static_cast<double>(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<double>::epsilon()) viewPosition /= viewPosition.w;
filament::math::double4 worldPosition = pending.Model * viewPosition;
if (std::abs(worldPosition.w) > std::numeric_limits<double>::epsilon()) worldPosition /= worldPosition.w;
converted.WorldPosition = {
static_cast<float>(worldPosition.x), static_cast<float>(worldPosition.y), static_cast<float>(worldPosition.z)
};
pending.Callback(converted);
});
return true;
}
uint32_t GetGLTextureId() const {
return GLTextureId_;
}
@ -3524,6 +3568,16 @@ private:
std::unordered_map<MetaCoreId, std::string> LastMaterialSyncDebugByObject_{};
std::unordered_map<MetaCoreId, glm::mat4> ObjectWorldMatrices_{};
std::unordered_map<MetaCoreId, std::pair<filament::gltfio::FilamentAsset*, utils::Entity>> ObjectToFilamentEntity_;
std::unordered_map<utils::Entity, MetaCoreId, utils::Entity::Hasher> 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<std::uint64_t, PendingPick> PickCallbacks_;
std::uint64_t NextPickCallbackId_ = 1;
std::unordered_map<MetaCoreId, filament::gltfio::FilamentAsset*> LoadedAssets_;
std::unordered_map<MetaCoreId, PendingResourceLoad> PendingResourceLoads_;
std::unordered_map<MetaCoreId, MetaCoreAssetGuid> 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();
}

View File

@ -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);

View File

@ -3,6 +3,7 @@
#include "MetaCoreFoundation/MetaCoreId.h"
#include <glm/mat4x4.hpp>
#include <glm/vec3.hpp>
#include <cstdint>
#include <filesystem>
@ -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<void(const PickResult&)>;
using RuntimeOverlayRenderCallback = std::function<void(
filament::Engine& engine,
filament::Renderer& renderer,
@ -47,6 +55,7 @@ public:
void SetEditorGridVisible(bool visible);
void SetRuntimeOverlayRenderCallback(RuntimeOverlayRenderCallback callback);
void RenderAll();
[[nodiscard]] bool RequestPick(std::uint32_t x, std::uint32_t y, PickCallback callback);
[[nodiscard]] uint32_t GetGLTextureId() const;
[[nodiscard]] void* GetFilamentTexturePointer() const;

View File

@ -119,6 +119,9 @@ struct MetaCoreRuntimeProjectDocument {
MC_PROPERTY()
std::filesystem::path UiManifestPath{};
MC_PROPERTY()
std::filesystem::path InputMapPath{};
};
MC_STRUCT()

View File

@ -0,0 +1,78 @@
#include "MetaCoreRuntimeInput/MetaCoreInputMap.h"
#include <algorithm>
#include <fstream>
#include <span>
#include <unordered_set>
namespace MetaCore {
std::vector<MetaCoreInputMapIssue> MetaCoreValidateInputMap(const MetaCoreInputMapDocument& document) {
std::vector<MetaCoreInputMapIssue> issues;
std::unordered_set<std::string> 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<const char*>(bytes->data()), static_cast<std::streamsize>(bytes->size()));
return output.good();
}
std::optional<MetaCoreInputMapDocument> 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<std::size_t>(input.tellg()); input.seekg(0);
std::vector<std::byte> bytes(size);
if (size && !input.read(reinterpret_cast<char*>(bytes.data()), static_cast<std::streamsize>(size))) return std::nullopt;
MetaCoreInputMapDocument result;
if (!MetaCoreDeserializeFromBytes(std::span<const std::byte>(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, "<Mouse>/leftButton", 1.0F, {}},
{"game.cancel.escape", "Cancel", MetaCoreInputBindingTarget::Action, "<Keyboard>/Escape", 1.0F, {}},
{"game.move.left", "MoveX", MetaCoreInputBindingTarget::Axis, "<Keyboard>/A", -1.0F, {}},
{"game.move.right", "MoveX", MetaCoreInputBindingTarget::Axis, "<Keyboard>/D", 1.0F, {}},
{"game.move.back", "MoveY", MetaCoreInputBindingTarget::Axis, "<Keyboard>/S", -1.0F, {}},
{"game.move.forward", "MoveY", MetaCoreInputBindingTarget::Axis, "<Keyboard>/W", 1.0F, {}},
{"game.look.x", "LookX", MetaCoreInputBindingTarget::Axis, "<Mouse>/delta/x", 1.0F, {}},
{"game.look.y", "LookY", MetaCoreInputBindingTarget::Axis, "<Mouse>/delta/y", 1.0F, {}},
{"game.pad.move.x", "MoveX", MetaCoreInputBindingTarget::Axis, "<Gamepad>/leftStick/x", 1.0F, {}},
{"game.pad.move.y", "MoveY", MetaCoreInputBindingTarget::Axis, "<Gamepad>/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

View File

@ -0,0 +1,124 @@
#include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h"
#include "MetaCorePlatform/MetaCoreInput.h"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <fstream>
#include <unordered_map>
#include <unordered_set>
namespace MetaCore {
class MetaCoreRuntimeInput::Impl {
public:
MetaCoreInputMapDocument Map{};
std::unordered_map<std::string, MetaCoreInputActionState> Actions{};
std::unordered_map<std::string, float> Axes{};
std::unordered_map<std::string, bool> Contexts{};
std::vector<std::string> ContextStack{};
std::unordered_map<std::string, std::string> Overrides{};
std::unordered_map<std::uint64_t, ActionCallback> Subscribers{};
std::vector<std::string> 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<Impl>()) {}
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<std::string, bool> nextActions;
std::unordered_map<std::string, float> nextAxes;
std::unordered_set<std::string> consumedPaths;
std::vector<const MetaCoreInputContextDefinition*> 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("<Keyboard>")) || (consumed.Mouse && path.starts_with("<Mouse>")) || (consumed.Gamepad && path.starts_with("<Gamepad>"))) 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<std::string>())) 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<std::string>& 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<unsigned char>(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

View File

@ -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 <algorithm>
#include <cmath>
#include <deque>
#include <memory>
#include <mutex>
#include <utility>
#include <unordered_map>
#include <vector>
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<Completed> CompletedQueries{};
bool Alive = true;
};
EventCallback Callback{};
std::shared_ptr<AsyncState> Async = std::make_shared<AsyncState>();
std::uint64_t NextSequence = 1;
std::uint64_t LatestHoverSequence = 0;
std::uint64_t InteractionRevision = 1;
std::deque<std::uint64_t> PointerSequenceOrder{};
std::unordered_map<std::uint64_t, Completed> 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<MetaCoreInteractableComponent>() && object.GetComponent<MetaCoreInteractableComponent>().Enabled &&
object.HasComponent<MetaCoreMeshRendererComponent>() && object.GetComponent<MetaCoreMeshRendererComponent>().Visible &&
(object.GetComponent<MetaCoreInteractableComponent>().EventMask & mask) != 0;
}
};
MetaCoreRuntimeInteraction::MetaCoreRuntimeInteraction() : Impl_(std::make_unique<Impl>()) {}
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<Impl::Completed> 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<MetaCoreInteractableComponent>() ? object.GetComponent<MetaCoreInteractableComponent>().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::uint32_t>(std::clamp(cursor.x - viewport.Left, 0.0F, viewport.Width - 1.0F));
const auto localY = static_cast<std::uint32_t>(std::clamp(viewport.Height - 1.0F - (cursor.y - viewport.Top), 0.0F, viewport.Height - 1.0F));
const std::weak_ptr<Impl::AsyncState> 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

View File

@ -0,0 +1,99 @@
#pragma once
#include "MetaCoreFoundation/MetaCoreReflection.h"
#include <cstdint>
#include <filesystem>
#include <optional>
#include <string>
#include <vector>
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<std::string> 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<std::string> BindingIds{};
};
MC_STRUCT()
struct MetaCoreInputMapDocument {
MC_GENERATED_BODY()
MC_PROPERTY()
std::uint32_t Version = 1;
MC_PROPERTY()
std::vector<MetaCoreInputActionDefinition> Actions{};
MC_PROPERTY()
std::vector<MetaCoreInputAxisDefinition> Axes{};
MC_PROPERTY()
std::vector<MetaCoreInputBindingDefinition> Bindings{};
MC_PROPERTY()
std::vector<MetaCoreInputContextDefinition> Contexts{};
};
struct MetaCoreInputMapIssue {
bool Error = false;
std::string Scope{};
std::string Message{};
};
[[nodiscard]] std::vector<MetaCoreInputMapIssue> MetaCoreValidateInputMap(const MetaCoreInputMapDocument& document);
[[nodiscard]] bool MetaCoreWriteInputMap(const std::filesystem::path& path, const MetaCoreInputMapDocument& document, const MetaCoreTypeRegistry& registry);
[[nodiscard]] std::optional<MetaCoreInputMapDocument> MetaCoreReadInputMap(const std::filesystem::path& path, const MetaCoreTypeRegistry& registry);
[[nodiscard]] MetaCoreInputMapDocument MetaCoreBuildDefaultInputMap();
} // namespace MetaCore

View File

@ -0,0 +1,67 @@
#pragma once
#include "MetaCoreRuntimeInput/MetaCoreInputMap.h"
#include <cstdint>
#include <filesystem>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
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<void(const MetaCoreInputActionEvent&)>;
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<std::string>& GetWarnings() const;
private:
class Impl;
std::unique_ptr<Impl> Impl_;
};
[[nodiscard]] std::filesystem::path MetaCoreGetInputOverridePath(std::string_view projectName);
} // namespace MetaCore

View File

@ -0,0 +1,50 @@
#pragma once
#include "MetaCoreFoundation/MetaCoreId.h"
#include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h"
#include <glm/vec2.hpp>
#include <glm/vec3.hpp>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
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<void(const MetaCorePointerEvent&)>;
using PickCallback = std::function<void(const MetaCoreInteractionPickResult&)>;
using PickRequest = std::function<bool(std::uint32_t, std::uint32_t, PickCallback)>;
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> Impl_;
};
} // namespace MetaCore

View File

@ -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<int>(cursor.x), static_cast<int>(cursor.y), 0);
for (int button = 0; button < 3; ++button) {
const auto mouseButton = static_cast<MetaCoreMouseButton>(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<Rml::Character>(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<int>(cursor.x), static_cast<int>(cursor.y), 0);
for (int button = 0; button < 3; ++button) {
const auto mouseButton = static_cast<MetaCoreMouseButton>(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<Rml::Character>(codePoint)) || consumed.Keyboard;
return consumed;
}
void MetaCoreRuntimeUiSystem::ApplyDataUpdates(const std::vector<MetaCoreRuntimeDataUpdate>& updates) {
if (!Impl_->Initialized_) return;
for (const MetaCoreRuntimeDataUpdate& update : updates) Impl_->Values_[update.DataPointId] = update.Value;

View File

@ -2,6 +2,7 @@
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h"
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h"
#include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h"
#include <filesystem>
#include <functional>
@ -40,6 +41,7 @@ public:
void AttachToViewportRenderer(MetaCoreEditorViewportRenderer& viewportRenderer);
void Update(float deltaSeconds);
[[nodiscard]] MetaCoreInputConsumption ProcessInput();
void ApplyDataUpdates(const std::vector<MetaCoreRuntimeDataUpdate>& updates);
[[nodiscard]] bool SetDocumentVisible(const std::string& documentId, bool visible);
[[nodiscard]] std::vector<MetaCoreRuntimeUiEvent> ConsumeEvents();

View File

@ -293,6 +293,8 @@ std::vector<MetaCoreId> MetaCoreScene::DuplicateGameObjects(const std::vector<Me
if (sourceObject.HasComponent<MetaCoreUiRendererComponent>())
clonedObject.AddComponent<MetaCoreUiRendererComponent>(sourceObject.GetComponent<MetaCoreUiRendererComponent>());
if (sourceObject.HasComponent<MetaCoreInteractableComponent>())
clonedObject.AddComponent<MetaCoreInteractableComponent>(sourceObject.GetComponent<MetaCoreInteractableComponent>());
if (sourceObject.HasComponent<MetaCorePrefabInstanceMetadata>())
clonedObject.AddComponent<MetaCorePrefabInstanceMetadata>(sourceObject.GetComponent<MetaCorePrefabInstanceMetadata>());
@ -459,6 +461,8 @@ MetaCoreSceneSnapshot MetaCoreScene::CaptureSnapshot() const {
data.Script = obj.GetComponent<MetaCoreScriptComponent>();
if (obj.HasComponent<MetaCoreUiRendererComponent>())
data.UiRenderer = obj.GetComponent<MetaCoreUiRendererComponent>();
if (obj.HasComponent<MetaCoreInteractableComponent>())
data.Interactable = obj.GetComponent<MetaCoreInteractableComponent>();
if (obj.HasComponent<MetaCorePrefabInstanceMetadata>())
data.PrefabInstance = obj.GetComponent<MetaCorePrefabInstanceMetadata>();
if (obj.HasComponent<MetaCoreModelRootTag>())
@ -494,6 +498,8 @@ void MetaCoreScene::RestoreSnapshot(const MetaCoreSceneSnapshot& snapshot) {
obj.AddComponent<MetaCoreScriptComponent>(data.Script.value());
if (data.UiRenderer.has_value())
obj.AddComponent<MetaCoreUiRendererComponent>(data.UiRenderer.value());
if (data.Interactable.has_value())
obj.AddComponent<MetaCoreInteractableComponent>(data.Interactable.value());
if (data.PrefabInstance.has_value())
obj.AddComponent<MetaCorePrefabInstanceMetadata>(data.PrefabInstance.value());
if (data.ModelRootTag.has_value())

View File

@ -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<MetaCoreCameraComponent>();
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<bool>();
if (value.contains("EventMask")) interactable.EventMask = value["EventMask"].get<std::uint32_t>();
if (value.contains("DragThresholdPixels")) interactable.DragThresholdPixels = value["DragThresholdPixels"].get<float>();
obj.Interactable = interactable;
} else if (objField.Name == "Camera" && objJson.contains("Camera")) {
const auto& camJson = objJson["Camera"];
const MetaCoreStructDescriptor* camDesc = registry.FindStruct<MetaCoreCameraComponent>();

View File

@ -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()

View File

@ -137,6 +137,8 @@ struct MetaCoreGameObjectData {
std::optional<MetaCoreScriptComponent> Script;
MC_PROPERTY()
std::optional<MetaCoreUiRendererComponent> UiRenderer;
MC_PROPERTY()
std::optional<MetaCoreInteractableComponent> Interactable;
};
} // namespace MetaCore

View File

@ -1,4 +1,5 @@
#include "MetaCoreScripting/MetaCoreScripting.h"
#include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h"
#include "MetaCoreFoundation/MetaCoreHash.h"
#include <nlohmann/json.hpp>
@ -432,7 +433,7 @@ std::optional<MetaCoreLoadedScriptModule> MetaCoreScriptModuleLoader::LoadCandid
struct MetaCoreScriptRuntime::Impl {
struct Entry { MetaCoreId ObjectId = 0; std::uint64_t InstanceId = 0; std::string TypeId; std::unique_ptr<MetaCoreIScriptBehaviour> Behaviour; bool Started = false; bool Faulted = false; };
struct Event { std::string Topic; std::vector<MetaCoreScriptValueV1> Fields; };
struct Event { MetaCoreId TargetObjectId = 0; std::string Topic; std::vector<MetaCoreScriptValueV1> 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<void()> 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<Impl>(*this, scene, registry, std::move(logger))) {}
MetaCoreScriptRuntime::MetaCoreScriptRuntime(MetaCoreScene& scene, MetaCoreScriptRegistry& registry, MetaCoreScriptLogHandler logger, MetaCoreRuntimeInput* input)
: Impl_(std::make_unique<Impl>(*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<MetaCoreScriptComponent>()) for (auto& i : o.GetComponent<MetaCoreScriptComponent>().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<MetaCoreScriptValueV1> fields) { Impl_->Events.push_back({std::move(topic), std::move(fields)}); }
void MetaCoreScriptRuntime::Publish(std::string topic, std::vector<MetaCoreScriptValueV1> fields) { Impl_->Events.push_back({0, std::move(topic), std::move(fields)}); }
void MetaCoreScriptRuntime::PublishToObject(MetaCoreId object, std::string topic, std::vector<MetaCoreScriptValueV1> 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<void()> 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<bool>(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<MetaCoreScriptRuntime*>(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<MetaCoreScriptRuntime*>(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<MetaCoreScriptRuntime*>(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<MetaCoreScriptRuntime*>(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<MetaCoreScriptRuntime*>(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<MetaCoreScriptRuntime*>(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<MetaCoreScriptRuntime*>(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<MetaCoreScriptRuntime*>(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 <MetaCoreScripting/MetaCoreScriptAbi.h>\n#include <exception>\n#include <string>\n#include <vector>\nnamespace MetaCoreScriptSdk { inline std::vector<const MetaCoreScriptBehaviourDescriptorV1*>& Registry(){ static std::vector<const MetaCoreScriptBehaviourDescriptorV1*> value; return value; } struct Registrar { explicit Registrar(const MetaCoreScriptBehaviourDescriptorV1* value){ Registry().push_back(value); } }; template<class F> 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 <MetaCoreScripting/MetaCoreScriptAbi.h>
#include <exception>
#include <string>
#include <vector>
namespace MetaCoreScriptSdk {
inline std::vector<const MetaCoreScriptBehaviourDescriptorV1*>& Registry(){ static std::vector<const MetaCoreScriptBehaviourDescriptorV1*> value; return value; }
struct Registrar { explicit Registrar(const MetaCoreScriptBehaviourDescriptorV1* value){ Registry().push_back(value); } };
template<class F> 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 <MetaCoreScripting/MetaCoreScriptSdk.h>\n#include <vector>\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<MetaCoreScriptBehaviourDescriptorV1> 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) ||

View File

@ -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"

View File

@ -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<MetaCoreScriptValueV1> fields = {});
void PublishToObject(MetaCoreId objectId, std::string topic, std::vector<MetaCoreScriptValueV1> fields = {});
[[nodiscard]] MetaCoreRuntimeInput* GetRuntimeInput() const;
std::uint64_t ScheduleTimer(MetaCoreId objectId, std::uint64_t instanceId, double delay, double interval, bool unscaled, std::function<void()> callback);
void CancelTimer(std::uint64_t timerId);

View File

@ -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 同步通过。
#### 验收标准

View File

@ -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 <algorithm>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <filesystem>
#include <fstream>
#include <vector>
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("<Keyboard>/W", 1.0F);
runtime.Update(input);
Expect(std::abs(runtime.GetAxis("MoveY") - 1.0F) < 0.001F, "W should drive MoveY");
input.BeginFrame(); input.SetControlValue("<Mouse>/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("<Mouse>/leftButton", 0.0F); runtime.Update(input);
Expect(runtime.GetAction("Interact").Released, "action should cancel");
Expect(runtime.Rebind("game.move.forward", "<Keyboard>/Up"), "rebind should succeed");
input.BeginFrame(); input.SetControlValue("<Keyboard>/W", 0.0F); input.SetControlValue("<Keyboard>/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, "<Keyboard>/Space", 1.0F, {}},
{"low", "Low", MetaCore::MetaCoreInputBindingTarget::Action, "<Keyboard>/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("<Keyboard>/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<MetaCore::MetaCoreMeshRendererComponent>();
interactiveObject.AddComponent<MetaCore::MetaCoreInteractableComponent>();
MetaCore::MetaCoreRuntimeInteraction interaction;
std::vector<MetaCore::MetaCorePointerEvent> pointerEvents;
std::vector<MetaCore::MetaCoreRuntimeInteraction::PickCallback> 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;
}