diff --git a/App/editor/EditorContext.h b/App/editor/EditorContext.h index 1ef843a..71b5827 100644 --- a/App/editor/EditorContext.h +++ b/App/editor/EditorContext.h @@ -9,6 +9,7 @@ #define HELLOVULKAN_EDITORCONTEXT_H #include +#include #include #include #include @@ -28,6 +29,8 @@ struct EditorContext { Engine::ECS::Scene *scene = nullptr; Engine::Assets::AssetManager *assets = nullptr; Engine::Application *app = nullptr; + // 游戏流状态机(PIE 状态权威;Editor::Init 注册快照/恢复监听) + Engine::GameFlow *flow = nullptr; // 编辑器相机(Scene view 专用,归 Renderer 持有——与场景相机实体 // 无关;面板经此做 F 焦点/Gizmo 视图/对齐跳转) Engine::EditorCamera *editorCamera = nullptr; diff --git a/App/editor/panels/ViewportPanel.cpp b/App/editor/panels/ViewportPanel.cpp index 43cf2ef..ee4dc0e 100644 --- a/App/editor/panels/ViewportPanel.cpp +++ b/App/editor/panels/ViewportPanel.cpp @@ -220,12 +220,12 @@ std::optional ViewportPanel::Draw(const EditorContext &ctx, } } - // ---- 工具热键(UE 语义):W/E/R 切 Gizmo 模式、T 切 - // local/world、F 焦点选中实体。仅 Viewport 持焦时生效—— - // 输入框打字(WantTextInput)由 BlockingKeyboard 拦游戏侧, - // 但 ImGui 自身仍收键,故必须限定持焦面板。RMB 按住 = 相机 - // 飞行模式(键盘归相机),工具热键全部抑制——W 前进与 - // W 平移 Gizmo 的键位冲突就此解耦 ---- + // ---- 工具热键(UE 语义):Q 纯选择(不出杆)、W/E/R 切 + // Gizmo 模式、T 切 local/world、F 焦点选中实体。仅 Viewport + // 持焦时生效——输入框打字(WantTextInput)由 BlockingKeyboard + // 拦游戏侧,但 ImGui 自身仍收键,故必须限定持焦面板。RMB 按 + // 住 = 相机飞行模式(键盘归相机),工具热键全部抑制——W 前 + // 进与 W 平移 Gizmo 的键位冲突就此解耦 ---- auto ® = ctx.scene->GetRegistry(); ECS::TransformComponent *tr = selected != entt::null @@ -236,6 +236,9 @@ std::optional ViewportPanel::Draw(const EditorContext &ctx, const bool playing = ctx.playState && *ctx.playState; if (!playing && out.focused && !ImGui::GetIO().WantTextInput && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) { + // Q = 纯选择模式(UE Q 工具语义):op=0 无任何轴位, + // ImGuizmo 不绘制不响应——选中实体只高亮不出杆 + if (ImGui::IsKeyPressed(ImGuiKey_Q)) gizmo.op = 0; if (ImGui::IsKeyPressed(ImGuiKey_W)) gizmo.op = ImGuizmo::TRANSLATE; if (ImGui::IsKeyPressed(ImGuiKey_E)) gizmo.op = ImGuizmo::ROTATE; if (ImGui::IsKeyPressed(ImGuiKey_R)) gizmo.op = ImGuizmo::SCALE; @@ -370,8 +373,8 @@ std::optional ViewportPanel::Draw(const EditorContext &ctx, : "PLAYING - Ctrl+P to stop"); } else { std::snprintf(tip, sizeof(tip), - "Space: %s [T] Snap: Ctrl F: Focus " - "Ctrl+Shift+F: To Selection", + "Q/W/E/R: Sel/Move/Rot/Scale T: %s " + "Snap: Ctrl F: Focus", gizmo.localSpace ? "Local" : "World"); } const ImVec2 imgMin = ImGui::GetItemRectMin(); diff --git a/App/editor/panels/ViewportPanel.h b/App/editor/panels/ViewportPanel.h index e0c65f6..395341b 100644 --- a/App/editor/panels/ViewportPanel.h +++ b/App/editor/panels/ViewportPanel.h @@ -29,8 +29,8 @@ struct ViewportInputState { // op 取 ImGuizmo::OPERATION 位掩码(TRANSLATE=7=三轴位或运算,非 0 // 起始枚举;0 = 无任何轴位 → Gizmo 完全不绘制),int 存储避免本 // 头文件 include ImGuizmo.h(拖入 imgui 宏依赖顺序) -// W/E/R 切换 op、T 切换 local/world 空间(UE 语义);dragging 拉起 -// Editor::BlockingMouse 门控 +// Q 纯选择(op=0 不出杆)、W/E/R 切换 op、T 切换 local/world 空间 +// (UE 语义);dragging 拉起 Editor::BlockingMouse 门控 struct GizmoState { int op = 7; // ImGuizmo::TRANSLATE(TRANSLATE_X|Y|Z) bool localSpace = true; // 操纵空间:true=模型轴(旋转/缩放语义), diff --git a/Engine/include/Engine/Core/Input.h b/Engine/include/Engine/Core/Input.h index 0de11e9..bb2675c 100644 --- a/Engine/include/Engine/Core/Input.h +++ b/Engine/include/Engine/Core/Input.h @@ -18,6 +18,7 @@ #include #include "Engine/Core/Types.h" +#include "Engine/Input/InputAction.h" namespace Engine { @@ -103,6 +104,37 @@ namespace Engine { // 滚轮增量(上行 = +1,一档 ≈1):读后清零。UI 捕获鼠标 //(面板悬停)时不累积——滚轮归 ImGui f32 ConsumeScrollDelta(); + + // ---- 输入动作层(具名动作,键鼠+手柄混绑;InputMap 藏在 cpp)---- + // 采样:主循环单点调用 Poll(物理/脚本/OnUpdate 之前),整帧 + // 一致。active=false(GameFlow 非 Running)时全部动作静默清零。 + // 默认绑定表在 Init 时内建(Move=Axis2D、Jump=Button) + + // 装配期(编辑器/游戏配置阶段调用;运行中 Define 也可,索引重建) + void DefineAction(const std::string& name, ActionKind kind); + // 追加绑定:绑定到具名动作(动作不存在则忽略) + void BindAction(const std::string& name, const ActionBinding& b); + // 就地重绑定(bindingIdx = 该动作绑定表下标;下帧生效) + bool RebindAction(const std::string& name, size_t bindingIdx, + const ActionBinding& b); + + // 持久化(yaml;Load 文件不存在静默失败保持现状表) + bool SaveActionMap(const std::string& path); + bool LoadActionMap(const std::string& path); + + // 每帧采样(Application::Run 专用单点;上层勿调) + void Poll(bool active); + + // 查询(按动作名;未定义动作返回 false/0)。Button 用 + // Pressed/Down/Released 边沿语义,Axis 用 Value/Value2 + bool ActionPressed(const std::string& name); + bool ActionDown(const std::string& name); + bool ActionReleased(const std::string& name); + f32 ActionValue(const std::string& name); + glm::vec2 ActionValue2(const std::string& name); + + // 手柄连接态(Poll 时更新) + bool IsGamepadConnected(); } } // namespace Engine diff --git a/Engine/include/Engine/Core/Window.h b/Engine/include/Engine/Core/Window.h index 5a83722..5b40da5 100644 --- a/Engine/include/Engine/Core/Window.h +++ b/Engine/include/Engine/Core/Window.h @@ -17,6 +17,16 @@ struct GLFWwindow; namespace Engine { + // 手柄状态快照(POD,PollGamepad 每帧填充)。 + // 值 = GLFW 标准游戏布局:axes 下标即 GLFW_GAMEPAD_AXIS_*(左摇杆 X/Y、 + // 右摇杆 X/Y、LT、RT),buttons 下标即 GLFW_GAMEPAD_BUTTON_*(A=0…), + // 调用方不得依赖具体数值(GLFW 头语义) + struct GamepadState { + bool connected = false; + f32 axes[6] = {}; + bool buttons[15] = {}; + }; + class Window { public: Window(const std::string& title, u32 width, u32 height); @@ -76,6 +86,11 @@ namespace Engine { void SetCursorCaptured(bool captured); bool IsCursorCaptured() const { return m_CursorCaptured; } + // 手柄轮询(JOYSTICK_1 标准游戏布局):连接且带标准映射时填充 + // state 并返回 true;否则置 connected=false 全零并返回 false。 + // 无分配(GLFW 平台态拉取),每帧调用成本可忽略 + bool PollGamepad(GamepadState& state) const; + private: diff --git a/Engine/include/Engine/Input/InputAction.h b/Engine/include/Engine/Input/InputAction.h new file mode 100644 index 0000000..9253bcd --- /dev/null +++ b/Engine/include/Engine/Input/InputAction.h @@ -0,0 +1,70 @@ +// +// InputAction.h:输入动作模型类型。 +// +// 动作层把"游戏语义"(Move/Jump)与"物理输入源"(键/鼠标/手柄)解耦: +// - 动作(Action)= 具名游戏语义,Button(按下沿)/Axis1D/Axis2D +// - 绑定(Binding)= 一个输入源到动作贡献的映射(code = GLFW 标准码) +// 上层(脚本/玩法系统)只认动作名,换键位/换手柄不改代码。 +// +// code 值约定 = GLFW 标准码(同 Input::Key 惯例:Key 即 glf3.h 键码, +// GamepadButton 即 GLFW_GAMEPAD_BUTTON_*,GamepadAxis 即 +// GLFW_GAMEPAD_AXIS_*);调用方不得依赖具体数值。 +// +// component:Axis2D 动作中该绑定贡献哪个分量(0=X,1=Y)。 +// 例如 W → {Key, KEY_W, scale=-1, component=1}(Y 分量负方向 = 前进), +// 左摇杆 Y 轴 → {GamepadAxis, AXIS_LEFT_Y, scale=+1, component=1} +//(GLFW 摇杆推上为负,与 W 的 -1 语义自然对齐)。 +// + +#ifndef HELLOVULKAN_INPUTACTION_H +#define HELLOVULKAN_INPUTACTION_H + +#include + +#include +#include + +#include "Engine/Core/Types.h" + +namespace Engine { + namespace Input { + + // 动作类型 + enum class ActionKind : u8 { + Button, // 0/1 按下态(Down/Pressed/Released 边沿查询) + Axis1D, // 标量 [-1,1](键绑定 scale 加权、手柄轴原始值×scale) + Axis2D, // 平面向量(2D 径向死区 + 单位圆钳制) + }; + + // 绑定源类型 + enum class SourceKind : u8 { + Key, // 键盘(code = GLFW 键码) + MouseButton, // 鼠标键(0=左 1=右 2=中) + GamepadButton, // 手柄按钮(0-14 标准布局) + GamepadAxis, // 手柄轴(0-5:左摇杆 X/Y、右摇杆 X/Y、LT、RT) + }; + + // 单条绑定:一个输入源对一个动作的贡献 + struct ActionBinding { + SourceKind kind; + i32 code; // 见头注释(GLFW 标准码) + f32 scale; // 贡献权重(方向:键 ±1;轴 ±1) + u8 component = 0; // 仅 Axis2D 消费:0=X 分量,1=Y 分量 + }; + + // 动作实例:定义 + 绑定表 + 双帧值(边沿派生用,见 InputMap) + struct InputAction { + std::string name; + ActionKind kind = ActionKind::Button; + std::vector bindings; + + // prev/cur 双帧值(Button/Axis1D 用 x;Axis2D 用 xy)。 + // Button 语义:Down = |cur.x|>0.5,Pressed/Released = cur/prev 边沿 + glm::vec2 prev{0.0f, 0.0f}; + glm::vec2 cur{0.0f, 0.0f}; + }; + + } // namespace Input +} // namespace Engine + +#endif //HELLOVULKAN_INPUTACTION_H diff --git a/Engine/include/Engine/Input/InputMap.h b/Engine/include/Engine/Input/InputMap.h new file mode 100644 index 0000000..833f01c --- /dev/null +++ b/Engine/include/Engine/Input/InputMap.h @@ -0,0 +1,88 @@ +// +// InputMap.h:动作表(定义/绑定/采样/重绑定/持久化)。 +// +// 装配期(DefineAction/Bind/SetDefaults/LoadYaml)与运行期(Poll + 查询) +// 分离:名字→下标哈希在 Define 期建好,Poll 只写值槽位——热路径零堆分配。 +// +// 采样语义(Poll): +// - active=false(GameFlow 非 Running):全部动作值清零且 prev 同步清零, +// 恢复激活后按住的键产生一次 Pressed 边(符合游戏语义,无脏边) +// - active=true:键鼠经 Window 轮询(自带 UI 捕获门控),手柄经 +// Window::PollGamepad;连接翻转打一条日志 +// - Axis2D:各绑定按 component 分量加权求和 → 2D 径向死区(0.15) → +// 超出单位圆时归一化(斜向不超速) +// + +#ifndef HELLOVULKAN_INPUTMAP_H +#define HELLOVULKAN_INPUTMAP_H + +#include + +#include +#include +#include + +#include "Engine/Core/Types.h" +#include "Engine/Input/InputAction.h" + +namespace Engine { + + class Window; + + namespace Input { + + class InputMap { + public: + // ---- 装配期 ---- + + // 定义动作(返回动作 id = 下标;重名忽略并返回已有 id) + size_t DefineAction(const std::string& name, ActionKind kind); + // 追加绑定(动作 id 非法时忽略) + void Bind(size_t id, ActionBinding b); + // 就地重绑定(下帧采样即生效)。成功返回 true + bool Rebind(size_t id, size_t bindingIdx, ActionBinding b); + // 内建默认表:Move(Axis2D)=WASD+方向键+左摇杆、Jump(Button)=Space+A + void SetDefaults(); + // 清空全部动作与索引(LoadYaml 重建用) + void Clear(); + + // ---- 运行期 ---- + + // 每帧采样(主循环单点调用:物理/脚本/OnUpdate 之前) + void Poll(const Window& win, bool active); + + bool Down(size_t id) const; // Button: 按住;Axis: |值|>0.5 + bool Pressed(size_t id) const; // 本帧按下边沿 + bool Released(size_t id) const; // 本帧松开边沿 + f32 Value(size_t id) const; // Axis1D;Button 返回 0/1 + glm::vec2 Value2(size_t id) const;// Axis2D;其他 kind 返回 (x,0) + + // 手柄连接态(Poll 时更新;连接翻转打 INFO) + bool IsGamepadConnected() const { return m_GamepadConnected; } + + // ---- 持久化(yaml)---- + + // 格式见 InputMap.cpp;保存失败(目录不存在等)返回 false + bool SaveYaml(const std::string& path) const; + // 文件不存在静默失败返回 false(保持现状表);存在则 + // SetDefaults 后按 yaml 覆盖同名动作的绑定表 + bool LoadYaml(const std::string& path); + + // ---- 查询辅助 ---- + size_t Count() const { return m_Actions.size(); } + // 名字→id;不存在返回 npos + static constexpr size_t npos = static_cast(-1); + size_t Find(const std::string& name) const; + + private: + void PollGamepadConnection(bool connected); + + std::vector m_Actions; + std::unordered_map m_ByName; + bool m_GamepadConnected = false; + }; + + } // namespace Input +} // namespace Engine + +#endif //HELLOVULKAN_INPUTMAP_H diff --git a/Engine/include/Engine/Renderer/Renderer.h b/Engine/include/Engine/Renderer/Renderer.h index 19e4895..2397d93 100644 --- a/Engine/include/Engine/Renderer/Renderer.h +++ b/Engine/include/Engine/Renderer/Renderer.h @@ -236,6 +236,7 @@ namespace Engine { // UniformBuffer——PhysicsDebugPass::PrepareFrame 上传,pass 内绑定) Scope m_PhysicsLineVB; Scope m_PhysicsTriVB; + Scope m_DebugLineVB; // 通用调试源(Engine::Debug 服务) // Editor gizmo 图元 ring VB(editor 专属,game 构建不分配): // 图标 billboard / 视锥线框 / 拾取代理壳三流——代理壳由 // HitProxyPass 经 AttachGizmoProxy 消费(同 VB 换 shader 重画) diff --git a/Engine/include/Engine/Renderer/SceneRenderer.h b/Engine/include/Engine/Renderer/SceneRenderer.h index 69f93ad..e10484b 100644 --- a/Engine/include/Engine/Renderer/SceneRenderer.h +++ b/Engine/include/Engine/Renderer/SceneRenderer.h @@ -205,7 +205,11 @@ namespace Engine { bool m_BloomEnabled = true; bool m_LodEnabled = true; // 关 = 全部强制 LOD0(选级跳过) int m_DebugView = 0; - int m_DisplayMode = kDisplayViewport; // Editor 视口为默认形态 + // 引擎默认 = 游戏直渲形态(与 Renderer::m_DesiredDisplayMode 的 + // 默认一致)。编辑器形态在 App OnInit 显式切 Viewport——若这里 + // 默认 Viewport,game 形态没人调 SetDisplayMode:Blit 恒空转 + + // ImGuiPass 清屏 → 纯黑屏(game 形态黑屏的根因) + int m_DisplayMode = kDisplayDirect; bool m_GameView = false; // PIE 内嵌播放(Editor Play/Stop 驱动) #ifdef HELLO_EDITOR diff --git a/Engine/src/Core/Input.cpp b/Engine/src/Core/Input.cpp index b0a5a57..804c8de 100644 --- a/Engine/src/Core/Input.cpp +++ b/Engine/src/Core/Input.cpp @@ -1,9 +1,10 @@ // -// Input.cpp:统一输入门面实现(转发 Window 后端轮询接口)。 +// Input.cpp:统一输入门面实现(转发 Window 后端轮询接口 + 动作层门面)。 // #include "Engine/Core/Input.h" #include "Engine/Core/Window.h" +#include "Engine/Input/InputMap.h" namespace Engine { namespace Input { @@ -13,11 +14,15 @@ namespace Engine { // 仅 Application::Run 期间被查询)。非 const:门面含 // 光标捕获等状态设置,不只查询 Window* s_Window = nullptr; + + // 动作表实例(Init 时建默认绑定:Move/Jump) + InputMap s_ActionMap; } void Init(Window& window) { s_Window = &window; + s_ActionMap.SetDefaults(); // 内建默认绑定表(LoadYaml 可覆盖) } bool IsKeyDown(const Key key) @@ -60,5 +65,76 @@ namespace Engine { return s_Window != nullptr ? s_Window->ConsumeScrollDelta() : 0.0f; } + // ---- 输入动作层 ---- + + void DefineAction(const std::string& name, ActionKind kind) + { + s_ActionMap.DefineAction(name, kind); + } + + void BindAction(const std::string& name, const ActionBinding& b) + { + const size_t id = s_ActionMap.Find(name); + if (id != InputMap::npos) s_ActionMap.Bind(id, b); + } + + bool RebindAction(const std::string& name, size_t bindingIdx, + const ActionBinding& b) + { + const size_t id = s_ActionMap.Find(name); + return id != InputMap::npos && s_ActionMap.Rebind(id, bindingIdx, b); + } + + bool SaveActionMap(const std::string& path) + { + return s_ActionMap.SaveYaml(path); + } + + bool LoadActionMap(const std::string& path) + { + return s_ActionMap.LoadYaml(path); + } + + void Poll(const bool active) + { + if (s_Window != nullptr) s_ActionMap.Poll(*s_Window, active); + } + + bool ActionPressed(const std::string& name) + { + const size_t id = s_ActionMap.Find(name); + return id != InputMap::npos && s_ActionMap.Pressed(id); + } + + bool ActionDown(const std::string& name) + { + const size_t id = s_ActionMap.Find(name); + return id != InputMap::npos && s_ActionMap.Down(id); + } + + bool ActionReleased(const std::string& name) + { + const size_t id = s_ActionMap.Find(name); + return id != InputMap::npos && s_ActionMap.Released(id); + } + + f32 ActionValue(const std::string& name) + { + const size_t id = s_ActionMap.Find(name); + return id != InputMap::npos ? s_ActionMap.Value(id) : 0.0f; + } + + glm::vec2 ActionValue2(const std::string& name) + { + const size_t id = s_ActionMap.Find(name); + return id != InputMap::npos ? s_ActionMap.Value2(id) + : glm::vec2{0.0f, 0.0f}; + } + + bool IsGamepadConnected() + { + return s_ActionMap.IsGamepadConnected(); + } + } } // namespace Engine diff --git a/Engine/src/Core/Window.cpp b/Engine/src/Core/Window.cpp index a1c09c9..5bde9c3 100644 --- a/Engine/src/Core/Window.cpp +++ b/Engine/src/Core/Window.cpp @@ -254,4 +254,35 @@ namespace Engine m_ScrollAccum = 0.0f; return d; } + + bool Window::PollGamepad(GamepadState& state) const + { + GLFWgamepadstate gp; + if (glfwGetGamepadState(GLFW_JOYSTICK_1, &gp)) + { + state.connected = true; + // 计数用 sizeof 推导(vendored GLFW 3.3 无 *_COUNT 常量), + // 拷贝上限取两侧较小值,防未来数组尺寸不一致越界 + constexpr int kAxisCap = static_cast( + sizeof(state.axes) / sizeof(state.axes[0]) < + sizeof(gp.axes) / sizeof(gp.axes[0]) + ? sizeof(state.axes) / sizeof(state.axes[0]) + : sizeof(gp.axes) / sizeof(gp.axes[0])); + constexpr int kButtonCap = static_cast( + sizeof(state.buttons) / sizeof(state.buttons[0]) < + sizeof(gp.buttons) / sizeof(gp.buttons[0]) + ? sizeof(state.buttons) / sizeof(state.buttons[0]) + : sizeof(gp.buttons) / sizeof(gp.buttons[0])); + for (int i = 0; i < kAxisCap; ++i) + state.axes[i] = gp.axes[i]; + for (int i = 0; i < kButtonCap; ++i) + state.buttons[i] = gp.buttons[i] == GLFW_PRESS; + return true; + } + // 未连接 / 无标准映射:全零(断开瞬间残留值清空,防卡键) + state.connected = false; + for (float& a : state.axes) a = 0.0f; + for (bool& b : state.buttons) b = false; + return false; + } } // namespace Engine diff --git a/Engine/src/ImGui/ImGuiLayer.cpp b/Engine/src/ImGui/ImGuiLayer.cpp index ecb9973..23d4098 100644 --- a/Engine/src/ImGui/ImGuiLayer.cpp +++ b/Engine/src/ImGui/ImGuiLayer.cpp @@ -57,6 +57,16 @@ namespace Engine { io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // docking 分支 // io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // 多视口 + const char* cjkFont = "assets/fonts/SourceHanSansSC-Regular.otf"; + if (io.Fonts->AddFontFromFileTTF( + cjkFont, 16.0f, nullptr, + io.Fonts->GetGlyphRangesChineseSimplifiedCommon()) != nullptr) { + ENGINE_INFO("ImGui font: {} (CJK common ranges)", cjkFont); + } else { + // 字体缺失不致命:图集退回内嵌英文默认字体(中文显示 ?) + ENGINE_WARN("ImGui CJK font not found: {} — fallback to default", cjkFont); + } + ImGui_ImplGlfw_InitForVulkan(window.GetHandle(), true); ImGui_ImplVulkan_InitInfo info = {}; @@ -132,25 +142,25 @@ namespace Engine { } } -#ifdef HELLO_EDITOR - void ImGuiLayer::UpdatePreviewTexture(u32 slot, vk::ImageView view) - { - if (slot >= kMaxFramesInFlight) return; - - // 语义与 UpdateSceneTexture 完全一致(回收旧描述符 + 注册新 view) - if (m_PreviewTextures[slot]) { - ImGui_ImplVulkan_RemoveTexture( - (VkDescriptorSet)(intptr_t)m_PreviewTextures[slot]); - m_PreviewTextures[slot] = ImTextureID_Invalid; - } - if (view) { - m_PreviewTextures[slot] = (ImTextureID)(intptr_t) - ImGui_ImplVulkan_AddTexture( - (VkImageView)view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); - } - } -#endif - +#ifdef HELLO_EDITOR + void ImGuiLayer::UpdatePreviewTexture(u32 slot, vk::ImageView view) + { + if (slot >= kMaxFramesInFlight) return; + + // 语义与 UpdateSceneTexture 完全一致(回收旧描述符 + 注册新 view) + if (m_PreviewTextures[slot]) { + ImGui_ImplVulkan_RemoveTexture( + (VkDescriptorSet)(intptr_t)m_PreviewTextures[slot]); + m_PreviewTextures[slot] = ImTextureID_Invalid; + } + if (view) { + m_PreviewTextures[slot] = (ImTextureID)(intptr_t) + ImGui_ImplVulkan_AddTexture( + (VkImageView)view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + } + } +#endif + void ImGuiLayer::NewFrame() { ImGui_ImplVulkan_NewFrame(); diff --git a/Engine/src/Input/InputMap.cpp b/Engine/src/Input/InputMap.cpp new file mode 100644 index 0000000..cf02004 --- /dev/null +++ b/Engine/src/Input/InputMap.cpp @@ -0,0 +1,312 @@ +// +// InputMap.cpp:动作表实现(采样 / 默认表 / yaml 持久化)。 +// + +#include "Engine/Input/InputMap.h" + +#include +#include + +#include + +#include +#include + +namespace Engine { + namespace Input { + + namespace { + + // GLFW 标准码(仅默认表使用;与 Input::Key 值一致——键码即 + // GLFW 物理键码,此处直接用数值避免 Core↔Input 头依赖) + constexpr i32 kKeyW = 87, kKeyA = 65, kKeyS = 83, kKeyD = 68; + constexpr i32 kKeyUp = 265, kKeyDown = 264, + kKeyLeft = 263, kKeyRight = 262; + constexpr i32 kKeySpace = 32; + // 手柄标准布局 + constexpr i32 kPadBtnA = 0; // GLFW_GAMEPAD_BUTTON_A + constexpr i32 kPadAxisLeftX = 0; // GLFW_GAMEPAD_AXIS_LEFT_X + constexpr i32 kPadAxisLeftY = 1; // GLFW_GAMEPAD_AXIS_LEFT_Y + + constexpr f32 kRadialDeadZone = 0.15f; + + // 单绑定对一维贡献(Axis2D 的分量提取在 Poll 内单独做) + f32 SampleScalar(const Window& win, const GamepadState& pad, + const ActionBinding& b) + { + switch (b.kind) { + case SourceKind::Key: + return win.IsKeyPressed(b.code) ? b.scale : 0.0f; + case SourceKind::MouseButton: + return win.IsMouseButtonPressed(b.code) ? b.scale : 0.0f; + case SourceKind::GamepadButton: + return (pad.connected && pad.buttons[b.code]) + ? b.scale : 0.0f; + case SourceKind::GamepadAxis: + return pad.connected ? b.scale * pad.axes[b.code] : 0.0f; + } + return 0.0f; + } + + // ---- yaml kind 字符串映射 ---- + const char* KindStr(ActionKind k) { + switch (k) { + case ActionKind::Button: return "Button"; + case ActionKind::Axis1D: return "Axis1D"; + case ActionKind::Axis2D: return "Axis2D"; + } + return "Button"; + } + const char* SourceStr(SourceKind k) { + switch (k) { + case SourceKind::Key: return "Key"; + case SourceKind::MouseButton: return "MouseButton"; + case SourceKind::GamepadButton: return "GamepadButton"; + case SourceKind::GamepadAxis: return "GamepadAxis"; + } + return "Key"; + } + ActionKind KindFromStr(const std::string& s) { + if (s == "Axis1D") return ActionKind::Axis1D; + if (s == "Axis2D") return ActionKind::Axis2D; + return ActionKind::Button; + } + SourceKind SourceFromStr(const std::string& s) { + if (s == "MouseButton") return SourceKind::MouseButton; + if (s == "GamepadButton") return SourceKind::GamepadButton; + if (s == "GamepadAxis") return SourceKind::GamepadAxis; + return SourceKind::Key; + } + + } // anonymous namespace + + // ---- 装配期 ---- + + size_t InputMap::DefineAction(const std::string& name, ActionKind kind) + { + if (const size_t exist = Find(name); exist != npos) { + ENGINE_WARN("InputMap: action '{}' already defined", name); + return exist; + } + InputAction a; + a.name = name; + a.kind = kind; + m_Actions.push_back(std::move(a)); + const size_t id = m_Actions.size() - 1; + m_ByName[name] = id; + return id; + } + + void InputMap::Bind(size_t id, ActionBinding b) + { + if (id >= m_Actions.size()) return; + m_Actions[id].bindings.push_back(b); + } + + bool InputMap::Rebind(size_t id, size_t bindingIdx, ActionBinding b) + { + if (id >= m_Actions.size() || bindingIdx >= m_Actions[id].bindings.size()) + return false; + m_Actions[id].bindings[bindingIdx] = b; // 就地改,下帧生效 + return true; + } + + void InputMap::Clear() + { + m_Actions.clear(); + m_ByName.clear(); + } + + void InputMap::SetDefaults() + { + Clear(); + constexpr u8 kX = 0, kY = 1; + + // Move:WASD + 方向键 + 左摇杆(2D) + const size_t move = DefineAction("Move", ActionKind::Axis2D); + Bind(move, {SourceKind::Key, kKeyW, -1.0f, kY}); + Bind(move, {SourceKind::Key, kKeyS, +1.0f, kY}); + Bind(move, {SourceKind::Key, kKeyA, -1.0f, kX}); + Bind(move, {SourceKind::Key, kKeyD, +1.0f, kX}); + Bind(move, {SourceKind::Key, kKeyUp, -1.0f, kY}); + Bind(move, {SourceKind::Key, kKeyDown, +1.0f, kY}); + Bind(move, {SourceKind::Key, kKeyLeft, -1.0f, kX}); + Bind(move, {SourceKind::Key, kKeyRight, +1.0f, kX}); + Bind(move, {SourceKind::GamepadAxis, kPadAxisLeftX, +1.0f, kX}); + Bind(move, {SourceKind::GamepadAxis, kPadAxisLeftY, +1.0f, kY}); + + // Jump:Space + 手柄 A + const size_t jump = DefineAction("Jump", ActionKind::Button); + Bind(jump, {SourceKind::Key, kKeySpace, 1.0f, kX}); + Bind(jump, {SourceKind::GamepadButton, kPadBtnA, 1.0f, kX}); + } + + // ---- 运行期 ---- + + void InputMap::Poll(const Window& win, bool active) + { + if (!active) { + // 静默:值与 prev 同步清零——恢复后按住的键产生一次 + // Pressed 边(prev=0 → cur!=0),无脏边 + for (InputAction& a : m_Actions) { + a.prev = {0.0f, 0.0f}; + a.cur = {0.0f, 0.0f}; + } + PollGamepadConnection(false); + return; + } + + // 手柄快照(栈上 POD,零堆分配) + GamepadState pad; + const bool connected = win.PollGamepad(pad); + PollGamepadConnection(connected); + + for (InputAction& a : m_Actions) { + a.prev = a.cur; + glm::vec2 v{0.0f, 0.0f}; + + switch (a.kind) { + case ActionKind::Button: + case ActionKind::Axis1D: + for (const ActionBinding& b : a.bindings) + v.x += SampleScalar(win, pad, b); + break; + + case ActionKind::Axis2D: { + for (const ActionBinding& b : a.bindings) { + const f32 s = SampleScalar(win, pad, b); + if (b.component == 0) v.x += s; + else v.y += s; + } + // 2D 径向死区 → 单位圆钳制(斜向不超速) + const f32 len = std::sqrt(v.x * v.x + v.y * v.y); + if (len < kRadialDeadZone) { + v = {0.0f, 0.0f}; + } else if (len > 1.0f) { + v /= len; + } + break; + } + } + a.cur = v; + } + } + + void InputMap::PollGamepadConnection(bool connected) + { + if (connected != m_GamepadConnected) { + m_GamepadConnected = connected; + ENGINE_INFO("InputMap: gamepad {}", connected ? "connected" + : "disconnected"); + } + } + + bool InputMap::Down(size_t id) const + { + if (id >= m_Actions.size()) return false; + const glm::vec2& c = m_Actions[id].cur; + return std::fabs(c.x) > 0.5f || std::fabs(c.y) > 0.5f; + } + + bool InputMap::Pressed(size_t id) const + { + if (id >= m_Actions.size()) return false; + const InputAction& a = m_Actions[id]; + return std::fabs(a.cur.x) > 0.5f && std::fabs(a.prev.x) <= 0.5f; + } + + bool InputMap::Released(size_t id) const + { + if (id >= m_Actions.size()) return false; + const InputAction& a = m_Actions[id]; + return std::fabs(a.cur.x) <= 0.5f && std::fabs(a.prev.x) > 0.5f; + } + + f32 InputMap::Value(size_t id) const + { + if (id >= m_Actions.size()) return 0.0f; + return m_Actions[id].cur.x; + } + + glm::vec2 InputMap::Value2(size_t id) const + { + if (id >= m_Actions.size()) return {0.0f, 0.0f}; + return m_Actions[id].cur; + } + + size_t InputMap::Find(const std::string& name) const + { + const auto it = m_ByName.find(name); + return it != m_ByName.end() ? it->second : npos; + } + + // ---- 持久化 ---- + + bool InputMap::SaveYaml(const std::string& path) const + { + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << "actions" << YAML::Value << YAML::BeginSeq; + for (const InputAction& a : m_Actions) { + out << YAML::BeginMap; + out << YAML::Key << "name" << YAML::Value << a.name; + out << YAML::Key << "kind" << YAML::Value << KindStr(a.kind); + out << YAML::Key << "bindings" << YAML::Value << YAML::BeginSeq; + for (const ActionBinding& b : a.bindings) { + out << YAML::Flow << YAML::BeginMap; + out << YAML::Key << "kind" << YAML::Value << SourceStr(b.kind); + out << YAML::Key << "code" << YAML::Value << b.code; + out << YAML::Key << "scale" << YAML::Value << b.scale; + out << YAML::Key << "component" << YAML::Value << b.component; + out << YAML::EndMap; + } + out << YAML::EndSeq; + out << YAML::EndMap; + } + out << YAML::EndSeq; + out << YAML::EndMap; + + std::ofstream file(path); + if (!file.is_open()) { + ENGINE_ERROR("InputMap::SaveYaml: cannot open '{}'", path); + return false; + } + file << out.c_str(); + return true; + } + + bool InputMap::LoadYaml(const std::string& path) + { + YAML::Node root; + try { + root = YAML::LoadFile(path); + } catch (const YAML::Exception&) { + return false; // 文件不存在 / 格式坏:静默用现状表 + } + + // 基线 = 默认表(yaml 缺的动作保留默认绑定) + SetDefaults(); + const YAML::Node actions = root["actions"]; + if (!actions.IsSequence()) return true; + + for (const YAML::Node& n : actions) { + const size_t id = Find(n["name"].as("")); + if (id == npos) continue; + m_Actions[id].bindings.clear(); + if (const YAML::Node& bs = n["bindings"]; bs.IsSequence()) { + for (const YAML::Node& bn : bs) { + ActionBinding b; + b.kind = SourceFromStr(bn["kind"].as("Key")); + b.code = bn["code"].as(0); + b.scale = bn["scale"].as(1.0f); + b.component = bn["component"].as(0); + m_Actions[id].bindings.push_back(b); + } + } + } + ENGINE_INFO("InputMap: loaded {}", path); + return true; + } + + } // namespace Input +} // namespace Engine diff --git a/Engine/src/Renderer/Camera.cpp b/Engine/src/Renderer/Camera.cpp index 71b14d6..7f70718 100644 --- a/Engine/src/Renderer/Camera.cpp +++ b/Engine/src/Renderer/Camera.cpp @@ -40,20 +40,38 @@ namespace Engine { { if (wantLook) { - // ---- 6DOF 真自由视角:pitch/yaw 各绕本地轴右乘增量 - // 四元数(q = q·Δyaw·Δpitch),无欧拉读改写、无 - // ±89° 钳制——可过顶/翻滚,存储层本就是四元数, - // 不存在万向锁。符号沿用旧约定:鼠标右移向右看 - // (右手系绕 Y 正角向左 → yaw 取负)、鼠标下移 - // 低头(绕 X 正角抬头 → pitch 取负)---- + // ---- FPS 式视角(UE/Unity 编辑器视口语义): + // yaw 绕世界 up 左乘、pitch 绕本地 X 右乘—— + // 右轴恒保持水平,水平线永不 roll。此前版本 + // yaw 也绕本地轴(6DOF 自由视角),俯视后再 + // 平移鼠标会带 roll,up 向量歪斜。pitch 用 + // 前向量的仰角钳 ±89°:过顶会倒置视图,编辑 + // 器视口不要这种行为。符号约定不变:鼠标右移 + // 向右看、鼠标下移低头 ---- const glm::quat dyaw = glm::angleAxis( glm::radians(-mouseDX * m_MouseSensitivity), glm::vec3(0, 1, 0)); - const glm::quat dpitch = glm::angleAxis( - glm::radians(-mouseDY * m_MouseSensitivity), - glm::vec3(1, 0, 0)); + + const glm::vec3 fwd = + transform.GetRotationQuat() * glm::vec3(0, 0, -1); + const float pitch = + std::asin(glm::clamp(fwd.y, -1.0f, 1.0f)); + float dPitch = glm::radians(-mouseDY * m_MouseSensitivity); + dPitch = glm::clamp(pitch + dPitch, + glm::radians(-89.0f), + glm::radians(89.0f)) - pitch; + const glm::quat dpitch = + glm::angleAxis(dPitch, glm::vec3(1, 0, 0)); + transform.SetRotationQuat( - transform.GetRotationQuat() * dyaw * dpitch); + dyaw * transform.GetRotationQuat() * dpitch); + + // 自由视角改写朝向后,旧环绕 pivot 已不在视线上—— + // 若保留,下一次 MMB/滚轮会绕老焦点猛拽位置(Focus + // 后飞远再 MMB 的"瞬移锁回":角增量 × 大半径 = + // 米级甩动)。采 Blender/UE 语义:pivot 跟随当前 + // 视线,失效待 MMB/滚轮按新视线自动重建 + m_HasPivot = false; } else if (Input::IsKeyDown(Input::Key::LeftShift)) { @@ -116,12 +134,16 @@ namespace Engine { if (wantLook || !flyRequiresRMB) { const f32 speed = m_MoveSpeed * deltaTime; - if (Input::IsKeyDown(Input::Key::W)) { transform.SetPosition(transform.GetPosition() + forward * speed); } - if (Input::IsKeyDown(Input::Key::S)) { transform.SetPosition(transform.GetPosition() - forward * speed); } - if (Input::IsKeyDown(Input::Key::A)) { transform.SetPosition(transform.GetPosition() - right * speed); } - if (Input::IsKeyDown(Input::Key::D)) { transform.SetPosition(transform.GetPosition() + right * speed); } - if (Input::IsKeyDown(Input::Key::Space)) { transform.SetPosition(transform.GetPosition() + glm::vec3(0, speed, 0)); } - if (Input::IsKeyDown(Input::Key::LeftShift)) { transform.SetPosition(transform.GetPosition() - glm::vec3(0, speed, 0)); } + bool flew = false; + if (Input::IsKeyDown(Input::Key::W)) { transform.SetPosition(transform.GetPosition() + forward * speed); flew = true; } + if (Input::IsKeyDown(Input::Key::S)) { transform.SetPosition(transform.GetPosition() - forward * speed); flew = true; } + if (Input::IsKeyDown(Input::Key::A)) { transform.SetPosition(transform.GetPosition() - right * speed); flew = true; } + if (Input::IsKeyDown(Input::Key::D)) { transform.SetPosition(transform.GetPosition() + right * speed); flew = true; } + if (Input::IsKeyDown(Input::Key::Space)) { transform.SetPosition(transform.GetPosition() + glm::vec3(0, speed, 0)); flew = true; } + if (Input::IsKeyDown(Input::Key::LeftShift)) { transform.SetPosition(transform.GetPosition() - glm::vec3(0, speed, 0)); flew = true; } + // 飞行位移同样令环绕 pivot 失效(同自由视角:位移后老 + // pivot 与视线脱钩,保留会让 MMB/滚轮绕老焦点拽位置) + if (flew) { m_HasPivot = false; } } // ---- 滚轮推拉:有 pivot 时改环绕距离(指数步进,近处精细/ diff --git a/Engine/src/Renderer/Renderer.cpp b/Engine/src/Renderer/Renderer.cpp index 92092de..3b5e80f 100644 --- a/Engine/src/Renderer/Renderer.cpp +++ b/Engine/src/Renderer/Renderer.cpp @@ -24,6 +24,7 @@ #endif #include #include +#include #include #include #include @@ -114,6 +115,11 @@ namespace Engine u64(Passes::PhysicsDebugPass::kMaxTriVerts) * sizeof(PhysicsDebugVertex), kMaxFramesInFlight, vk::BufferUsageFlagBits::eVertexBuffer); + // 通用调试源 ring VB(Engine::Debug 服务;顶点布局与物理源一致) + m_DebugLineVB = CreateScope( + *m_Device, *m_Allocator, + u64(Engine::Debug::kMaxVerts) * sizeof(Engine::Debug::Vertex), + kMaxFramesInFlight, vk::BufferUsageFlagBits::eVertexBuffer); #ifdef HELLO_EDITOR // 3.3 Editor gizmo 图元 ring VB(editor 专属;game 构建不分配)。 // eVertexBuffer 复用 UniformBuffer 的 per-slot fence 语义, @@ -502,7 +508,8 @@ namespace Engine // TAA 之前)——调试图元进 TAA/Bloom/Tonemap 主链路 m_Passes.push_back(CreateScope( *m_Device, *m_Allocator, *m_ShaderManager, *m_DescriptorManager, - frame, m_HDRColor, m_Depth, *m_PhysicsLineVB, *m_PhysicsTriVB)); + frame, m_HDRColor, m_Depth, *m_PhysicsLineVB, *m_PhysicsTriVB, + *m_DebugLineVB)); m_PhysicsDebugPass = static_cast(m_Passes.back().get()); #ifdef HELLO_EDITOR @@ -813,10 +820,16 @@ namespace Engine // MDI 命令/count 同窗口落槽位 ring(fence 已等,写-读竞争关闭) m_SceneGpu->UploadIndirect(frameSlot, frame); - // 物理调试图元同窗口上传(fence 已等);空帧计数归零,pass 零开销 + // 物理调试图元同窗口上传(fence 已等);空帧计数归零,pass 零开销。 + // 通用调试源(Engine::Debug)同窗口取走;EndFrame 翻页(清瞬时 + + // 清过期持久项)——即使物理源缺席也必须翻页,防瞬时缓冲累积 if (m_PhysicsDebug && m_PhysicsDebugPass) { - m_PhysicsDebugPass->PrepareFrame(frameSlot, *m_PhysicsDebug); + m_PhysicsDebugPass->PrepareFrame( + frameSlot, *m_PhysicsDebug, + Engine::Debug::IsEnabled() ? &Engine::Debug::Frame() + : nullptr); } + Engine::Debug::EndFrame(); #ifdef HELLO_EDITOR // Editor gizmo 图元同窗口上传(fence 已等);空帧不写,pass 端