Compare commits

...
4 Commits
Author SHA1 Message Date
atdunbg 26e222a9cf 修复编辑器相机与 game 形态黑屏;中文字体与 Q 选择热键
FPS 式视角合成(yaw 世界轴/pitch 本地轴,永不 roll);
RMB 自由视角后环绕 pivot 失效待重建(修 Focus 后 MMB/滚轮
瞬移锁回);SceneRenderer 默认 kDisplayDirect(game 形态
Blit 空转+清屏黑屏根因);swapchain 优先 Opaque 合成;
ImGui 加载思源黑体(CJK 常用集);Q=纯选择不出杆;
新增 InputMap 动作映射
2026-10-08 17:02:39 +08:00
atdunbg 36b1fc0566 新增调试绘制体系:DebugDraw 服务 + 物理调试 pass + 脚本侧力学线
通用图元(线/箭头四翼头/球/胶囊/盒)瞬时+持久双缓冲聚合;
物理 pass 画刚体速度/重力/接触反力;角色 collider 与力学
线归位(引擎只画资产、脚本画行为);线宽按设备上限钳制
2026-10-08 17:02:27 +08:00
atdunbg ff955b9e63 新增脚本参数系统:声明式参数 + Inspector 自动控件 + 场景序列化
DeclareParams/ParamBuilder 声明字段,运行时反射读写;
Inspector 按类型生成控件,SceneSerializer 直通存取;
动画系统接入参数化裁剪;新增 OrbitMove 示例脚本
2026-10-08 17:02:19 +08:00
atdunbg f327f44269 新增 GameFlow 运行时状态机:PIE 与独立运行统一生命周期
Stopped→Loading→Running↔Paused 单一状态源,编辑器 Play/Stop
与 main.cpp 独立运行共用同一套加载/恢复/暂停语义
2026-10-08 17:02:09 +08:00
43 changed files with 2913 additions and 189 deletions
+56 -35
View File
@@ -21,7 +21,6 @@
#include "panels/StatsPanel.h" #include "panels/StatsPanel.h"
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
#include <Engine/Core/Time.h>
#include <Engine/Serialization/SceneSerializer.h> #include <Engine/Serialization/SceneSerializer.h>
#include <imgui.h> #include <imgui.h>
@@ -40,9 +39,22 @@ Editor::~Editor() = default;
void Editor::Init(const EditorContext& ctx) void Editor::Init(const EditorContext& ctx)
{ {
m_Ctx = ctx; m_Ctx = ctx;
// PIE 运行/暂停态指针:指向本对象成员(ctx 拷贝后补接,面板只读) // PIE 运行/暂停镜像指针:指向本对象成员(ctx 拷贝后补接,面板只读;
m_Ctx.playState = &m_Playing; // 权威 = GameFlow,镜像在 Draw 开头同步——面板接口保持 const bool*)
m_Ctx.pausedState = &m_Paused; m_Ctx.playState = &m_PlayingMirror;
m_Ctx.pausedState = &m_PausedMirror;
// PIE 转换副作用挂监听(GameFlow 不知场景;快照/恢复是编辑器职责)
if (m_Ctx.flow)
{
m_Ctx.flow->AddListener(
[this](GameFlowState, GameFlowState to) {
if (to == GameFlowState::Loading) OnFlowEnterLoading();
else if (to == GameFlowState::Stopped) OnFlowEnterStopped();
// →Running/→Paused:timeScale 由 GameFlow 权威处理,
// 编辑器无额外副作用
});
}
m_Hierarchy = std::make_unique<HierarchyPanel>(); m_Hierarchy = std::make_unique<HierarchyPanel>();
m_Inspector = std::make_unique<InspectorPanel>(); m_Inspector = std::make_unique<InspectorPanel>();
@@ -84,52 +96,52 @@ void Editor::Init(const EditorContext& ctx)
// ---- PIE(Play-In-Editor)---- // ---- PIE(Play-In-Editor)----
// Play:当前场景 → 内存 YAML 快照 → 切 gameView(Viewport 面板内嵌 // →Loading 监听器:Play 半程。当前场景 → 内存 YAML 快照 → 切 gameView
// primary 相机渲染,UE PIE 语义——编辑器 UI 保持可用,不切全屏; //(Viewport 面板内嵌 primary 相机渲染,UE PIE 语义——编辑器 UI 保持
// kDisplayDirect 全屏预览是后期发布形态,手动经 Stats 切换) // 可用,不切全屏)→ 同步加载完成即 CompleteLoading(快照是同步操作,
// → 解冻时间(物理/脚本/动画开始跑)。快照失败不进 Play(现场不动) // 不完成的 Loading 会卡死主循环的物理/脚本门控)。快照失败 → RequestStop
void Editor::DoPlay() // 回滚(现场不动)
void Editor::OnFlowEnterLoading()
{ {
if (!m_Ctx.scene || !m_Ctx.assets || !m_Ctx.renderer) return; if (!m_Ctx.scene || !m_Ctx.assets || !m_Ctx.renderer || !m_Ctx.flow) return;
m_PlaySnapshot = Serialization::SceneSerializer::SerializeToString( m_PlaySnapshot = Serialization::SceneSerializer::SerializeToString(
*m_Ctx.scene, *m_Ctx.assets, "PlaySnapshot"); *m_Ctx.scene, *m_Ctx.assets, "PlaySnapshot");
if (m_PlaySnapshot.empty()) if (m_PlaySnapshot.empty())
{ {
ENGINE_ERROR("PIE: snapshot failed, play aborted"); ENGINE_ERROR("PIE: snapshot failed, play aborted");
m_Ctx.flow->RequestStop(); // 回滚 → 触发 →Stopped 监听器(空快照跳过恢复)
return; return;
} }
m_Playing = true;
m_Paused = false;
m_Gizmo.dragging = false; // 编辑态拖杆中点 Play:杆运行态不出,复位 m_Gizmo.dragging = false; // 编辑态拖杆中点 Play:杆运行态不出,复位
m_Ctx.renderer->SetGameView(true); m_Ctx.renderer->SetGameView(true);
Time::SetTimeScale(1.0f); m_Ctx.flow->CompleteLoading(); // 同步加载完成 → Running(解冻)
ENGINE_INFO("PIE: playing (scene snapshot {} bytes)", ENGINE_INFO("PIE: playing (scene snapshot {} bytes)",
m_PlaySnapshot.size()); m_PlaySnapshot.size());
} }
// Stop:快照原子换血恢复 → 回编辑器渲染 → 冻结时间。 // →Stopped 监听器:Stop 半程。快照原子换血恢复 → 回编辑器渲染。
// 换血后所有 Entity 句柄悬空:清选中(描边同步清)、Gizmo 拖拽态 // 换血后所有 Entity 句柄悬空:清选中(描边同步清)、Gizmo 拖拽态
// 复位。物理 body/约束/角色由对账系统随实体重建/清理,无需干预。 // 复位。物理 body/约束/角色由对账系统随实体重建/清理,无需干预。
// 恢复失败(快照损坏,理论罕见)也必须回编辑态,现场留给用户 // 恢复失败(快照损坏,理论罕见)也必须回编辑态,现场留给用户
// File→Load 或退出前 Save // File→Load 或退出前 Save。空快照 = 快照失败回滚(未真 Play 过),跳过恢复
void Editor::DoStop() void Editor::OnFlowEnterStopped()
{ {
if (!m_Ctx.scene || !m_Ctx.assets || !m_Ctx.renderer) return; if (!m_Ctx.scene || !m_Ctx.assets || !m_Ctx.renderer) return;
if (!Serialization::SceneSerializer::DeserializeFromString( if (!m_PlaySnapshot.empty())
*m_Ctx.scene, *m_Ctx.assets, m_PlaySnapshot))
{ {
ENGINE_ERROR("PIE: snapshot restore failed (scene is now the " if (!Serialization::SceneSerializer::DeserializeFromString(
"played-out state)"); *m_Ctx.scene, *m_Ctx.assets, m_PlaySnapshot))
{
ENGINE_ERROR("PIE: snapshot restore failed (scene is now the "
"played-out state)");
}
m_PlaySnapshot.clear();
} }
m_PlaySnapshot.clear();
m_Playing = false;
m_Paused = false;
m_Ctx.renderer->SetGameView(false); m_Ctx.renderer->SetGameView(false);
Time::SetTimeScale(0.0f);
SelectEntity(entt::null); SelectEntity(entt::null);
m_Gizmo.dragging = false; m_Gizmo.dragging = false;
ENGINE_INFO("PIE: stopped, editor scene restored"); ENGINE_INFO("PIE: stopped, editor scene restored");
@@ -137,19 +149,28 @@ void Editor::DoStop()
void Editor::TogglePlay() void Editor::TogglePlay()
{ {
m_Playing ? DoStop() : DoPlay(); if (!m_Ctx.flow) return;
IsPlaying() ? m_Ctx.flow->RequestStop() : m_Ctx.flow->RequestStart();
} }
// PIE 暂停(UE Pause 语义):timeScale 0 ↔ 1。运行态才有意义 // PIE 暂停(UE Pause 语义):Running ↔ Paused。运行态才有意义
//(编辑器态本来就冻结,Pause 按钮不出现) //(编辑器态本来就冻结,Pause 按钮不出现)
void Editor::TogglePause() void Editor::TogglePause()
{ {
m_Paused = !m_Paused; if (!m_Ctx.flow) return;
Time::SetTimeScale(m_Paused ? 0.0f : 1.0f); IsPaused() ? m_Ctx.flow->RequestResume() : m_Ctx.flow->RequestPause();
} }
void Editor::Draw() void Editor::Draw()
{ {
// PIE 镜像同步(面板只读指针消费;权威 = GameFlow,此处适配)
if (m_Ctx.flow)
{
m_PlayingMirror = m_Ctx.flow->IsIn(GameFlowState::Running) ||
m_Ctx.flow->IsIn(GameFlowState::Paused);
m_PausedMirror = m_Ctx.flow->IsIn(GameFlowState::Paused);
}
// ImGuizmo 每帧首调(官方约定):内部会清 mbOverGizmoHotspot 等 // ImGuizmo 每帧首调(官方约定):内部会清 mbOverGizmoHotspot 等
// 帧级状态——不调则 hover 过一次杆后 hotspot 永久置位,GetMoveType // 帧级状态——不调则 hover 过一次杆后 hotspot 永久置位,GetMoveType
// 恒返 MT_NONE → 杆可见但永远抓不住(只能看不能拖) // 恒返 MT_NONE → 杆可见但永远抓不住(只能看不能拖)
@@ -253,7 +274,7 @@ void Editor::DrawMenuBar()
{ {
// 运行态禁存取:Save 会把"玩出来的状态"当编辑结果落盘, // 运行态禁存取:Save 会把"玩出来的状态"当编辑结果落盘,
// Load 会撕掉快照恢复链(UE 运行态同样禁) // Load 会撕掉快照恢复链(UE 运行态同样禁)
ImGui::BeginDisabled(m_Playing); ImGui::BeginDisabled(IsPlaying());
if (ImGui::MenuItem("Save Scene...", nullptr)) if (ImGui::MenuItem("Save Scene...", nullptr))
{ {
m_ShowSaveModal = true; m_ShowSaveModal = true;
@@ -302,11 +323,11 @@ void Editor::DrawToolbar()
// 按钮加大触感(工具栏行高 = 一个按钮高;DockSpace avail 自适应) // 按钮加大触感(工具栏行高 = 一个按钮高;DockSpace avail 自适应)
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(12.0f, 4.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(12.0f, 4.0f));
const ImGuiStyle& st = ImGui::GetStyle(); const ImGuiStyle& st = ImGui::GetStyle();
const char* pauseLabel = m_Paused ? "Resume" : "Pause"; const char* pauseLabel = IsPaused() ? "Resume" : "Pause";
// Play 组宽度 → 水平居中(编辑态 1 钮 / 运行态 2 钮) // Play 组宽度 → 水平居中(编辑态 1 钮 / 运行态 2 钮)
float groupW = 0.0f; float groupW = 0.0f;
if (m_Playing) if (IsPlaying())
{ {
groupW = ImGui::CalcTextSize(pauseLabel).x + groupW = ImGui::CalcTextSize(pauseLabel).x +
ImGui::CalcTextSize("Stop").x + ImGui::CalcTextSize("Stop").x +
@@ -318,14 +339,14 @@ void Editor::DrawToolbar()
} }
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - groupW) * 0.5f); ImGui::SetCursorPosX((ImGui::GetWindowWidth() - groupW) * 0.5f);
if (m_Playing) if (IsPlaying())
{ {
// Pause/Resume:暂停中亮黄常驻(UE 按下态),运行中暗黄 // Pause/Resume:暂停中亮黄常驻(UE 按下态),运行中暗黄
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::PushStyleColor(ImGuiCol_Button,
m_Paused ? ImVec4(0.85f, 0.62f, 0.12f, 1.0f) IsPaused() ? ImVec4(0.85f, 0.62f, 0.12f, 1.0f)
: ImVec4(0.55f, 0.42f, 0.10f, 1.0f)); : ImVec4(0.55f, 0.42f, 0.10f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
m_Paused ? ImVec4(0.92f, 0.70f, 0.18f, 1.0f) IsPaused() ? ImVec4(0.92f, 0.70f, 0.18f, 1.0f)
: ImVec4(0.65f, 0.50f, 0.14f, 1.0f)); : ImVec4(0.65f, 0.50f, 0.14f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(0.95f, 0.76f, 0.24f, 1.0f)); ImVec4(0.95f, 0.76f, 0.24f, 1.0f));
@@ -336,7 +357,7 @@ void Editor::DrawToolbar()
ImGui::PopStyleColor(3); ImGui::PopStyleColor(3);
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
{ {
ImGui::SetTooltip(m_Paused ? "Resume simulation" ImGui::SetTooltip(IsPaused() ? "Resume simulation"
: "Pause simulation"); : "Pause simulation");
} }
+30 -13
View File
@@ -17,7 +17,8 @@
#include "EditorContext.h" #include "EditorContext.h"
#include "panels/ViewportPanel.h" // ViewportInputState(成员按值持有) #include "panels/ViewportPanel.h" // ViewportInputState(成员按值持有)
#include <Engine/Core/Types.h> // u32(SelectEntity 位模式转换) #include <Engine/Core/Types.h> // u32(SelectEntity 位模式转换)
#include <Engine/Core/GameFlow.h> // PIE 状态权威(IsPlaying 直查)
#include <entt/entt.hpp> #include <entt/entt.hpp>
@@ -60,19 +61,35 @@ public:
void RequestQuit() { m_ShowQuitConfirm = true; } void RequestQuit() { m_ShowQuitConfirm = true; }
// ---- PIE(Play-In-Editor,UE 主工具栏语义)---- // ---- PIE(Play-In-Editor,UE 主工具栏语义)----
bool IsPlaying() const { return m_Playing; } // 状态权威 = GameFlow(EditorContext::flow);编辑器只是转换的
bool IsPaused() const { return m_Paused; } // 发起者(Request*)与监听器(快照/恢复挂在 →Loading/→Stopped)
// Play:内存快照当前场景 → 切 gameView(Viewport 内嵌 primary 相机 bool IsPlaying() const
// 渲染,UE PIE 语义;不切全屏)→ 解冻时间; {
// Stop:快照原子换血恢复 → 回编辑器渲染 → 冻结时间。 return m_Ctx.flow &&
// 编辑器态默认 timeScale=0(物理冻结 = UE 语义),Play 解冻 (m_Ctx.flow->IsIn(Engine::GameFlowState::Running) ||
m_Ctx.flow->IsIn(Engine::GameFlowState::Paused));
}
bool IsPaused() const
{
return m_Ctx.flow &&
m_Ctx.flow->IsIn(Engine::GameFlowState::Paused);
}
// Play:RequestStart → →Loading 监听器做快照 + 切 gameView(Viewport
// 内嵌 primary 相机渲染,UE PIE 语义;不切全屏)→ 同步加载完成
// CompleteLoading → Running 解冻;Stop:RequestStop → →Stopped 监听器
// 快照原子换血恢复 → 回编辑器渲染。编辑器态默认冻结(GameFlow 构造
// 即 timeScale=0),Play 解冻由 GameFlow 权威处理
void TogglePlay(); void TogglePlay();
// PIE 暂停切换(UE Pause 语义):timeScale 0 ↔ 1 // PIE 暂停切换(UE Pause 语义):Running ↔ Paused(timeScale 由
// GameFlow 权威处理)。运行态才有意义(编辑器态本来就冻结,
// Pause 按钮不出现)
void TogglePause(); void TogglePause();
private: private:
void DoPlay(); // TogglePlay 的 Play 半程(快照 + 切渲染 + 解冻) // GameFlow 监听器(Init 注册):编辑器快照/恢复挂在此处——
void DoStop(); // TogglePlay 的 Stop 半程(恢复 + 回编辑器 + 冻结) // GameFlow 不知场景,转换的副作用由消费端自己挂
void OnFlowEnterLoading(); // →Loading:快照 + 切 gameView + 同步完成加载
void OnFlowEnterStopped(); // →Stopped:快照换血恢复 + 回编辑器渲染
// ---- 选中状态 ---- // ---- 选中状态 ----
// 同步描边:Renderer 落帧后 SelectionMask/Outline 只画选中实体。 // 同步描边:Renderer 落帧后 SelectionMask/Outline 只画选中实体。
@@ -135,9 +152,9 @@ private:
bool m_ShowLoadModal = false; bool m_ShowLoadModal = false;
char m_ScenePathBuf[256] = "scenes/sandbox.scene"; char m_ScenePathBuf[256] = "scenes/sandbox.scene";
// PIE 状态:Play 时场景 YAML 内存快照,Stop 时原子换血恢复 // PIE 镜像态(面板只读指针消费;权威 = GameFlow,Draw 开头同步)
bool m_Playing = false; bool m_PlayingMirror = false;
bool m_Paused = false; // PIE 暂停(timeScale=0;Stop 时复位) bool m_PausedMirror = false;
std::string m_PlaySnapshot; std::string m_PlaySnapshot;
}; };
+3
View File
@@ -9,6 +9,7 @@
#define HELLOVULKAN_EDITORCONTEXT_H #define HELLOVULKAN_EDITORCONTEXT_H
#include <Engine/Core/Application.h> #include <Engine/Core/Application.h>
#include <Engine/Core/GameFlow.h>
#include <Engine/ECS/Scene.h> #include <Engine/ECS/Scene.h>
#include <Engine/Assets/AssetManager.h> #include <Engine/Assets/AssetManager.h>
#include <Engine/Renderer/Renderer.h> #include <Engine/Renderer/Renderer.h>
@@ -28,6 +29,8 @@ struct EditorContext {
Engine::ECS::Scene *scene = nullptr; Engine::ECS::Scene *scene = nullptr;
Engine::Assets::AssetManager *assets = nullptr; Engine::Assets::AssetManager *assets = nullptr;
Engine::Application *app = nullptr; Engine::Application *app = nullptr;
// 游戏流状态机(PIE 状态权威;Editor::Init 注册快照/恢复监听)
Engine::GameFlow *flow = nullptr;
// 编辑器相机(Scene view 专用,归 Renderer 持有——与场景相机实体 // 编辑器相机(Scene view 专用,归 Renderer 持有——与场景相机实体
// 无关;面板经此做 F 焦点/Gizmo 视图/对齐跳转) // 无关;面板经此做 F 焦点/Gizmo 视图/对齐跳转)
Engine::EditorCamera *editorCamera = nullptr; Engine::EditorCamera *editorCamera = nullptr;
+120
View File
@@ -17,6 +17,7 @@
#include "Engine/Renderer/MaterialAsset.h" // 材质覆盖槽(.material 资产) #include "Engine/Renderer/MaterialAsset.h" // 材质覆盖槽(.material 资产)
#include "Engine/Physics/PhysicsWorld.h" // Apply Impulse 调试(GetPhysics 门面) #include "Engine/Physics/PhysicsWorld.h" // Apply Impulse 调试(GetPhysics 门面)
#include "Engine/Scripting/Script.h" // 脚本下拉(ScriptRegistry::Names) #include "Engine/Scripting/Script.h" // 脚本下拉(ScriptRegistry::Names)
#include "Engine/Scripting/ScriptRuntime.h" // 参数编辑 → RefreshParams
#include "Engine/Core/Log.h" // Create Override 失败告警 #include "Engine/Core/Log.h" // Create Override 失败告警
#include <imgui.h> #include <imgui.h>
@@ -38,6 +39,28 @@ namespace
// 材质槽循环索引用(u32 定义于 namespace Engine,App 层显式引入) // 材质槽循环索引用(u32 定义于 namespace Engine,App 层显式引入)
using Engine::u32; using Engine::u32;
// 脚本参数对齐 schema:缺失 → 声明默认补齐;类型不符 → 重置默认
//(换脚本类/手编档防呆)。返回对齐后的参数项指针(每帧幂等)
Engine::Scripting::ScriptParam* EnsureParam(
Engine::ECS::ScriptComponent::Item& item,
const Engine::Scripting::ParamBuilder::Desc& d)
{
auto it = std::find_if(
item.params.begin(), item.params.end(),
[&](const Engine::Scripting::ScriptParam& p)
{ return p.name == d.name; });
if (it == item.params.end())
{
item.params.push_back({d.name, d.def});
return &item.params.back();
}
if (it->value.Type() != d.type)
{
it->value = d.def;
}
return &*it;
}
// 递归收集 assets/ 下模型容器文件(模型槽与动画 attach 槽共用; // 递归收集 assets/ 下模型容器文件(模型槽与动画 attach 槽共用;
// 无法从扩展名区分"模型 fbx"与"纯动画 fbx",一并列出)。 // 无法从扩展名区分"模型 fbx"与"纯动画 fbx",一并列出)。
// 会话级缓存:目录条目几十量级,避免每帧递归扫描 // 会话级缓存:目录条目几十量级,避免每帧递归扫描
@@ -1156,6 +1179,7 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
ImGui::SeparatorText("Movement"); ImGui::SeparatorText("Movement");
ImGui::DragFloat("Max Slope (deg)", &cc->maxSlopeAngle, 1.0f, 1.0f, 89.0f); ImGui::DragFloat("Max Slope (deg)", &cc->maxSlopeAngle, 1.0f, 1.0f, 89.0f);
ImGui::DragFloat("Step Height", &cc->stepHeight, 0.02f, 0.0f, 2.0f); ImGui::DragFloat("Step Height", &cc->stepHeight, 0.02f, 0.0f, 2.0f);
ImGui::DragFloat("Gravity Scale", &cc->gravityScale, 0.1f, 0.0f, 10.0f);
// ---- Debug:遥测(只读,运行时状态在 PhysicsWorld)---- // ---- Debug:遥测(只读,运行时状态在 PhysicsWorld)----
if (auto& phy = ctx.app->GetPhysics(); phy.IsEnabled()) if (auto& phy = ctx.app->GetPhysics(); phy.IsEnabled())
@@ -1249,6 +1273,102 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
{ {
removeIdx = i; removeIdx = i;
} }
// ---- 参数区(按脚本 schema 自动生成;缺省补齐 + 类型
// 防呆;编辑 → RefreshParams 重灌实例成员,PIE 当帧生效)----
if (!item.name.empty() &&
!Engine::Scripting::ScriptRegistry::Schema(item.name)
.empty())
{
const auto& schema =
Engine::Scripting::ScriptRegistry::Schema(item.name);
ImGui::Indent();
bool anyEdited = false;
for (const auto& d : schema)
{
auto* p = EnsureParam(item, d);
bool changed = false;
// 局部变量禁用 f32(glm::f32 歧义前科),用 float
switch (d.type)
{
case Engine::Scripting::ParamType::F32:
{
float v = std::get<float>(p->value.v);
if (ImGui::DragFloat(d.name.c_str(), &v, 0.1f))
{
p->value = Engine::Scripting::ParamValue{v};
changed = true;
}
break;
}
case Engine::Scripting::ParamType::I32:
{
int v = std::get<Engine::i32>(p->value.v);
if (ImGui::DragInt(d.name.c_str(), &v))
{
p->value = Engine::Scripting::ParamValue{
static_cast<Engine::i32>(v)};
changed = true;
}
break;
}
case Engine::Scripting::ParamType::Bool:
changed = ImGui::Checkbox(
d.name.c_str(), &std::get<bool>(p->value.v));
break;
case Engine::Scripting::ParamType::String:
{
char buf[256] = {};
std::snprintf(buf, sizeof(buf), "%s",
std::get<std::string>(p->value.v)
.c_str());
if (ImGui::InputText(d.name.c_str(), buf,
sizeof(buf)))
{
p->value = Engine::Scripting::ParamValue{
std::string(buf)};
changed = true;
}
break;
}
case Engine::Scripting::ParamType::Vec2:
{
const glm::vec2 g =
std::get<glm::vec2>(p->value.v);
float v[2] = {g.x, g.y};
if (ImGui::DragFloat2(d.name.c_str(), v, 0.1f))
{
p->value = Engine::Scripting::ParamValue{
glm::vec2(v[0], v[1])};
changed = true;
}
break;
}
case Engine::Scripting::ParamType::Vec3:
{
const glm::vec3 g =
std::get<glm::vec3>(p->value.v);
float v[3] = {g.x, g.y, g.z};
if (ImGui::DragFloat3(d.name.c_str(), v, 0.1f))
{
p->value = Engine::Scripting::ParamValue{
glm::vec3(v[0], v[1], v[2])};
changed = true;
}
break;
}
}
if (changed)
{
anyEdited = true;
}
}
if (anyEdited)
{
ctx.app->GetScripts().RefreshParams(
static_cast<uint32_t>(entt::to_integral(selected)));
}
ImGui::Unindent();
}
ImGui::PopID(); ImGui::PopID();
// 已填名但注册表查无此类:场景加载早于脚本注册/改名都会走到这 // 已填名但注册表查无此类:场景加载早于脚本注册/改名都会走到这
if (!item.name.empty() && cur == 0) if (!item.name.empty() && cur == 0)
+11 -8
View File
@@ -220,12 +220,12 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
} }
} }
// ---- 工具热键(UE 语义):W/E/R 切 Gizmo 模式、T 切 // ---- 工具热键(UE 语义):Q 纯选择(不出杆)、W/E/R 切
// local/world、F 焦点选中实体。仅 Viewport 持焦时生效—— // Gizmo 模式、T 切 local/world、F 焦点选中实体。仅 Viewport
// 输入框打字(WantTextInput)由 BlockingKeyboard 拦游戏侧, // 持焦时生效——输入框打字(WantTextInput)由 BlockingKeyboard
// 但 ImGui 自身仍收键,故必须限定持焦面板。RMB 按住 = 相机 // 拦游戏侧,但 ImGui 自身仍收键,故必须限定持焦面板。RMB 按
// 飞行模式(键盘归相机),工具热键全部抑制——W 前进与 // 住 = 相机飞行模式(键盘归相机),工具热键全部抑制——W 前
// W 平移 Gizmo 的键位冲突就此解耦 ---- // 进与 W 平移 Gizmo 的键位冲突就此解耦 ----
auto &reg = ctx.scene->GetRegistry(); auto &reg = ctx.scene->GetRegistry();
ECS::TransformComponent *tr = ECS::TransformComponent *tr =
selected != entt::null selected != entt::null
@@ -236,6 +236,9 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
const bool playing = ctx.playState && *ctx.playState; const bool playing = ctx.playState && *ctx.playState;
if (!playing && out.focused && !ImGui::GetIO().WantTextInput && if (!playing && out.focused && !ImGui::GetIO().WantTextInput &&
!ImGui::IsMouseDown(ImGuiMouseButton_Right)) { !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_W)) gizmo.op = ImGuizmo::TRANSLATE;
if (ImGui::IsKeyPressed(ImGuiKey_E)) gizmo.op = ImGuizmo::ROTATE; if (ImGui::IsKeyPressed(ImGuiKey_E)) gizmo.op = ImGuizmo::ROTATE;
if (ImGui::IsKeyPressed(ImGuiKey_R)) gizmo.op = ImGuizmo::SCALE; if (ImGui::IsKeyPressed(ImGuiKey_R)) gizmo.op = ImGuizmo::SCALE;
@@ -370,8 +373,8 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
: "PLAYING - Ctrl+P to stop"); : "PLAYING - Ctrl+P to stop");
} else { } else {
std::snprintf(tip, sizeof(tip), std::snprintf(tip, sizeof(tip),
"Space: %s [T] Snap: Ctrl F: Focus " "Q/W/E/R: Sel/Move/Rot/Scale T: %s "
"Ctrl+Shift+F: To Selection", "Snap: Ctrl F: Focus",
gizmo.localSpace ? "Local" : "World"); gizmo.localSpace ? "Local" : "World");
} }
const ImVec2 imgMin = ImGui::GetItemRectMin(); const ImVec2 imgMin = ImGui::GetItemRectMin();
+2 -2
View File
@@ -29,8 +29,8 @@ struct ViewportInputState {
// op 取 ImGuizmo::OPERATION 位掩码(TRANSLATE=7=三轴位或运算,非 0 // op 取 ImGuizmo::OPERATION 位掩码(TRANSLATE=7=三轴位或运算,非 0
// 起始枚举;0 = 无任何轴位 → Gizmo 完全不绘制),int 存储避免本 // 起始枚举;0 = 无任何轴位 → Gizmo 完全不绘制),int 存储避免本
// 头文件 include ImGuizmo.h(拖入 imgui 宏依赖顺序) // 头文件 include ImGuizmo.h(拖入 imgui 宏依赖顺序)
// W/E/R 切换 op、T 切换 local/world 空间(UE 语义);dragging 拉起 // Q 纯选择(op=0 不出杆)、W/E/R 切换 op、T 切换 local/world 空间
// Editor::BlockingMouse 门控 // (UE 语义);dragging 拉起 Editor::BlockingMouse 门控
struct GizmoState { struct GizmoState {
int op = 7; // ImGuizmo::TRANSLATE(TRANSLATE_X|Y|Z) int op = 7; // ImGuizmo::TRANSLATE(TRANSLATE_X|Y|Z)
bool localSpace = true; // 操纵空间:true=模型轴(旋转/缩放语义), bool localSpace = true; // 操纵空间:true=模型轴(旋转/缩放语义),
+39 -8
View File
@@ -23,9 +23,12 @@
#include <Engine/Core/Stats.h> #include <Engine/Core/Stats.h>
#include <Engine/Renderer/Renderer.h> #include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/EditorCamera.h> #include <Engine/Renderer/EditorCamera.h>
#include <Engine/Serialization/SceneSerializer.h>
#include <Engine/Core/Input.h> #include <Engine/Core/Input.h>
#include <filesystem>
// editor/game 编译期分离(HELLOVULKAN_EDITOR CMake option):editor 形态 // editor/game 编译期分离(HELLOVULKAN_EDITOR CMake option):editor 形态
// 挂接 docking Editor;game 形态 = Direct 直渲 + primary 相机,零编辑器代码 // 挂接 docking Editor;game 形态 = Direct 直渲 + primary 相机,零编辑器代码
#ifdef HELLO_EDITOR #ifdef HELLO_EDITOR
@@ -66,7 +69,9 @@ namespace
InitEditor(); InitEditor();
#else #else
// game 形态:全窗口 Direct 直渲(Renderer 默认即 kDisplayDirect), // game 形态:全窗口 Direct 直渲(Renderer 默认即 kDisplayDirect),
// 相机由场景 primary camera 提供,无 UI // 相机由场景 primary camera 提供,无 UI。
// 游戏流:Stopped → Loading(OnUpdate 泵场景加载)→ Running
GetGameFlow().RequestStart();
#endif #endif
} }
@@ -84,7 +89,31 @@ namespace
GetRenderer().GetEditorCamera().Update(frameTime.UnscaledDt()); GetRenderer().GetEditorCamera().Update(frameTime.UnscaledDt());
} }
#else #else
(void)frameTime; // game 形态:本沙盒无游戏逻辑 // Loading 泵(同步示例):场景文件存在则加载,不存在 =
// 空场景照常进 Running(引擎冒烟路径)。加载完成后必须
// CompleteLoading,否则物理/脚本门控永远跳过(卡死 Loading)
if (GetGameFlow().IsIn(GameFlowState::Loading))
{
if (std::filesystem::exists(m_ScenePath))
{
if (Serialization::SceneSerializer::Deserialize(
GetScene(), GetAssets(), m_ScenePath))
{
ENGINE_INFO("Game: scene loaded from {}", m_ScenePath);
}
else
{
ENGINE_ERROR("Game: scene load failed: {}",
m_ScenePath);
}
}
else
{
ENGINE_WARN("Game: scene file not found ({}), "
"starting empty", m_ScenePath);
}
GetGameFlow().CompleteLoading();
}
#endif #endif
} }
@@ -99,16 +128,14 @@ namespace
// ---- 事件 ---- // ---- 事件 ----
#ifdef HELLO_EDITOR
void OnWindowClose(const Event& event) override void OnWindowClose(const Event& event) override
{ {
#ifdef HELLO_EDITOR
// 退出确认走 Editor 的模态框(ImGui 必须可见才有 UI) // 退出确认走 Editor 的模态框(ImGui 必须可见才有 UI)
SetImGuiEnabled(true); SetImGuiEnabled(true);
m_Editor->RequestQuit(); m_Editor->RequestQuit();
#else
(void)event;
#endif
} }
#endif
void OnKeyPress(const Event& event) override void OnKeyPress(const Event& event) override
{ {
@@ -141,9 +168,10 @@ namespace
ctx.scene = &GetScene(); ctx.scene = &GetScene();
ctx.assets = &GetAssets(); ctx.assets = &GetAssets();
ctx.app = this; ctx.app = this;
ctx.flow = &GetGameFlow();
ctx.editorCamera = &GetRenderer().GetEditorCamera(); ctx.editorCamera = &GetRenderer().GetEditorCamera();
// 继承原沙盒默认:初始暂停(StatsPanel 的 Pause Simulation 开关恢复) // 编辑器态默认冻结:GameFlow 构造即 timeScale=0(Stopped 语义),
Time::SetTimeScale(0.0f); // timeScale 权威在 GameFlow,此处不再手设
m_Editor->Init(ctx); m_Editor->Init(ctx);
// 输入归属:覆盖 EnableImGui 的全局 WantCapture* 谓词 // 输入归属:覆盖 EnableImGui 的全局 WantCapture* 谓词
@@ -158,6 +186,9 @@ namespace
private: private:
std::unique_ptr<Editor> m_Editor = std::make_unique<Editor>(); std::unique_ptr<Editor> m_Editor = std::make_unique<Editor>();
#else
// game 形态:启动场景路径(CWD = bin;编辑器 Save 的产物位置)
const char* m_ScenePath = "assets/scenes/sandbox.scene";
#endif #endif
}; };
} }
+46
View File
@@ -0,0 +1,46 @@
//
// OrbitMove.cpp:参数管线验证样本——实体绕中心点圆周运动。
//
// 参数全链路:DeclareParams 声明(Speed/Radius/Center)→ Inspector
// 自动出控件 → 随场景序列化 → 实例化/RefreshParams 灌值。PIE 中拖
// Radius 当帧生效;Stop 由快照天然回滚。
// 未声明成员(m_Angle)= 纯运行时状态,不进 Inspector 不落盘。
//
#include <Engine/Scripting/Script.h>
#include <glm/glm.hpp>
#include <cmath>
namespace {
class OrbitMove final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "OrbitMove"; }
// 参数声明(Inspector 控件 + 序列化 + 灌值共用 schema)
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
pb.Field("Speed", &m_Speed, 1.0f); // 角速度(弧度/秒)
pb.Field("Radius", &m_Radius, 3.0f); // 轨道半径
pb.Field("Center", &m_Center, glm::vec3{0.0f}); // 轨道中心
}
void OnUpdate(float dt) override {
m_Angle += m_Speed * dt;
// 直接驱动 Transform(相对父空间的圆周;无父 = 世界)
Transform().SetPosition(
m_Center + glm::vec3(std::cos(m_Angle) * m_Radius, 0.0f,
std::sin(m_Angle) * m_Radius));
}
private:
float m_Speed = 1.0f;
float m_Radius = 3.0f;
glm::vec3 m_Center{0.0f};
float m_Angle = 0.0f; // 运行时相位(非参数)
};
ENGINE_REGISTER_SCRIPT(OrbitMove);
} // namespace
+52
View File
@@ -0,0 +1,52 @@
//
// ScriptDebug.h:脚本侧力学调试封装(Unity Gizmos/Debug.DrawRay 的
// 游戏内同位物)。把调试惯例固化成默认值,脚本不用记参数序:
// - width = 像素(≥1),默认 3px
// - duration 恒 0(OnUpdate 每帧重画,事件类需求再裸调 Debug::)
// - depthTest 恒 false(跟角色跑的线被模型遮挡就失去意义)
// - headSize = 世界单位(箭头翼头空间尺寸),默认 0.08
//
#pragma once
#include <Engine/Debug/DebugDraw.h>
#include <glm/glm.hpp>
namespace ScriptDbg {
// 基础箭头:colorHex 语义色见下(重力红/支撑黄/意图蓝/真实绿)
inline void Arrow(const glm::vec3& from, const glm::vec3& to,
Engine::u32 colorHex, float width = 3.0f) {
Engine::Debug::Arrow(from, to, Engine::Debug::Rgb(colorHex),
/*headSize*/ 0.08f, /*width*/ width,
/*duration*/ 0.0f, /*depthTest*/ false);
}
// 重力:anchor=脚底,len=视觉长度。建议 ∝ gravityScale,且与支撑
// 力同长——静置时红(下)黄(上)等长 = 力平衡,一眼可读
inline void Gravity(const glm::vec3& anchor, float len) {
Arrow(anchor, anchor - glm::vec3(0.0f, len, 0.0f), 0xDC3C3C);
}
// 接触反力(法向支撑):只在有接触时存在——空中传 len=0 内部直接
// 不画(力学语义:离地即失支撑)
inline void Support(const glm::vec3& anchor, float len) {
if (len > 1e-3f) {
Arrow(anchor, anchor + glm::vec3(0.0f, len, 0.0f), 0xD9C33C);
}
}
// 加速度(深橙):脚本侧差分出的测量加速度(F=ma 的 a)。scale =
// m/s² → 世界长度换算;mag 低于 minMag 视为静置/匀速("没有线"=
// 没有加速度,本身就是信息),len 钳到 maxLen 防冲击尖峰爆屏
inline void Accel(const glm::vec3& anchor, const glm::vec3& accel,
float scale = 0.05f, float maxLen = 2.0f) {
const float mag = glm::length(accel);
if (mag > 0.1f) {
const float len = glm::min(mag * scale, maxLen);
Arrow(anchor, anchor + (accel / mag) * len, 0xE0640A);
}
}
} // namespace ScriptDbg
+9 -3
View File
@@ -1,10 +1,12 @@
// //
// Spinner.cpp:C++ 原生脚本示范——Y 轴匀速自转。 // Spinner.cpp:C++ 原生脚本示范——Y 轴匀速自转。
// //
// 写脚本三步: // 写脚本四步:
// 1. 新建 .cpp 放本目录(CMake 自动进构建) // 1. 新建 .cpp 放本目录(CMake 自动进构建)
// 2. 继承 ScriptBehavior,覆写 Name/OnCreate/OnUpdate/OnDestroy // 2. 继承 ScriptBehavior,覆写 Name/OnCreate/OnUpdate/OnDestroy
// 3. 文件级 ENGINE_REGISTER_SCRIPT(类名) 自注册——Inspector 下拉 // 3. DeclareParams 声明可调参数(Inspector 自动出控件 + 随场景
// 序列化 + 实例化灌值)
// 4. 文件级 ENGINE_REGISTER_SCRIPT(类名) 自注册——Inspector 下拉
// 与场景序列化按该名解析 // 与场景序列化按该名解析
// //
@@ -18,6 +20,10 @@ namespace {
public: public:
const char* Name() const override { return "Spinner"; } const char* Name() const override { return "Spinner"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
pb.Field("Speed", &m_Speed, 90.0f); // 度/秒
}
void OnCreate() override { void OnCreate() override {
// 实体与组件已就位;需要别的组件时用 Get<T>()(不存在会炸) // 实体与组件已就位;需要别的组件时用 Get<T>()(不存在会炸)
} }
@@ -31,7 +37,7 @@ namespace {
} }
private: private:
float m_Speed = 90.0f; // 度/秒 float m_Speed = 90.0f; // 度/秒(声明默认;实例化期被存档值覆盖)
}; };
ENGINE_REGISTER_SCRIPT(Spinner); ENGINE_REGISTER_SCRIPT(Spinner);
+174 -15
View File
@@ -1,10 +1,13 @@
// //
// WASDMove.cpp:WASD 控制实体移动(相机水平朝向参考,UE gameplay 语义)。 // WASDMove.cpp:WASD 控制实体移动(相机水平朝向参考,UE gameplay 语义)。
// //
// 移动语义分派: // 移动语义分派(按实体配置自动选择,优先级从上到下):
// - CharacterControllerComponent → 角色控制器路径(扫掠运动学:
// 水平喂意图速度 / 重力自动积分 / Space 跳跃,Unity
// CharacterController.Move 同构)
// - Dynamic 刚体 → 走物理速度门面(每帧喂水平速度,Y 归物理—— // - Dynamic 刚体 → 走物理速度门面(每帧喂水平速度,Y 归物理——
// 重力/跳跃不受影响;松键喂零速 = 立即停,Unity velocity 语义) // 重力/跳跃不受影响;松键喂零速 = 立即停,Unity velocity 语义)
// - 无刚体 → 直接平移 Transform // - 都没有 → 直接平移 Transform
// //
// 编辑器形态键位天然不冲突:RMB 按住 = 相机吃 WASD 飞行(脚本也读 // 编辑器形态键位天然不冲突:RMB 按住 = 相机吃 WASD 飞行(脚本也读
// 到按键 → 同时在动,测移动时松开 RMB 即可);游戏形态 WASD 常开。 // 到按键 → 同时在动,测移动时松开 RMB 即可);游戏形态 WASD 常开。
@@ -12,14 +15,18 @@
#include <Engine/Scripting/Script.h> #include <Engine/Scripting/Script.h>
#include <Engine/Core/Application.h> #include <Engine/Core/Application.h>
#include <Engine/Physics/PhysicsWorld.h>
#include <Engine/Core/Input.h> #include <Engine/Core/Input.h>
#include <Engine/Debug/DebugDraw.h>
#include "ScriptDebug.h"
#include <Engine/Renderer/Renderer.h> #include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/EditorCamera.h> #include <Engine/Renderer/EditorCamera.h>
#include <Engine/Physics/PhysicsWorld.h>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp> #include <glm/gtc/quaternion.hpp>
#include <cmath>
using namespace Engine; // Input 门面(统一输入入口) using namespace Engine; // Input 门面(统一输入入口)
namespace { namespace {
@@ -28,10 +35,27 @@ namespace {
public: public:
const char* Name() const override { return "WASDMove"; } const char* Name() const override { return "WASDMove"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
pb.Field("Speed", &m_Speed, 3.0f); // 走路 m/s
pb.Field("RunSpeed", &m_RunSpeed, 6.0f); // 跑步 m/s
pb.Field("TurnResponse", &m_TurnResponse, 12.0f); // 转向阻尼
pb.Field("AccelResponse", &m_AccelResponse, 8.0f); // 加速/转向响应
pb.Field("StopResponse", &m_StopResponse, 14.0f); // 松手制动响应
pb.Field("JumpSpeed", &m_JumpSpeed, 6.0f); // 起跳初速 m/s
// clip 按名引用(Unity anim.Play("Walk") 语义)——名字在
// Inspector 里改,attach 顺序变了脚本零改动
pb.Field("IdleClip", &m_IdleClip, std::string("idle"));
pb.Field("WalkClip", &m_WalkClip, std::string("walk"));
pb.Field("RunClip", &m_RunClip, std::string("run"));
}
void OnUpdate(float dt) override { void OnUpdate(float dt) override {
// 位移方向 = 视线水平投影(W 前进 / A 左 / D 右),UE Scene // 位移方向 = 视线水平投影(W 前进 / A 左 / D 右),UE Scene
// view 移动惯例;Y 锁 0(升降/跳跃归物理或将来扩展)。 // view 移动惯例;Y 锁 0(升降/跳跃归物理或将来扩展)。
// 键鼠查询统一走 Input 门面(脚本层零 GLFW 依赖) // 输入直读按键(Unity Input.GetKey(KeyCode.W) 同形);UI 捕获
// 键盘时门面静默。要手柄/运行时改键再换动作层
//(Input::ActionValue2("Move")——Unity6 新 Input System /
// UE5 Enhanced Input 对应物),本示例保持最简
const glm::quat cq = App().GetRenderer() const glm::quat cq = App().GetRenderer()
.GetEditorCamera() .GetEditorCamera()
.GetTransform() .GetTransform()
@@ -39,32 +63,167 @@ namespace {
const glm::vec3 fwd = cq * glm::vec3(0.0f, 0.0f, -1.0f); const glm::vec3 fwd = cq * glm::vec3(0.0f, 0.0f, -1.0f);
const glm::vec3 right = cq * glm::vec3(1.0f, 0.0f, 0.0f); const glm::vec3 right = cq * glm::vec3(1.0f, 0.0f, 0.0f);
glm::vec3 move(0.0f); // 约定:X+ = 右(D),Y- = 前(W);方向合成 = fwd*(-Y) + right*(+X)
if (Input::IsKeyDown(Input::Key::W)) move += fwd; glm::vec2 mv(0.0f);
if (Input::IsKeyDown(Input::Key::S)) move -= fwd; if (Input::IsKeyDown(Input::Key::W)) mv.y -= 1.0f;
if (Input::IsKeyDown(Input::Key::D)) move += right; if (Input::IsKeyDown(Input::Key::S)) mv.y += 1.0f;
if (Input::IsKeyDown(Input::Key::A)) move -= right; if (Input::IsKeyDown(Input::Key::A)) mv.x -= 1.0f;
if (Input::IsKeyDown(Input::Key::D)) mv.x += 1.0f;
if (glm::length(mv) > 1.0f) mv = glm::normalize(mv);
glm::vec3 move = fwd * (-mv.y) + right * mv.x;
move.y = 0.0f; move.y = 0.0f;
if (glm::length(move) > 1e-4f) { if (glm::length(move) > 1e-4f) {
move = glm::normalize(move); move = glm::normalize(move);
} }
const glm::vec3 vel = move * m_Speed; // 走/跑
const bool moving = glm::length(mv) > 0.1f;
const bool running =
moving && Input::IsKeyDown(Input::Key::LeftShift);
const float speed = running ? m_RunSpeed : m_Speed;
const glm::vec3 desired = move * speed; // 期望速度(输入意图)
if (auto* rb = TryGetComponent<Engine::ECS::RigidBodyComponent>(); auto& phy = App().GetPhysics();
const bool hasCC =
TryGetComponent<Engine::ECS::CharacterControllerComponent>() !=
nullptr;
const bool grounded =
hasCC ? phy.IsCharacterGrounded(EntityBits()) : true;
// ---- 动量整形(惯性的本体,纯脚本):真实速度逐帧逼近期
// 望速度——起步渐快、转向沿弧线漂移、折返经零减速再反向、
// 松手滑行刹车。加速/转向用 AccelResponse,松手用更硬的
// StopResponse(角色要"想停就停");空中操纵性降到 1/4
//(起跳后不宜急转弯)。指数逼近,帧率无关
float response = moving ? m_AccelResponse : m_StopResponse;
if (!grounded) {
response *= 0.25f;
}
m_PlanarVel +=
(desired - m_PlanarVel) * (1.0f - std::exp(-response * dt));
if (glm::length(desired - m_PlanarVel) < 0.02f) {
m_PlanarVel = desired; // 贴齐,防低速蠕动
}
// ---- 路径 1:角色控制器(挂了 CharacterControllerComponent)----
// 水平喂动量整形后的真实速度;Y 保留现值——重力由控制器每
// 步自动积分,每帧覆写 Y 会把下落速度清零(空中变悬浮)
if (hasCC) {
const glm::vec3 cur = phy.GetCharacterVelocity(EntityBits());
phy.SetCharacterVelocity(
EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z});
// 跳跃:着地 + 上升沿触发。IsKeyDown 是电平量(按住
// 每帧都真),不设边沿会变成贴地火箭;IsGrounded 防
// 空中连跳
const bool jumpDown = Input::IsKeyDown(Input::Key::Space);
if (jumpDown && !m_JumpWasDown &&
phy.IsCharacterGrounded(EntityBits())) {
phy.Jump(EntityBits(), m_JumpSpeed);
}
m_JumpWasDown = jumpDown;
} else if (auto* rb =
TryGetComponent<Engine::ECS::RigidBodyComponent>();
rb != nullptr && rb->bodyValid && rb != nullptr && rb->bodyValid &&
rb->type == Engine::ECS::RigidBodyComponent::BodyType::Dynamic) { rb->type == Engine::ECS::RigidBodyComponent::BodyType::Dynamic) {
// 物理驱动:只覆写水平速度,Y 保留物理积分离散结果 // 物理驱动:只覆写水平速度,Y 保留物理积分离散结果
//(整体喂 0 会抵消重力 → 空中物体悬浮) //(整体喂 0 会抵消重力 → 空中物体悬浮)
auto& phy = App().GetPhysics();
const glm::vec3 cur = phy.GetLinearVelocity(EntityBits()); const glm::vec3 cur = phy.GetLinearVelocity(EntityBits());
phy.SetLinearVelocity(EntityBits(), {vel.x, cur.y, vel.z}); phy.SetLinearVelocity(
EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z});
} else { } else {
Transform().SetPosition(Transform().GetPosition() + vel * dt); Transform().SetPosition(Transform().GetPosition() +
m_PlanarVel * dt);
} }
// ---- 朝向:面向"真实速度"方向(不是输入方向,模型默认 +Z)----
// 这是消"平移感"的关键:动量整形后速度滞后于输入,若面朝
// 输入方向,身体朝前脚却在侧滑——正是割裂感的来源。面朝
// 真实速度 = 脚底朝向与位移一致,转向呈现弧线漂移。速度
// 低于阈值(起步瞬间/折返过零)锁定上帧朝向:折返表现成
// U 弯回转而不是原地瞬转。只取水平面(角色不俯仰翻滚)
if (glm::length(m_PlanarVel) > 0.3f) {
const float targetYaw =
std::atan2(m_PlanarVel.x, m_PlanarVel.z);
const glm::vec3 modelFwd =
Transform().GetRotationQuat() * glm::vec3(0.0f, 0.0f, 1.0f);
const float curYaw = std::atan2(modelFwd.x, modelFwd.z);
const float dyaw = std::atan2(std::sin(targetYaw - curYaw),
std::cos(targetYaw - curYaw));
const float k = 1.0f - std::exp(-m_TurnResponse * dt);
Transform().SetRotationQuat(
glm::angleAxis(curYaw + dyaw * k, glm::vec3(0, 1, 0)));
}
// ---- 动画:idle/walk/run 三态切换(Direct 模式 crossfade)----
// 目标 clip 是"电平量":Shift 中途按下/松开都要能换,所以
// 每帧重算目标,只在目标与上帧不同才 Play(边沿)。每帧无
// 条件 Play 会不停重置播放头,动画永远播不完
const std::string& target = !moving ? m_IdleClip : (running ? m_RunClip : m_WalkClip);
if (target != m_CurClip) {
if (auto* anim = TryGetComponent<Engine::ECS::AnimationComponent>();
anim != nullptr && anim->Play(target, 0.35f)) {
// fade 0.35s:locomotion 混合的经验值(Unity 默认
// 0.25 太短,0.5+ 显拖沓);相位同步在引擎侧做
m_CurClip = target; // 播放成功才记账(模型未就绪下帧重试)
}
}
const glm::vec3 anchor =
(hasCC ? phy.GetCharacterPosition(EntityBits())
: Transform().GetPosition()) +
glm::vec3(0.0f, 0.05f, 0.0f);
// ---- 运动线(ScriptDbg 封装:像素线宽 3px / 当帧瞬时 /
// 透视直显已固化为默认值,不用记参数序):蓝 = 输入期
// 望速度,绿 = 动量整形后的真实速度——同一起点,夹角
// 即惯性滞量(转向弧线/U 弯/刹车可视化)
ScriptDbg::Arrow(anchor, anchor + desired * 0.2f, 0x2B5FD0);
ScriptDbg::Arrow(anchor, anchor + m_PlanarVel * 0.2f, 0x1E9E42);
// ---- 力学线:重力(红,下)+ 接触反力(黄,上,仅着地)。
// 长度 ∝ gravityScale:静置时红黄等长 = 力平衡;起跳
// 离地黄箭头消失 = 离地即失支撑;gravityScale 调大两
// 箭同步变长(重量感可视化)
float gs = 1.0f;
if (auto* cc = TryGetComponent<
Engine::ECS::CharacterControllerComponent>()) {
gs = cc->gravityScale;
}
const float gLen = 0.5f * gs;
ScriptDbg::Gravity(anchor, gLen);
ScriptDbg::Support(anchor, grounded ? gLen : 0.0f);
const glm::vec3 vel = hasCC
? phy.GetCharacterVelocity(EntityBits())
: m_PlanarVel;
if (dt > 1e-5f) {
m_AccelS = glm::mix(m_AccelS, (vel - m_PrevVel) / dt, 0.35f);
}
m_PrevVel = vel;
ScriptDbg::Accel(anchor, m_AccelS);
}
void DebugShow() {
} }
private: private:
float m_Speed = 5.0f; // m/s float m_Speed = 3.0f; // 走路 m/s(声明默认;存档值覆盖)
float m_RunSpeed = 6.0f; // 跑步 m/s(Shift 按住)
float m_TurnResponse = 12.0f; // 转向阻尼强度(越大越跟手)
float m_AccelResponse = 8.0f; // 加速/转向响应(动量整形,越小越"重")
float m_StopResponse = 14.0f; // 松手制动响应(越大刹车越硬)
glm::vec3 m_PlanarVel{0.0f}; // 平滑后的水平真实速度(动量状态)
float m_JumpSpeed = 6.0f; // 起跳初速(配合组件 gravityScale 调手感)
std::string m_IdleClip = "idle"; // clip 名(Inspector 可改)
std::string m_WalkClip = "walk";
std::string m_RunClip = "run";
std::string m_CurClip = "idle"; // 当前目标 clip(边沿检测基准)
bool m_JumpWasDown = false; // 上帧跳跃键(上升沿检测)
glm::vec3 m_PrevVel{0.0f}; // 上帧测量速度(加速度差分基准)
glm::vec3 m_AccelS{0.0f}; // 平滑测量加速度(EMA 0.35,调试图)
}; };
ENGINE_REGISTER_SCRIPT(WASDMove) ENGINE_REGISTER_SCRIPT(WASDMove)
+5
View File
@@ -24,6 +24,7 @@
#include <Engine/Core/Types.h> #include <Engine/Core/Types.h>
#include <Engine/Core/Event.h> #include <Engine/Core/Event.h>
#include <Engine/Core/GameFlow.h>
#include <Engine/Core/Time.h> #include <Engine/Core/Time.h>
#include <chrono> #include <chrono>
#include <string> #include <string>
@@ -75,6 +76,7 @@ namespace Engine {
Assets::AssetManager& GetAssets() const { return *m_Assets; } Assets::AssetManager& GetAssets() const { return *m_Assets; }
PhysicsWorld& GetPhysics() const { return *m_Physics; } PhysicsWorld& GetPhysics() const { return *m_Physics; }
Scripting::ScriptRuntime& GetScripts() const { return *m_Scripts; } Scripting::ScriptRuntime& GetScripts() const { return *m_Scripts; }
GameFlow& GetGameFlow() { return m_GameFlow; }
u64 GetFrameCount() const { return m_FrameCount; } u64 GetFrameCount() const { return m_FrameCount; }
// ImGui相关 // ImGui相关
@@ -127,6 +129,9 @@ namespace Engine {
// 其后端销毁需要 Device 与 GLFWwindow 存活) // 其后端销毁需要 Device 与 GLFWwindow 存活)
Scope<ImGuiLayer> m_ImGuiLayer; Scope<ImGuiLayer> m_ImGuiLayer;
// 游戏流状态机(构造即冻结:Stopped → timeScale=0)
GameFlow m_GameFlow;
bool m_Running = false; bool m_Running = false;
u64 m_FrameCount = 0; u64 m_FrameCount = 0;
+71
View File
@@ -0,0 +1,71 @@
//
// GameFlow.h:游戏流状态机(引擎层唯一的"游戏在干什么"权威)。
//
// 状态:Stopped(编辑态/未启动)→ Loading(加载中)→ Running ⇄ Paused,
// 任意活动态可 RequestStop 回 Stopped。转换矩阵外的请求拒绝 + warn。
//
// 职责边界:
// - timeScale 单一权威:每次转换统一设 Time::SetTimeScale
// (Running/Loading→1,Paused/Stopped→0)。上层(编辑器/游戏)
// 不再自行碰 timeScale——冻结语义由状态推导,不再散落
// - 状态转换广播:AddListener 注册监听(组合根/编辑器 Init 期注册,
// 非每帧路径)。编辑器在 →Loading 挂场景快照、→Stopped 挂恢复;
// 脚本/系统将来同样可订阅(跨系统解耦)
// - 不知场景不知资产:加载本身(读文件/反序列化)是调用方的事,
// 本类只管状态与转换纪律(CompleteLoading 由加载方显式调)
//
// 与主循环的契约(Application::Run):
// - Input::Poll 仅 Running 激活(Paused/Stopped 动作静默)
// - 物理步进仅 Loading 跳过(编辑态对账照跑,停摆靠 dt=0)
// - 脚本仅 Running 执行(Unity Play Mode / UE PIE 语义——脚本副
// 作用不依赖 dt,编辑态不得运行);→Stopped 由监听 EndPlay 终
// 局脚本实例(组合根注册,先于编辑器回滚监听)
//
#ifndef HELLOVULKAN_GAMEFLOW_H
#define HELLOVULKAN_GAMEFLOW_H
#include "Engine/Core/Types.h"
#include <functional>
#include <vector>
namespace Engine {
enum class GameFlowState : u8 {
Stopped, // 编辑态 / 未启动(timeScale=0)
Loading, // 加载中(物理/脚本步进跳过;timeScale=1 无消费方)
Running, // 游戏运行(timeScale=1)
Paused, // 暂停(timeScale=0;编辑器 Pause / 游戏菜单挂起)
};
class GameFlow {
public:
// (from, to);注册期 = 组合根/编辑器 Init,非每帧路径
using Listener = std::function<void(GameFlowState, GameFlowState)>;
GameFlow(); // 构造即冻结(Stopped 语义:timeScale=0)
void AddListener(Listener l) { m_Listeners.push_back(std::move(l)); }
GameFlowState State() const { return m_State; }
bool IsIn(GameFlowState s) const { return m_State == s; }
// ---- 转换请求(矩阵外拒绝 + warn,返回是否生效)----
bool RequestStart(); // Stopped → Loading
void CompleteLoading(); // Loading → Running(加载方显式调)
bool RequestPause(); // Running → Paused
bool RequestResume(); // Paused → Running
bool RequestStop(); // Loading/Running/Paused → Stopped
private:
// 立即生效转换:设 timeScale + 广播(同既有 DoPlay 同步语义)
void Transition(GameFlowState to);
GameFlowState m_State = GameFlowState::Stopped;
std::vector<Listener> m_Listeners;
};
} // namespace Engine
#endif //HELLOVULKAN_GAMEFLOW_H
+32
View File
@@ -18,6 +18,7 @@
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include "Engine/Core/Types.h" #include "Engine/Core/Types.h"
#include "Engine/Input/InputAction.h"
namespace Engine { namespace Engine {
@@ -103,6 +104,37 @@ namespace Engine {
// 滚轮增量(上行 = +1,一档 ≈1):读后清零。UI 捕获鼠标 // 滚轮增量(上行 = +1,一档 ≈1):读后清零。UI 捕获鼠标
//(面板悬停)时不累积——滚轮归 ImGui //(面板悬停)时不累积——滚轮归 ImGui
f32 ConsumeScrollDelta(); 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 } // namespace Engine
+15
View File
@@ -17,6 +17,16 @@ struct GLFWwindow;
namespace Engine { 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 { class Window {
public: public:
Window(const std::string& title, u32 width, u32 height); Window(const std::string& title, u32 width, u32 height);
@@ -76,6 +86,11 @@ namespace Engine {
void SetCursorCaptured(bool captured); void SetCursorCaptured(bool captured);
bool IsCursorCaptured() const { return m_CursorCaptured; } bool IsCursorCaptured() const { return m_CursorCaptured; }
// 手柄轮询(JOYSTICK_1 标准游戏布局):连接且带标准映射时填充
// state 并返回 true;否则置 connected=false 全零并返回 false。
// 无分配(GLFW 平台态拉取),每帧调用成本可忽略
bool PollGamepad(GamepadState& state) const;
private: private:
+139
View File
@@ -0,0 +1,139 @@
//
// DebugDraw:引擎级通用调试图元服务(Unity Debug.DrawLine / UE
// DrawDebugHelpers 的对应物)。
//
// 分层定位:全局静态 API,任何层(脚本/物理/动画/AI)调用即收集;
// Renderer 每帧取走缓冲,PhysicsDebugPass 统一上传叠加渲染。引擎侧
// 只写这一次,之后一切新可视化 = 调用方加几行,不再碰引擎代码。
//
// 语义对齐 Unity/UE:
// duration <= 0 → 只画当前帧(调用方每帧重调,Unity DrawLine 同)
// duration > 0 → 存活 duration 秒(内部持久列表,期间无需重调;
// 固定步长系统用 2×步长 即可跨渲染帧无缝续接)
// depthTest=false → 透视遮挡直显(UE DrawDebugLine bPersistent 位)
// width → 线宽(需 wideLines 特性,设备不支持时钳回 1.0)
//
// 线程模型:主线程单写单读(脚本 OnUpdate 收集 → Renderer 上传 →
// EndFrame 翻页)。
// 预算:kMaxVerts 截断 + 一次告警(调试图元打满是滥用信号)。
//
#ifndef HELLOVULKAN_DEBUGDRAW_H
#define HELLOVULKAN_DEBUGDRAW_H
#include <Engine/Core/Types.h>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <vector>
namespace Engine::Debug {
// 顶点(28B):与 PhysicsDebugVertex 同布局——渲染 pass 复用同一
// 顶点输入态(pos vec3 + color vec4)
struct Vertex {
glm::vec3 pos;
glm::vec4 color;
};
// 渲染批次:同 (拓扑, 线宽, 深度测试) 的连续顶点段。调用方按
// 参数自动分组,draw 侧每批一次状态切换(动态状态零管线对象)
struct Batch {
bool tris; // true = triangleList / false = lineList
f32 width;
bool depthTest;
u32 first; // 顶点偏移(顶点数,非字节)
u32 count;
};
// 单帧收集结果(Renderer 每帧消费,勿手动修改)
struct DebugFrame {
std::vector<Vertex> verts;
std::vector<Batch> batches;
void Clear() { verts.clear(); batches.clear(); }
bool Empty() const { return batches.empty(); }
};
// 顶点预算(每渲染帧):调试线框足够用,溢出截断 + 告警一次
inline constexpr u32 kMaxVerts = 1 << 16;
// ---- 全局开关 ----
void SetEnabled(bool on);
bool IsEnabled();
void Clear(); // 清空瞬时 + 持久图元(PIE 停止/场景切换时调用)
// ---- 线类(全部世界空间)----
void Line(const glm::vec3& a, const glm::vec3& b,
const glm::vec4& color,
f32 width = 1.0f, f32 duration = 0.0f,
bool depthTest = true);
// Ray:from 出发沿单位方向 dirNorm 画 length 长(Unity DrawRay 同形)
void Ray(const glm::vec3& from, const glm::vec3& dirNorm, f32 length,
const glm::vec4& color,
f32 width = 1.0f, f32 duration = 0.0f, bool depthTest = true);
// 箭头:主干 + 两翼头(正交基自动构建,零向量方向安全退化成点)
void Arrow(const glm::vec3& from, const glm::vec3& to,
const glm::vec4& color, f32 headSize = 0.12f,
f32 width = 1.0f, f32 duration = 0.0f,
bool depthTest = true);
// 十字标记(Unity Debug.DrawCross 同位)
void Cross(const glm::vec3& pos, f32 size, const glm::vec4& color,
f32 width = 1.0f, f32 duration = 0.0f,
bool depthTest = true);
// ---- 形状线框 ----
// 三大圆经线圈(UE DrawDebugSphere 同形)
void WireSphere(const glm::vec3& center, f32 radius,
const glm::vec4& color, int segments = 16,
f32 width = 1.0f, f32 duration = 0.0f,
bool depthTest = true);
// 朝向 normal 的平面圆
void WireCircle(const glm::vec3& center, f32 radius,
const glm::vec3& normal, const glm::vec4& color,
int segments = 24,
f32 width = 1.0f, f32 duration = 0.0f,
bool depthTest = true);
// 轴对齐 / 旋转盒(rot 单位四元数 = AABB;UE DrawDebugBox 同形)
void WireBox(const glm::vec3& center, const glm::vec3& halfExtents,
const glm::vec4& color,
const glm::quat& rot = glm::quat(1.0f, 0.0f, 0.0f, 0.0f),
f32 width = 1.0f, f32 duration = 0.0f,
bool depthTest = true);
// 胶囊线框:轴向 local +Y(Jolt 惯例),halfHeight = 圆柱段半长
//(总高 = 2×(halfHeight+radius),与引擎 CharacterController 参数一致)
void WireCapsule(const glm::vec3& center, f32 radius, f32 halfHeight,
const glm::vec4& color,
const glm::quat& rot = glm::quat(1.0f, 0.0f, 0.0f, 0.0f),
f32 width = 1.0f, f32 duration = 0.0f,
bool depthTest = true);
// ---- 便捷色:0xRRGGBB → 线性 HDR vec4(可 >1 过 tonemap 依旧醒目)----
constexpr glm::vec4 Rgb(u32 hex, f32 alpha = 1.0f)
{
constexpr f32 kInv = 1.0f / 255.0f;
return {(static_cast<f32>((hex >> 16) & 0xFF)) * kInv,
(static_cast<f32>((hex >> 8) & 0xFF)) * kInv,
(static_cast<f32>(hex & 0xFF)) * kInv,
alpha};
}
// ---- Renderer 消费端(业务代码勿调用)----
// 取当前帧缓冲(禁用/空 = Empty)。Renderer 上传窗口调用
const DebugFrame& Frame();
// 翻页:清瞬时缓冲 + 清过期持久项。Renderer 上传完成后调用
void EndFrame();
} // namespace Engine::Debug
#endif //HELLOVULKAN_DEBUGDRAW_H
+46
View File
@@ -12,6 +12,7 @@
#include <Engine/Core/UUID.h> #include <Engine/Core/UUID.h>
#include <Engine/Core/AssetRef.h> #include <Engine/Core/AssetRef.h>
#include <Engine/Renderer/Camera.h> #include <Engine/Renderer/Camera.h>
#include <Engine/Scripting/ScriptParam.h>
#include <entt/entt.hpp> #include <entt/entt.hpp>
@@ -233,6 +234,10 @@ namespace Engine::ECS {
f32 fadeElapsed = 0.0f; // 已过渡时长 f32 fadeElapsed = 0.0f; // 已过渡时长
f32 fadeDuration = 0.3f; // 过渡总时长(秒) f32 fadeDuration = 0.3f; // 过渡总时长(秒)
// 按名播放的运行时反指(瞬态,不落盘不序列化):AnimationSystem
// 每帧回填(同 RigidBody::bodyValid 模式)。空 = 模型未就绪
const Model* runtimeModel = nullptr;
// ---- 资产引用(序列化落盘)---- // ---- 资产引用(序列化落盘)----
// 外部动画资产(编辑器 attach 的动画容器文件,随场景持久化)。 // 外部动画资产(编辑器 attach 的动画容器文件,随场景持久化)。
// clips 本体运行时并入 Model 资产(共享);重启后由 Inspector // clips 本体运行时并入 Model 资产(共享);重启后由 Inspector
@@ -253,6 +258,16 @@ namespace Engine::ECS {
std::unordered_map<std::string, bool> boolParams; std::unordered_map<std::string, bool> boolParams;
std::vector<std::string> pendingTriggers; // 消费即清(瞬态) std::vector<std::string> pendingTriggers; // 消费即清(瞬态)
// 阻尼参数推进状态(瞬态,不落盘——重启从 floatParams 当前值
// 重新起步):current 逐帧指数逼近 target,写回 floatParams 供
// 状态机条件统一读取
struct DampedParam {
float current = 0.0f;
float target = 0.0f;
float response = 8.0f; // 逼近速率(1/s,越大越快)
};
std::unordered_map<std::string, DampedParam> dampedParams;
// 状态机瞬态(不落盘,重启从 entry state 开始) // 状态机瞬态(不落盘,重启从 entry state 开始)
std::string currentState; // 空 = 未初始化(System 首帧置 entry) std::string currentState; // 空 = 未初始化(System 首帧置 entry)
f32 stateTime = 0.0f; // 当前状态播放头(秒) f32 stateTime = 0.0f; // 当前状态播放头(秒)
@@ -266,6 +281,22 @@ namespace Engine::ECS {
{ {
floatParams[name] = value; floatParams[name] = value;
} }
// 阻尼变体(Unity SetFloat(name, target, dampTime) 同构):登记
// 目标值,AnimationSystem Controller 驱动期逐帧指数逼近(帧率
// 无关)。首次登记从 floatParams 当前值起步(无则取目标),
// 避免跳变。与 SetFloat 同名混用 = 直接 set 会被下一帧阻尼值
// 覆盖,别混
void SetFloatDamped(const std::string& name, float target, float response)
{
auto [it, inserted] = dampedParams.try_emplace(name);
it->second.target = target;
it->second.response = glm::max(response, 0.01f);
if (inserted) {
const auto fp = floatParams.find(name);
it->second.current =
fp != floatParams.end() ? fp->second : target;
}
}
void SetBool(const std::string& name, bool value) void SetBool(const std::string& name, bool value)
{ {
boolParams[name] = value; boolParams[name] = value;
@@ -309,6 +340,13 @@ namespace Engine::ECS {
time = 0.0f; time = 0.0f;
playing = true; playing = true;
} }
// 按名播放(Unity Animator.Play("Walk") 同形,推荐):clip 名经
// runtimeModel 的 clip 表解析,消掉下标硬耦合(attach 顺序变了
// 脚本不改)。未命中/模型未就绪返回 false。实现住
// AnimationSystem.cpp——需要 Model 完整类型,组件头保持零资产
// 依赖(Model.h 拖 vulkan/assimp,进本头会污染全部包含者)
bool Play(const std::string& clipName, f32 fadeSeconds = 0.3f);
}; };
// 相机。一个场景中 primary=true 的实体是当前渲染视角。 // 相机。一个场景中 primary=true 的实体是当前渲染视角。
@@ -430,6 +468,9 @@ namespace Engine::ECS {
f32 mass = 70.0f; f32 mass = 70.0f;
f32 maxSlopeAngle = 45.0f; // 度:可站立的最大坡角 f32 maxSlopeAngle = 45.0f; // 度:可站立的最大坡角
f32 stepHeight = 0.4f; // 台阶自动攀爬高(WalkStairs) f32 stepHeight = 0.4f; // 台阶自动攀爬高(WalkStairs)
// 重力倍率(UE CharacterMovement.GravityScale 同位):真实 9.81
// 对人形角色偏"棉花糖"(滞空 1.2s+),游戏手感普遍 2~3 倍
f32 gravityScale = 2.0f;
bool operator==(const CharacterControllerComponent&) const = default; bool operator==(const CharacterControllerComponent&) const = default;
// 实体原点 = 脚底(胶囊底端),符合角色 pivot 习惯 // 实体原点 = 脚底(胶囊底端),符合角色 pivot 习惯
@@ -444,6 +485,11 @@ namespace Engine::ECS {
struct Item { struct Item {
std::string name; std::string name;
bool active = true; // 禁用 = 实例保留但停 OnUpdate bool active = true; // 禁用 = 实例保留但停 OnUpdate
// 参数(权威数据,随场景序列化)。脚本成员 = 运行时真值,
// 实例化期由 ScriptRuntime 按 schema 灌入:键缺失 → 声明
// 默认,多余键忽略;运行时 SetParam 写穿回这里(PIE 实时
// 反映,Stop 由快照天然回滚)
std::vector<Engine::Scripting::ScriptParam> params;
bool operator==(const Item&) const = default; bool operator==(const Item&) const = default;
}; };
+70
View File
@@ -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
+88
View File
@@ -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
@@ -99,6 +99,9 @@ namespace Engine {
void Jump(u32 entityBits, f32 speed); // 垂直速度置 speed void Jump(u32 entityBits, f32 speed); // 垂直速度置 speed
bool IsCharacterGrounded(u32 entityBits) const; bool IsCharacterGrounded(u32 entityBits) const;
glm::vec3 GetCharacterVelocity(u32 entityBits) const; glm::vec3 GetCharacterVelocity(u32 entityBits) const;
// 角色脚底位置(物理权威世界坐标):调试图元/特效锚点用它,
// 而不是 Transform().GetPosition()——后者在有父级时是本地坐标
glm::vec3 GetCharacterPosition(u32 entityBits) const;
private: private:
// 创建时缓存的参数(变更检测 → 重建 body) // 创建时缓存的参数(变更检测 → 重建 body)
@@ -1,25 +1,28 @@
// //
// PhysicsDebugPass:物理调试图元叠加(collider 线框/约束/重力/速度/ // PhysicsDebugPass:调试图元叠加 pass(双源)。
// 加速度箭头)。 // 物理源 = PhysicsWorld 收集的 PhysicsDebugFrame(collider 线框/
// 约束,Application 注入帧缓冲指针)
// 通用源 = Engine::Debug 调试服务(脚本/任意层的 Line/Arrow/Wire*,
// Unity Debug.DrawLine 同位;批次自带线宽/深度测试/拓扑)
// //
// 画在 HDRColor(Forward 之后、TAA 之前):调试图元进入场景主链路—— // 画在 HDRColor(Forward 之后、TAA 之前):调试图元进入场景主链路——
// TAA 时域累积抗锯齿、Bloom/Tonemap 正常消费,kDebugTransparent 诊断 // TAA 时域累积抗锯齿、Bloom/Tonemap 正常消费,kDebugTransparent 诊断
// 模式下也可见。深度只测不写(被不透明几何正确遮挡),无混合直写 // 模式下也可见。深度只测不写(被不透明几何正确遮挡),无混合直写
// (fullscreen pass 状态泄漏教训)。 // (fullscreen pass 状态泄漏教训)。
// //
// 数据源 = PhysicsWorld 每帧收集的 PhysicsDebugFrame(Application 把 // 空帧零绘制:两源顶点数均 0 时提前短路。
// PhysicsWorld 的帧缓冲指针注入 Renderer)。空帧零绘制;debug 开关
// 全关时 PhysicsWorld 不收集 → 顶点数为 0 → 本 pass 自然零开销。
// //
#ifndef HELLOVULKAN_PHYSICSDEBUGPASS_H #ifndef HELLOVULKAN_PHYSICSDEBUGPASS_H
#define HELLOVULKAN_PHYSICSDEBUGPASS_H #define HELLOVULKAN_PHYSICSDEBUGPASS_H
#include <Engine/Core/Types.h> #include <Engine/Core/Types.h>
#include <Engine/Debug/DebugDraw.h>
#include <Engine/Renderer/Passes/RenderPass.h> #include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/SceneTypes.h> #include <Engine/Renderer/SceneTypes.h>
#include <Engine/Vulkan/UniformBuffer.h> #include <Engine/Vulkan/UniformBuffer.h>
#include <Engine/Vulkan/ShaderManager.h> #include <Engine/Vulkan/ShaderManager.h>
#include <vector>
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
namespace Engine { namespace Engine {
@@ -44,7 +47,8 @@ namespace Engine {
FrameSceneData& frame, FrameSceneData& frame,
RG::TextureHandle hdrColor, RG::TextureHandle hdrColor,
RG::TextureHandle depth, RG::TextureHandle depth,
UniformBuffer& lineVB, UniformBuffer& triVB); UniformBuffer& lineVB, UniformBuffer& triVB,
UniformBuffer& debugVB);
~PhysicsDebugPass(); ~PhysicsDebugPass();
PhysicsDebugPass(const PhysicsDebugPass&) = delete; PhysicsDebugPass(const PhysicsDebugPass&) = delete;
@@ -52,23 +56,36 @@ namespace Engine {
void Register(RG::RenderGraph& rg) override; void Register(RG::RenderGraph& rg) override;
// 每帧 Renderer::Render 里(该槽位 fence 等待后)上传当帧图元 // 每帧 Renderer::Render 里(该槽位 fence 等待后)上传当帧
void PrepareFrame(u32 frameSlot, const PhysicsDebugFrame& src); // 图元。dbg 为 nullptr = 通用调试源禁用/空
void PrepareFrame(u32 frameSlot, const PhysicsDebugFrame& src,
const Debug::DebugFrame* dbg);
private: private:
// 一个可绘制段:独立拓扑/线宽/深度测试(动态状态逐批设置)
struct Span {
bool tris;
f32 width;
bool depthTest;
u32 first; // 顶点偏移(对应 VB 内顶点序号)
u32 count;
};
Device& m_Device; Device& m_Device;
DescriptorManager& m_Descriptors; DescriptorManager& m_Descriptors;
FrameSceneData& m_Frame; FrameSceneData& m_Frame;
RG::TextureHandle m_HDRColor; RG::TextureHandle m_HDRColor;
RG::TextureHandle m_Depth; RG::TextureHandle m_Depth;
UniformBuffer& m_LineVB; // per-slot ring(Renderer 持有生命周期) UniformBuffer& m_LineVB; // per-slot ring(物理源,Renderer 持有)
UniformBuffer& m_TriVB; UniformBuffer& m_TriVB;
UniformBuffer& m_DebugVB; // 通用调试源 ring(Renderer 持有)
vk::PipelineLayout m_Layout; // 无描述符,push = viewProj 64B vk::PipelineLayout m_Layout; // 无描述符,push = viewProj 64B
ShaderManager::ShaderRef m_Vert, m_Frag; // 析构自动销毁 ShaderManager::ShaderRef m_Vert, m_Frag; // 析构自动销毁
std::array<u32, kMaxFramesInFlight> m_LineCounts{}; std::array<u32, kMaxFramesInFlight> m_LineCounts{};
std::array<u32, kMaxFramesInFlight> m_TriCounts{}; std::array<u32, kMaxFramesInFlight> m_TriCounts{};
std::array<std::vector<Span>, kMaxFramesInFlight> m_Spans;
bool m_TruncationWarned = false; bool m_TruncationWarned = false;
}; };
@@ -236,6 +236,7 @@ namespace Engine {
// UniformBuffer——PhysicsDebugPass::PrepareFrame 上传,pass 内绑定) // UniformBuffer——PhysicsDebugPass::PrepareFrame 上传,pass 内绑定)
Scope<UniformBuffer> m_PhysicsLineVB; Scope<UniformBuffer> m_PhysicsLineVB;
Scope<UniformBuffer> m_PhysicsTriVB; Scope<UniformBuffer> m_PhysicsTriVB;
Scope<UniformBuffer> m_DebugLineVB; // 通用调试源(Engine::Debug 服务)
// Editor gizmo 图元 ring VB(editor 专属,game 构建不分配): // Editor gizmo 图元 ring VB(editor 专属,game 构建不分配):
// 图标 billboard / 视锥线框 / 拾取代理壳三流——代理壳由 // 图标 billboard / 视锥线框 / 拾取代理壳三流——代理壳由
// HitProxyPass 经 AttachGizmoProxy 消费(同 VB 换 shader 重画) // HitProxyPass 经 AttachGizmoProxy 消费(同 VB 换 shader 重画)
@@ -205,7 +205,11 @@ namespace Engine {
bool m_BloomEnabled = true; bool m_BloomEnabled = true;
bool m_LodEnabled = true; // 关 = 全部强制 LOD0(选级跳过) bool m_LodEnabled = true; // 关 = 全部强制 LOD0(选级跳过)
int m_DebugView = 0; 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 驱动) bool m_GameView = false; // PIE 内嵌播放(Editor Play/Stop 驱动)
#ifdef HELLO_EDITOR #ifdef HELLO_EDITOR
+119 -2
View File
@@ -6,6 +6,13 @@
// 基类接口——将来接 Lua/C# 时各实现一种 foreign behavior(内部持 // 基类接口——将来接 Lua/C# 时各实现一种 foreign behavior(内部持
// VM 状态),引擎零改动。 // VM 状态),引擎零改动。
// //
// 瘦基类(对齐 Unity6/UE5 现役形态):基类只供生命周期 + 参数 +
// 实体访问器,零领域 API——物理住 PhysicsWorld(App().GetPhysics()
// 直调),动画住 AnimationComponent(GetComponent 直调,Animator
// 同形)。DeclareParams = C# 反射缺失下 UPROPERTY 的等价物:Unity
// 里你从不写声明是因为语言替你做了,C++ 里每个引擎都得补这一环
//(UE 让你标 UPROPERTY,这里让你写一行 Declare)。
//
// 组件只存 scriptName 字符串(POD,序列化天然支持:Lua/C# 同样按名 // 组件只存 scriptName 字符串(POD,序列化天然支持:Lua/C# 同样按名
// 解析);实例由 ScriptRuntime 对账式管理(照 PhysicsWorld 模式), // 解析);实例由 ScriptRuntime 对账式管理(照 PhysicsWorld 模式),
// 不进组件——组件可拷贝/可比较。 // 不进组件——组件可拷贝/可比较。
@@ -21,10 +28,12 @@
#include <Engine/Core/Types.h> #include <Engine/Core/Types.h>
#include <Engine/ECS/Components.h> #include <Engine/ECS/Components.h>
#include <Engine/ECS/Scene.h> #include <Engine/ECS/Scene.h>
#include <Engine/Scripting/ScriptParam.h>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#include <algorithm>
namespace Engine { namespace Engine {
@@ -39,11 +48,27 @@ namespace Engine {
// 注册表键(= 组件 scriptName),须与 ENGINE_REGISTER_SCRIPT 名一致 // 注册表键(= 组件 scriptName),须与 ENGINE_REGISTER_SCRIPT 名一致
virtual const char* Name() const = 0; virtual const char* Name() const = 0;
// 生命周期(吃 scaled dt:编辑器暂停 timeScale=0 时自然停摆) // 生命周期(吃 scaled dt:编辑器暂停 timeScale=0 时自然停摆)。
// 顺序对齐 Unity:OnCreate(= Awake,实例化即调)→ OnEnable
// → OnStart(首次 OnUpdate 前一次)→ OnUpdate* → OnDisable →
// OnDestroy(可能因实体销毁触发,句柄已失效只做自身清理)。
// 跨实体 OnEnable 顺序不保证(实例表无序),与 Unity 一致
virtual void OnCreate() {} virtual void OnCreate() {}
virtual void OnEnable() {}
virtual void OnStart() {}
virtual void OnUpdate(f32 dt) {} virtual void OnUpdate(f32 dt) {}
virtual void OnDisable() {}
virtual void OnDestroy() {} virtual void OnDestroy() {}
// 参数声明(实例化灌值与注册表 schema 缓存共用入口;外语绑定
// 同键接入)。契约:只写成员指针与常量,禁触场景/应用——
// schema 缓存路径用未 Bind 的临时实例调用
virtual void DeclareParams(ParamBuilder& pb) { (void)pb; }
// 参数灌值后回调(外语侧收值接缝:C++ 成员已直写,VM 绑定
// 在此按名取值)
virtual void OnParamsApplied() {}
protected: protected:
// 便捷访问:绑定实体的 TransformComponent // 便捷访问:绑定实体的 TransformComponent
ECS::TransformComponent& Transform() { ECS::TransformComponent& Transform() {
@@ -60,7 +85,8 @@ namespace Engine {
return m_Scene->GetRegistry().try_get<T>(m_Entity); return m_Scene->GetRegistry().try_get<T>(m_Entity);
} }
// 绑定实体的 entityBits(物理门面按键值寻址) // 绑定实体的 entityBits(PhysicsWorld 按此键寻址:直调
// App().GetPhysics() 的各 API 时作第一个参数)
u32 EntityBits() const { u32 EntityBits() const {
return static_cast<u32>(entt::to_integral(m_Entity)); return static_cast<u32>(entt::to_integral(m_Entity));
} }
@@ -70,6 +96,66 @@ namespace Engine {
bool Valid() const { return m_Scene != nullptr && m_Entity != entt::null; } bool Valid() const { return m_Scene != nullptr && m_Entity != entt::null; }
// ---- 组件启停(Unity SetActive / Behaviour.enabled 语义)----
// 自身组件总开关(整个脚本组件停 tick,实例保留)
void SetActive(bool on) {
if (auto* sc = m_Scene->GetRegistry().try_get<ECS::ScriptComponent>(m_Entity)) {
sc->active = on;
}
}
bool IsActive() const {
const auto* sc =
m_Scene->GetRegistry().try_get<ECS::ScriptComponent>(m_Entity);
return sc != nullptr && sc->active;
}
// ---- 自身参数(写成员 + 写穿 Item.params:PIE Inspector
// 实时反映,Stop 由快照天然回滚;首次写新名一次分配)----
// 返回成员直写是否命中(未声明参数 false,存档仍记录——前向
// 兼容)。跨脚本/跨实体访问走 App().GetScripts().SetParam/GetParam
bool SetParam(const std::string& name, const ParamValue& v) {
const bool hit = WriteParam(name, v);
if (auto* sc = Valid()
? m_Scene->GetRegistry().try_get<ECS::ScriptComponent>(m_Entity)
: nullptr) {
const auto item = std::find_if(
sc->items.begin(), sc->items.end(),
[&](const ECS::ScriptComponent::Item& it) {
return it.name == Name();
});
if (item != sc->items.end()) {
const auto pit = std::find_if(
item->params.begin(), item->params.end(),
[&](const ScriptParam& p) { return p.name == name; });
if (pit != item->params.end()) {
pit->value = v;
} else {
item->params.push_back({name, v});
}
}
}
return hit;
}
template<typename T>
bool SetParam(const std::string& name, const T& v) {
return SetParam(name, ParamValue{v});
}
bool GetParam(const std::string& name, ParamValue& out) const {
return ReadParam(name, out);
}
template<typename T>
bool GetParam(const std::string& name, T& out) const {
ParamValue v;
if (!ReadParam(name, v)) {
return false;
}
if (auto* p = std::get_if<T>(&v.v)) {
out = *p;
return true;
}
return false; // 类型不符
}
private: private:
friend class ScriptRuntime; friend class ScriptRuntime;
void Bind(ECS::Scene& scene, entt::entity e, Application* app) { void Bind(ECS::Scene& scene, entt::entity e, Application* app) {
@@ -77,10 +163,19 @@ namespace Engine {
m_Entity = e; m_Entity = e;
m_App = app; m_App = app;
} }
// 参数绑定表交接(实例化/RefreshParams 时由运行时填充)
void SetParamBinds(std::vector<ParamBuilder::Bind> binds) {
m_ParamBinds = std::move(binds);
}
// 按名写/读成员(经绑定表;未声明或类型不符返回 false)。
// 写穿 Item.params 由运行时侧完成(需查组件项)
bool WriteParam(const std::string& name, const ParamValue& v);
bool ReadParam(const std::string& name, ParamValue& out) const;
ECS::Scene* m_Scene = nullptr; ECS::Scene* m_Scene = nullptr;
entt::entity m_Entity = entt::null; entt::entity m_Entity = entt::null;
Application* m_App = nullptr; Application* m_App = nullptr;
std::vector<ParamBuilder::Bind> m_ParamBinds;
}; };
// ---- 按名注册表:脚本类文件级自注册,运行时按名实例化 ---- // ---- 按名注册表:脚本类文件级自注册,运行时按名实例化 ----
@@ -108,6 +203,28 @@ namespace Engine {
return v; return v;
} }
// 参数 schema(惰性缓存):临时实例调 DeclareParams 取声明
// ——每脚本类一次性(注册先于 main,加载期必就绪)。未注册
// 脚本 = 空 schema(读端未知键全忽略,弱前向兼容)
static const std::vector<ParamBuilder::Desc>&
Schema(const std::string& name) {
static const std::vector<ParamBuilder::Desc> kEmpty;
static std::unordered_map<std::string,
std::vector<ParamBuilder::Desc>>
cache;
const auto it = cache.find(name);
if (it != cache.end()) {
return it->second;
}
std::vector<ParamBuilder::Desc> descs;
if (auto behavior = Create(name)) {
ParamBuilder pb;
behavior->DeclareParams(pb);
descs = pb.Descs();
}
return cache.emplace(name, std::move(descs)).first->second;
}
private: private:
static std::unordered_map<std::string, Factory>& Map() { static std::unordered_map<std::string, Factory>& Map() {
static std::unordered_map<std::string, Factory> m; static std::unordered_map<std::string, Factory> m;
@@ -0,0 +1,215 @@
//
// ScriptParam.h:脚本参数——按名变体值 + 声明式 schema 构建器。
//
// 设计本质(UE/Unity 共识):参数数据按名存在序列化层(组件
// ScriptComponent::Item.params,随场景持久化),C++ 脚本成员只是运行时
// 真值缓存——这条边界正是将来 Lua/C# 外语绑定的接缝:
// - C++ 脚本:DeclareParams 里 Field(name, &成员, 默认值) 双写声明
// (灌值直写成员,零回收代码);
// - 外语脚本:Declare(name, 类型, 默认值) 无指针声明,值经
// OnParamsApplied 回收(VM 侧按名取)。
//
// schema 由注册表按脚本类名惰性缓存(ScriptRegistry::Schema)——描述
// 里不含指针,缓存无悬垂;成员指针(Bind)只随实例存亡,不进缓存。
//
#ifndef HELLOVULKAN_SCRIPT_PARAM_H
#define HELLOVULKAN_SCRIPT_PARAM_H
#include <Engine/Core/Types.h>
#include <glm/glm.hpp>
#include <algorithm>
#include <cassert>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
namespace Engine {
namespace Scripting {
// 参数类型(序列化分支 + Inspector 控件选择)。枚举序 = variant
// 臂序(ParamValue::Type() 直转);新类型 = 枚举尾插 + 追加臂,
// 旧档位前向兼容
enum class ParamType : u8 { F32, I32, Bool, String, Vec2, Vec3 };
// 类型 → 枚举映射(Field 重载与消费端共用)
template<typename T> struct ParamTypeOf;
template<> struct ParamTypeOf<f32> { static constexpr ParamType value = ParamType::F32; };
template<> struct ParamTypeOf<i32> { static constexpr ParamType value = ParamType::I32; };
template<> struct ParamTypeOf<bool> { static constexpr ParamType value = ParamType::Bool; };
template<> struct ParamTypeOf<std::string> { static constexpr ParamType value = ParamType::String; };
template<> struct ParamTypeOf<glm::vec2> { static constexpr ParamType value = ParamType::Vec2; };
template<> struct ParamTypeOf<glm::vec3> { static constexpr ParamType value = ParamType::Vec3; };
// 单参数的存储形态(类型标签由 variant 臂序自持)
struct ParamValue {
std::variant<f32, i32, bool, std::string, glm::vec2, glm::vec3>
v{f32{0.0f}};
ParamType Type() const { return static_cast<ParamType>(v.index()); }
template<typename T>
static constexpr ParamType TypeOf() { return ParamTypeOf<T>::value; }
bool operator==(const ParamValue&) const = default;
};
// 单参数(组件 Item.params 的元素,随场景序列化)
struct ScriptParam {
std::string name;
ParamValue value;
bool operator==(const ScriptParam&) const = default;
};
// 参数声明构建器:DeclareParams(pb) 里逐项 Field/Declare,产出
// 描述列表(schema,持久)+ 绑定表(成员指针,瞬态)。
// Field 即把声明默认写进成员——未灌值前成员就是默认值
class ParamBuilder {
public:
// ---- C++ 侧:声明 + 绑定成员(灌值直写)----
void Field(std::string_view name, f32* dst, f32 def) {
*dst = def;
AddDesc(name, ParamType::F32, ParamValue{def});
m_Binds.push_back({std::string(name), ParamType::F32, dst});
}
void Field(std::string_view name, i32* dst, i32 def) {
*dst = def;
AddDesc(name, ParamType::I32, ParamValue{def});
m_Binds.push_back({std::string(name), ParamType::I32, dst});
}
void Field(std::string_view name, bool* dst, bool def) {
*dst = def;
AddDesc(name, ParamType::Bool, ParamValue{def});
m_Binds.push_back({std::string(name), ParamType::Bool, dst});
}
void Field(std::string_view name, std::string* dst,
const std::string& def) {
*dst = def;
AddDesc(name, ParamType::String, ParamValue{def});
m_Binds.push_back({std::string(name), ParamType::String, dst});
}
void Field(std::string_view name, glm::vec2* dst,
const glm::vec2& def) {
*dst = def;
AddDesc(name, ParamType::Vec2, ParamValue{def});
m_Binds.push_back({std::string(name), ParamType::Vec2, dst});
}
void Field(std::string_view name, glm::vec3* dst,
const glm::vec3& def) {
*dst = def;
AddDesc(name, ParamType::Vec3, ParamValue{def});
m_Binds.push_back({std::string(name), ParamType::Vec3, dst});
}
// ---- 外语侧:仅声明不绑定(值经 OnParamsApplied 回收)----
void Declare(std::string_view name, ParamType t, ParamValue def) {
AddDesc(name, t, std::move(def));
}
// 声明描述(持久——进注册表 schema 缓存,无指针无悬垂)
struct Desc {
std::string name;
ParamType type;
ParamValue def;
};
// 成员绑定(瞬态——指针随声明实例存亡,只进 ScriptBehavior)
struct Bind {
std::string name;
ParamType type;
void* ptr;
};
const std::vector<Desc>& Descs() const { return m_Descs; }
std::vector<Bind> TakeBinds() { return std::move(m_Binds); }
// 单绑定写值:类型不符返回 false(SetParam/跨脚本写共用)
static bool WriteBind(const Bind& b, const ParamValue& v) {
if (b.type != v.Type()) {
return false;
}
switch (b.type) {
case ParamType::F32:
*static_cast<f32*>(b.ptr) = std::get<f32>(v.v);
return true;
case ParamType::I32:
*static_cast<i32*>(b.ptr) = std::get<i32>(v.v);
return true;
case ParamType::Bool:
*static_cast<bool*>(b.ptr) = std::get<bool>(v.v);
return true;
case ParamType::String:
*static_cast<std::string*>(b.ptr) =
std::get<std::string>(v.v);
return true;
case ParamType::Vec2:
*static_cast<glm::vec2*>(b.ptr) = std::get<glm::vec2>(v.v);
return true;
case ParamType::Vec3:
*static_cast<glm::vec3*>(b.ptr) = std::get<glm::vec3>(v.v);
return true;
}
return false;
}
// 单绑定读值(按绑定类型出值;GetParam/跨脚本读共用)
static bool ReadBind(const Bind& b, ParamValue& out) {
switch (b.type) {
case ParamType::F32:
out = ParamValue{*static_cast<f32*>(b.ptr)};
return true;
case ParamType::I32:
out = ParamValue{*static_cast<i32*>(b.ptr)};
return true;
case ParamType::Bool:
out = ParamValue{*static_cast<bool*>(b.ptr)};
return true;
case ParamType::String:
out = ParamValue{*static_cast<std::string*>(b.ptr)};
return true;
case ParamType::Vec2:
out = ParamValue{*static_cast<glm::vec2*>(b.ptr)};
return true;
case ParamType::Vec3:
out = ParamValue{*static_cast<glm::vec3*>(b.ptr)};
return true;
}
return false;
}
// 按名灌值进绑定成员:命中→直写;未绑定(外语声明)→跳过;
// 类型不符/多余键→忽略(读端已校正,这里兜底)。
// 缺失键不写——Field 已把声明默认写进成员
void Apply(const std::vector<ScriptParam>& params) const {
for (const auto& p : params) {
const auto b = std::find_if(
m_Binds.begin(), m_Binds.end(),
[&](const Bind& it) { return it.name == p.name; });
if (b != m_Binds.end()) {
WriteBind(*b, p.value);
}
}
}
private:
void AddDesc(std::string_view name, ParamType t, ParamValue def) {
assert(!name.empty() && "参数名不能为空");
assert(std::none_of(m_Descs.begin(), m_Descs.end(),
[&](const Desc& d) {
return d.name == name;
}) &&
"参数重名");
m_Descs.push_back({std::string(name), t, std::move(def)});
}
std::vector<Desc> m_Descs;
std::vector<Bind> m_Binds;
};
} // namespace Scripting
} // namespace Engine
#endif //HELLOVULKAN_SCRIPT_PARAM_H
@@ -1,7 +1,11 @@
// //
// ScriptRuntime.h:脚本生命周期驱动。对账式管理(组件项加/删/改名 ↔ // ScriptRuntime.h:脚本生命周期驱动。对账式管理(组件项加/删/改名 ↔
// 实例建/毁),照 PhysicsWorld.SyncScene 模式——每帧对账,编辑器操作 // 实例建/毁),照 PhysicsWorld.SyncScene 模式——Running 中每帧对账,
// 即时生效,场景加载/实体销毁自动消化。 // PIE 中组件增删/改名即时生效,场景加载/实体销毁自动消化。
//
// Play Mode 语义(Unity/UE PIE 对齐):编辑态不实例化任何脚本——
// 实例生命周期 = 一次 PIE 局,Play 后首次 Update 对账重建,Stop 时
// EndPlay 统一终局,成员状态不跨局泄漏。
// //
// 多脚本:一个实体可挂任意多个脚本项(ScriptComponent::items),实例 // 多脚本:一个实体可挂任意多个脚本项(ScriptComponent::items),实例
// 按名字与 items 对账——重命名/删除走 OnDestroy 清理,未动的实例不重建。 // 按名字与 items 对账——重命名/删除走 OnDestroy 清理,未动的实例不重建。
@@ -23,10 +27,13 @@ namespace Engine {
namespace Scripting { namespace Scripting {
// 实例簿记:行为对象 + OnCreate 是否已调 // 实例簿记:行为对象 + 生命周期位(created = OnCreate 已调;
// enabled = 当前启停态,翻转检测基准;started = OnStart 已发)
struct ScriptInstance { struct ScriptInstance {
Scope<ScriptBehavior> behavior; Scope<ScriptBehavior> behavior;
bool created = false; bool created = false;
bool enabled = false;
bool started = false;
}; };
class ScriptRuntime { class ScriptRuntime {
@@ -34,9 +41,29 @@ namespace Engine {
explicit ScriptRuntime(ECS::Scene& scene, Application* app = nullptr) explicit ScriptRuntime(ECS::Scene& scene, Application* app = nullptr)
: m_Scene(scene), m_App(app) {} : m_Scene(scene), m_App(app) {}
// 每帧调用(吃 scaled dt):对账 → OnCreate(新实例)→ OnUpdate // 每帧调用(吃 scaled dt):对账 → OnCreate(新实例)→
// 启停翻转(OnEnable/OnDisable)→ OnStart(首次)→ OnUpdate。
// 仅 Running 态被调(Application 门控)
void Update(f32 dt); void Update(f32 dt);
// 终局(GameFlow →Stopped 广播):全部实例 OnDisable →
// OnDestroy 并清空簿记。Unity 退出 Play 语义——成员状态
// (动量/边沿/当前 clip)不跨局泄漏,下次 Play 对账重建
void EndPlay();
// 参数热刷新(编辑期一次性):实体全部实例重新 DeclareParams
// + 按组件存档灌值(Inspector 编辑参数后调)
void RefreshParams(u32 entityBits);
// 跨脚本按名参数访问(本实体兄弟项或跨实体):SetParam 写
// 目标实例成员 + 写穿 Item.params(PIE Inspector 实时反映);
// GetParam 读目标实例成员。实体无组件/项不存在/实例未建
// 返回 false
bool SetParam(u32 entityBits, const std::string& scriptName,
const std::string& param, const ParamValue& value);
bool GetParam(u32 entityBits, const std::string& scriptName,
const std::string& param, ParamValue& out) const;
private: private:
void Sync(); void Sync();
+4
View File
@@ -48,6 +48,8 @@ public:
// 各向异性过滤:纹理采样器清晰度(掠射角材质锐利度)的硬件开关 // 各向异性过滤:纹理采样器清晰度(掠射角材质锐利度)的硬件开关
bool IsSamplerAnisotropySupported() const { return m_SamplerAnisotropySupported; } bool IsSamplerAnisotropySupported() const { return m_SamplerAnisotropySupported; }
float MaxSamplerAnisotropy() const { return m_MaxSamplerAnisotropy; } float MaxSamplerAnisotropy() const { return m_MaxSamplerAnisotropy; }
bool IsWideLinesSupported() const { return m_WideLinesSupported; }
float MaxLineWidth() const { return m_MaxLineWidth; }
private: private:
void CreateInstance(); void CreateInstance();
@@ -81,6 +83,8 @@ private:
bool m_DrawIndirectCountSupported = false; bool m_DrawIndirectCountSupported = false;
bool m_SamplerAnisotropySupported = false; bool m_SamplerAnisotropySupported = false;
float m_MaxSamplerAnisotropy = 1.0f; float m_MaxSamplerAnisotropy = 1.0f;
bool m_WideLinesSupported = false;
float m_MaxLineWidth = 1.0f; // 设备线宽上限(像素,limits.lineWidthRange[1])
std::vector<const char*> m_ValidationLayers = { "VK_LAYER_KHRONOS_validation" }; std::vector<const char*> m_ValidationLayers = { "VK_LAYER_KHRONOS_validation" };
std::vector<const char*> m_DeviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; std::vector<const char*> m_DeviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
+4 -3
View File
@@ -131,10 +131,11 @@ namespace Engine::Animation {
std::vector<glm::mat4>& outGlobals) std::vector<glm::mat4>& outGlobals)
{ {
// crossfade 双层 = N 层混合的特例(Direct 模式与 Controller // crossfade 双层 = N 层混合的特例(Direct 模式与 Controller
// 过渡共用同一套加权数学)。src 层权重 = 1 - wCur 与历史 // 过渡共用同一套加权数学)。两权重和恒为 1,渐进归一化后精确
// mix(src, cur, wCur) 语义等价 // 等价 mix(src, cur, wCur):起点全 src、终点全 cur,全程平滑
//(cur 恒 1.0 的话起点就瞬间跳 50%,fade 有效时长减半——割裂感)
PoseLayer layers[2]; PoseLayer layers[2];
layers[0] = {&cur, curTime, 1.0f}; layers[0] = {&cur, curTime, xf.wCur};
u32 count = 1; u32 count = 1;
if (xf.src != nullptr) { if (xf.src != nullptr) {
layers[1] = {xf.src, xf.srcTime, 1.0f - xf.wCur}; layers[1] = {xf.src, xf.srcTime, 1.0f - xf.wCur};
+49 -3
View File
@@ -20,8 +20,40 @@
#include <cmath> #include <cmath>
#include <unordered_map> #include <unordered_map>
namespace Engine::Animation { // ---- AnimationComponent::Play(按名) 实现(声明在 Components.h)----
// 住本 TU:需要 Model 完整类型做 clip 表查找,组件头保持零资产依赖
//(Model.h 拖 vulkan/assimp)。定义放全局全限定(类所属命名空间的
// 外层才允许成员函数定义)
bool Engine::ECS::AnimationComponent::Play(const std::string& clipName,
f32 fadeSeconds) {
if (!runtimeModel) {
return false; // 模型未就绪(首帧前/无渲染组件/换模型间隙)
}
const auto& clips = runtimeModel->GetClips();
for (u32 i = 0; i < clips.size(); ++i) {
// 同 clip = 确保在播即可(播放头不动,防下方相位覆写跳帧)
if (clips[i].name == clipName) {
if (static_cast<int>(i) == clipIndex) {
playing = true;
return true;
}
// 相位对齐(Unity 状态机过渡的默认语义:按归一化时间同
// 步)——不消毒的话 walk 播到左脚在前、run 却从 0(右脚
// 在前)起步,混合的两脚错相,fade 再长也割裂。按下标
// Play 不同步(编辑器预览要从头看)
const AnimationClip& src = clips[clipIndex];
const f32 norm = src.duration > 0.0f
? glm::mod(time, src.duration) / src.duration
: 0.0f;
Play(static_cast<int>(i), fadeSeconds); // 内部 time 归 0
time = clips[i].duration * norm; // 覆写为同相位起步
return true;
}
}
return false; // 名字未命中(是否告警由调用侧决定)
}
namespace Engine::Animation {
void AnimationSystem::Update(ECS::Scene& scene, Assets::AssetManager& assets, void AnimationSystem::Update(ECS::Scene& scene, Assets::AssetManager& assets,
f32 dt, FrameSceneData& frame) f32 dt, FrameSceneData& frame)
{ {
@@ -76,10 +108,11 @@ namespace Engine::Animation {
ECS::AnimationComponent& anim) { ECS::AnimationComponent& anim) {
if (anim.mode != ECS::AnimationComponent::AnimMode::Direct) if (anim.mode != ECS::AnimationComponent::AnimMode::Direct)
return; // Controller 实体走段 2 return; // Controller 实体走段 2
if (!anim.active) return; // 组件禁用 = 暂停采样(播放头不动)
if (!mr.model.Valid()) return; // 未指派/手清 = 无效句柄(Get 对无效 FATAL) if (!mr.model.Valid()) return; // 未指派/手清 = 无效句柄(Get 对无效 FATAL)
const Model* model = assets.GetModel(mr.model); const Model* model = assets.GetModel(mr.model);
if (!model || !model->HasAnimations()) return; if (!model || !model->HasAnimations()) return;
anim.runtimeModel = model; // 按名播放反指(暂停态也刷新)
if (!anim.active) return; // 组件禁用 = 暂停采样(播放头不动)
const auto& clips = model->GetClips(); const auto& clips = model->GetClips();
// clip 下标合法性(模型资产可能已换) // clip 下标合法性(模型资产可能已换)
@@ -144,10 +177,11 @@ namespace Engine::Animation {
ECS::AnimationComponent::AnimMode::Controller) { ECS::AnimationComponent::AnimMode::Controller) {
return; return;
} }
if (!anim.active) return;
if (!mr.model.Valid()) return; if (!mr.model.Valid()) return;
const Model* model = assets.GetModel(mr.model); const Model* model = assets.GetModel(mr.model);
if (!model || !model->HasAnimations()) return; if (!model || !model->HasAnimations()) return;
anim.runtimeModel = model; // 按名播放反指(暂停态也刷新)
if (!anim.active) return;
const auto* ctrl = assets.FindController(anim.controllerPath); const auto* ctrl = assets.FindController(anim.controllerPath);
if (!ctrl) return; // 未加载/路径无效 → rest 兜底 if (!ctrl) return; // 未加载/路径无效 → rest 兜底
const auto& states = ctrl->GetStates(); const auto& states = ctrl->GetStates();
@@ -173,6 +207,18 @@ namespace Engine::Animation {
} }
} }
// 阻尼参数推进(帧率无关指数平滑):current 逐帧逼近
// target,写回 floatParams 供条件评估统一读取;收敛贴齐
// 消除渐近尾噪(AnimSetFloatDamped 的引擎侧推进端)
for (auto& [dname, d] : anim.dampedParams) {
const float k = 1.0f - std::exp(-d.response * dt);
d.current += (d.target - d.current) * k;
if (std::abs(d.target - d.current) < 1e-4f) {
d.current = d.target;
}
anim.floatParams[dname] = d.current;
}
// clip 名 → 下标表 // clip 名 → 下标表
auto& lookup = clipLookup[model]; auto& lookup = clipLookup[model];
if (lookup.empty()) { if (lookup.empty()) {
+31 -6
View File
@@ -44,6 +44,17 @@ namespace Engine {
// 统一输入门面装配:脚本/游戏逻辑经 Input 查询键鼠,不直接 // 统一输入门面装配:脚本/游戏逻辑经 Input 查询键鼠,不直接
// 依赖 Window 后端(GLFW 解耦) // 依赖 Window 后端(GLFW 解耦)
Input::Init(*m_Window); Input::Init(*m_Window);
// GameFlow →Stopped:脚本终局(Unity 退出 Play 语义——实例与
// 成员状态不跨局存活,下次 Play 由对账重建)。注册在组合根最
// 早处:先于编辑器 Init 的监听执行,EndPlay 跑在场景回滚之前,
// OnDestroy 见到的仍是上局实体。m_Scripts 在 Run 里才创建,而
// →Stopped 转换最早发生于 PIE Stop(届时已存在),空指针兜底
m_GameFlow.AddListener([this](GameFlowState, GameFlowState to) {
if (to == GameFlowState::Stopped && m_Scripts) {
m_Scripts->EndPlay();
}
});
} }
Application::~Application() Application::~Application()
@@ -132,15 +143,29 @@ namespace Engine {
const FrameTime& frame = Time::Current(); const FrameTime& frame = Time::Current();
m_LastFrameTime = now; m_LastFrameTime = now;
// 输入动作层采样(单点,整帧一致):仅 Running 激活——
// Paused/Stopped(编辑器态)动作静默清零,编辑器相机继续走
// 原始 IsKeyDown 通道(自带 UI 捕获门控,两套互不干扰)
Input::Poll(m_GameFlow.IsIn(GameFlowState::Running));
// 物理步进(OnUpdate 之前,Unity FixedUpdate→Update 语义): // 物理步进(OnUpdate 之前,Unity FixedUpdate→Update 语义):
// 动态体姿态写回 Transform 并标脏,UpdateTransforms 统一重算; // 动态体姿态写回 Transform 并标脏,UpdateTransforms 统一重算;
// 吃 scaled dt → timeScale=0(编辑器暂停)时 accumulator 冻结, // 吃 scaled dt → timeScale=0(Paused/Stopped 冻结)时
// SyncScene 对账仍跑(拖静态体照常同步) // accumulator 冻结,SyncScene 对账仍跑(拖静态体照常同步)。
m_Physics->Step(frame.Dt()); // Loading 态整段跳过(加载中不步进)
if (!m_GameFlow.IsIn(GameFlowState::Loading)) {
m_Physics->Step(frame.Dt());
// 脚本更新(物理之后、业务 OnUpdate 之前,Unity FixedUpdate→Update // 脚本只在 Running 执行(Unity Play Mode / UE PIE 语义):
// 语义;吃 scaled dt——timeScale=0 暂停时 dt=0 脚本自然停摆) // 编辑态/暂停态 gameplay 脚本一律不跑——脚本副作用不依赖
m_Scripts->Update(frame.Dt()); // dt(读输入/画调试图元/翻转动画状态),"dt=0 自然停摆"
// 不成立,曾致编辑态按 WASD 就画箭头+翻动画状态。实例
// 生命周期 = 一次 PIE 局:→Stopped 由 GameFlow 监听
// EndPlay 统一销毁
if (m_GameFlow.IsIn(GameFlowState::Running)) {
m_Scripts->Update(frame.Dt());
}
}
OnUpdate(frame); OnUpdate(frame);
+97
View File
@@ -0,0 +1,97 @@
//
// GameFlow.cpp:游戏流状态机实现。
//
#include "Engine/Core/GameFlow.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Time.h"
namespace Engine {
namespace {
const char* StateName(GameFlowState s)
{
switch (s) {
case GameFlowState::Stopped: return "Stopped";
case GameFlowState::Loading: return "Loading";
case GameFlowState::Running: return "Running";
case GameFlowState::Paused: return "Paused";
}
return "?";
}
} // anonymous namespace
GameFlow::GameFlow()
{
// Stopped 语义即冻结:替代各处散落的初始 SetTimeScale(0)
Time::SetTimeScale(0.0f);
}
bool GameFlow::RequestStart()
{
if (m_State != GameFlowState::Stopped) {
ENGINE_WARN("GameFlow: RequestStart rejected in {}",
StateName(m_State));
return false;
}
Transition(GameFlowState::Loading);
return true;
}
void GameFlow::CompleteLoading()
{
if (m_State != GameFlowState::Loading) {
ENGINE_WARN("GameFlow: CompleteLoading rejected in {}",
StateName(m_State));
return;
}
Transition(GameFlowState::Running);
}
bool GameFlow::RequestPause()
{
if (m_State != GameFlowState::Running) {
ENGINE_WARN("GameFlow: RequestPause rejected in {}",
StateName(m_State));
return false;
}
Transition(GameFlowState::Paused);
return true;
}
bool GameFlow::RequestResume()
{
if (m_State != GameFlowState::Paused) {
ENGINE_WARN("GameFlow: RequestResume rejected in {}",
StateName(m_State));
return false;
}
Transition(GameFlowState::Running);
return true;
}
bool GameFlow::RequestStop()
{
if (m_State == GameFlowState::Stopped) {
ENGINE_WARN("GameFlow: RequestStop rejected in Stopped");
return false;
}
Transition(GameFlowState::Stopped);
return true;
}
void GameFlow::Transition(GameFlowState to)
{
const GameFlowState from = m_State;
m_State = to;
// timeScale 单一权威:Running/Loading 解冻,Paused/Stopped 冻结
Time::SetTimeScale(
to == GameFlowState::Running || to == GameFlowState::Loading
? 1.0f : 0.0f);
ENGINE_INFO("GameFlow: {} -> {}", StateName(from), StateName(to));
for (const Listener& l : m_Listeners) l(from, to);
}
} // namespace Engine
+77 -1
View File
@@ -1,9 +1,10 @@
// //
// Input.cpp:统一输入门面实现(转发 Window 后端轮询接口)。 // Input.cpp:统一输入门面实现(转发 Window 后端轮询接口 + 动作层门面)。
// //
#include "Engine/Core/Input.h" #include "Engine/Core/Input.h"
#include "Engine/Core/Window.h" #include "Engine/Core/Window.h"
#include "Engine/Input/InputMap.h"
namespace Engine { namespace Engine {
namespace Input { namespace Input {
@@ -13,11 +14,15 @@ namespace Engine {
// 仅 Application::Run 期间被查询)。非 const:门面含 // 仅 Application::Run 期间被查询)。非 const:门面含
// 光标捕获等状态设置,不只查询 // 光标捕获等状态设置,不只查询
Window* s_Window = nullptr; Window* s_Window = nullptr;
// 动作表实例(Init 时建默认绑定:Move/Jump)
InputMap s_ActionMap;
} }
void Init(Window& window) void Init(Window& window)
{ {
s_Window = &window; s_Window = &window;
s_ActionMap.SetDefaults(); // 内建默认绑定表(LoadYaml 可覆盖)
} }
bool IsKeyDown(const Key key) bool IsKeyDown(const Key key)
@@ -60,5 +65,76 @@ namespace Engine {
return s_Window != nullptr ? s_Window->ConsumeScrollDelta() : 0.0f; 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 } // namespace Engine
+31
View File
@@ -254,4 +254,35 @@ namespace Engine
m_ScrollAccum = 0.0f; m_ScrollAccum = 0.0f;
return d; 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 } // namespace Engine
+359
View File
@@ -0,0 +1,359 @@
//
// DebugDraw.cpp:通用调试图元服务实现。
//
// 结构:瞬时缓冲(duration=0,调用方每帧重画)+ 持久列表(duration>0,
// 按到期时间淘汰)→ Frame() 按 (拓扑,线宽,深度测试) 键聚合为连续顶点段
// → Renderer 上传 → EndFrame() 翻页。聚合器不缓存跨帧顶点,持久项每帧
// 重拷贝(调试工具量级,< 数千顶点,非热路径)。
//
#include <Engine/Debug/DebugDraw.h>
#include <Engine/Core/Log.h>
#include <algorithm>
#include <cmath>
#include <chrono>
namespace Engine::Debug {
namespace {
constexpr f32 kPi = 3.14159265358979f;
// 瞬时批次:Draw 调用直接追加(批次键匹配复用,保留容量)
struct TransientBatch {
f32 width;
bool depthTest;
std::vector<Vertex> verts;
};
// 持久项:快照顶点 + 到期时刻(steady_clock 秒)
struct Persistent {
f32 width;
bool depthTest;
std::vector<Vertex> verts;
f64 expiry;
};
bool g_Enabled = true;
bool g_Warned = false;
std::vector<TransientBatch> g_Transient;
std::vector<Persistent> g_Persistent;
DebugFrame g_Frame;
using Clock = std::chrono::steady_clock;
f64 NowSec()
{
return std::chrono::duration<f64>(Clock::now().time_since_epoch())
.count();
}
TransientBatch& BatchFor(f32 width, bool depthTest)
{
for (auto& b : g_Transient) {
if (b.depthTest == depthTest &&
std::abs(b.width - width) < 1e-3f) {
return b;
}
}
g_Transient.push_back(TransientBatch{width, depthTest, {}});
return g_Transient.back();
}
void LineImpl(const glm::vec3& a, const glm::vec3& b,
const glm::vec4& color, f32 width, f32 duration,
bool depthTest)
{
if (!g_Enabled) return;
if (duration > 0.0f) {
Persistent p;
p.width = width;
p.depthTest = depthTest;
p.expiry = NowSec() + static_cast<f64>(duration);
p.verts.push_back({a, color});
p.verts.push_back({b, color});
g_Persistent.push_back(std::move(p));
} else {
auto& batch = BatchFor(width, depthTest);
batch.verts.push_back({a, color});
batch.verts.push_back({b, color});
}
}
// 圆折线发射器:center + u·cosθ + v·sinθ(u/v 已含半径)
void CircleImpl(const glm::vec3& center, const glm::vec3& u,
const glm::vec3& v, const glm::vec4& color,
int segments, f32 width, f32 duration,
bool depthTest)
{
glm::vec3 prev = center + u;
for (int i = 1; i <= segments; ++i) {
const f32 t =
static_cast<f32>(i) / static_cast<f32>(segments) * 2.0f * kPi;
const glm::vec3 p = center + u * std::cos(t) + v * std::sin(t);
LineImpl(prev, p, color, width, duration, depthTest);
prev = p;
}
}
} // namespace
void SetEnabled(bool on)
{
g_Enabled = on;
if (!on) {
Clear();
}
}
bool IsEnabled() { return g_Enabled; }
void Clear()
{
g_Transient.clear();
g_Persistent.clear();
g_Frame.Clear();
}
void Line(const glm::vec3& a, const glm::vec3& b, const glm::vec4& color,
f32 width, f32 duration, bool depthTest)
{
LineImpl(a, b, color, width, duration, depthTest);
}
void Ray(const glm::vec3& from, const glm::vec3& dirNorm, f32 length,
const glm::vec4& color, f32 width, f32 duration, bool depthTest)
{
LineImpl(from, from + dirNorm * length, color, width, duration,
depthTest);
}
void Arrow(const glm::vec3& from, const glm::vec3& to,
const glm::vec4& color, f32 headSize, f32 width, f32 duration,
bool depthTest)
{
const glm::vec3 d = to - from;
const f32 lenSq = glm::dot(d, d);
if (lenSq < 1e-12f) {
// 零长退化:画十字点标(而不是未定义方向的箭头)
Cross(from, headSize * 0.5f, color, width, duration, depthTest);
return;
}
LineImpl(from, to, color, width, duration, depthTest);
// 箭头四翼:轴周围 90° 均布两对 V(60° 张角)。单 V 在视线与
// 翼面共面时退化成一条线;四翼保证任意视角至少一对正对相机
const glm::vec3 dir = d * (1.0f / std::sqrt(lenSq));
const glm::vec3 up =
std::abs(dir.y) < 0.99f ? glm::vec3(0.0f, 1.0f, 0.0f)
: glm::vec3(1.0f, 0.0f, 0.0f);
const glm::vec3 side = glm::normalize(glm::cross(dir, up));
const glm::vec3 bit = glm::normalize(glm::cross(dir, side));
const glm::vec3 heads[4] = {
glm::normalize(dir + side * 0.6f),
glm::normalize(dir - side * 0.6f),
glm::normalize(dir + bit * 0.6f),
glm::normalize(dir - bit * 0.6f),
};
for (const glm::vec3& h : heads) {
LineImpl(to, to - h * headSize, color, width, duration, depthTest);
}
}
void Cross(const glm::vec3& pos, f32 size, const glm::vec4& color,
f32 width, f32 duration, bool depthTest)
{
const glm::vec3 s(size, size, size);
LineImpl(pos - glm::vec3(size, 0, 0), pos + glm::vec3(size, 0, 0),
color, width, duration, depthTest);
LineImpl(pos - glm::vec3(0, size, 0), pos + glm::vec3(0, size, 0),
color, width, duration, depthTest);
LineImpl(pos - glm::vec3(0, 0, size), pos + glm::vec3(0, 0, size),
color, width, duration, depthTest);
(void)s;
}
void WireSphere(const glm::vec3& center, f32 radius,
const glm::vec4& color, int segments, f32 width,
f32 duration, bool depthTest)
{
// 三大圆:XY / XZ / YZ 平面
CircleImpl(center, {radius, 0, 0}, {0, radius, 0}, color, segments,
width, duration, depthTest);
CircleImpl(center, {radius, 0, 0}, {0, 0, radius}, color, segments,
width, duration, depthTest);
CircleImpl(center, {0, radius, 0}, {0, 0, radius}, color, segments,
width, duration, depthTest);
}
void WireCircle(const glm::vec3& center, f32 radius,
const glm::vec3& normal, const glm::vec4& color,
int segments, f32 width, f32 duration, bool depthTest)
{
const f32 nSq = glm::dot(normal, normal);
if (nSq < 1e-12f) return;
const glm::vec3 n = normal * (1.0f / std::sqrt(nSq));
const glm::vec3 up = std::abs(n.y) < 0.99f
? glm::vec3(0.0f, 1.0f, 0.0f)
: glm::vec3(1.0f, 0.0f, 0.0f);
const glm::vec3 u = glm::normalize(glm::cross(n, up)) * radius;
const glm::vec3 v = glm::normalize(glm::cross(n, u / radius)) * radius;
CircleImpl(center, u, v, color, segments, width, duration, depthTest);
}
void WireBox(const glm::vec3& center, const glm::vec3& halfExtents,
const glm::vec4& color, const glm::quat& rot, f32 width,
f32 duration, bool depthTest)
{
glm::vec3 corner[8];
int ci = 0;
for (int i = 0; i < 8; ++i) {
const glm::vec3 local(
(i & 1) ? halfExtents.x : -halfExtents.x,
(i & 2) ? halfExtents.y : -halfExtents.y,
(i & 4) ? halfExtents.z : -halfExtents.z);
corner[ci++] = center + rot * local;
}
// 12 条边
static constexpr int kEdges[12][2] = {
{0, 1}, {2, 3}, {4, 5}, {6, 7}, // X 向
{0, 2}, {1, 3}, {4, 6}, {5, 7}, // Y 向
{0, 4}, {1, 5}, {2, 6}, {3, 7}, // Z 向
};
for (const auto& e : kEdges) {
LineImpl(corner[e[0]], corner[e[1]], color, width, duration,
depthTest);
}
}
void WireCapsule(const glm::vec3& center, f32 radius, f32 halfHeight,
const glm::vec4& color, const glm::quat& rot, f32 width,
f32 duration, bool depthTest)
{
// 轴向 local +Y:两端圆 + 4 侧线 + 两盖各 2 半圆(X-Y / Z-Y 平面)
auto W = [&](const glm::vec3& local) { return center + rot * local; };
auto CircleY = [&](f32 y) {
glm::vec3 prev;
for (int i = 0; i <= 16; ++i) {
const f32 t = static_cast<f32>(i) / 16.0f * 2.0f * kPi;
const glm::vec3 p = W({radius * std::cos(t), y,
radius * std::sin(t)});
if (i > 0) {
LineImpl(prev, p, color, width, duration, depthTest);
}
prev = p;
}
};
CircleY(halfHeight);
CircleY(-halfHeight);
LineImpl(W({radius, halfHeight, 0}), W({radius, -halfHeight, 0}),
color, width, duration, depthTest);
LineImpl(W({-radius, halfHeight, 0}), W({-radius, -halfHeight, 0}),
color, width, duration, depthTest);
LineImpl(W({0, halfHeight, radius}), W({0, -halfHeight, radius}),
color, width, duration, depthTest);
LineImpl(W({0, halfHeight, -radius}), W({0, -halfHeight, -radius}),
color, width, duration, depthTest);
auto CapArc = [&](f32 cap, f32 xz) {
// xz: 半圆所在竖直平面(1 = X-Y 面,0 = Z-Y 面)
glm::vec3 prev;
for (int i = 0; i <= 8; ++i) {
const f32 a = static_cast<f32>(i) / 8.0f * kPi;
const f32 radial = radius * std::cos(a);
const f32 axial = cap * (halfHeight + radius * std::sin(a));
const glm::vec3 p =
W({radial * xz, axial, radial * (1.0f - xz)});
if (i > 0) {
LineImpl(prev, p, color, width, duration, depthTest);
}
prev = p;
}
};
CapArc(1.0f, 1.0f);
CapArc(-1.0f, 1.0f);
CapArc(1.0f, 0.0f);
CapArc(-1.0f, 0.0f);
}
const DebugFrame& Frame()
{
g_Frame.Clear();
if (!g_Enabled) {
return g_Frame;
}
// ---- 按 (拓扑=线, 线宽, 深度) 聚合:瞬时 + 未过期持久项 ----
// groups = 聚合组暂存(键少量,线性扫)→ 展开为连续顶点段
std::vector<std::pair<Batch, std::vector<Vertex>>> groups;
auto findGroup = [&](f32 w, bool d) -> std::vector<Vertex>& {
for (auto& [b, vs] : groups) {
if (!b.tris && b.depthTest == d &&
std::abs(b.width - w) < 1e-3f) {
return vs;
}
}
groups.push_back({Batch{false, w, d, 0, 0}, {}});
return groups.back().second;
};
for (const auto& t : g_Transient) {
findGroup(t.width, t.depthTest)
.insert(findGroup(t.width, t.depthTest).end(),
t.verts.begin(), t.verts.end());
}
const f64 now = NowSec();
for (const auto& p : g_Persistent) {
if (p.expiry > now) {
findGroup(p.width, p.depthTest)
.insert(findGroup(p.width, p.depthTest).end(),
p.verts.begin(), p.verts.end());
}
}
// ---- 展开 + 预算截断 ----
u64 total = 0;
for (const auto& [b, vs] : groups) {
total += vs.size();
}
const u64 cap = kMaxVerts;
if (total > cap && !g_Warned) {
g_Warned = true;
ENGINE_WARN("DebugDraw: vertex budget exceeded ({} > {}), "
"clamped",
total, cap);
}
for (auto& [b, vs] : groups) {
if (g_Frame.verts.size() >= cap) break;
b.first = static_cast<u32>(g_Frame.verts.size());
const u64 room = cap - g_Frame.verts.size();
const u64 n = std::min<u64>(vs.size(), room);
g_Frame.verts.insert(g_Frame.verts.end(), vs.begin(),
vs.begin() + static_cast<i64>(n));
b.count = static_cast<u32>(n);
if (b.count > 0) {
g_Frame.batches.push_back(b);
}
}
return g_Frame;
}
void EndFrame()
{
g_Frame.Clear();
const f64 now = NowSec();
g_Persistent.erase(std::remove_if(g_Persistent.begin(),
g_Persistent.end(),
[now](const Persistent& p) {
return p.expiry <= now;
}),
g_Persistent.end());
for (auto& t : g_Transient) {
t.verts.clear(); // 保留批次与容量(稳态零重分配)
}
}
} // namespace Engine::Debug
+29 -19
View File
@@ -57,6 +57,16 @@ namespace Engine {
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // docking 分支 io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // docking 分支
// io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // 多视口 // 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_ImplGlfw_InitForVulkan(window.GetHandle(), true);
ImGui_ImplVulkan_InitInfo info = {}; ImGui_ImplVulkan_InitInfo info = {};
@@ -132,25 +142,25 @@ namespace Engine {
} }
} }
#ifdef HELLO_EDITOR #ifdef HELLO_EDITOR
void ImGuiLayer::UpdatePreviewTexture(u32 slot, vk::ImageView view) void ImGuiLayer::UpdatePreviewTexture(u32 slot, vk::ImageView view)
{ {
if (slot >= kMaxFramesInFlight) return; if (slot >= kMaxFramesInFlight) return;
// 语义与 UpdateSceneTexture 完全一致(回收旧描述符 + 注册新 view) // 语义与 UpdateSceneTexture 完全一致(回收旧描述符 + 注册新 view)
if (m_PreviewTextures[slot]) { if (m_PreviewTextures[slot]) {
ImGui_ImplVulkan_RemoveTexture( ImGui_ImplVulkan_RemoveTexture(
(VkDescriptorSet)(intptr_t)m_PreviewTextures[slot]); (VkDescriptorSet)(intptr_t)m_PreviewTextures[slot]);
m_PreviewTextures[slot] = ImTextureID_Invalid; m_PreviewTextures[slot] = ImTextureID_Invalid;
} }
if (view) { if (view) {
m_PreviewTextures[slot] = (ImTextureID)(intptr_t) m_PreviewTextures[slot] = (ImTextureID)(intptr_t)
ImGui_ImplVulkan_AddTexture( ImGui_ImplVulkan_AddTexture(
(VkImageView)view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); (VkImageView)view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
} }
} }
#endif #endif
void ImGuiLayer::NewFrame() void ImGuiLayer::NewFrame()
{ {
ImGui_ImplVulkan_NewFrame(); ImGui_ImplVulkan_NewFrame();
+312
View File
@@ -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
+81 -16
View File
@@ -3,6 +3,7 @@
// //
#include <Engine/Physics/PhysicsWorld.h> #include <Engine/Physics/PhysicsWorld.h>
#include <Engine/Debug/DebugDraw.h>
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
#include <Engine/ECS/Components.h> #include <Engine/ECS/Components.h>
@@ -274,12 +275,14 @@ namespace Engine {
void PhysicsWorld::CollectDebugData() { void PhysicsWorld::CollectDebugData() {
m_DebugDraw->BeginCollect(); m_DebugDraw->BeginCollect();
// 1) Jolt 原生:collider 线框 + 速度箭头(软体 mDrawSoftBody* 后续加这里) // 1) Jolt 原生:collider 线框(软体 mDrawSoftBody* 后续加这里)。
if ((m_DebugBits & (kPhyDbgColliders | kPhyDbgVelocity)) != 0) { // 速度箭头不交 Jolt 画——其 BodyManager::DrawBodies 无零速保
// 护,静止刚体的零向量箭头退化成 +X 方向(本次修复的突兀箭
// 头)→ 统一在下方手画并加阈值
if ((m_DebugBits & kPhyDbgColliders) != 0) {
JPH::BodyManager::DrawSettings ds; JPH::BodyManager::DrawSettings ds;
ds.mDrawShape = (m_DebugBits & kPhyDbgColliders) != 0; ds.mDrawShape = true;
ds.mDrawShapeWireframe = true; ds.mDrawShapeWireframe = true;
ds.mDrawVelocity = (m_DebugBits & kPhyDbgVelocity) != 0;
m_System->DrawBodies(ds, m_DebugDraw.get()); m_System->DrawBodies(ds, m_DebugDraw.get());
} }
@@ -289,39 +292,86 @@ namespace Engine {
} }
// 3) 手绘箭头(Jolt 无现成 API):重力 = 常量方向指示(长度固定), // 3) 手绘箭头(Jolt 无现成 API):重力 = 常量方向指示(长度固定),
// 加速度 = 速度差分(方向/长度随值,静止噪声阈值截断) // 加速度 = 速度差分(方向/长度随值,静止噪声阈值截断)。
// 走引擎通用 DebugDraw 服务。收集按渲染帧率跑(Step 末尾
// 无步进帧也重画,每帧全量重建)→ duration 必须 0(当帧瞬
// 时):此前沿用 step 率时代的 2×步长续期,高帧率下每根箭
// 头叠 4-5 个滞后副本,物体一动副本脱锚拖尾——表现为断断
// 续续乱跳
auto &bi = m_System->GetBodyInterface(); auto &bi = m_System->GetBodyInterface();
const JPH::Vec3 gravity = m_System->GetGravity(); const JPH::Vec3 gravity = m_System->GetGravity();
constexpr f32 kGravityLen = 1.0f; // 世界单位 constexpr f32 kGravityLen = 1.0f; // 世界单位
constexpr f32 kAccelScale = 0.05f; constexpr f32 kAccelScale = 0.05f;
constexpr f32 kAccelMaxLen = 2.0f; constexpr f32 kAccelMaxLen = 2.0f;
const JPH::Color kGravityColor(220, 60, 60); // 红:重力 constexpr f32 kVelScale = 0.2f; // 速度箭头比例(单位/速度)
const JPH::Color kAccelColor(255, 170, 40); // 橙:加速度 constexpr f32 kDbgDur = 0.0f; // 渲染帧率收集 → 瞬时图元
constexpr glm::vec4 kGravityColor = Debug::Rgb(0xDC3C3C); // 红:重力
constexpr glm::vec4 kAccelColor = Debug::Rgb(0xE0640A); // 深橙:加速度
constexpr glm::vec4 kVelColor = Debug::Rgb(0x5ADC5A); // 绿:速度
constexpr glm::vec4 kContactColor = Debug::Rgb(0xD9C33C); // 黄:接触反力
auto ToG = [](const JPH::RVec3Arg v) {
return glm::vec3{static_cast<f32>(v.GetX()),
static_cast<f32>(v.GetY()),
static_cast<f32>(v.GetZ())};
};
for (auto &[bits, rec]: m_Bodies) { for (auto &[bits, rec]: m_Bodies) {
if (rec.type != static_cast<u8>(ECS::RigidBodyComponent::BodyType::Dynamic)) { if (rec.type != static_cast<u8>(ECS::RigidBodyComponent::BodyType::Dynamic)) {
continue; continue;
} }
const JPH::RVec3 com = bi.GetCenterOfMassPosition(rec.id); const glm::vec3 com = ToG(bi.GetCenterOfMassPosition(rec.id));
// 速度箭头(绿):零速阈值——Jolt 原生 DrawBodies 无零速保
// 护(静止零向量箭头退化成 +X),此处阈值手画
if ((m_DebugBits & kPhyDbgVelocity) != 0) {
const JPH::Vec3 v = bi.GetLinearVelocity(rec.id);
if (v.LengthSq() > 1e-4f) { // >0.01 m/s 才画
Debug::Arrow(com, com + ToG(JPH::RVec3(v * kVelScale)),
kVelColor,
/*headSize*/ 0.12f, /*width*/ 2.0f,
/*duration*/ kDbgDur, /*depthTest*/ false);
}
}
if ((m_DebugBits & kPhyDbgGravity) != 0) { if ((m_DebugBits & kPhyDbgGravity) != 0) {
const JPH::Vec3 g = gravity * bi.GetGravityFactor(rec.id); const JPH::Vec3 g = gravity * bi.GetGravityFactor(rec.id);
const f32 len = g.Length(); const f32 len = g.Length();
if (len > 1e-4f) { if (len > 1e-4f) {
const JPH::RVec3 tip = com + JPH::RVec3(g * (kGravityLen / len)); Debug::Arrow(
m_DebugDraw->DrawArrow(com, tip, kGravityColor, kGravityLen * 0.12f); com,
com + ToG(JPH::RVec3(g * (kGravityLen / len))),
kGravityColor,
/*headSize*/ 0.12f, /*width*/ 2.0f,
/*duration*/ kDbgDur, /*depthTest*/ false);
} }
} }
if ((m_DebugBits & kPhyDbgAccel) != 0) { if ((m_DebugBits & kPhyDbgAccel) != 0) {
// 接触合力(比力 F/m = a − g,从测量加速度反推):静置
// = +g 纯法向(与重力箭头等长反向 = 力平衡);滑动出现
// 水平分量(摩擦);自由落体 ≈ 0 不画。长度按重力箭头
// 同一比例归一 → 静置红下黄上视觉等长
const JPH::Vec3 gB = gravity * bi.GetGravityFactor(rec.id);
const glm::vec3 spec = rec.lastAccel -
glm::vec3(gB.GetX(), gB.GetY(), gB.GetZ());
const f32 smag = glm::length(spec);
if (smag > 0.2f && gravity.Length() > 1e-4f) {
const f32 slen = std::min(
smag * kGravityLen / gravity.Length(), kAccelMaxLen);
Debug::Arrow(com, com + (spec / smag) * slen,
kContactColor,
/*headSize*/ 0.12f, /*width*/ 2.0f,
/*duration*/ kDbgDur, /*depthTest*/ false);
}
const f32 mag = glm::length(rec.lastAccel); const f32 mag = glm::length(rec.lastAccel);
if (mag > 0.05f) { if (mag > 0.05f) {
const glm::vec3 dir = rec.lastAccel / mag;
const f32 len = std::min(mag * kAccelScale, kAccelMaxLen); const f32 len = std::min(mag * kAccelScale, kAccelMaxLen);
const JPH::RVec3 tip = Debug::Arrow(com, com + (rec.lastAccel / mag) * len,
com + JPH::RVec3(dir.x, dir.y, dir.z) * len; kAccelColor,
m_DebugDraw->DrawArrow( /*headSize*/ 0.12f,
com, tip, kAccelColor, std::max(len * 0.15f, 0.05f)); /*width*/ 2.0f,
/*duration*/ kDbgDur, /*depthTest*/ false);
} }
} }
} }
@@ -334,6 +384,10 @@ namespace Engine {
} }
} }
// 角色运动类线条(速度/加速度)一律归用户脚本画(Unity
// CharacterController 同语义:引擎只画 collider,移动调试归
// 移动逻辑的拥有者);物理侧不保留任何角色箭头
// 释放上一帧缓存不再引用的通用几何 batch(DebugRenderer 内部池) // 释放上一帧缓存不再引用的通用几何 batch(DebugRenderer 内部池)
m_DebugDraw->NextFrame(); m_DebugDraw->NextFrame();
} }
@@ -675,7 +729,12 @@ namespace Engine {
if (tr == nullptr) { if (tr == nullptr) {
continue; continue;
} }
entry.cc->Update(dt, gravity, *m_TempAllocator); // 每角色重力倍率(组件直读:Inspector 调完下个物理步即生效,
// 不触发重建——重力只是 Update 的入参,不是形状参数)
const auto *cc = reg.try_get<ECS::CharacterControllerComponent>(
static_cast<entt::entity>(bits));
const f32 gscale = cc != nullptr ? cc->gravityScale : 1.0f;
entry.cc->Update(dt, gravity * gscale, *m_TempAllocator);
const glm::vec3 p = entry.cc->GetPosition(); // 原点 = 脚底(世界系) const glm::vec3 p = entry.cc->GetPosition(); // 原点 = 脚底(世界系)
// 写回走 world→local 换算(层级实体挂父下仍正确;标脏 → // 写回走 world→local 换算(层级实体挂父下仍正确;标脏 →
// UpdateTransforms 重算 world) // UpdateTransforms 重算 world)
@@ -1047,6 +1106,12 @@ namespace Engine {
: glm::vec3{0.0f}; : glm::vec3{0.0f};
} }
glm::vec3 PhysicsWorld::GetCharacterPosition(const u32 entityBits) const {
const auto found = m_Characters.find(entityBits);
return found != m_Characters.end() ? found->second.cc->GetPosition()
: glm::vec3{0.0f};
}
// ---- 写回:动态体姿态 → Transform(标脏,帧循环统一 Compose)---- // ---- 写回:动态体姿态 → Transform(标脏,帧循环统一 Compose)----
void PhysicsWorld::Writeback() { void PhysicsWorld::Writeback() {
+38 -16
View File
@@ -40,20 +40,38 @@ namespace Engine {
{ {
if (wantLook) if (wantLook)
{ {
// ---- 6DOF 真自由视角:pitch/yaw 各绕本地轴右乘增量 // ---- FPS 式视角(UE/Unity 编辑器视口语义):
// 四元数(q = q·Δyaw·Δpitch),无欧拉读改写、无 // yaw 绕世界 up 左乘、pitch 绕本地 X 右乘——
// ±89° 钳制——可过顶/翻滚,存储层本就是四元数, // 右轴恒保持水平,水平线永不 roll。此前版本
// 不存在万向锁。符号沿用旧约定:鼠标右移向右看 // yaw 也绕本地轴(6DOF 自由视角),俯视后再
// (右手系绕 Y 正角向左 → yaw 取负)、鼠标下移 // 平移鼠标会带 roll,up 向量歪斜。pitch 用
// 低头(绕 X 正角抬头 → pitch 取负)---- // 前向量的仰角钳 ±89°:过顶会倒置视图,编辑
// 器视口不要这种行为。符号约定不变:鼠标右移
// 向右看、鼠标下移低头 ----
const glm::quat dyaw = glm::angleAxis( const glm::quat dyaw = glm::angleAxis(
glm::radians(-mouseDX * m_MouseSensitivity), glm::radians(-mouseDX * m_MouseSensitivity),
glm::vec3(0, 1, 0)); glm::vec3(0, 1, 0));
const glm::quat dpitch = glm::angleAxis(
glm::radians(-mouseDY * m_MouseSensitivity), const glm::vec3 fwd =
glm::vec3(1, 0, 0)); 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.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)) else if (Input::IsKeyDown(Input::Key::LeftShift))
{ {
@@ -116,12 +134,16 @@ namespace Engine {
if (wantLook || !flyRequiresRMB) if (wantLook || !flyRequiresRMB)
{ {
const f32 speed = m_MoveSpeed * deltaTime; const f32 speed = m_MoveSpeed * deltaTime;
if (Input::IsKeyDown(Input::Key::W)) { transform.SetPosition(transform.GetPosition() + forward * speed); } bool flew = false;
if (Input::IsKeyDown(Input::Key::S)) { transform.SetPosition(transform.GetPosition() - forward * speed); } if (Input::IsKeyDown(Input::Key::W)) { transform.SetPosition(transform.GetPosition() + forward * speed); flew = true; }
if (Input::IsKeyDown(Input::Key::A)) { transform.SetPosition(transform.GetPosition() - right * speed); } if (Input::IsKeyDown(Input::Key::S)) { transform.SetPosition(transform.GetPosition() - forward * speed); flew = true; }
if (Input::IsKeyDown(Input::Key::D)) { transform.SetPosition(transform.GetPosition() + right * speed); } if (Input::IsKeyDown(Input::Key::A)) { transform.SetPosition(transform.GetPosition() - right * speed); flew = true; }
if (Input::IsKeyDown(Input::Key::Space)) { transform.SetPosition(transform.GetPosition() + glm::vec3(0, speed, 0)); } if (Input::IsKeyDown(Input::Key::D)) { transform.SetPosition(transform.GetPosition() + right * speed); flew = true; }
if (Input::IsKeyDown(Input::Key::LeftShift)) { transform.SetPosition(transform.GetPosition() - glm::vec3(0, speed, 0)); } 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 时改环绕距离(指数步进,近处精细/ // ---- 滚轮推拉:有 pivot 时改环绕距离(指数步进,近处精细/
@@ -64,9 +64,11 @@ namespace Engine::Passes
FrameSceneData& frame, FrameSceneData& frame,
RG::TextureHandle hdrColor, RG::TextureHandle hdrColor,
RG::TextureHandle depth, RG::TextureHandle depth,
UniformBuffer& lineVB, UniformBuffer& triVB) UniformBuffer& lineVB, UniformBuffer& triVB,
UniformBuffer& debugVB)
: m_Device(device), m_Descriptors(descriptors), m_Frame(frame), : m_Device(device), m_Descriptors(descriptors), m_Frame(frame),
m_HDRColor(hdrColor), m_Depth(depth), m_LineVB(lineVB), m_TriVB(triVB) m_HDRColor(hdrColor), m_Depth(depth), m_LineVB(lineVB), m_TriVB(triVB),
m_DebugVB(debugVB)
{ {
// 无描述符 set:viewProj 走 push constant(shader 侧声明一致) // 无描述符 set:viewProj 走 push constant(shader 侧声明一致)
m_Layout = descriptors.CreatePipelineLayout( m_Layout = descriptors.CreatePipelineLayout(
@@ -98,7 +100,8 @@ namespace Engine::Passes
} }
void PhysicsDebugPass::PrepareFrame(u32 frameSlot, void PhysicsDebugPass::PrepareFrame(u32 frameSlot,
const PhysicsDebugFrame& src) const PhysicsDebugFrame& src,
const Debug::DebugFrame* dbg)
{ {
// 预算截断:物理调试不应打满(打满说明该收敛可视化了) // 预算截断:物理调试不应打满(打满说明该收敛可视化了)
const u32 lineVerts = std::min<u32>( const u32 lineVerts = std::min<u32>(
@@ -115,18 +118,40 @@ namespace Engine::Passes
// fence 已等(Renderer::Render 的 WaitFence 之后调用), // fence 已等(Renderer::Render 的 WaitFence 之后调用),
// 写-读竞争关闭——与 UBO/SSBO 上传同一窗口语义 // 写-读竞争关闭——与 UBO/SSBO 上传同一窗口语义
m_Spans[frameSlot].clear();
if (lineVerts > 0) { if (lineVerts > 0) {
m_LineVB.Update(frameSlot, src.lines.data(), m_LineVB.Update(frameSlot, src.lines.data(),
static_cast<u64>(lineVerts) * static_cast<u64>(lineVerts) *
sizeof(PhysicsDebugVertex)); sizeof(PhysicsDebugVertex));
m_Spans[frameSlot].push_back({false, 1.0f, true, 0, lineVerts});
} }
if (triVerts > 0) { if (triVerts > 0) {
m_TriVB.Update(frameSlot, src.triangles.data(), m_TriVB.Update(frameSlot, src.triangles.data(),
static_cast<u64>(triVerts) * static_cast<u64>(triVerts) *
sizeof(PhysicsDebugVertex)); sizeof(PhysicsDebugVertex));
m_Spans[frameSlot].push_back({true, 1.0f, true, 0, triVerts});
} }
m_LineCounts[frameSlot] = lineVerts; m_LineCounts[frameSlot] = lineVerts;
m_TriCounts[frameSlot] = triVerts; m_TriCounts[frameSlot] = triVerts;
// 通用调试源(独立 ring VB,与物理源隔离):批次按收集侧的
// (拓扑, 线宽, 深度) 原样展开为绘制段;超预算部分截断
if (dbg != nullptr && !dbg->Empty() && !dbg->verts.empty()) {
const u32 total = static_cast<u32>(dbg->verts.size());
const u32 n = std::min<u32>(total, Debug::kMaxVerts);
m_DebugVB.Update(frameSlot, dbg->verts.data(),
static_cast<u64>(n) * sizeof(Debug::Vertex));
u32 consumed = 0;
for (const Debug::Batch& b : dbg->batches) {
if (consumed >= n) break;
const u32 take = std::min<u32>(b.count, n - consumed);
if (take > 0) {
m_Spans[frameSlot].push_back(
{b.tris, b.width, b.depthTest, consumed, take});
}
consumed += take;
}
}
} }
void PhysicsDebugPass::Register(RG::RenderGraph& rg) void PhysicsDebugPass::Register(RG::RenderGraph& rg)
@@ -134,7 +159,10 @@ namespace Engine::Passes
rg.AddPass("PhysicsDebug", [this](RG::RenderGraphContext& ctx) { rg.AddPass("PhysicsDebug", [this](RG::RenderGraphContext& ctx) {
const u32 lineCount = m_LineCounts[ctx.frame.frameSlot]; const u32 lineCount = m_LineCounts[ctx.frame.frameSlot];
const u32 triCount = m_TriCounts[ctx.frame.frameSlot]; const u32 triCount = m_TriCounts[ctx.frame.frameSlot];
if (lineCount == 0 && triCount == 0) return; // 空帧零开销 const std::vector<Span>& spans = m_Spans[ctx.frame.frameSlot];
if (lineCount == 0 && triCount == 0 && spans.empty()) {
return; // 空帧零开销
}
ZoneScopedN("PhysicsDebug"); ZoneScopedN("PhysicsDebug");
const vk::CommandBuffer cmd = ctx.cmd; const vk::CommandBuffer cmd = ctx.cmd;
@@ -236,6 +264,29 @@ namespace Engine::Passes
cmd.draw(triCount, 1, 0, 0); cmd.draw(triCount, 1, 0, 0);
} }
// 通用调试源批次:逐批动态状态(拓扑/线宽/深度测试)。
// 线宽 >1 需 wideLines 特性,不支持时钳回 1.0(VUID-07937)
if (!spans.empty()) {
const std::array<vk::Buffer, 1> bufs{m_DebugVB.Get(ctx.frame.frameSlot)};
const std::array<vk::DeviceSize, 1> offs{0};
cmd.bindVertexBuffers(0, bufs, offs);
for (const Span& s : spans) {
cmd.setPrimitiveTopology(
s.tris ? vk::PrimitiveTopology::eTriangleList
: vk::PrimitiveTopology::eLineList);
// 线宽单位 = 像素,设备范围 [1.0, lineWidthRange[1]]:
// 低于下限的值(如 0.18)是校验错误,驱动按 1px 处
// 理——必须钳进范围;wideLines 不支持则恒 1px
cmd.setLineWidth(m_Device.IsWideLinesSupported()
? std::clamp(s.width, 1.0f,
m_Device.MaxLineWidth())
: 1.0f);
cmd.setDepthTestEnable(s.depthTest ? VK_TRUE : VK_FALSE);
cmd.draw(s.count, 1, s.first, 0);
}
cmd.setLineWidth(1.0f); // 恢复默认,防状态外泄
}
cmd.endRendering(); cmd.endRendering();
}) })
// Write use(WAW 链):保证排在 Forward/ForwardTrans 之后、 // Write use(WAW 链):保证排在 Forward/ForwardTrans 之后、
+16 -3
View File
@@ -24,6 +24,7 @@
#endif #endif
#include <Engine/Renderer/Passes/OutlinePass.h> #include <Engine/Renderer/Passes/OutlinePass.h>
#include <Engine/Physics/PhysicsDebugTypes.h> #include <Engine/Physics/PhysicsDebugTypes.h>
#include <Engine/Debug/DebugDraw.h>
#include <Engine/Renderer/Cubemap.h> #include <Engine/Renderer/Cubemap.h>
#include <Engine/Renderer/IblGenerator.h> #include <Engine/Renderer/IblGenerator.h>
#include <Engine/Renderer/TracyGpuProfiler.h> #include <Engine/Renderer/TracyGpuProfiler.h>
@@ -114,6 +115,11 @@ namespace Engine
u64(Passes::PhysicsDebugPass::kMaxTriVerts) * u64(Passes::PhysicsDebugPass::kMaxTriVerts) *
sizeof(PhysicsDebugVertex), sizeof(PhysicsDebugVertex),
kMaxFramesInFlight, vk::BufferUsageFlagBits::eVertexBuffer); 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 #ifdef HELLO_EDITOR
// 3.3 Editor gizmo 图元 ring VB(editor 专属;game 构建不分配)。 // 3.3 Editor gizmo 图元 ring VB(editor 专属;game 构建不分配)。
// eVertexBuffer 复用 UniformBuffer 的 per-slot fence 语义, // eVertexBuffer 复用 UniformBuffer 的 per-slot fence 语义,
@@ -502,7 +508,8 @@ namespace Engine
// TAA 之前)——调试图元进 TAA/Bloom/Tonemap 主链路 // TAA 之前)——调试图元进 TAA/Bloom/Tonemap 主链路
m_Passes.push_back(CreateScope<Passes::PhysicsDebugPass>( m_Passes.push_back(CreateScope<Passes::PhysicsDebugPass>(
*m_Device, *m_Allocator, *m_ShaderManager, *m_DescriptorManager, *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 = m_PhysicsDebugPass =
static_cast<Passes::PhysicsDebugPass*>(m_Passes.back().get()); static_cast<Passes::PhysicsDebugPass*>(m_Passes.back().get());
#ifdef HELLO_EDITOR #ifdef HELLO_EDITOR
@@ -813,10 +820,16 @@ namespace Engine
// MDI 命令/count 同窗口落槽位 ring(fence 已等,写-读竞争关闭) // MDI 命令/count 同窗口落槽位 ring(fence 已等,写-读竞争关闭)
m_SceneGpu->UploadIndirect(frameSlot, frame); m_SceneGpu->UploadIndirect(frameSlot, frame);
// 物理调试图元同窗口上传(fence 已等);空帧计数归零,pass 零开销 // 物理调试图元同窗口上传(fence 已等);空帧计数归零,pass 零开销。
// 通用调试源(Engine::Debug)同窗口取走;EndFrame 翻页(清瞬时 +
// 清过期持久项)——即使物理源缺席也必须翻页,防瞬时缓冲累积
if (m_PhysicsDebug && m_PhysicsDebugPass) { 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 #ifdef HELLO_EDITOR
// Editor gizmo 图元同窗口上传(fence 已等);空帧不写,pass 端 // Editor gizmo 图元同窗口上传(fence 已等);空帧不写,pass 端
+184 -18
View File
@@ -4,6 +4,7 @@
#include <Engine/Scripting/ScriptRuntime.h> #include <Engine/Scripting/ScriptRuntime.h>
#include <Engine/Core/Application.h>
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
#include <Engine/ECS/Components.h> #include <Engine/ECS/Components.h>
@@ -18,6 +19,28 @@ namespace Engine {
return *m_App; return *m_App;
} }
// ---- 参数按名读写(ScriptBehavior 声明,经绑定表;写穿
// Item.params 由运行时 SetParam 完成)----
bool ScriptBehavior::WriteParam(const std::string& name,
const ParamValue& v) {
for (const auto& b : m_ParamBinds) {
if (b.name == name) {
return ParamBuilder::WriteBind(b, v);
}
}
return false;
}
bool ScriptBehavior::ReadParam(const std::string& name,
ParamValue& out) const {
for (const auto& b : m_ParamBinds) {
if (b.name == name) {
return ParamBuilder::ReadBind(b, out);
}
}
return false;
}
void ScriptRuntime::Update(f32 dt) { void ScriptRuntime::Update(f32 dt) {
Sync(); Sync();
@@ -26,40 +49,73 @@ namespace Engine {
const auto e = static_cast<entt::entity>(bits); const auto e = static_cast<entt::entity>(bits);
const ECS::ScriptComponent* sc = reg.valid(e) const ECS::ScriptComponent* sc = reg.valid(e)
? reg.try_get<ECS::ScriptComponent>(e) : nullptr; ? reg.try_get<ECS::ScriptComponent>(e) : nullptr;
if (!sc) {
continue; // 延迟销毁队列里,下帧 Sync 清理
}
if (!sc->active) {
continue; // 组件总开关:全部实例停 tick(Unity disable)
}
for (auto& inst : list) { for (auto& inst : list) {
if (!inst.behavior) { if (!inst.behavior) {
continue; continue;
} }
// per-script 启停:按名查项(对账保证存在;新项要 // 启停翻转检测:wantOn = 组件总开关 && 项开关(组件
// 下帧 Sync 才有实例,这里查不到就跳过) // 关闭时零查找;项按名查找沿用每帧既有的那一次)。
const auto item = std::find_if( // 与上帧 inst.enabled 比较出沿:true→OnEnable,
sc->items.begin(), sc->items.end(), // false→OnDisable(Unity enabled 翻转语义)
[&](const ECS::ScriptComponent::Item& it) { bool wantOn = false;
return it.name == inst.behavior->Name(); if (sc && sc->active) {
}); const auto item = std::find_if(
if (item == sc->items.end() || !item->active) { sc->items.begin(), sc->items.end(),
[&](const ECS::ScriptComponent::Item& it) {
return it.name == inst.behavior->Name();
});
wantOn = item != sc->items.end() && item->active;
}
if (!wantOn) {
if (inst.enabled) {
inst.enabled = false;
inst.behavior->OnDisable();
}
continue; // 禁用 = 实例保留停 OnUpdate continue; // 禁用 = 实例保留停 OnUpdate
} }
if (!inst.created) { if (!inst.created) {
inst.behavior->OnCreate(); inst.behavior->OnCreate();
inst.created = true; inst.created = true;
} }
if (!inst.enabled) {
inst.enabled = true;
inst.behavior->OnEnable();
}
if (!inst.started) {
inst.started = true;
inst.behavior->OnStart(); // 首次 OnUpdate 前一次
}
inst.behavior->OnUpdate(dt); inst.behavior->OnUpdate(dt);
} }
} }
} }
// ---- 终局(GameFlow →Stopped 广播):全实例 OnDisable →
// OnDestroy。与 Sync 的孤儿清理同序,范围 = 全部且不重建;
// Application 的监听先于编辑器回滚注册(组合根构造序),
// OnDestroy 里读到的仍是上局实体 ----
void ScriptRuntime::EndPlay() {
for (auto& [bits, list] : m_Instances) {
for (auto& inst : list) {
if (!inst.behavior) {
continue;
}
if (inst.enabled) {
inst.enabled = false;
inst.behavior->OnDisable();
}
inst.behavior->OnDestroy();
}
}
m_Instances.clear();
}
void ScriptRuntime::Sync() { void ScriptRuntime::Sync() {
auto& reg = m_Scene.GetRegistry(); auto& reg = m_Scene.GetRegistry();
// 1. 孤儿/改名清理:实体或组件没了 → 整组 OnDestroy; // 1. 孤儿/改名清理:实体或组件没了 → 整组 OnDestroy;
// 实例名不在 items 里(改名/移除)→ 单个 OnDestroy // 实例名不在 items 里(改名/移除)→ 单个 OnDestroy。
// 销毁次序 = OnDisable(守卫防双发)→ OnDestroy
for (auto it = m_Instances.begin(); it != m_Instances.end();) { for (auto it = m_Instances.begin(); it != m_Instances.end();) {
const auto e = static_cast<entt::entity>(it->first); const auto e = static_cast<entt::entity>(it->first);
const ECS::ScriptComponent* sc = const ECS::ScriptComponent* sc =
@@ -67,9 +123,14 @@ namespace Engine {
: nullptr; : nullptr;
if (!sc) { if (!sc) {
for (auto& inst : it->second) { for (auto& inst : it->second) {
if (inst.behavior) { if (!inst.behavior) {
inst.behavior->OnDestroy(); continue;
} }
if (inst.enabled) {
inst.enabled = false;
inst.behavior->OnDisable();
}
inst.behavior->OnDestroy();
} }
it = m_Instances.erase(it); it = m_Instances.erase(it);
continue; continue;
@@ -86,6 +147,10 @@ namespace Engine {
continue; continue;
} }
if (li->behavior) { if (li->behavior) {
if (li->enabled) {
li->enabled = false;
li->behavior->OnDisable();
}
li->behavior->OnDestroy(); li->behavior->OnDestroy();
} }
li = list.erase(li); li = list.erase(li);
@@ -98,7 +163,9 @@ namespace Engine {
} }
// 2. 缺失实例化:逐项按名查注册表(未知名警告——场景含未注册 // 2. 缺失实例化:逐项按名查注册表(未知名警告——场景含未注册
// 脚本时弱前向兼容,跳过不影响其余脚本) // 脚本时弱前向兼容,跳过不影响其余脚本)。灌值次序对齐
// Unity:声明(成员=声明默认)→ 组件存档覆盖 → 收值回调
// → OnCreate(= Awake,拿到最终参数值)
const auto view = reg.view<ECS::ScriptComponent>(); const auto view = reg.view<ECS::ScriptComponent>();
for (auto [e, sc] : view.each()) { for (auto [e, sc] : view.each()) {
const u32 bits = static_cast<u32>(entt::to_integral(e)); const u32 bits = static_cast<u32>(entt::to_integral(e));
@@ -124,6 +191,12 @@ namespace Engine {
} }
behavior->Bind(m_Scene, e, m_App); behavior->Bind(m_Scene, e, m_App);
ParamBuilder pb;
behavior->DeclareParams(pb);
pb.Apply(item.params);
behavior->SetParamBinds(pb.TakeBinds());
behavior->OnParamsApplied();
ScriptInstance inst; ScriptInstance inst;
inst.behavior = std::move(behavior); inst.behavior = std::move(behavior);
inst.behavior->OnCreate(); // Unity Awake 语义:实例化即回调 inst.behavior->OnCreate(); // Unity Awake 语义:实例化即回调
@@ -133,5 +206,98 @@ namespace Engine {
} }
} }
// ---- 参数热刷新(编辑期一次性):重新声明 + 按存档灌值 ----
void ScriptRuntime::RefreshParams(u32 entityBits) {
const auto it = m_Instances.find(entityBits);
if (it == m_Instances.end()) {
return;
}
auto& reg = m_Scene.GetRegistry();
const auto e = static_cast<entt::entity>(entityBits);
const ECS::ScriptComponent* sc = reg.valid(e)
? reg.try_get<ECS::ScriptComponent>(e) : nullptr;
if (!sc) {
return;
}
for (auto& inst : it->second) {
if (!inst.behavior) {
continue;
}
ParamBuilder pb;
inst.behavior->DeclareParams(pb);
const auto item = std::find_if(
sc->items.begin(), sc->items.end(),
[&](const ECS::ScriptComponent::Item& it2) {
return it2.name == inst.behavior->Name();
});
if (item != sc->items.end()) {
pb.Apply(item->params);
}
inst.behavior->SetParamBinds(pb.TakeBinds());
inst.behavior->OnParamsApplied();
}
}
// ---- 跨脚本按名参数访问 ----
bool ScriptRuntime::SetParam(u32 entityBits,
const std::string& scriptName,
const std::string& param,
const ParamValue& value) {
auto& reg = m_Scene.GetRegistry();
const auto e = static_cast<entt::entity>(entityBits);
ECS::ScriptComponent* sc = reg.valid(e)
? reg.try_get<ECS::ScriptComponent>(e) : nullptr;
if (!sc) {
return false;
}
const auto item = std::find_if(
sc->items.begin(), sc->items.end(),
[&](const ECS::ScriptComponent::Item& it) {
return it.name == scriptName;
});
if (item == sc->items.end()) {
return false;
}
// 目标实例成员直写(实例未建/未声明则跳过,存档仍写穿)
const auto ii = m_Instances.find(entityBits);
if (ii != m_Instances.end()) {
for (auto& inst : ii->second) {
if (inst.behavior &&
inst.behavior->Name() == scriptName) {
inst.behavior->WriteParam(param, value);
break;
}
}
}
// 写穿 Item.params:首次写新名一次分配,此后零分配
const auto pit = std::find_if(
item->params.begin(), item->params.end(),
[&](const ScriptParam& p) { return p.name == param; });
if (pit != item->params.end()) {
pit->value = value;
} else {
item->params.push_back({param, value});
}
return true;
}
bool ScriptRuntime::GetParam(u32 entityBits,
const std::string& scriptName,
const std::string& param,
ParamValue& out) const {
const auto ii = m_Instances.find(entityBits);
if (ii == m_Instances.end()) {
return false;
}
for (const auto& inst : ii->second) {
if (inst.behavior && inst.behavior->Name() == scriptName) {
return inst.behavior->ReadParam(param, out);
}
}
return false;
}
} // namespace Scripting } // namespace Scripting
} // namespace Engine } // namespace Engine
@@ -22,6 +22,7 @@
#include <Engine/Assets/AssetManager.h> #include <Engine/Assets/AssetManager.h>
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
#include <Engine/Core/UUID.h> #include <Engine/Core/UUID.h>
#include <Engine/Scripting/Script.h>
#include <yaml-cpp/yaml.h> #include <yaml-cpp/yaml.h>
@@ -50,6 +51,11 @@ namespace Engine::Serialization {
<< YAML::EndSeq; << YAML::EndSeq;
} }
void EmitVec2(YAML::Emitter& e, const glm::vec2& v)
{
e << YAML::Flow << YAML::BeginSeq << v.x << v.y << YAML::EndSeq;
}
void EmitVec4(YAML::Emitter& e, const glm::vec4& v) void EmitVec4(YAML::Emitter& e, const glm::vec4& v)
{ {
e << YAML::Flow << YAML::BeginSeq << v.x << v.y << v.z << v.w e << YAML::Flow << YAML::BeginSeq << v.x << v.y << v.z << v.w
@@ -71,6 +77,71 @@ namespace Engine::Serialization {
n[2].as<f32>(def.z)}; n[2].as<f32>(def.z)};
} }
glm::vec2 ReadVec2(const YAML::Node& n, const glm::vec2& def)
{
if (!n || !n.IsSequence() || n.size() < 2) return def;
return {n[0].as<f32>(def.x), n[1].as<f32>(def.y)};
}
// ---- 脚本参数读端:按声明 schema 收值(调用方保证键已知)----
// 数值臂互转(YAML 标量无类型:"3" 作 f32/i32 均自然,i32 声明
// 遇浮点值截断);形不符回声明默认 + 警告(加载期一次,不刷屏)
template<typename T>
bool TryAs(const YAML::Node& n, T& out)
{
try {
out = n.as<T>();
return true;
} catch (const std::exception&) {
return false; // 类型不符 / 僵尸节点硬化异常统一兜住
}
}
Scripting::ParamValue ReadParamValue(
const YAML::Node& n, const Scripting::ParamBuilder::Desc& d)
{
switch (d.type) {
case Scripting::ParamType::F32: {
f32 v{};
if (TryAs<f32>(n, v)) return {v};
i32 iv{};
if (TryAs<i32>(n, iv)) return {static_cast<f32>(iv)};
break;
}
case Scripting::ParamType::I32: {
i32 v{};
if (TryAs<i32>(n, v)) return {v};
f32 fv{};
if (TryAs<f32>(n, fv)) return {static_cast<i32>(fv)};
break;
}
case Scripting::ParamType::Bool: {
bool v{};
if (TryAs<bool>(n, v)) return {v};
break;
}
case Scripting::ParamType::String: {
if (n.IsScalar()) return {n.Scalar()};
break;
}
case Scripting::ParamType::Vec2: {
if (n && n.IsSequence() && n.size() >= 2) {
return {ReadVec2(n, glm::vec2{0.0f})};
}
break;
}
case Scripting::ParamType::Vec3: {
if (n && n.IsSequence() && n.size() >= 3) {
return {ReadVec3(n, glm::vec3{0.0f})};
}
break;
}
}
ENGINE_WARN("Script param '{}' type mismatch, use default",
d.name);
return d.def;
}
glm::vec4 ReadVec4(const YAML::Node& n, const glm::vec4& def) glm::vec4 ReadVec4(const YAML::Node& n, const glm::vec4& def)
{ {
if (!n || !n.IsSequence() || n.size() < 4) return def; if (!n || !n.IsSequence() || n.size() < 4) return def;
@@ -452,6 +523,8 @@ namespace Engine::Serialization {
<< cc->maxSlopeAngle; << cc->maxSlopeAngle;
out << YAML::Key << "StepHeight" << YAML::Value out << YAML::Key << "StepHeight" << YAML::Value
<< cc->stepHeight; << cc->stepHeight;
out << YAML::Key << "GravityScale" << YAML::Value
<< cc->gravityScale;
out << YAML::Key << "Active" << YAML::Value << cc->active; out << YAML::Key << "Active" << YAML::Value << cc->active;
out << YAML::EndMap; out << YAML::EndMap;
} }
@@ -467,6 +540,39 @@ namespace Engine::Serialization {
out << YAML::BeginMap; out << YAML::BeginMap;
out << YAML::Key << "Name" << YAML::Value << item.name; out << YAML::Key << "Name" << YAML::Value << item.name;
out << YAML::Key << "Active" << YAML::Value << item.active; out << YAML::Key << "Active" << YAML::Value << item.active;
if (!item.params.empty()) {
out << YAML::Key << "Params" << YAML::Value
<< YAML::BeginMap;
for (const auto& p : item.params) {
switch (p.value.Type()) {
case Engine::Scripting::ParamType::F32:
out << YAML::Key << p.name << YAML::Value
<< std::get<f32>(p.value.v);
break;
case Engine::Scripting::ParamType::I32:
out << YAML::Key << p.name << YAML::Value
<< std::get<i32>(p.value.v);
break;
case Engine::Scripting::ParamType::Bool:
out << YAML::Key << p.name << YAML::Value
<< std::get<bool>(p.value.v);
break;
case Engine::Scripting::ParamType::String:
out << YAML::Key << p.name << YAML::Value
<< std::get<std::string>(p.value.v);
break;
case Engine::Scripting::ParamType::Vec2:
out << YAML::Key << p.name << YAML::Value;
EmitVec2(out, std::get<glm::vec2>(p.value.v));
break;
case Engine::Scripting::ParamType::Vec3:
out << YAML::Key << p.name << YAML::Value;
EmitVec3(out, std::get<glm::vec3>(p.value.v));
break;
}
}
out << YAML::EndMap;
}
out << YAML::EndMap; out << YAML::EndMap;
} }
out << YAML::EndSeq; out << YAML::EndSeq;
@@ -855,6 +961,7 @@ namespace Engine::Serialization {
cc.mass = n["Mass"].as<f32>(cc.mass); cc.mass = n["Mass"].as<f32>(cc.mass);
cc.maxSlopeAngle = n["MaxSlopeAngle"].as<f32>(cc.maxSlopeAngle); cc.maxSlopeAngle = n["MaxSlopeAngle"].as<f32>(cc.maxSlopeAngle);
cc.stepHeight = n["StepHeight"].as<f32>(cc.stepHeight); cc.stepHeight = n["StepHeight"].as<f32>(cc.stepHeight);
cc.gravityScale = n["GravityScale"].as<f32>(cc.gravityScale);
cc.active = n["Active"].as<bool>(true); cc.active = n["Active"].as<bool>(true);
// 状态(速度/着地)由 PhysicsWorld 对账重建,不存 // 状态(速度/着地)由 PhysicsWorld 对账重建,不存
} }
@@ -868,6 +975,27 @@ namespace Engine::Serialization {
ECS::ScriptComponent::Item item; ECS::ScriptComponent::Item item;
item.name = it["Name"].as<std::string>(""); item.name = it["Name"].as<std::string>("");
item.active = it["Active"].as<bool>(true); item.active = it["Active"].as<bool>(true);
// 参数:按脚本 schema 对齐收值——未知键忽略(前向
// 兼容),缺失键不存(运行时用声明默认)
const YAML::Node ps = it["Params"];
if (ps) {
const auto& schema =
Scripting::ScriptRegistry::Schema(item.name);
for (const auto& kv : ps) {
const std::string pname =
kv.first.as<std::string>("");
const auto dit = std::find_if(
schema.begin(), schema.end(),
[&](const Scripting::ParamBuilder::Desc& d) {
return d.name == pname;
});
if (dit == schema.end()) {
continue;
}
item.params.push_back(
{pname, ReadParamValue(kv.second, *dit)});
}
}
// 去重兜底:同名项只收第一份(UI 已杜绝重复, // 去重兜底:同名项只收第一份(UI 已杜绝重复,
// 手编场景仍可能写出重复) // 手编场景仍可能写出重复)
const bool dup = std::any_of( const bool dup = std::any_of(
+10
View File
@@ -170,6 +170,12 @@ void Device::PickPhysicalDevice()
m_SamplerAnisotropySupported = features2.features.samplerAnisotropy == VK_TRUE; m_SamplerAnisotropySupported = features2.features.samplerAnisotropy == VK_TRUE;
m_MaxSamplerAnisotropy = props.limits.maxSamplerAnisotropy; m_MaxSamplerAnisotropy = props.limits.maxSamplerAnisotropy;
// 宽线(core 特性位;调试图元 >1 线宽必需,桌面 GPU 普遍支持)
m_WideLinesSupported = features2.features.wideLines == VK_TRUE;
// 线宽设备范围(像素):[0]=下限(普遍 1.0),[1]=上限——debug 线
// 宽钳制用。wideLines 不支持时只有 1.0 合法
m_MaxLineWidth = props.limits.lineWidthRange[1];
// core 特性:MDI(drawCount>1)必需 // core 特性:MDI(drawCount>1)必需
m_MultiDrawIndirectSupported = features2.features.multiDrawIndirect == VK_TRUE; m_MultiDrawIndirectSupported = features2.features.multiDrawIndirect == VK_TRUE;
// 1.2 特性:drawIndexedIndirectCount 必需(VUID-04445) // 1.2 特性:drawIndexedIndirectCount 必需(VUID-04445)
@@ -192,6 +198,8 @@ void Device::PickPhysicalDevice()
ENGINE_INFO(" Dynamic Rendering Local Read (1.4): {}", localReadSupported ? "Yes" : "No"); ENGINE_INFO(" Dynamic Rendering Local Read (1.4): {}", localReadSupported ? "Yes" : "No");
ENGINE_INFO(" Push Descriptor (1.4): {}", pushDescriptorSupported ? "Yes" : "No"); ENGINE_INFO(" Push Descriptor (1.4): {}", pushDescriptorSupported ? "Yes" : "No");
ENGINE_INFO(" Shader Object (EXT): {}", m_ShaderObjectSupported ? "Yes" : "No"); ENGINE_INFO(" Shader Object (EXT): {}", m_ShaderObjectSupported ? "Yes" : "No");
ENGINE_INFO(" Wide Lines (1.0 core): {}",
m_WideLinesSupported ? "Yes" : "No");
ENGINE_INFO(" Multi Draw Indirect (1.0 core): {}", ENGINE_INFO(" Multi Draw Indirect (1.0 core): {}",
m_MultiDrawIndirectSupported ? "Yes" : "No"); m_MultiDrawIndirectSupported ? "Yes" : "No");
ENGINE_INFO(" Draw Indirect Count (1.2): {}", ENGINE_INFO(" Draw Indirect Count (1.2): {}",
@@ -261,6 +269,8 @@ void Device::CreateLogicalDevice()
features.setMultiDrawIndirect(m_MultiDrawIndirectSupported ? VK_TRUE : VK_FALSE); features.setMultiDrawIndirect(m_MultiDrawIndirectSupported ? VK_TRUE : VK_FALSE);
// 纹理各向异性过滤(材质掠射角清晰度) // 纹理各向异性过滤(材质掠射角清晰度)
features.setSamplerAnisotropy(m_SamplerAnisotropySupported ? VK_TRUE : VK_FALSE); features.setSamplerAnisotropy(m_SamplerAnisotropySupported ? VK_TRUE : VK_FALSE);
// 宽线(调试图元线宽 >1;不支持时 pass 侧钳回 1.0)
features.setWideLines(m_WideLinesSupported ? VK_TRUE : VK_FALSE);
// ======================================================== // ========================================================
// 启用 1.4 特性链 // 启用 1.4 特性链