- 动画:HumanoidRig 三层架构(standard 标准槽 / dialects 方言映射 / mapper 配对), 指/趾统一走世界增量两遍式烘焙;AnimationAttach 拆为纯算法 + facade;修正 HumanoidBasis 行走基朝向与跨骨架去重、根运动通道 - 物理:CharacterController 角色互碰撞 / shapeOffset / 接触事件;PhysicsWorld 接触合成与 字符冲量判定 - 渲染与材质:Model 多材质按材质拆子 mesh 与贴图路径解析、DDS 直传、GLB 内嵌纹理落盘、 不透明通道 alpha 固定 1.0、glTF FlipUVs 对齐;SceneSerializer 支持 meshIndex 与材质覆盖 - 脚本:FractureLib 抽为共享库(HostView 桥接)、WallFracture 参数直绑、新增 ThirdPersonCamera / Follower / WASDMove 玩法;移除 GameDirector / PlayerController / Target - 编辑器:新增 GamepadPanel、LogPanel(LogRing),改进 Inspector / 材质 / 统计 / 视口面板 - 依赖:vendored ufbx(FBX 5.x/6.x/7.x 全版本解析,替代 assimp 对 FBX 6.1 的拒读)
260 lines
11 KiB
C++
260 lines
11 KiB
C++
//
|
||
// Created by atdunbg on 2026/9/22.
|
||
//
|
||
|
||
#include <Engine/Core/Application.h>
|
||
#include <Engine/Core/Window.h>
|
||
#include <Engine/Core/Input.h>
|
||
#include <Engine/Core/Log.h>
|
||
#include <Engine/Core/LogRing.h>
|
||
#include <Engine/Core/Stats.h>
|
||
#include <Engine/ECS/Scene.h>
|
||
#include <Engine/Physics/PhysicsWorld.h>
|
||
#include <Engine/Scripting/ScriptRuntime.h>
|
||
#include <Engine/Renderer/Renderer.h>
|
||
#include <Engine/Assets/AssetManager.h>
|
||
#include <Engine/Vulkan/Device.h>
|
||
#include <Engine/ImGui/ImGuiLayer.h>
|
||
|
||
#include <imgui.h>
|
||
|
||
#include <tracy/Tracy.hpp>
|
||
|
||
#include <algorithm>
|
||
#include <thread>
|
||
|
||
namespace Engine {
|
||
|
||
Application::Application(const std::string& title, u32 width, u32 height)
|
||
{
|
||
|
||
m_Window = CreateScope<Window>(title, width, height);
|
||
|
||
// 事件通道:GLFW 回调 → 本类 DispatchEvent → 派生类钩子
|
||
m_Window->SetEventCallback(
|
||
[this](const Event& e) { DispatchEvent(e); });
|
||
|
||
// 场景(ECS 世界)+ 渲染器
|
||
m_Scene = CreateScope<ECS::Scene>();
|
||
m_Renderer = CreateScope<Renderer>(*m_Window);
|
||
|
||
// 资产层:上传服务由渲染层提供(IAssetUploader 实现),
|
||
// 资产层不感知 Vulkan 具体类。
|
||
// 声明在 Renderer 之后 → 析构先于 Renderer(它引用其子模块)
|
||
m_Assets = CreateScope<Assets::AssetManager>(m_Renderer->GetAssetUploader());
|
||
|
||
// 统一输入门面装配:脚本/游戏逻辑经 Input 查询键鼠,不直接
|
||
// 依赖 Window 后端(GLFW 解耦)
|
||
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()
|
||
{
|
||
// 关键:必须在成员析构之前等 GPU 空闲。
|
||
//
|
||
// 成员析构序(后声明先析构)是 m_Assets → m_Renderer → m_Scene → m_Window:
|
||
// 资产层持有全部 GPU buffer/texture/sampler,会先于 Renderer 析构,
|
||
// 而 Renderer 的 waitIdle 在它自己的析构里——那时资产已经销毁了。
|
||
// 若最后一帧命令缓冲仍在 GPU 上执行,就会出现
|
||
// VUID-vkDestroyBuffer-buffer-00922(资源销毁时仍在使用)。
|
||
// 这里显式等待,保证成员按任意顺序析构都是安全的。
|
||
if (m_Renderer) {
|
||
m_Renderer->GetDevice().GetDevice().waitIdle();
|
||
}
|
||
|
||
// ✅ 进程退出前持久化 GUID 注册表(GetOrCreateGUID 只标 dirty,
|
||
// 此处统一落盘,避免开发期每次新增都触发全量 yaml 重写)
|
||
if (m_Assets) {
|
||
m_Assets->FlushGUIDRegistry();
|
||
}
|
||
|
||
|
||
if (m_ImGuiLayer) m_ImGuiLayer.reset();
|
||
|
||
// 注意:这里不能 Logger::Shutdown()——成员析构(Device 销毁等)
|
||
// 在函数体之后才执行,期间 Vulkan validation 回调仍会打日志,
|
||
// logger 先亡会导致悬空调用 SIGSEGV。Shutdown 移到 main 的
|
||
// app 作用域结束之后(所有成员析构完毕)。
|
||
}
|
||
|
||
void Application::EnableImGui()
|
||
{
|
||
if (m_ImGuiLayer) {
|
||
ENGINE_WARN("ImGui layer already enabled, do not enable twice!");
|
||
return;
|
||
}
|
||
|
||
m_ImGuiLayer = CreateScope<ImGuiLayer>(GetWindow(), *m_Renderer);
|
||
m_Renderer->SetImGuiLayer(m_ImGuiLayer.get());
|
||
|
||
// 输入隔离:ImGui 想要的输入不再透给游戏侧
|
||
//(事件回调与轮询接口在 Window 层统一门控)
|
||
GetWindow().SetInputCapture(
|
||
[] { return ImGui::GetIO().WantCaptureKeyboard; },
|
||
[] { return ImGui::GetIO().WantCaptureMouse; });
|
||
}
|
||
|
||
void Application::SetVSync(bool vsync)
|
||
{
|
||
if (m_VSync == vsync) return;
|
||
m_VSync = vsync;
|
||
m_Renderer->SetVSync(vsync);
|
||
}
|
||
|
||
void Application::Run()
|
||
{
|
||
// Log Panel 开闸:到此为止之前的启动日志(窗口/Vulkan/UBO/
|
||
// IBL/网格构建)只落终端与文件——Unity Console 语义,面板只
|
||
// 收运行期日志(场景/物理/脚本),启动噪音不进面板
|
||
Engine::Debug::LogRing().SetEnabled(true);
|
||
ENGINE_DEBUG("[APP] 主循环启动(Log Panel 开启;此行为 DEBUG 级,"
|
||
"勾选 Trace 过滤可见青色 DEBUG 徽标)");
|
||
ENGINE_INFO("Application starting");
|
||
|
||
m_Running = true;
|
||
m_LastFrameTime = std::chrono::steady_clock::now();
|
||
|
||
tracy::SetThreadName("Main"); // 线程命名:否则 GUI 显示 Unknown
|
||
|
||
OnInit();
|
||
|
||
// 物理世界(场景实体搭建完后创建)
|
||
m_Physics = CreateScope<PhysicsWorld>(*m_Scene, m_Assets.get());
|
||
// 物理调试图元 → 渲染侧(PhysicsDebugPass 每帧上传绘制)
|
||
m_Renderer->SetPhysicsDebugSource(&m_Physics->GetDebugFrame());
|
||
|
||
// 脚本生命周期(对账式,组件增删/改名即时生效);this 注入脚本
|
||
// 上下文(Window/Renderer/Physics 门面)
|
||
m_Scripts = CreateScope<Scripting::ScriptRuntime>(*m_Scene, this);
|
||
|
||
while (m_Running) {
|
||
const auto frameStart = std::chrono::steady_clock::now();
|
||
|
||
STATS.BeginFrame(); // 帧内资源分配清零(面板帧流失真修正)
|
||
|
||
m_Window->PollEvents();
|
||
|
||
auto now = std::chrono::steady_clock::now();
|
||
const f64 dt = std::chrono::duration<f64>(now - m_LastFrameTime).count();
|
||
// 钳制卡顿(断点/窗口拖动导致的超大 dt),避免物理/动画穿模;
|
||
// 帧首落定 FrameTime(时间单源),业务当帧只读
|
||
Time::BeginFrame(std::clamp(dt, 0.0, 0.25), m_FrameCount);
|
||
const FrameTime& frame = Time::Current();
|
||
m_LastFrameTime = now;
|
||
|
||
// 输入动作层采样(单点,整帧一致):仅 Running 激活——
|
||
// Paused/Stopped(编辑器态)动作静默清零,编辑器相机继续走
|
||
// 原始 IsKeyDown 通道(自带 UI 捕获门控,两套互不干扰)
|
||
Input::Poll(m_GameFlow.IsIn(GameFlowState::Running));
|
||
|
||
// 物理步进(OnUpdate 之前,Unity FixedUpdate→Update 语义):
|
||
// 动态体姿态写回 Transform 并标脏,UpdateTransforms 统一重算;
|
||
// 吃 scaled dt → timeScale=0(Paused/Stopped 冻结)时
|
||
// accumulator 冻结,SyncScene 对账仍跑(拖静态体照常同步)。
|
||
// Loading 态整段跳过(加载中不步进)
|
||
if (!m_GameFlow.IsIn(GameFlowState::Loading)) {
|
||
m_Physics->Step(frame.Dt());
|
||
|
||
// 脚本只在 Running 执行(Unity Play Mode / UE PIE 语义):
|
||
// 编辑态/暂停态 gameplay 脚本一律不跑——脚本副作用不依赖
|
||
// dt(读输入/画调试图元/翻转动画状态),"dt=0 自然停摆"
|
||
// 不成立,曾致编辑态按 WASD 就画箭头+翻动画状态。实例
|
||
// 生命周期 = 一次 PIE 局:→Stopped 由 GameFlow 监听
|
||
// EndPlay 统一销毁
|
||
if (m_GameFlow.IsIn(GameFlowState::Running)) {
|
||
m_Scripts->Update(frame.Dt());
|
||
}
|
||
}
|
||
|
||
OnUpdate(frame);
|
||
|
||
// 延迟销毁在渲染前统一 flush,保证渲染遍历时池结构不变
|
||
m_Scene->FlushDestroyQueue();
|
||
m_Scene->UpdateTransforms(); // UE transform-update 阶段:渲染前保证 world 新鲜
|
||
|
||
// 帧首同步:窗口 resize / 渲染尺寸(面板跟随)/ 显示模式 / vsync
|
||
// 统一在 UI 构建"之前"落定——此处可能重建 swapchain 与 RG 目标
|
||
// 并重注册 ImGui 场景纹理;若挪到 UI 构建之后,本帧 draw data
|
||
// 会引用刚被销毁的描述符(validation: Invalid VkDescriptorSet)
|
||
m_Renderer->BeginFrameSync();
|
||
|
||
|
||
// ---- UI 构建段:OnRenderOverlay 只"声明"控件,不直接碰 GPU ----
|
||
if (m_ImGuiLayer && m_ImGuiLayer->IsEnabled())
|
||
{
|
||
const auto uiStart = std::chrono::steady_clock::now();
|
||
m_ImGuiLayer->NewFrame();
|
||
OnImGuiRender();
|
||
m_ImGuiLayer->BuildDrawData();
|
||
const auto uiEnd = std::chrono::steady_clock::now();
|
||
}
|
||
|
||
// ---- 主渲染(框架行为):RG 顺序 Forward → ImGui(loadOp=Load) →
|
||
// Present,draw data 在 ImGui pass 里才真正提交 GPU ----
|
||
m_Renderer->Render(*m_Scene, *m_Assets, frame.Dt());
|
||
|
||
// ---- 帧率控制:vsync 开启时 present 自带节奏;否则按目标限帧 ----
|
||
// 策略:先 sleep 到剩 ~2ms,再自旋——精度高且不烧 CPU
|
||
// TargetFps <= 0 表示不限帧(跳过 sleep,避免 1e6/0 除零 UB)
|
||
if (!m_VSync && m_TargetFps > 0) {
|
||
const auto deadline = frameStart +
|
||
std::chrono::microseconds(static_cast<long long>(
|
||
1e6 / m_TargetFps));
|
||
auto now2 = std::chrono::steady_clock::now();
|
||
if (now2 < deadline) {
|
||
const auto remaining = std::chrono::duration_cast<
|
||
std::chrono::milliseconds>(deadline - now2);
|
||
if (remaining.count() > 2) {
|
||
std::this_thread::sleep_for(
|
||
remaining - std::chrono::milliseconds(2));
|
||
}
|
||
while (std::chrono::steady_clock::now() < deadline) {
|
||
// 自旋等待最后 ~2ms
|
||
}
|
||
}
|
||
}
|
||
++m_FrameCount;
|
||
FrameMark; // Tracy:帧边界(一帧的时间线切分)
|
||
}
|
||
|
||
OnShutdown();
|
||
ENGINE_INFO("Application stopped ({} frames)", m_FrameCount);
|
||
}
|
||
|
||
void Application::DispatchEvent(const Event& event)
|
||
{
|
||
OnEvent(event);
|
||
|
||
switch (event.type) {
|
||
case EventType::WindowClose: OnWindowClose(event); break;
|
||
case EventType::WindowResize: OnWindowResize(event); break;
|
||
case EventType::WindowFocus: OnWindowFocus(event); break;
|
||
case EventType::WindowLostFocus: OnWindowLostFocus(event); break;
|
||
case EventType::KeyPress: OnKeyPress(event); break;
|
||
case EventType::KeyRelease: OnKeyRelease(event); break;
|
||
case EventType::MouseButtonPress: OnMouseButtonPress(event); break;
|
||
case EventType::MouseButtonRelease: OnMouseButtonRelease(event); break;
|
||
case EventType::MouseMove: OnMouseMove(event); break;
|
||
case EventType::MouseScroll: OnMouseScroll(event); break;
|
||
default: break;
|
||
}
|
||
}
|
||
|
||
void Application::OnWindowClose(const Event& event)
|
||
{
|
||
Close();
|
||
}
|
||
|
||
} // namespace Engine
|