修复编辑器相机与 game 形态黑屏;中文字体与 Q 选择热键
FPS 式视角合成(yaw 世界轴/pitch 本地轴,永不 roll); RMB 自由视角后环绕 pivot 失效待重建(修 Focus 后 MMB/滚轮 瞬移锁回);SceneRenderer 默认 kDisplayDirect(game 形态 Blit 空转+清屏黑屏根因);swapchain 优先 Opaque 合成; ImGui 加载思源黑体(CJK 常用集);Q=纯选择不出杆; 新增 InputMap 动作映射
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 <glm/glm.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<ActionBinding> 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
|
||||
@@ -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 <glm/glm.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#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<size_t>(-1);
|
||||
size_t Find(const std::string& name) const;
|
||||
|
||||
private:
|
||||
void PollGamepadConnection(bool connected);
|
||||
|
||||
std::vector<InputAction> m_Actions;
|
||||
std::unordered_map<std::string, size_t> m_ByName;
|
||||
bool m_GamepadConnected = false;
|
||||
};
|
||||
|
||||
} // namespace Input
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_INPUTMAP_H
|
||||
@@ -236,6 +236,7 @@ namespace Engine {
|
||||
// UniformBuffer——PhysicsDebugPass::PrepareFrame 上传,pass 内绑定)
|
||||
Scope<UniformBuffer> m_PhysicsLineVB;
|
||||
Scope<UniformBuffer> m_PhysicsTriVB;
|
||||
Scope<UniformBuffer> m_DebugLineVB; // 通用调试源(Engine::Debug 服务)
|
||||
// Editor gizmo 图元 ring VB(editor 专属,game 构建不分配):
|
||||
// 图标 billboard / 视锥线框 / 拾取代理壳三流——代理壳由
|
||||
// HitProxyPass 经 AttachGizmoProxy 消费(同 VB 换 shader 重画)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<int>(
|
||||
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<int>(
|
||||
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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
//
|
||||
// InputMap.cpp:动作表实现(采样 / 默认表 / yaml 持久化)。
|
||||
//
|
||||
|
||||
#include "Engine/Input/InputMap.h"
|
||||
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/Core/Window.h>
|
||||
|
||||
#include <yaml-cpp/yaml.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
|
||||
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<std::string>(""));
|
||||
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<std::string>("Key"));
|
||||
b.code = bn["code"].as<i32>(0);
|
||||
b.scale = bn["scale"].as<f32>(1.0f);
|
||||
b.component = bn["component"].as<u8>(0);
|
||||
m_Actions[id].bindings.push_back(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
ENGINE_INFO("InputMap: loaded {}", path);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Input
|
||||
} // namespace Engine
|
||||
@@ -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 时改环绕距离(指数步进,近处精细/
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#endif
|
||||
#include <Engine/Renderer/Passes/OutlinePass.h>
|
||||
#include <Engine/Physics/PhysicsDebugTypes.h>
|
||||
#include <Engine/Debug/DebugDraw.h>
|
||||
#include <Engine/Renderer/Cubemap.h>
|
||||
#include <Engine/Renderer/IblGenerator.h>
|
||||
#include <Engine/Renderer/TracyGpuProfiler.h>
|
||||
@@ -114,6 +115,11 @@ namespace Engine
|
||||
u64(Passes::PhysicsDebugPass::kMaxTriVerts) *
|
||||
sizeof(PhysicsDebugVertex),
|
||||
kMaxFramesInFlight, vk::BufferUsageFlagBits::eVertexBuffer);
|
||||
// 通用调试源 ring VB(Engine::Debug 服务;顶点布局与物理源一致)
|
||||
m_DebugLineVB = CreateScope<UniformBuffer>(
|
||||
*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<Passes::PhysicsDebugPass>(
|
||||
*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<Passes::PhysicsDebugPass*>(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 端
|
||||
|
||||
Reference in New Issue
Block a user