Compare commits

..
21 Commits
Author SHA1 Message Date
atdunbg 74a2044441 chore(scripts): rename script API to GetComponent/TryGetComponent; misc
Clearer accessor naming on ScriptBehavior (Get -> GetComponent,
TryGet -> TryGetComponent), WASDMove follows. Teleport marked const
(JPH Ref does not propagate constness). Spinner: auto-rotation
disabled for the moment, registration switched from the registration
macro to an explicit lambda to verify the mechanism.
2026-10-04 00:33:47 +08:00
atdunbg 134812dae3 feat(physics): debug-draw character controller capsule
CharacterVirtual is not a registered body, so PhysicsSystem::DrawBodies
never renders it (Jolt provides no CharacterVirtual::DebugDraw either).
Add CharacterController::DebugDraw (shape wireframe at foot-origin +
shapeOffset, green) and drive it from CollectDebugData under the
colliders bit.
2026-10-03 19:01:57 +08:00
atdunbg 29c413579d refactor(physics): rename isSensor to isTrigger (Unity/UE terminology)
UE calls it Trigger (Overlap Events / Trigger preset) and Unity's UI
says "Is Trigger"; "sensor" is the physics-backend term (Jolt sensor,
Box2D sensor). Rename the component/record flag and the Inspector
label. Scene files now persist "IsTrigger" with backward-compatible
read of the old "IsSensor" key.
2026-10-03 18:53:38 +08:00
atdunbg b6d0868e15 feat(ecs): per-component enable/disable + inspector rework
Add `active` flag to the 8 behavior components (Unity enabled / UE
SetActive semantics; structural Id/Name/Transform excluded). Consumers
skip disabled components: renderer (mesh/light/anim), primary camera
election, physics reconciliation (body/constraint/character destroyed
and rebuilt on toggle), scripts keep instance but stop ticking.

Inspector: component headers now carry an enable checkbox and a remove
button, Add-Component menu greys out attached entries, and conflict
hints added (RigidBody <-> CharacterController mutual exclusion, Joint
requires RigidBody, Animation requires MeshRenderer, unregistered
script name). Scenes persist Active per component (default true).
2026-10-03 18:45:03 +08:00
atdunbg cad0481bc0 feat(app): WASDMove script (WASD camera-relative movement)
ScriptBehavior gains App() host access (Application injected via
ScriptRuntime), TryGet<T> and EntityBits for physics facade addressing.
WASDMove maps WASD to the editor camera's horizontal heading (UE
gameplay convention): dynamic bodies go through the physics velocity
facade preserving the vertical component (zeroing Y would cancel
gravity mid-fall), bodiless entities translate their transform directly.
Editor-mode key conflict is naturally avoided: RMB-held flight consumes
WASD, release hands keys to the script.
2026-10-03 18:27:12 +08:00
atdunbg 3403822a52 feat(engine): C++ native script system (ScriptBehavior + registry)
ScriptComponent stores only a script name string (POD, serializable);
instances are reconciled by ScriptRuntime each frame (PhysicsWorld
pattern): orphan/rename cleanup with OnDestroy, per-name instantiation
from a file-level self-registering registry (ENGINE_REGISTER_SCRIPT),
OnCreate on bind, OnUpdate after physics step on scaled dt. Engine
framerate loop only sees the base interface, so future Lua/C# behaviors
plug in as foreign ScriptBehavior implementations with zero engine
changes. Ships a Spinner sample script and Inspector script picker.
2026-10-03 18:02:49 +08:00
atdunbg ea6beee593 fix(physics): editor drag teleports dynamic bodies
SyncScene only reconciled static/kinematic transforms into Jolt bodies;
dragging a dynamic entity (esp. while paused, where Writeback never runs)
moved the visual but left the body -- and the collider debug wireframe --
at the old pose. Dynamic bodies now get the same drag reconciliation
(teleport + zero velocity + activate), and Writeback syncs
lastPos/lastRot to the body pose so physics motion is never misread as
editor dragging (which would teleport the body back).
2026-10-03 17:51:11 +08:00
atdunbg d5a86c170e fix(renderer): mesh collider source positions in render space for skinned meshes
Skinned meshes keep vertices in raw mesh space (bakeTransform=false);
rendering transforms them via bind palette (global(bone) x invBind, folds
FBX unit scale). Physics MeshShape/ConvexHull and the mesh bounding sphere
consumed the raw positions directly -> M1911 hull measured ~10x the visual
size. ParseFromDisk now bakes bind-palette-skinned positions per vertex
(same formula as the skinning shader) into ParsedMesh::cpuPositions; Mesh
uses them for m_CpuPositions and bounds. GPU vertex data and rendering are
untouched.
2026-10-03 17:43:55 +08:00
atdunbg e1cef53158 physics: character controller (Jolt CharacterVirtual)
- CharacterControllerComponent: capsule params (radius/halfHeight, mass,
  max slope, step height); entity origin = feet via shape offset
- CharacterController per-entity wrapper: gravity integration + grounded
  clamp, ExtendedUpdate (sweep/slide + stick-to-floor + walk-stairs),
  SetLinearVelocity/Jump/IsGrounded facade
- PhysicsWorld: SyncCharacters reconcile (create/rebuild on param
  change, editor-drag teleport), UpdateCharacters after physics Update
  (Jolt recommended order), writes back to Transform
- Inspector: params + grounded/velocity telemetry + jump debug button
- Serialization: params only, state rebuilt by reconcile
2026-10-03 16:34:20 +08:00
atdunbg 82910c41e8 physics: joint constraints (fixed/hinge/ball)
- JointComponent: type, target UUID, per-body local anchors, hinge axis
  + angle limits (data only, zero Jolt deps)
- PhysicsWorld::SyncConstraints: create/remove/rebuild on snapshot
  change; body rebuild contagion via invalidated-bits list; target
  resolved lazily by UUID each sync (no two-pass load needed)
- Jolt FixedConstraint/HingeConstraint/PointConstraint in
  LocalToBodyCOM space (anchors minus shape COM); fixed frames lock
  creation-time relative pose; hinge axis aligned across bodies
- Inspector: Joint section with target entity name dropdown
- Serialization: Joint map with UUID target, as<T> fallbacks
2026-10-03 16:26:20 +08:00
atdunbg a744b332eb physics: capsule/cylinder/compound collider shapes
- ColliderType + Capsule/Cylinder/Compound; CompoundChild list (Box/
  Sphere/Capsule, local pos/quat, non-recursive)
- CreateBody: CapsuleShape/CylinderShape (ctor param orders differ) +
  StaticCompoundShape; empty/failed compound falls back to Box
- BodyRecord caches radius/halfHeight/compound; SyncScene rebuilds on
  change (per-collider param extraction)
- Inspector: combo entries + shape params + compound child list editor
- Serialization: new fields with as<T> fallback, compound seq round-trip
2026-10-03 16:17:44 +08:00
atdunbg 6c81bfd70d physics: rigid body drive API (force/impulse/torque/velocity)
PhysicsWorld facade methods addressed by entity bits via body records:
AddForce/AddImpulse/AddTorque (dynamic bodies only, auto-wake), Set/
GetLinearVelocity/SetAngularVelocity (dynamic + kinematic). Inspector
RigidBody block gains impulse debug controls for manual response tests.
Transient by design — nothing serialized.
2026-10-03 16:10:22 +08:00
atdunbg fae6614e70 editor camera: dedicated Scene-view camera decoupled from camera entities
EditorCamera (UE FEditorViewportClient semantics) owned by SceneRenderer:
own TransformComponent + reused Camera controller. BuildFrame dispatches
render source by display mode — editor form always renders from the
editor camera (usable in empty scenes), game form renders from the
scene's primary camera entity. AlignTo copies an entity pose into the
editor camera ("Align Viewport to Camera" button in Inspector). F focus
and viewport gizmo now follow the same dispatch. main.cpp stripped of
dangling sandbox camera/spinner state; onSceneLoaded hook removed.
2026-10-03 15:52:28 +08:00
atdunbg e2a9c21e68 fix(serialization): load failure on hardened yaml-cpp + flow-style arrays
yaml-cpp master hardens zombie nodes: operator=, size(), Type() and
chained operator[] all throw InvalidNode instead of silently passing.
Storing per-component nodes via Node::operator= threw for every key
that was absent, so all 7 entities were skipped on load (0 loaded).
Reads are now level-by-level guarded (copy-init + IsDefined), which
only uses the safe surface (as<T>(fallback), begin(), IsDefined).

Save side switched from node-tree streaming to a manual Emitter:
vector/quat fields emit as single-line flow arrays ([0, 1.2, -6])
and model refs as inline maps, with 9-digit float precision for
lossless float32 round-trips.

Verified offline against the previously saved scene: 7/7 entities
parse with correct component counts; emitter output round-trips.
2026-10-03 15:30:36 +08:00
atdunbg 007d175314 serialization: scene save/load (Unity-style YAML + UE-style GUID registry)
Engine-layer SceneSerializer (yaml-cpp submodule): entities/components
to versioned YAML, atomic clear-and-rebuild on load with per-entity
fault tolerance, missing-field defaults, unknown-component skip and
UUID dedup. Asset references are {Guid, Path} pairs resolved through
the new AssetManager GUID registry (assets/asset_registry.yaml) with
path fallback; procedural:// pseudo-paths rebuild from parameters.
Editor File menu gets Save/Load Scene modals; app re-resolves stale
entity handles via onSceneLoaded callback.
2026-10-03 14:47:02 +08:00
atdunbg ffe33e9967 camera: UE Scene-view semantics — RMB fly, MMB focus-orbit, wheel dolly, F focus
Key conflict fix (UE semantics): while RMB is held the keyboard belongs
to the camera, so WASD no longer collides with the gizmo W/E/R tool
hotkeys. Editor mode gates fly movement behind RMB; game mode keeps
always-on WASD. Viewport tool hotkeys (W/E/R/T/F) are suppressed while
RMB is held.

Focus orbit (pivot): MMB drag rotates the camera around a pivot keeping
distance; first orbit picks a pivot 5m ahead on the view ray. Shift+MMB
pans in screen plane (camera and pivot move together). Mouse wheel
dollies toward/away from the pivot with exponential steps (no pivot:
move along view ray). F focuses the selected entity (frame-selection):
pivot set to its position, camera slides along the view ray; orbit
distance persists.

Window gains a polled scroll-delta channel (ConsumeScrollDelta) that
respects ImGui mouse capture; Camera::Update mutates orbit state (no
longer const).
2026-10-03 13:02:55 +08:00
atdunbg 9ff08651f0 editor: revert self-drawn scale gizmo, clip degenerate axes instead
User feedback: the self-drawn scale handles clash visually with the
ImGuizmo translate/rotate rods (different size/style, clearly not one
widget). Keep the library widget for all three ops for visual
consistency and prevent mis-drags by masking degenerate axes via
SetAxisMask:

- translate: axis ~ parallel to camera-to-object line (sin < 0.25)
- scale: axis ~ perpendicular to camera-to-object line (|dot| < 0.1,
  intersection numerator identically zero -> grab fails/values jump)
  OR axis ~ parallel to view direction (|dot| > 0.98, zero screen
  displacement -> value never changes)
- rotate: screen-space math, no degeneracy, no mask

Masked axes are neither drawn nor activatable; a slight view change
restores them. Center uniform-scale square (MT_SCALE_XYZ, pure screen
space) stays available in any view.
2026-10-03 12:46:07 +08:00
atdunbg c9d4eed043 editor: scale gizmo fully self-drawn (screen-space, view-independent)
ImGuizmo scale hover/drag relies on ray-plane intersection with the
axis as plane normal; in front views (axis ⟂ camera-to-object) the
intersection numerator is identically zero, so handles can't be
grabbed and values jump randomly; axes parallel to the view have zero
screen displacement. Submodule is read-only and AxisMask can only hide
degenerate axes, not replace them.

Replace the library scale path entirely with a screen-space gizmo:
- three axis rods (line + end square) + center uniform-scale square,
  constant screen size in any view
- axis drag: mouse delta projected onto the axis' screen direction
- axis pointing at camera: radial mapping (drag away = grow)
- center square: uniform scale (library MT_SCALE_XYZ semantics)
- Ctrl snapping quantizes the result; scale clamped >= 0.01
- pick suppressed while hovering/dragging the self-drawn handles
2026-10-03 12:23:00 +08:00
atdunbg 21b33d7d7f fix(editor): 缩放轴病态判据与平移互补,按 op 分派 AxisMask
缩放独有老问题根因(源码实证):缩放 hover(GetScaleType)与拖拽
(HandleScale ratio 分母)依赖"法线=轴"平面求交,交点距离分子 numer
= -dist·dot(camToObj, 轴)。正对视角相机与物体无横偏时 numer≡0 →
len=0/垃圾 → 杆 hover 不到、ratio 分子分母乱跳(数值随机猛变)。
平移 hover 是纯屏幕距离、平面法线经正交化自适应;旋转是屏幕角数学
——故唯独缩放有此病。SCALE op 下判据换 |dot(camToObj,轴)|<0.1,
ROTATE 不 mask,TRANSLATE 保持 sinθ<0.25。中心块等比缩放
(MT_SCALE_XYZ,屏幕空间数学)不受 mask 恒可用。
2026-10-03 12:11:16 +08:00
atdunbg a130850276 feat(editor): Gizmo 退化轴自动隐藏(调用层 SetAxisMask,UE 语义)
正对视角 + 物体竖直偏移时,单轴拖拽灵敏度 sinθ(轴与相机连线夹角)→0:
拖拽投影系数趋 0("数值不变"),激活平面与鼠标射线共面(IntersectRayPlane
denom→0 返回 -1 被当交点距离)。按灵敏度 <0.25(θ<14.5°)将病态轴
SetAxisMask 隐藏(库公开 API,按轴关绘制+激活,保留含该轴的平面手柄
与中心块兜底),相机移动后自动恢复——可激活性决定可见性,零库改动。
2026-10-03 12:03:07 +08:00
atdunbg 1d360369c9 revert: 删除 SetPlaneLimit 覆盖,Gizmo 参数与 Horizon 对齐(全默认)
对照 Horizon 实现(同官方 ImGuizmo submodule、零定制、Manipulate 全
默认参数)确认库默认行为就是可用的出货行为。0.034 把轴隐藏阈值从
11.5° 抬到 20°,常规视角轴被过早隐藏("箭头又没了");之前 0.35 同
根因。两次阈值折腾全部推翻:自行覆盖库内部可见度参数本身就是错的方向。
2026-10-03 02:27:46 +08:00
43 changed files with 3134 additions and 308 deletions
+3
View File
@@ -32,3 +32,6 @@
[submodule "Lib/ImGuizmo"] [submodule "Lib/ImGuizmo"]
path = Lib/ImGuizmo path = Lib/ImGuizmo
url = https://github.com/CedricGuillemet/ImGuizmo.git url = https://github.com/CedricGuillemet/ImGuizmo.git
[submodule "Lib/yaml-cpp"]
path = Lib/yaml-cpp
url = https://github.com/jbeder/yaml-cpp.git
+5 -2
View File
@@ -1,9 +1,12 @@
# App 层源文件:main + editor/(docking 编辑器,纯 App 代码) # App 层源文件:main + editor/(docking 编辑器)+ scripts/(C++ 原生脚本)
file(GLOB EDITOR_SOURCES CONFIGURE_DEPENDS file(GLOB EDITOR_SOURCES CONFIGURE_DEPENDS
${CMAKE_CURRENT_SOURCE_DIR}/editor/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/editor/*.cpp
${CMAKE_CURRENT_SOURCE_DIR}/editor/*.h) ${CMAKE_CURRENT_SOURCE_DIR}/editor/*.h)
add_executable(HelloVulkan main.cpp ${EDITOR_SOURCES}) file(GLOB SCRIPT_SOURCES CONFIGURE_DEPENDS
${CMAKE_CURRENT_SOURCE_DIR}/scripts/*.cpp)
add_executable(HelloVulkan main.cpp ${EDITOR_SOURCES} ${SCRIPT_SOURCES})
target_include_directories(HelloVulkan PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/editor) target_include_directories(HelloVulkan PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/editor)
+153
View File
@@ -15,12 +15,17 @@
#include "ViewportPanel.h" #include "ViewportPanel.h"
#include "StatsPanel.h" #include "StatsPanel.h"
#include <Engine/Core/Log.h>
#include <Engine/Serialization/SceneSerializer.h>
#include <imgui.h> #include <imgui.h>
#include <imgui_internal.h> // DockBuilder*(内部 API) #include <imgui_internal.h> // DockBuilder*(内部 API)
#include <ImGuizmo.h> #include <ImGuizmo.h>
#include <GLFW/glfw3.h> #include <GLFW/glfw3.h>
#include <filesystem>
using namespace Engine; // kDisplayViewport / kDisplayDirect(SceneTypes.h) using namespace Engine; // kDisplayViewport / kDisplayDirect(SceneTypes.h)
Editor::Editor() = default; Editor::Editor() = default;
@@ -79,6 +84,7 @@ void Editor::Draw()
if (m_ShowStats) m_Stats->Draw(m_Ctx); if (m_ShowStats) m_Stats->Draw(m_Ctx);
DrawQuitModal(); DrawQuitModal();
DrawSceneModals();
} }
// 输入归属判定:ImGui 侧经自身后端链式回调永远正常收输入,这里 // 输入归属判定:ImGui 侧经自身后端链式回调永远正常收输入,这里
@@ -126,6 +132,15 @@ void Editor::DrawMenuBar()
if (ImGui::BeginMenu("File")) if (ImGui::BeginMenu("File"))
{ {
if (ImGui::MenuItem("Save Scene...", nullptr))
{
m_ShowSaveModal = true;
}
if (ImGui::MenuItem("Load Scene...", nullptr))
{
m_ShowLoadModal = true;
}
ImGui::Separator();
if (ImGui::MenuItem("Quit", "Alt+F4")) if (ImGui::MenuItem("Quit", "Alt+F4"))
{ {
RequestQuit(); RequestQuit();
@@ -215,3 +230,141 @@ void Editor::DrawQuitModal()
m_Ctx.app->Close(); m_Ctx.app->Close();
} }
} }
void Editor::DrawSceneModals()
{
// 保存/加载共用一个路径输入(UE 同款:默认路径记住上次输入)
if (m_ShowSaveModal)
{
ImGui::OpenPopup("Save Scene");
m_ShowSaveModal = false;
}
if (m_ShowLoadModal)
{
ImGui::OpenPopup("Load Scene");
m_ShowLoadModal = false;
}
ImGui::SetNextWindowPos(
ImGui::GetMainViewport()->GetCenter(),
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("Save Scene", nullptr,
ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::TextUnformatted("Scene file path (relative to working dir):");
ImGui::SetNextItemWidth(400.0f);
ImGui::InputText("##SavePath", m_ScenePathBuf, sizeof(m_ScenePathBuf));
ImGui::Separator();
if (ImGui::Button("Save", ImVec2(120, 0)))
{
DoSaveScene();
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(120, 0)))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
if (ImGui::BeginPopupModal("Load Scene", nullptr,
ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::TextUnformatted("Scene file path (relative to working dir):");
ImGui::SetNextItemWidth(400.0f);
ImGui::InputText("##LoadPath", m_ScenePathBuf, sizeof(m_ScenePathBuf));
ImGui::Separator();
if (ImGui::Button("Load", ImVec2(120, 0)))
{
DoLoadScene();
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(120, 0)))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
void Editor::DoSaveScene()
{
const std::string path = m_ScenePathBuf;
if (path.empty() || !m_Ctx.scene || !m_Ctx.assets)
{
return;
}
// scenes/ 目录不存在时自动创建(空父目录/已存在均合法)
std::error_code ec;
std::filesystem::create_directories(
std::filesystem::path(path).parent_path(), ec);
// 环境路径:引擎序列化器只认字符串,查表由编辑器侧负责
std::string envPath;
if (m_Ctx.getEnvIndex)
{
const int idx = m_Ctx.getEnvIndex();
if (idx >= 0 && idx < kEnvCount) envPath = kEnvPaths[idx];
}
if (Serialization::SceneSerializer::Serialize(*m_Ctx.scene, *m_Ctx.assets,
path, envPath))
{
ENGINE_INFO("Editor: scene saved to '{}'", path);
}
else
{
ENGINE_ERROR("Editor: failed to save scene to '{}'", path);
}
}
void Editor::DoLoadScene()
{
const std::string path = m_ScenePathBuf;
if (path.empty() || !m_Ctx.scene || !m_Ctx.assets)
{
return;
}
std::string envPath;
if (!Serialization::SceneSerializer::Deserialize(*m_Ctx.scene,
*m_Ctx.assets, path,
envPath))
{
ENGINE_ERROR("Editor: failed to load scene '{}'", path);
return;
}
ENGINE_INFO("Editor: scene loaded from '{}'", path);
// 旧实体已销毁:清选中(连带清描边,见 SelectEntity)
SelectEntity(entt::null);
// 环境恢复:场景带环境且命中环境表才切换(镜像 StatsPanel 同步路径)
if (!envPath.empty() && m_Ctx.getEnvIndex && m_Ctx.setEnvIndex)
{
int found = -1;
for (int i = 0; i < kEnvCount; ++i)
{
if (envPath == kEnvPaths[i]) { found = i; break; }
}
if (found >= 0)
{
m_Ctx.setEnvIndex(found);
const auto env = m_Ctx.assets->LoadEnvironment(kEnvPaths[found]);
if (env.Valid())
{
m_Ctx.renderer->SetEnvironment(
m_Ctx.assets->GetEnvironment(env));
}
}
else
{
ENGINE_WARN("Editor: scene env '{}' not in env table, "
"keeping current", envPath);
}
}
}
+8
View File
@@ -73,6 +73,9 @@ private:
void DrawMenuBar(); // host 窗口内顶栏 void DrawMenuBar(); // host 窗口内顶栏
void DrawDockSpace(); // 全屏 host + dockspace + 默认布局 void DrawDockSpace(); // 全屏 host + dockspace + 默认布局
void DrawQuitModal(); // 退出确认模态框 void DrawQuitModal(); // 退出确认模态框
void DrawSceneModals(); // 保存/加载路径模态框(共用一个路径输入)
void DoSaveScene(); // 模态确认动作:目录创建 + 序列化落盘
void DoLoadScene(); // 模态确认动作:反序列化 + 选中清空 + 环境恢复 + 回调
EditorContext m_Ctx{}; EditorContext m_Ctx{};
@@ -98,6 +101,11 @@ private:
// 退出确认链 // 退出确认链
bool m_ShowQuitConfirm = false; bool m_ShowQuitConfirm = false;
bool m_QuitConfirmed = false; bool m_QuitConfirmed = false;
// 场景保存/加载(File 菜单 → 路径模态框;无文件对话框库,InputText 手输)
bool m_ShowSaveModal = false;
bool m_ShowLoadModal = false;
char m_ScenePathBuf[256] = "scenes/sandbox.scene";
}; };
#endif //HELLOVULKAN_EDITOR_H #endif //HELLOVULKAN_EDITOR_H
+4
View File
@@ -12,6 +12,7 @@
#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>
#include <Engine/Renderer/EditorCamera.h>
#include <Engine/Renderer/SceneTypes.h> // kDisplayViewport / kDisplayDirect #include <Engine/Renderer/SceneTypes.h> // kDisplayViewport / kDisplayDirect
#include <functional> #include <functional>
@@ -37,6 +38,9 @@ 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;
// 编辑器相机(Scene view 专用,归 Renderer 持有——与场景相机实体
// 无关;面板经此做 F 焦点/Gizmo 视图/对齐跳转)
Engine::EditorCamera *editorCamera = nullptr;
std::function<int()> getEnvIndex; // 当前环境表下标 std::function<int()> getEnvIndex; // 当前环境表下标
std::function<void(int)> setEnvIndex; std::function<void(int)> setEnvIndex;
+410 -38
View File
@@ -8,11 +8,14 @@
#include "InspectorPanel.h" #include "InspectorPanel.h"
#include "Engine/ECS/Components.h" #include "Engine/ECS/Components.h"
#include "Engine/Renderer/Model.h" #include "Engine/Renderer/Model.h"
#include "Engine/Physics/PhysicsWorld.h" // Apply Impulse 调试(GetPhysics 门面)
#include "Engine/Scripting/Script.h" // 脚本下拉(ScriptRegistry::Names)
#include <imgui.h> #include <imgui.h>
#include <cstdio> #include <cstdio>
#include <vector> #include <vector>
#include <type_traits>
namespace namespace
{ {
@@ -62,6 +65,44 @@ namespace
ImGui::PopID(); ImGui::PopID();
return changed; return changed;
} }
// 组件区块头(Unity 语义):折叠标题 + 右缘"启用"勾选与"删除"按钮。
// active 传 nullptr = 无启停(结构性组件);removable = 显示删除钮,
// 点击时置 *removeFlag,调用方在区块末尾统一移除(避免边画边删)
bool ComponentHeader(const char* label, bool* active, bool removable,
bool* removeFlag)
{
ImGui::PushID(label);
bool open;
if (active != nullptr)
{
open = ImGui::CollapsingHeader(
label, ImGuiTreeNodeFlags_DefaultOpen |
ImGuiTreeNodeFlags_AllowOverlap);
const float rowH = ImGui::GetItemRectSize().y;
float x = ImGui::GetWindowWidth() - 32.0f;
if (removable)
{
ImGui::SameLine(x);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 1.0f);
if (ImGui::Button("-", ImVec2(24.0f, rowH - 4.0f)))
{
*removeFlag = true;
}
x -= 30.0f;
}
ImGui::SameLine(x);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 1.0f);
ImGui::Checkbox("##active", active);
}
else
{
open = ImGui::CollapsingHeader(label,
ImGuiTreeNodeFlags_DefaultOpen);
}
ImGui::PopID();
return open;
}
} }
void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
@@ -83,46 +124,47 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
} }
ImGui::Separator(); ImGui::Separator();
// ---- Add Component ---- // ---- Add Component(已挂载项置灰:Unity Add Component 语义)----
if (ImGui::Button("Add Component")) if (ImGui::Button("Add Component"))
{ {
ImGui::OpenPopup("add_component"); ImGui::OpenPopup("add_component");
} }
if (ImGui::BeginPopup("add_component")) if (ImGui::BeginPopup("add_component"))
{ {
if (!reg.all_of<Engine::ECS::LightComponent>(selected) && auto menuItem = [&](auto typeTag, const char* label)
ImGui::MenuItem("Light"))
{ {
reg.emplace<Engine::ECS::LightComponent>(selected); using T = std::remove_pointer_t<decltype(typeTag)>;
} const bool has = reg.all_of<T>(selected);
if (!reg.all_of<Engine::ECS::CameraComponent>(selected) && if (has)
ImGui::MenuItem("Camera")) {
{ ImGui::BeginDisabled(true);
reg.emplace<Engine::ECS::CameraComponent>(selected); }
} if (ImGui::MenuItem(label))
if (!reg.all_of<Engine::ECS::AnimationComponent>(selected) && {
ImGui::MenuItem("Animation")) reg.emplace<T>(selected);
{ }
reg.emplace<Engine::ECS::AnimationComponent>(selected); if (has)
} {
if (!reg.all_of<Engine::ECS::NameComponent>(selected) && ImGui::EndDisabled();
ImGui::MenuItem("Name")) }
{ };
reg.emplace<Engine::ECS::NameComponent>(selected); menuItem(static_cast<Engine::ECS::LightComponent*>(nullptr), "Light");
} menuItem(static_cast<Engine::ECS::CameraComponent*>(nullptr), "Camera");
if (!reg.all_of<Engine::ECS::RigidBodyComponent>(selected) && menuItem(static_cast<Engine::ECS::AnimationComponent*>(nullptr), "Animation");
ImGui::MenuItem("RigidBody")) menuItem(static_cast<Engine::ECS::NameComponent*>(nullptr), "Name");
{ menuItem(static_cast<Engine::ECS::RigidBodyComponent*>(nullptr), "RigidBody");
reg.emplace<Engine::ECS::RigidBodyComponent>(selected); menuItem(static_cast<Engine::ECS::JointComponent*>(nullptr), "Joint");
} menuItem(static_cast<Engine::ECS::CharacterControllerComponent*>(nullptr),
"Character Controller");
menuItem(static_cast<Engine::ECS::ScriptComponent*>(nullptr), "Script");
ImGui::EndPopup(); ImGui::EndPopup();
} }
ImGui::Separator(); ImGui::Separator();
// ---- Transform ---- // ---- Transform(结构性组件:不可删、无启停)----
if (auto* t = reg.try_get<Engine::ECS::TransformComponent>(selected)) if (auto* t = reg.try_get<Engine::ECS::TransformComponent>(selected))
{ {
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) if (ComponentHeader("Transform", nullptr, false, nullptr))
{ {
glm::vec3 pos = t->GetPosition(); glm::vec3 pos = t->GetPosition();
if (DragVec3Row("position", pos, 0.05f)) if (DragVec3Row("position", pos, 0.05f))
@@ -145,9 +187,13 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
// ---- MeshRenderer ---- // ---- MeshRenderer ----
if (auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected)) if (auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected))
{ {
if (ImGui::CollapsingHeader("MeshRenderer", bool removeMr = false;
ImGuiTreeNodeFlags_DefaultOpen)) if (ComponentHeader("MeshRenderer", &mr->active, true, &removeMr))
{ {
if (!mr->active)
{
ImGui::BeginDisabled(true); // 禁用 = 内容置灰(Unity 语义)
}
if (const Engine::Model* model = ctx.assets->GetModel(mr->model); if (const Engine::Model* model = ctx.assets->GetModel(mr->model);
model != nullptr) model != nullptr)
{ {
@@ -162,14 +208,27 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"model: INVALID (dangling ref)"); "model: INVALID (dangling ref)");
} }
if (!mr->active)
{
ImGui::EndDisabled();
}
}
if (removeMr)
{
reg.remove<Engine::ECS::MeshRendererComponent>(selected);
} }
} }
// ---- Light ---- // ---- Light ----
if (auto* light = reg.try_get<Engine::ECS::LightComponent>(selected)) if (auto* light = reg.try_get<Engine::ECS::LightComponent>(selected))
{ {
if (ImGui::CollapsingHeader("Light", ImGuiTreeNodeFlags_DefaultOpen)) bool removeLight = false;
if (ComponentHeader("Light", &light->active, true, &removeLight))
{ {
if (!light->active)
{
ImGui::BeginDisabled(true);
}
int type = static_cast<int>(light->type); int type = static_cast<int>(light->type);
if (ImGui::Combo("type", &type, "Directional\0Point\0")) if (ImGui::Combo("type", &type, "Directional\0Point\0"))
{ {
@@ -187,18 +246,31 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
ImGui::DragFloat("radius", &light->radius, 0.1f, 0.0f, 100.0f); ImGui::DragFloat("radius", &light->radius, 0.1f, 0.0f, 100.0f);
ImGui::TextDisabled("(position = entity Transform)"); ImGui::TextDisabled("(position = entity Transform)");
} }
if (!light->active)
{
ImGui::EndDisabled();
}
}
if (removeLight)
{
reg.remove<Engine::ECS::LightComponent>(selected);
} }
} }
// ---- Camera ---- // ---- Camera ----
if (auto* cam = reg.try_get<Engine::ECS::CameraComponent>(selected)) if (auto* cam = reg.try_get<Engine::ECS::CameraComponent>(selected))
{ {
if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen)) bool removeCam = false;
if (ComponentHeader("Camera", &cam->active, true, &removeCam))
{ {
if (!cam->active)
{
ImGui::BeginDisabled(true);
}
if (ImGui::Checkbox("primary", &cam->primary) && cam->primary) if (ImGui::Checkbox("primary", &cam->primary) && cam->primary)
{ {
// primary 互斥:启用本相机时清掉其余实体的 primary // primary 互斥:启用本相机时清掉其余实体的 primary
//(引擎取第一个 primary 作为渲染视角) //(引擎取第一个 primary 作为游戏形态渲染视角)
reg.view<Engine::ECS::CameraComponent>().each( reg.view<Engine::ECS::CameraComponent>().each(
[&](entt::entity e, Engine::ECS::CameraComponent& c) [&](entt::entity e, Engine::ECS::CameraComponent& c)
{ {
@@ -208,6 +280,26 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
} }
}); });
} }
// 对齐跳转:把本实体位姿复制给编辑器相机,Scene view 立即
// 从该位置观看(只取位姿,不接管渲染——渲染源分派在
// SceneRenderer,编辑器形态恒用编辑器相机)
if (ImGui::Button("Align Viewport to Camera") && ctx.editorCamera)
{
if (auto* tr = reg.try_get<Engine::ECS::TransformComponent>(
selected))
{
ctx.editorCamera->AlignTo(*tr);
}
}
if (!cam->active)
{
ImGui::EndDisabled();
}
}
if (removeCam)
{
reg.remove<Engine::ECS::CameraComponent>(selected);
} }
} }
@@ -215,15 +307,29 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
// type/collider/mass 变更 → PhysicsWorld 重建 body;其余参数即时生效 // type/collider/mass 变更 → PhysicsWorld 重建 body;其余参数即时生效
if (auto* rb = reg.try_get<Engine::ECS::RigidBodyComponent>(selected)) if (auto* rb = reg.try_get<Engine::ECS::RigidBodyComponent>(selected))
{ {
if (ImGui::CollapsingHeader("RigidBody", ImGuiTreeNodeFlags_DefaultOpen)) bool removeRb = false;
if (ComponentHeader("RigidBody", &rb->active, true, &removeRb))
{ {
if (!rb->active)
{
ImGui::BeginDisabled(true);
}
// 冲突提示:同实体挂 CharacterController 会同时建 body 与
// CharacterVirtual,两个"运动主体"互相打架
if (reg.all_of<Engine::ECS::CharacterControllerComponent>(selected))
{
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
"! conflicts with Character Controller "
"(disable or remove one)");
}
int type = static_cast<int>(rb->type); int type = static_cast<int>(rb->type);
if (ImGui::Combo("type", &type, "Static\0Dynamic\0Kinematic\0")) if (ImGui::Combo("type", &type, "Static\0Dynamic\0Kinematic\0"))
{ {
rb->type = static_cast<Engine::ECS::RigidBodyComponent::BodyType>(type); rb->type = static_cast<Engine::ECS::RigidBodyComponent::BodyType>(type);
} }
int collider = static_cast<int>(rb->collider); int collider = static_cast<int>(rb->collider);
if (ImGui::Combo("collider", &collider, "Box\0Sphere\0Mesh\0")) if (ImGui::Combo("collider", &collider,
"Box\0Sphere\0Mesh\0Capsule\0Cylinder\0Compound\0"))
{ {
rb->collider = static_cast<Engine::ECS::RigidBodyComponent::ColliderType>(collider); rb->collider = static_cast<Engine::ECS::RigidBodyComponent::ColliderType>(collider);
} }
@@ -235,6 +341,61 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
{ {
ImGui::DragFloat("radius", &rb->sphereRadius, 0.05f, 0.01f, 500.0f); ImGui::DragFloat("radius", &rb->sphereRadius, 0.05f, 0.01f, 500.0f);
} }
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Capsule)
{
// 半高 = 圆柱段半高(不含半球端盖),总高 = 2*(halfHeight + radius)
ImGui::DragFloat("radius", &rb->capsuleRadius, 0.05f, 0.01f, 500.0f);
ImGui::DragFloat("halfHeight", &rb->capsuleHalfHeight, 0.05f, 0.0f, 500.0f);
}
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Cylinder)
{
ImGui::DragFloat("radius", &rb->cylinderRadius, 0.05f, 0.01f, 500.0f);
ImGui::DragFloat("halfHeight", &rb->cylinderHalfHeight, 0.05f, 0.01f, 500.0f);
}
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Compound)
{
// 子形状列表:尺寸/位置变更 → PhysicsWorld 重建 body(既有模式)
ImGui::Text("children: %d", static_cast<int>(rb->compound.size()));
ImGui::SameLine();
if (ImGui::Button("+ add"))
{
rb->compound.emplace_back();
}
int remove = -1;
for (int i = 0; i < static_cast<int>(rb->compound.size()); ++i)
{
auto& c = rb->compound[i];
ImGui::PushID(i);
ImGui::Separator();
int sh = static_cast<int>(c.shape);
if (ImGui::Combo("shape", &sh, "Box\0Sphere\0Capsule\0"))
{
c.shape = static_cast<Engine::ECS::RigidBodyComponent::CompoundChild::Shape>(sh);
}
if (c.shape == Engine::ECS::RigidBodyComponent::CompoundChild::Shape::Box)
{
ImGui::DragFloat3("halfExtents", &c.boxHalfExtents.x, 0.05f, 0.01f, 500.0f);
}
else
{
ImGui::DragFloat("radius", &c.radius, 0.05f, 0.01f, 500.0f);
if (c.shape == Engine::ECS::RigidBodyComponent::CompoundChild::Shape::Capsule)
{
ImGui::DragFloat("halfHeight", &c.capsuleHalfHeight, 0.05f, 0.0f, 500.0f);
}
}
ImGui::DragFloat3("offset", &c.position.x, 0.05f);
if (ImGui::Button("remove"))
{
remove = i;
}
ImGui::PopID();
}
if (remove >= 0)
{
rb->compound.erase(rb->compound.begin() + remove);
}
}
else else
{ {
// Mesh:跟随 MeshRenderer 视觉网格(Static=精确网格, // Mesh:跟随 MeshRenderer 视觉网格(Static=精确网格,
@@ -245,27 +406,230 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
ImGui::SliderFloat("gravityScale", &rb->gravityScale, -1.0f, 5.0f); ImGui::SliderFloat("gravityScale", &rb->gravityScale, -1.0f, 5.0f);
ImGui::SliderFloat("friction", &rb->friction, 0.0f, 2.0f); ImGui::SliderFloat("friction", &rb->friction, 0.0f, 2.0f);
ImGui::SliderFloat("restitution", &rb->restitution, 0.0f, 1.0f); ImGui::SliderFloat("restitution", &rb->restitution, 0.0f, 1.0f);
ImGui::Checkbox("isSensor", &rb->isSensor); ImGui::Checkbox("isTrigger", &rb->isTrigger);
if (rb->bodyValid && if (rb->bodyValid &&
rb->type == Engine::ECS::RigidBodyComponent::BodyType::Dynamic) rb->type == Engine::ECS::RigidBodyComponent::BodyType::Dynamic)
{ {
ImGui::Text("vel: %.2f, %.2f, %.2f m/s", ImGui::Text("vel: %.2f, %.2f, %.2f m/s",
rb->velocity.x, rb->velocity.y, rb->velocity.z); rb->velocity.x, rb->velocity.y, rb->velocity.z);
// 驱动调试(冲量瞬态不序列化):直接推一把看响应
static glm::vec3 s_debugImpulse{0.0f, 5.0f, 0.0f};
ImGui::DragFloat3("impulse", &s_debugImpulse.x, 0.1f);
if (ImGui::Button("Apply Impulse"))
{
ctx.app->GetPhysics().AddImpulse(
static_cast<uint32_t>(entt::to_integral(selected)),
s_debugImpulse);
}
} }
if (!rb->active)
{
ImGui::EndDisabled();
}
}
if (removeRb)
{
reg.remove<Engine::ECS::RigidBodyComponent>(selected);
}
}
// ---- Joint(约束:两端 body 就绪后由 PhysicsWorld 对账创建)----
if (auto* jc = reg.try_get<Engine::ECS::JointComponent>(selected))
{
bool removeJc = false;
if (ComponentHeader("Joint", &jc->active, true, &removeJc))
{
if (!jc->active)
{
ImGui::BeginDisabled(true);
}
// 依赖提示:约束挂在两端刚体上,本体无 RigidBody 无法创建
if (!reg.all_of<Engine::ECS::RigidBodyComponent>(selected))
{
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
"! requires a RigidBody on this entity");
}
int type = static_cast<int>(jc->type);
if (ImGui::Combo("type", &type, "Fixed\0Hinge\0Ball\0"))
{
jc->type = static_cast<Engine::ECS::JointComponent::Type>(type);
}
// target 实体下拉:实体名列表,选中态按 UUID 匹配(变更写回)
std::string items;
std::vector<Engine::UUID> uuids;
int current = -1;
reg.view<Engine::ECS::IdComponent, Engine::ECS::NameComponent>().each(
[&](entt::entity e, const Engine::ECS::IdComponent& id,
const Engine::ECS::NameComponent& n)
{
if (e == selected)
{
return; // 自连无意义
}
if (id.uuid == jc->target)
{
current = static_cast<int>(uuids.size());
}
uuids.push_back(id.uuid);
items.append(n.name);
items.push_back('\0');
});
items.push_back('\0'); // ImGui Combo 双 \0 结尾
if (ImGui::Combo("target", &current, items.c_str()) && current >= 0)
{
jc->target = uuids[static_cast<size_t>(current)];
}
ImGui::DragFloat3("anchor A", &jc->anchorA.x, 0.05f);
ImGui::DragFloat3("anchor B", &jc->anchorB.x, 0.05f);
if (jc->type == Engine::ECS::JointComponent::Type::Hinge)
{
ImGui::DragFloat3("hinge axis", &jc->hingeAxis.x, 0.05f);
ImGui::Checkbox("enable limit", &jc->enableLimit);
if (jc->enableLimit)
{
ImGui::DragFloat("min deg", &jc->limitMin, 1.0f, -180.0f, 0.0f);
ImGui::DragFloat("max deg", &jc->limitMax, 1.0f, 0.0f, 180.0f);
}
}
if (!jc->active)
{
ImGui::EndDisabled();
}
}
if (removeJc)
{
reg.remove<Engine::ECS::JointComponent>(selected);
}
}
// ---- CharacterController(参数编辑;状态在 PhysicsWorld 运行时)----
if (auto* cc = reg.try_get<Engine::ECS::CharacterControllerComponent>(selected))
{
bool removeCc = false;
if (ComponentHeader("Character Controller", &cc->active, true, &removeCc))
{
if (!cc->active)
{
ImGui::BeginDisabled(true);
}
// 冲突提示:同实体挂 RigidBody 会同时建 CharacterVirtual 与 body
if (reg.all_of<Engine::ECS::RigidBodyComponent>(selected))
{
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
"! conflicts with RigidBody "
"(disable or remove one)");
}
// 实体原点 = 脚底(胶囊底端),总高 = 2*(halfHeight + radius)
ImGui::DragFloat("radius", &cc->radius, 0.02f, 0.05f, 5.0f);
ImGui::DragFloat("halfHeight", &cc->capsuleHalfHeight, 0.02f, 0.0f, 5.0f);
ImGui::DragFloat("mass", &cc->mass, 1.0f, 1.0f, 500.0f, "%.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);
// 遥测(只读):运行时状态在 PhysicsWorld 的 CharacterController
if (auto& phy = ctx.app->GetPhysics(); phy.IsEnabled())
{
const uint32_t bits =
static_cast<uint32_t>(entt::to_integral(selected));
ImGui::Text("grounded: %s",
phy.IsCharacterGrounded(bits) ? "yes" : "no");
const glm::vec3 v = phy.GetCharacterVelocity(bits);
ImGui::Text("vel: %.2f, %.2f, %.2f m/s", v.x, v.y, v.z);
// 跳跃调试按钮
if (ImGui::Button("Jump"))
{
phy.Jump(bits, 5.0f);
}
}
if (!cc->active)
{
ImGui::EndDisabled();
}
}
if (removeCc)
{
reg.remove<Engine::ECS::CharacterControllerComponent>(selected);
}
}
// ---- Script(按名解析注册表脚本;实例生命周期在 ScriptRuntime)----
if (auto* sc = reg.try_get<Engine::ECS::ScriptComponent>(selected))
{
bool removeSc = false;
if (ComponentHeader("Script", &sc->active, true, &removeSc))
{
if (!sc->active)
{
ImGui::BeginDisabled(true);
}
// 下拉列出全部已注册脚本(注册序);"<none>" = 清空(对账自动销毁实例)
const auto& names = Engine::Scripting::ScriptRegistry::Names();
std::vector<const char*> items;
items.push_back("<none>");
for (const auto& n : names)
{
items.push_back(n.c_str());
}
int cur = 0;
for (size_t i = 0; i < names.size(); ++i)
{
if (names[i] == sc->scriptName)
{
cur = static_cast<int>(i) + 1;
}
}
if (ImGui::Combo("script", &cur, items.data(),
static_cast<int>(items.size())))
{
sc->scriptName = (cur <= 0) ? "" : names[static_cast<size_t>(cur) - 1];
}
if (sc->scriptName.empty())
{
ImGui::TextDisabled("no script assigned");
}
// 已填名但注册表查无此类:场景加载早于脚本注册/改名都会走到这
else if (cur == 0)
{
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
"! script not registered: %s",
sc->scriptName.c_str());
}
if (!sc->active)
{
ImGui::EndDisabled();
}
}
if (removeSc)
{
reg.remove<Engine::ECS::ScriptComponent>(selected);
} }
} }
// ---- Animation ---- // ---- Animation ----
if (auto* anim = reg.try_get<Engine::ECS::AnimationComponent>(selected)) if (auto* anim = reg.try_get<Engine::ECS::AnimationComponent>(selected))
{ {
if (ImGui::CollapsingHeader("Animation", bool removeAnim = false;
ImGuiTreeNodeFlags_DefaultOpen)) if (ComponentHeader("Animation", &anim->active, true, &removeAnim))
{ {
if (!anim->active)
{
ImGui::BeginDisabled(true);
}
const auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected); const auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected);
const Engine::Model* model = const Engine::Model* model =
mr ? ctx.assets->GetModel(mr->model) : nullptr; mr ? ctx.assets->GetModel(mr->model) : nullptr;
if (model != nullptr && model->HasAnimations()) // 依赖提示:动画作用在 MeshRenderer 的模型骨骼上
if (mr == nullptr)
{
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
"! requires a MeshRenderer component");
}
else if (model != nullptr && model->HasAnimations())
{ {
const auto& clips = model->GetClips(); const auto& clips = model->GetClips();
std::vector<const char*> clipNames; std::vector<const char*> clipNames;
@@ -296,6 +660,14 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
{ {
ImGui::TextDisabled("(no animation clips on model)"); ImGui::TextDisabled("(no animation clips on model)");
} }
if (!anim->active)
{
ImGui::EndDisabled();
}
}
if (removeAnim)
{
reg.remove<Engine::ECS::AnimationComponent>(selected);
} }
} }
+15 -6
View File
@@ -19,18 +19,27 @@ void StatsPanel::Draw(const EditorContext& ctx)
auto& renderer = *ctx.renderer; auto& renderer = *ctx.renderer;
auto& app = *ctx.app; auto& app = *ctx.app;
// ---- Camera:主相机位姿(只读遥测;位姿编辑走 Inspector)---- // ---- Camera:视图相机位姿(只读遥测;跟随渲染源分派)----
{ {
Engine::ECS::TransformComponent* camT = nullptr; if (ctx.renderer->GetDisplayMode() == kDisplayViewport && ctx.editorCamera)
if (ctx.scene->GetPrimaryCamera(&camT) != nullptr && camT != nullptr)
{ {
const glm::vec3& pos = camT->GetPosition(); const glm::vec3& pos = ctx.editorCamera->GetTransform().GetPosition();
ImGui::Text("Camera pos"); ImGui::Text("Editor camera pos");
ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z); ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z);
} }
else else
{ {
ImGui::TextDisabled("(no primary camera)"); Engine::ECS::TransformComponent* camT = nullptr;
if (ctx.scene->GetPrimaryCamera(&camT) != nullptr && camT != nullptr)
{
const glm::vec3& pos = camT->GetPosition();
ImGui::Text("Game camera pos");
ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z);
}
else
{
ImGui::TextDisabled("(no primary camera)");
}
} }
ImGui::Separator(); ImGui::Separator();
} }
+107 -31
View File
@@ -40,6 +40,20 @@ namespace
constexpr float kSnapRotateDeg = 15.0f; // 度 constexpr float kSnapRotateDeg = 15.0f; // 度
constexpr float kSnapScale = 0.1f; // 缩放步进 constexpr float kSnapScale = 0.1f; // 缩放步进
// 平移拖拽灵敏度下限:sinθ(轴与"相机→物体"连线夹角)。低于该值
// 时拖拽投影系数 <0.25 且激活平面与鼠标射线近共面——视觉上
// "看得见、抓了没反应"。≈14.5°,略宽于库默认可见角(≈11.5°),
// 只藏真正病态的轴,不动可用轴
constexpr float kMoveMinSin = 0.25f;
// 缩放退化轴裁剪(防误拖动)阈值。库内缩放 hover/拖拽依赖
// "法线=轴"平面求交,两种病态几何:
// - 轴⊥"相机→物体"连线(正对视角无横偏):交点分子 numer =
// -dist·dot(camToObj, 轴) ≡ 0 → 杆抓不到、数值随机猛跳
// - 轴∥视线(轴指向相机):屏幕零位移 → 拖了数值不变
constexpr float kScaleMinDot = 0.1f; // ≈84.3°
constexpr float kScaleMaxFwdDot = 0.98f; // ≈11.5°,与库藏杆角一致
// Vulkan proj → ImGuizmo 期望的 OpenGL 形式(Y-up、Z∈[-1,1]): // Vulkan proj → ImGuizmo 期望的 OpenGL 形式(Y-up、Z∈[-1,1]):
// Y 行翻回(抵消 Vulkan clip Y 向下翻转);z 行 = 2*z - w 即 // Y 行翻回(抵消 Vulkan clip Y 向下翻转);z 行 = 2*z - w 即
// 深度 [0,1] → [-1,1]。glm 列主序 m[col][row],逐列改 row1/row2 // 深度 [0,1] → [-1,1]。glm 列主序 m[col][row],逐列改 row1/row2
@@ -52,6 +66,22 @@ namespace
} }
return p; return p;
} }
// 视图相机分派(必须与 SceneRenderer::BuildFrame 渲染源同一规则,
// 否则 Gizmo 杆与画面错位):编辑器形态 = EditorCamera(Scene view
// 专用,与相机实体无关);游戏形态 = primary 相机实体。
// 返回 nullptr = 游戏形态且无 primary 相机(本帧无 Gizmo)
Engine::Camera* ViewCamera(const EditorContext& ctx,
ECS::TransformComponent*& outT)
{
if (ctx.renderer->GetDisplayMode() == kDisplayViewport)
{
outT = &ctx.editorCamera->GetTransform();
return &ctx.editorCamera->GetController();
}
auto* cc = ctx.scene->GetPrimaryCamera(&outT);
return cc ? &cc->camera : nullptr;
}
} }
std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx, std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
@@ -111,48 +141,52 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
else size.y = avail.x / aspect; else size.y = avail.x / aspect;
ImGui::Image((ImTextureID) (intptr_t) sceneTex, size); ImGui::Image((ImTextureID) (intptr_t) sceneTex, size);
// ---- W/E/R 切换 Gizmo 模式(UE 语义:W 平移 / E 旋转 / // ---- 工具热键(UE 语义):W/E/R 切 Gizmo 模式、T 切
// R 缩放),T 切换 local/world 操纵空间。仅 Viewport 持焦时 // local/world、F 焦点选中实体。仅 Viewport 持焦时生效——
// 生效——输入框打字(WantTextInput)由 BlockingKeyboard 拦 // 输入框打字(WantTextInput)由 BlockingKeyboard 拦游戏侧,
// 游戏侧,但 ImGui 自身仍收键,故必须限定持焦面板,避免在 // 但 ImGui 自身仍收键,故必须限定持焦面板。RMB 按住 = 相机
// Inspector 拖数值时误切 ---- // 飞行模式(键盘归相机),工具热键全部抑制——W 前进与
if (out.focused && !ImGui::GetIO().WantTextInput) { // W 平移 Gizmo 的键位冲突就此解耦 ----
if (ImGui::IsKeyPressed(ImGuiKey_W)) gizmo.op = ImGuizmo::TRANSLATE;
if (ImGui::IsKeyPressed(ImGuiKey_E)) gizmo.op = ImGuizmo::ROTATE;
if (ImGui::IsKeyPressed(ImGuiKey_R)) gizmo.op = ImGuizmo::SCALE;
if (ImGui::IsKeyPressed(ImGuiKey_T)) gizmo.localSpace = !gizmo.localSpace;
}
// ---- Gizmo 操纵杆(ImGui drawlist 叠加,无 GPU pass)----
// 选中实体 + 有 Transform 才出杆;矩阵分解直接写回
// TransformComponent(与 Inspector 同源,下一帧数值自动跟随)
auto &reg = ctx.scene->GetRegistry(); auto &reg = ctx.scene->GetRegistry();
ECS::TransformComponent *tr = ECS::TransformComponent *tr =
selected != entt::null selected != entt::null
? reg.try_get<ECS::TransformComponent>(selected) ? reg.try_get<ECS::TransformComponent>(selected)
: nullptr; : nullptr;
if (out.focused && !ImGui::GetIO().WantTextInput &&
!ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
if (ImGui::IsKeyPressed(ImGuiKey_W)) gizmo.op = ImGuizmo::TRANSLATE;
if (ImGui::IsKeyPressed(ImGuiKey_E)) gizmo.op = ImGuizmo::ROTATE;
if (ImGui::IsKeyPressed(ImGuiKey_R)) gizmo.op = ImGuizmo::SCALE;
if (ImGui::IsKeyPressed(ImGuiKey_T)) gizmo.localSpace = !gizmo.localSpace;
if (ImGui::IsKeyPressed(ImGuiKey_F) && tr != nullptr) {
// F = 焦点选中实体(frame-selection):编辑器相机
// 环绕 pivot 设为选中实体位置,沿视线平移对准(环
// 绕距离沿用当前值,滚轮可调)
ctx.editorCamera->FocusOn(tr->GetPosition());
}
}
// ---- Gizmo 操纵杆(ImGui drawlist 叠加,无 GPU pass)----
// 选中实体 + 有 Transform 才出杆;矩阵分解直接写回
// TransformComponent(与 Inspector 同源,下一帧数值自动跟随)
if (tr != nullptr) { if (tr != nullptr) {
// 相机矩阵:primary 相机 view + Vulkan proj(转 GL 形式), // 相机矩阵:跟随渲染源(编辑器形态 = EditorCamera,游戏
// 参数与渲染侧一致(aspect = 渲染分辨率、near/far 0.1/100) // 形态 = primary 相机实体;与 SceneRenderer::BuildFrame
// 同一分派),参数与渲染侧一致(aspect = 渲染分辨率、
// near/far 0.1/100)
ECS::TransformComponent *camT = nullptr; ECS::TransformComponent *camT = nullptr;
if (const auto *camComp = ctx.scene->GetPrimaryCamera(&camT); if (Engine::Camera *cam = ViewCamera(ctx, camT);
camComp != nullptr && camT != nullptr) { cam != nullptr && camT != nullptr) {
const glm::mat4 view = camComp->camera.GetView(*camT); const glm::mat4 view = cam->GetView(*camT);
const glm::mat4 projGL = ProjVulkanToGL( const glm::mat4 projGL = ProjVulkanToGL(
camComp->camera.GetProjection(aspect, 0.1f, 100.0f)); cam->GetProjection(aspect, 0.1f, 100.0f));
ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList()); ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList());
// 杆画在 Image 区域内(等比适配后的 item rect) // 杆画在 Image 区域内(等比适配后的 item rect)
const ImVec2 mn = ImGui::GetItemRectMin(); const ImVec2 mn = ImGui::GetItemRectMin();
ImGuizmo::SetRect(mn.x, mn.y, size.x, size.y); ImGuizmo::SetRect(mn.x, mn.y, size.x, size.y);
// 轴可见度阈值:库内轴恒定屏幕尺寸(mGizmoSizeClipSpace // 不调 SetPlaneLimit/SetAxisLimit——可见度保持库默认
// =0.1),可见判定 = sinθ(轴与视线夹角) > 阈值/0.1。
// 默认 0.02 ⇒ θ>11.5° 就可见——"有高度差"的正对视角
// (θ≈15°)轴"看得见但几何上不可操作"(⊥视线轴悬停
// 失效、∥视线轴拖拽无一阶位移)。取 0.034 ⇒ θ<20° 的
// 轴隐藏(UE 正对轴隐藏语义),≥20° 正常可拖
ImGuizmo::SetPlaneLimit(0.034f);
glm::mat4 m = tr->World(); glm::mat4 m = tr->World();
// 操作空间:local(模型轴,旋转/缩放随物体朝向直观)/ // 操作空间:local(模型轴,旋转/缩放随物体朝向直观)/
@@ -160,11 +194,53 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
// decompose 写回链路不变 // decompose 写回链路不变
const ImGuizmo::MODE mode = const ImGuizmo::MODE mode =
gizmo.localSpace ? ImGuizmo::LOCAL : ImGuizmo::WORLD; gizmo.localSpace ? ImGuizmo::LOCAL : ImGuizmo::WORLD;
const glm::mat3 rotM(m);
// Ctrl 按住 → 吸附(UE Grid Snap 语义)。库的约定 // 退化轴裁剪(防误拖动):被裁轴不可见、不可激活,
// 转个视角即可用。三操作病态几何不同,按 op 分派:
// - 平移:拖拽灵敏度 = sinθ(轴与相机连线夹角),θ→0
// 时鼠标位移在轴上投影→0("数值不变")
// - 缩放:库内 hover/拖拽依赖"法线=轴"平面求交——
// 轴⊥相机连线(正对无横偏)时交点分子 numer≡0
// (抓不到/数值乱跳);轴∥视线时屏幕零位移(数值
// 不变)。两判据并集裁剪
// - 旋转:屏幕空间角度数学,无退化,不裁
const bool isTranslate = (gizmo.op
& int(ImGuizmo::TRANSLATE)) == int(ImGuizmo::TRANSLATE);
const bool isScale = (gizmo.op
& int(ImGuizmo::SCALE)) == int(ImGuizmo::SCALE);
if (isTranslate || isScale) {
bool maskAxis[3] = {false, false, false};
const glm::vec3 camPos(glm::inverse(view)[3]);
glm::vec3 camToObj = glm::vec3(m[3]) - camPos;
const float camDist = glm::length(camToObj);
if (camDist > 1e-4f) {
camToObj /= camDist;
const glm::vec3 viewFwd = -glm::normalize(
glm::vec3(view[0][2], view[1][2],
view[2][2]));
for (int i = 0; i < 3; ++i) {
const glm::vec3 axis = gizmo.localSpace
? glm::normalize(rotM[i])
: glm::vec3(float(i == 0), float(i == 1),
float(i == 2));
maskAxis[i] = isScale
? glm::abs(glm::dot(camToObj, axis))
< kScaleMinDot ||
glm::abs(glm::dot(viewFwd, axis))
> kScaleMaxFwdDot
: glm::length(glm::cross(camToObj, axis))
< kMoveMinSin;
}
}
ImGuizmo::SetAxisMask(maskAxis[0], maskAxis[1],
maskAxis[2]);
}
// Ctrl 吸附(UE Grid Snap 语义)。库的约定
//(docs/documentation.md):平移 = 世界单位 float[3] //(docs/documentation.md):平移 = 世界单位 float[3]
// 逐轴;旋转 = snap[0] 按度(库内自行 DEG2RAD,传弧度 // 逐轴;旋转 = snap[0] 按度(库内自行 DEG2RAD,传弧
// 会被解释成极小步进);缩放 = 只取 snap[0] // 度会被解释成极小步进);缩放 = 只取 snap[0]
float snap[3] = {0.0f, 0.0f, 0.0f}; float snap[3] = {0.0f, 0.0f, 0.0f};
const bool snapping = ImGui::GetIO().KeyCtrl; const bool snapping = ImGui::GetIO().KeyCtrl;
if (snapping) { if (snapping) {
+14 -166
View File
@@ -1,15 +1,18 @@
// //
// Created by atdunbg on 2026/9/20. // Created by atdunbg on 2026/9/20.
// //
// SandboxApp:第一个基于 Application 框架的程序。 // SandboxApp:基于 Application 框架的编辑器宿主程序。
// //
// main 的职责边界: // main 的职责边界:
// - 定义 Application 派生类并运行它 // - 定义 Application 派生类并运行它
// - "程序逻辑":场景搭建(实体/组件)、逐帧更新、事件响应、 // - 编辑器形态驱动 EditorCamera(Scene view 专用相机,非场景对象)
// 异步环境加载 // - 异步环境加载、全局热键、输入归属门控
// - UI 全部委托 App/editor/ 的 docking Editor(本文件不再出现 // - UI 全部委托 App/editor/ 的 docking Editor(本文件不再出现
// 任何 ImGui 控件) // 任何 ImGui 控件)
// //
// 场景内容(实体/组件)由编辑器 File 菜单 Save/Load 场景文件管理,
// 相机实体属于场景数据、只在游戏形态参与渲染(UE 语义)。
//
// 窗口、渲染器、事件泵、帧计时、场景容器全部由框架管理。 // 窗口、渲染器、事件泵、帧计时、场景容器全部由框架管理。
// //
@@ -18,16 +21,8 @@
#include <Engine/Core/Window.h> // SetInputCapture(Application.h 只前置声明) #include <Engine/Core/Window.h> // SetInputCapture(Application.h 只前置声明)
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
#include <Engine/Core/Stats.h> #include <Engine/Core/Stats.h>
#include <Engine/Core/AsyncTask.h>
#include <Engine/ECS/Scene.h>
#include <Engine/ECS/EntityHandle.h>
#include <Engine/ECS/Components.h>
#include <Engine/Assets/AssetManager.h>
#include <Engine/Renderer/Renderer.h> #include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/Model.h> #include <Engine/Renderer/EditorCamera.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <GLFW/glfw3.h> #include <GLFW/glfw3.h>
@@ -35,12 +30,9 @@
#include "EditorContext.h" // kEnvPaths / kDefaultEnv(与 StatsPanel 共用) #include "EditorContext.h" // kEnvPaths / kDefaultEnv(与 StatsPanel 共用)
using namespace Engine; using namespace Engine;
using namespace Engine::ECS;
namespace namespace
{ {
constexpr const char* kTemplePath = "assets/models/SunTemple/SunTemple.fbx";
class SandboxApp final : public Application class SandboxApp final : public Application
{ {
public: public:
@@ -58,7 +50,6 @@ namespace
void OnInit() override void OnInit() override
{ {
Scene& scene = GetScene();
Renderer& renderer = GetRenderer(); Renderer& renderer = GetRenderer();
renderer.SetDisplayMode(kDisplayViewport); renderer.SetDisplayMode(kDisplayViewport);
@@ -68,155 +59,17 @@ namespace
// Editor 注入:引擎门面 + 沙盒能力回调(环境异步切换等) // Editor 注入:引擎门面 + 沙盒能力回调(环境异步切换等)
InitEditor(); InitEditor();
// 默认环境(IBL 四件套):经资产层加载(compute 预积分,毫秒级),
// 其余环境在 Stats 面板里按需 lazy 加载后切换
const auto env = GetAssets().LoadEnvironment(kEnvPaths[0]);
if (env.Valid()) {
GetRenderer().SetEnvironment(GetAssets().GetEnvironment(env));
}
/*
const auto suntemple = GetAssets().LoadModel(kTemplePath);
auto entityhandle = scene.CreateEntity("SunTemple");
entityhandle.AddComponent<MeshRendererComponent>(MeshRendererComponent{suntemple});
*/
scene.CreateEntity("test entity");
scene.CreateEntity("test entity");
scene.CreateEntity("test entity");
const auto gun = GetAssets().LoadModel("assets/models/m1911/M1911.fbx");
if (gun.Valid()) {
m_Gun = scene.CreateEntity("M1911");
m_Gun.AddComponent<MeshRendererComponent>(MeshRendererComponent{gun});
m_Gun.AddComponent<AnimationComponent>();
auto& gt = m_Gun.GetComponent<TransformComponent>();
gt.SetPosition(glm::vec3(0.0f, 1.2f, -6.0f));
// 物理验证:Dynamic 刚体(默认 Box halfExtents 0.5),
// 出生位置悬空 → 应下落至地面静止(Transform 由物理写回)
m_Gun.AddComponent<RigidBodyComponent>();
} else {
ENGINE_WARN("m1911 model not loaded, animation test skipped");
}
// 地面:程序化纯色平面(半边长 15,中心 y=-0.5)+ 静态盒刚体
const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145);
m_Ground = scene.CreateEntity("Ground");
m_Ground.GetComponent<TransformComponent>()
.SetPosition(glm::vec3(0.0f, -0.5f, 0.0f)); // dirty → 帧循环统一 Compose
m_Ground.AddComponent<MeshRendererComponent>(MeshRendererComponent{ground});
auto& groundRb = m_Ground.AddComponent<RigidBodyComponent>();
groundRb.type = RigidBodyComponent::BodyType::Static;
groundRb.boxHalfExtents = glm::vec3(15.0f, 0.1f, 15.0f); // 平面顶点烘焙 ±15,不能用默认 0.5
/*
const auto gun = GetAssets().LoadModel("assets/models/m1911/M1911.fbx");
if (gun.Valid()) {
m_Gun = scene.CreateEntity("M1911");
m_Gun.AddComponent<MeshRendererComponent>(MeshRendererComponent{gun});
m_Gun.AddComponent<AnimationComponent>();
auto& gt = m_Gun.GetComponent<TransformComponent>();
gt.SetPosition(glm::vec3(0.0f, 1.2f, -6.0f));
} else {
ENGINE_WARN("m1911 model not loaded, animation test skipped");
}
int countSize = 0;
if (countSize) {
// 模型资产(PBR 验证场景:backpack 为旧式贴图资产,
// 退化为电介质因子路径——albedo+normal,metallic=0/roughness=0.5)
const auto model = GetAssets().LoadModel("assets/models/backpack/backpack.obj");
// const auto model = GetAssets().LoadModel("assets/models/pocket_watch_4k.gltf/pocket_watch_4k.gltf");
for (int i = 0; i < countSize; i ++)
{
for (int j = 0; j < countSize; j ++)
{
auto m = scene.CreateEntity();
m.AddComponent<MeshRendererComponent>(MeshRendererComponent{model});
m.GetComponent<TransformComponent>().SetPosition(glm::vec3{(i - countSize * 0.5) * 2, 0, (j - countSize * 0.5) * 2});
m_Spinners.push_back(m);
}
}
}
// 地面:程序化纯色平面(半边长 15,中心 y=-2)
const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145);
m_Ground = scene.CreateEntity("Ground");
m_Ground.GetComponent<TransformComponent>()
.SetPosition(glm::vec3(0.0f, -0.5f, 0.0f)); // dirty → 帧循环统一 Compose
m_Ground.AddComponent<MeshRendererComponent>(MeshRendererComponent{ground});
*/
// 相机实体(primary = 当前渲染视角;位姿存实体 Transform)
m_Camera = scene.CreateEntity("MainCamera");
auto& camera = m_Camera.AddComponent<CameraComponent>();
camera.primary = true;
m_Camera.GetComponent<TransformComponent>()
.SetPosition(glm::vec3(0.0f, 12.0f, 22.0f));
m_Camera.GetComponent<TransformComponent>()
.SetRotation(glm::vec3(-25.0f, 0.0f, 0.0f));
// 灯光实体(第一个方向光 = 阴影光源)
m_KeyLight = scene.CreateEntity("KeyLight");
m_KeyLight.AddComponent<LightComponent>(
LightComponent{
.type = LightComponent::Type::Directional,
.direction = glm::vec3(-0.5f, -0.6f, -0.35f),
.color = glm::vec4(1.0f, 0.95f, 0.9f, 5.0f),
.castShadow = true,
});
/*
scene.CreateEntity("FillLight")
.AddComponent<LightComponent>(
LightComponent{
.type = LightComponent::Type::Directional,
.direction = glm::vec3(0.5f, -0.5f, 0.3f),
.color = glm::vec4(0.3f, 0.5f, 1.0f, 1.5f)
});
*/
/*
// 点光源(位置 = 实体 Transform,出生即有;无阴影,距离衰减)
auto fire = scene.CreateEntity("FirePitLight");
fire.GetComponent<TransformComponent>().SetPosition(glm::vec3(2.0f, 1.5f, 1.0f));
fire.AddComponent<LightComponent>(
LightComponent{
.type = LightComponent::Type::Point,
.color = glm::vec4(1.0f, 0.6f, 0.3f, 1.0f), // 暖橙
.radius = 8.0f,
});
auto cool = scene.CreateEntity("CoolFillLight");
cool.GetComponent<TransformComponent>().SetPosition(glm::vec3(-2.5f, 2.0f, -2.0f));
cool.AddComponent<LightComponent>(
LightComponent{
.type = LightComponent::Type::Point,
.color = glm::vec4(0.3f, 0.5f, 1.0f, 1.2f), // 冷蓝
.radius = 10.0f,
});
*/
} }
void OnUpdate(const FrameTime& frameTime) override void OnUpdate(const FrameTime& frameTime) override
{ {
// 相机(WASD/Space/Shift 移动;位姿读写实体 Transform)。 // 编辑器形态:驱动编辑器相机(RMB 飞行 / MMB 焦点环绕 /
// 编辑器相机走真实轨:暂停(timeScale=0)时仍可飞行观察 // 滚轮推拉)。暂停(timeScale=0)时仍可飞行——unscaled dt。
m_Camera.GetComponent<CameraComponent>() // 游戏形态:相机实体由游戏逻辑驱动(本沙盒无逻辑 = 静止)
.camera.Update(GetWindow(), if (GetRenderer().GetDisplayMode() == kDisplayViewport)
m_Camera.GetComponent<TransformComponent>(),
frameTime.UnscaledDt());
// 旋转动画:编辑 Transform 分量后 Compose 重算 world。
// 动画时钟 = Time::Elapsed()(scaled 累计,暂停自动冻结——
// 帧历史验证:暂停后 Temporal Diff 视图 2 帧内整屏变黑)
for (size_t i = 0; i < m_Spinners.size(); ++i)
{ {
auto& t = m_Spinners[i].GetComponent<TransformComponent>(); GetRenderer().GetEditorCamera().Update(GetWindow(),
t.SetRotation(glm::vec3( frameTime.UnscaledDt());
0.0f, static_cast<f32>(Time::Elapsed()) * 60.0f +
static_cast<f32>(i) * 40.0f, 0.0f));
} }
} }
@@ -263,6 +116,7 @@ namespace
ctx.scene = &GetScene(); ctx.scene = &GetScene();
ctx.assets = &GetAssets(); ctx.assets = &GetAssets();
ctx.app = this; ctx.app = this;
ctx.editorCamera = &GetRenderer().GetEditorCamera();
ctx.getEnvIndex = [this] { return m_EnvIndex; }; ctx.getEnvIndex = [this] { return m_EnvIndex; };
ctx.setEnvIndex = [this](int index) { m_EnvIndex = index; }; ctx.setEnvIndex = [this](int index) { m_EnvIndex = index; };
// 继承原沙盒默认:初始暂停(StatsPanel 的 Pause Simulation 开关恢复) // 继承原沙盒默认:初始暂停(StatsPanel 的 Pause Simulation 开关恢复)
@@ -281,12 +135,6 @@ namespace
private: private:
std::unique_ptr<Editor> m_Editor = std::make_unique<Editor>(); std::unique_ptr<Editor> m_Editor = std::make_unique<Editor>();
EntityHandle m_Camera{}; // Scene::CreateEntity 之前为空
EntityHandle m_KeyLight{};
EntityHandle m_Ground{};
EntityHandle m_Gun{}; // 骨骼动画验证实体(M1911)
std::vector<EntityHandle> m_Spinners;
int m_EnvIndex = kDefaultEnv; int m_EnvIndex = kDefaultEnv;
}; };
} }
+47
View File
@@ -0,0 +1,47 @@
//
// Spinner.cpp:C++ 原生脚本示范——Y 轴匀速自转。
//
// 写脚本三步:
// 1. 新建 .cpp 放本目录(CMake 自动进构建)
// 2. 继承 ScriptBehavior,覆写 Name/OnCreate/OnUpdate/OnDestroy
// 3. 文件级 ENGINE_REGISTER_SCRIPT(类名) 自注册——Inspector 下拉
// 与场景序列化按该名解析
//
#include <Engine/Scripting/Script.h>
#include <glm/gtc/quaternion.hpp>
namespace {
class Spinner final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "Spinner"; }
void OnCreate() override {
// 实体与组件已就位;需要别的组件时用 Get<T>()(不存在会炸)
}
void OnUpdate(float dt) override {
// 四元数复合绕 Y(避免每帧欧拉↔四元数往返累积失真)
/*
const glm::quat step = glm::angleAxis(
glm::radians(m_Speed * dt), glm::vec3(0.0f, 1.0f, 0.0f));
Transform().SetRotationQuat(step * Transform().GetRotationQuat());
*/
}
private:
float m_Speed = 90.0f; // 度/秒
};
static const bool s_Spinner_registered = [] {
::Engine::Scripting::ScriptRegistry::Register(
"Spinner", []() -> ::Engine::Scope<::Engine::Scripting::ScriptBehavior> {
return ::Engine::CreateScope<Spinner>();
});
return true;
}();
} // namespace
+71
View File
@@ -0,0 +1,71 @@
//
// WASDMove.cpp:WASD 控制实体移动(相机水平朝向参考,UE gameplay 语义)。
//
// 移动语义分派:
// - Dynamic 刚体 → 走物理速度门面(每帧喂水平速度,Y 归物理——
// 重力/跳跃不受影响;松键喂零速 = 立即停,Unity velocity 语义)
// - 无刚体 → 直接平移 Transform
//
// 编辑器形态键位天然不冲突:RMB 按住 = 相机吃 WASD 飞行(脚本也读
// 到按键 → 同时在动,测移动时松开 RMB 即可);游戏形态 WASD 常开。
//
#include <Engine/Scripting/Script.h>
#include <Engine/Core/Application.h>
#include <Engine/Core/Window.h>
#include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/EditorCamera.h>
#include <Engine/Physics/PhysicsWorld.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
namespace {
class WASDMove final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "WASDMove"; }
void OnUpdate(float dt) override {
// 位移方向 = 视线水平投影(W 前进 / A 左 / D 右),UE Scene
// view 移动惯例;Y 锁 0(升降/跳跃归物理或将来扩展)
const glm::quat cq = App().GetRenderer()
.GetEditorCamera()
.GetTransform()
.GetRotationQuat();
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);
auto& win = App().GetWindow();
glm::vec3 move(0.0f);
if (win.IsKeyPressed(GLFW_KEY_W)) move += fwd;
if (win.IsKeyPressed(GLFW_KEY_S)) move -= fwd;
if (win.IsKeyPressed(GLFW_KEY_D)) move += right;
if (win.IsKeyPressed(GLFW_KEY_A)) move -= right;
move.y = 0.0f;
if (glm::length(move) > 1e-4f) {
move = glm::normalize(move);
}
const glm::vec3 vel = move * m_Speed;
if (auto* rb = TryGetComponent<Engine::ECS::RigidBodyComponent>();
rb != nullptr && rb->bodyValid &&
rb->type == Engine::ECS::RigidBodyComponent::BodyType::Dynamic) {
// 物理驱动:只覆写水平速度,Y 保留物理积分离散结果
//(整体喂 0 会抵消重力 → 空中物体悬浮)
auto& phy = App().GetPhysics();
const glm::vec3 cur = phy.GetLinearVelocity(EntityBits());
phy.SetLinearVelocity(EntityBits(), {vel.x, cur.y, vel.z});
} else {
Transform().SetPosition(Transform().GetPosition() + vel * dt);
}
}
private:
float m_Speed = 5.0f; // m/s
};
ENGINE_REGISTER_SCRIPT(WASDMove)
} // namespace
+7
View File
@@ -59,6 +59,13 @@ set(ASSIMP_BUILD_STL_IMPORTER ON CACHE BOOL "" FORCE)
set(ASSIMP_BUILD_PLY_IMPORTER ON CACHE BOOL "" FORCE) set(ASSIMP_BUILD_PLY_IMPORTER ON CACHE BOOL "" FORCE)
add_subdirectory(Lib/assimp) add_subdirectory(Lib/assimp)
# ---- yaml-cpp(submodule,场景序列化;只编静态库本体)----
set(YAML_CPP_BUILD_CONTRIB OFF CACHE BOOL "" FORCE)
set(YAML_CPP_BUILD_TOOLS OFF CACHE BOOL "" FORCE)
set(YAML_CPP_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(YAML_CPP_INSTALL OFF CACHE BOOL "" FORCE)
add_subdirectory(Lib/yaml-cpp)
# Vulkan # Vulkan
+1
View File
@@ -73,6 +73,7 @@ target_link_libraries(Engine PUBLIC
assimp::assimp assimp::assimp
meshoptimizer meshoptimizer
Jolt::Jolt Jolt::Jolt
yaml-cpp::yaml-cpp
imgui imgui
ImGuizmo ImGuizmo
tracy) tracy)
@@ -172,14 +172,31 @@ namespace Engine
AssetCache<Model>& Models() { return m_Models; } AssetCache<Model>& Models() { return m_Models; }
// ---- 资产 GUID 注册表(UE AssetRegistry 模式:中央注册表)----
// 磁盘路径资产持久映射 GUID→路径(存 assets/asset_registry.yaml),
// 场景文件以 GUID 为主键引用资产——重命名/移动资产不断引用,
// Path 仅作人工可读提示与解析兜底。procedural:// 伪路径(参数
// 确定性生成,无需稳定身份)不参与
// 查询/分配(新路径分配 GUID 并立即持久化,崩溃安全)
u64 GetOrCreateGUID(const std::string& path);
// GUID → 路径;未登记返回空串
std::string ResolveGUID(u64 guid) const;
// 后续扩展点:TextureHandle / MaterialHandle / ShaderHandle // 后续扩展点:TextureHandle / MaterialHandle / ShaderHandle
// 各自一个 AssetCache<T> + 便捷方法,模式与 Model 完全一致。 // 各自一个 AssetCache<T> + 便捷方法,模式与 Model 完全一致。
private: private:
void LoadGUIDRegistry();
void SaveGUIDRegistry() const;
IAssetUploader& m_Uploader; IAssetUploader& m_Uploader;
AssetCache<Model> m_Models; AssetCache<Model> m_Models;
AssetCache<Cubemap> m_Environments; AssetCache<Cubemap> m_Environments;
// GUID 注册表(双向);dirty 即时保存,无析构落盘依赖
std::unordered_map<u64, std::string> m_GuidToPath;
std::unordered_map<std::string, u64> m_PathToGuid;
}; };
} // namespace Assets } // namespace Assets
} // namespace Engine } // namespace Engine
+3
View File
@@ -36,6 +36,7 @@ namespace Engine {
class Renderer; class Renderer;
class ImGuiLayer; class ImGuiLayer;
class PhysicsWorld; class PhysicsWorld;
namespace Scripting { class ScriptRuntime; }
namespace ECS { class Scene; } namespace ECS { class Scene; }
namespace Assets { class AssetManager; } namespace Assets { class AssetManager; }
@@ -73,6 +74,7 @@ namespace Engine {
ECS::Scene& GetScene() const { return *m_Scene; } ECS::Scene& GetScene() const { return *m_Scene; }
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; }
u64 GetFrameCount() const { return m_FrameCount; } u64 GetFrameCount() const { return m_FrameCount; }
// ImGui相关 // ImGui相关
@@ -120,6 +122,7 @@ namespace Engine {
Scope<Renderer> m_Renderer; Scope<Renderer> m_Renderer;
Scope<Assets::AssetManager> m_Assets; Scope<Assets::AssetManager> m_Assets;
Scope<PhysicsWorld> m_Physics; // Jolt 物理(Run 起循环时创建) Scope<PhysicsWorld> m_Physics; // Jolt 物理(Run 起循环时创建)
Scope<Scripting::ScriptRuntime> m_Scripts; // 脚本生命周期(Run 起循环时创建)
// 注意成员序:ImGuiLayer 必须后声明(先于 Renderer/Window 析构, // 注意成员序:ImGuiLayer 必须后声明(先于 Renderer/Window 析构,
// 其后端销毁需要 Device 与 GLFWwindow 存活) // 其后端销毁需要 Device 与 GLFWwindow 存活)
Scope<ImGuiLayer> m_ImGuiLayer; Scope<ImGuiLayer> m_ImGuiLayer;
+5
View File
@@ -67,6 +67,10 @@ namespace Engine {
bool IsMouseButtonPressed(int button) const; bool IsMouseButtonPressed(int button) const;
void GetMouseDelta(float& dx, float& dy) const; void GetMouseDelta(float& dx, float& dy) const;
// 轮询式滚轮增量(上行 = +1,一档 ≈1):读后清零。UI 捕获鼠标
//(面板悬停)时不累积——滚轮归 ImGui
float ConsumeScrollDelta();
// 光标捕获(自由视角):DISABLED 隐藏光标出无界增量,NORMAL 恢复。 // 光标捕获(自由视角):DISABLED 隐藏光标出无界增量,NORMAL 恢复。
// 重复设置同值无操作;切换后鼠标增量参考点重置(防跳变) // 重复设置同值无操作;切换后鼠标增量参考点重置(防跳变)
void SetCursorCaptured(bool captured); void SetCursorCaptured(bool captured);
@@ -92,6 +96,7 @@ namespace Engine {
mutable bool m_FirstMouse = true; mutable bool m_FirstMouse = true;
mutable double m_LastMouseX = 0.0; mutable double m_LastMouseX = 0.0;
mutable double m_LastMouseY = 0.0; mutable double m_LastMouseY = 0.0;
mutable float m_ScrollAccum = 0.0f; // 滚轮累积(ConsumeScrollDelta)
// GLFW 静态回调(通过 window user pointer 找回 this) // GLFW 静态回调(通过 window user pointer 找回 this)
static void FramebufferResizeCallback(GLFWwindow* window, int width, int height); static void FramebufferResizeCallback(GLFWwindow* window, int width, int height);
+84 -4
View File
@@ -19,6 +19,7 @@
#include <cmath> #include <cmath>
#include <algorithm> #include <algorithm>
#include <string> #include <string>
#include <vector>
namespace Engine { namespace Engine {
class Model; class Model;
@@ -122,12 +123,14 @@ namespace Engine::ECS {
// ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。 // ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。
struct MeshRendererComponent { struct MeshRendererComponent {
ModelRef model; ModelRef model;
bool active = true; // 禁用 = 渲染/拾取跳过(Unity enabled / UE SetActive)
}; };
// 骨骼动画播放状态(纯数据 + 便捷控制)。 // 骨骼动画播放状态(纯数据 + 便捷控制)。
// 采样/局部层混合逻辑在 SceneRenderer::BuildFrame(渲染侧唯一消费者); // 采样/局部层混合逻辑在 SceneRenderer::BuildFrame(渲染侧唯一消费者);
// ImGui 面板经 Play()/字段直接驱动。无此组件 = 按绑定姿势渲染。 // ImGui 面板经 Play()/字段直接驱动。无此组件 = 按绑定姿势渲染。
struct AnimationComponent { struct AnimationComponent {
bool active = true; // 禁用 = 暂停采样(按当前帧姿态定格)
int clipIndex = 0; // 当前 clip(Model::GetClips 下标) int clipIndex = 0; // 当前 clip(Model::GetClips 下标)
bool playing = true; bool playing = true;
bool loop = true; bool loop = true;
@@ -157,6 +160,7 @@ namespace Engine::ECS {
// 相机。一个场景中 primary=true 的实体是当前渲染视角。 // 相机。一个场景中 primary=true 的实体是当前渲染视角。
struct CameraComponent { struct CameraComponent {
bool active = true; // 禁用 = 退出 primary 候选
Camera camera; Camera camera;
bool primary = false; bool primary = false;
}; };
@@ -165,6 +169,7 @@ namespace Engine::ECS {
// Directional——方向即照射方向(无位置,阴影光源用第 0 盏方向光) // Directional——方向即照射方向(无位置,阴影光源用第 0 盏方向光)
// Point——位置取实体 Transform(固有的),向四周发光 + 距离衰减 // Point——位置取实体 Transform(固有的),向四周发光 + 距离衰减
struct LightComponent { struct LightComponent {
bool active = true; // 禁用 = 不进光照
enum class Type : u8 { Directional = 0, Point = 1 }; enum class Type : u8 { Directional = 0, Point = 1 };
Type type = Type::Directional; Type type = Type::Directional;
glm::vec3 direction{0.0f, -1.0f, 0.0f}; // 仅方向光 glm::vec3 direction{0.0f, -1.0f, 0.0f}; // 仅方向光
@@ -182,20 +187,44 @@ namespace Engine::ECS {
// 角色 = CharacterControllerComponent(CharacterVirtual),载具 = VehicleComponent // 角色 = CharacterControllerComponent(CharacterVirtual),载具 = VehicleComponent
// (VehicleConstraint)——都走 PhysicsWorld 门面,不复用本组件。 // (VehicleConstraint)——都走 PhysicsWorld 门面,不复用本组件。
struct RigidBodyComponent { struct RigidBodyComponent {
bool active = true; // 禁用 = body 销毁/不建(对账自然消化)
enum class BodyType : u8 { Static = 0, Dynamic = 1, Kinematic = 2 }; enum class BodyType : u8 { Static = 0, Dynamic = 1, Kinematic = 2 };
// Mesh:用实体 MeshRenderer 的视觉网格建碰撞(Unity MeshCollider // Mesh:用实体 MeshRenderer 的视觉网格建碰撞(Unity MeshCollider
// 语义)——Static → Jolt MeshShape(精确三角网格,不可移动); // 语义)——Static → Jolt MeshShape(精确三角网格,不可移动);
// Dynamic/Kinematic → ConvexHullShape(凸包近似,MeshShape 太贵 // Dynamic/Kinematic → ConvexHullShape(凸包近似,MeshShape 太贵
// 不能动)。数据源 Mesh 的 CPU 位置副本,无 MeshRenderer 回退 Box // 不能动)。数据源 Mesh 的 CPU 位置副本,无 MeshRenderer 回退 Box
enum class ColliderType : u8 { Box = 0, Sphere = 1, Mesh = 2 }; enum class ColliderType : u8 {
Box = 0, Sphere = 1, Mesh = 2,
Capsule = 3, Cylinder = 4, Compound = 5
};
// Compound 子形状:局部 pos/rot 相对刚体原点,不递归嵌套
//(child 不能再是 Compound)。尺寸语义同顶层(× Transform 缩放烘焙)
struct CompoundChild {
enum class Shape : u8 { Box = 0, Sphere = 1, Capsule = 2 };
Shape shape = Shape::Box;
glm::vec3 boxHalfExtents{0.25f, 0.25f, 0.25f};
f32 radius = 0.25f; // Sphere 半径 / Capsule 半径
f32 capsuleHalfHeight = 0.25f; // 仅 Capsule:圆柱段半高
glm::vec3 position{0.0f};
glm::quat rotation{1.0f, 0.0f, 0.0f, 0.0f};
bool operator==(const CompoundChild&) const = default;
};
BodyType type = BodyType::Dynamic; // Unity 语义:加 Rigidbody 即受物理驱动 BodyType type = BodyType::Dynamic; // Unity 语义:加 Rigidbody 即受物理驱动
ColliderType collider = ColliderType::Box; ColliderType collider = ColliderType::Box;
// 碰撞体尺寸(× Transform 缩放,Unity BoxCollider.size 语义)—— // 碰撞体尺寸(× Transform 缩放,Unity BoxCollider.size 语义)——
// 程序化模型顶点烘焙尺寸,不能从 scale 反推 // 程序化模型顶点烘焙尺寸,不能从 scale 反推。
// Capsule/Cylinder 的"半高"指圆柱段半高(不含半球端盖),总高 =
// 2*(halfHeight + radius);Jolt 两 ctor 参数序相反见 PhysicsWorld
glm::vec3 boxHalfExtents{0.5f, 0.5f, 0.5f}; glm::vec3 boxHalfExtents{0.5f, 0.5f, 0.5f};
f32 sphereRadius = 0.5f; f32 sphereRadius = 0.5f;
f32 capsuleRadius = 0.5f; // Unity CapsuleCollider 默认值同构
f32 capsuleHalfHeight = 0.5f;
f32 cylinderRadius = 0.5f;
f32 cylinderHalfHeight = 0.5f;
std::vector<CompoundChild> compound; // 仅 ColliderType::Compound
// 动态体参数 // 动态体参数
f32 mass = 1.0f; f32 mass = 1.0f;
@@ -203,8 +232,9 @@ namespace Engine::ECS {
f32 friction = 0.5f; f32 friction = 0.5f;
f32 restitution = 0.2f; f32 restitution = 0.2f;
// 传感器(Unity isTrigger 语义):只产生 Added/Removed 事件,无碰撞解算 // 触发器(Unity isTrigger / UE Overlap Events 语义):只产生
bool isSensor = false; // Added/Removed 事件,无碰撞解算;底层 Jolt sensor
bool isTrigger = false;
// 运行时(PhysicsWorld 维护,勿手改) // 运行时(PhysicsWorld 维护,勿手改)
u32 bodyId = 0; // JPH::BodyID 裸位 u32 bodyId = 0; // JPH::BodyID 裸位
@@ -212,6 +242,56 @@ namespace Engine::ECS {
glm::vec3 velocity{0.0f}; // 只读调试视图:线速度(m/s) glm::vec3 velocity{0.0f}; // 只读调试视图:线速度(m/s)
}; };
// 约束/关节(Jolt TwoBodyConstraint 家族)。纯数据 POD 零 Jolt 依赖;
// 创建/对账在 PhysicsWorld::SyncConstraints(kPhyDbgConstraints 出图元)。
// target 存 UUID(跨存档稳定),惰性解析——SyncScene 每帧按 UUID 反查
// 实体:序列化无需两遍解析,UUID 再生后自然失联,加载顺序无关
struct JointComponent {
bool active = true; // 禁用 = 约束销毁/不建(对账自然消化)
enum class Type : u8 { Fixed = 0, Hinge = 1, Ball = 2 };
Type type = Type::Fixed;
UUID target{}; // 目标实体(0 = 未设置,不建约束)
// 锚点:各体 Transform 局部系(Unity connectedAnchor 语义,
// × 本体缩放烘焙)。Fixed/Hinge/Ball 都连这两点
glm::vec3 anchorA{0.0f}; // 本体(挂组件的实体)
glm::vec3 anchorB{0.0f}; // 目标体
// Hinge 专用:转轴(本体局部系,方向量不吃缩放)+ 角度限位(度)
glm::vec3 hingeAxis{1.0f, 0.0f, 0.0f};
bool enableLimit = false;
f32 limitMin = -90.0f; // Jolt 语义 min∈[-180,0] max∈[0,180]
f32 limitMax = 90.0f;
bool operator==(const JointComponent&) const = default;
};
// 角色控制器参数(Jolt CharacterVirtual 扫掠运动学,非刚体——不受
// 物理模拟驱动,游戏逻辑经 PhysicsWorld 门面喂速度/跳跃)。本组件
// 只存参数(序列化:存参数不存状态),速度/着地等运行时状态在
// PhysicsWorld 的 CharacterController 里(对账重建)
struct CharacterControllerComponent {
bool active = true; // 禁用 = 角色移除(重启用按当前 Transform 重建)
f32 radius = 0.3f; // 胶囊半径
f32 capsuleHalfHeight = 0.4f; // 圆柱段半高(总高 = 2*(hh+r))
f32 mass = 70.0f;
f32 maxSlopeAngle = 45.0f; // 度:可站立的最大坡角
f32 stepHeight = 0.4f; // 台阶自动攀爬高(WalkStairs)
bool operator==(const CharacterControllerComponent&) const = default;
// 实体原点 = 脚底(胶囊底端),符合角色 pivot 习惯
};
// 脚本组件:只存脚本类名(注册表键,POD 可序列化)。C++ 原生脚本经
// ENGINE_REGISTER_SCRIPT 自注册;将来 Lua/C# 脚本同键解析。
// 实例生命周期由 ScriptRuntime 对账管理(不进组件,保持可拷贝)
struct ScriptComponent {
bool active = true; // 禁用 = 实例保留但停 OnUpdate(Unity disable)
std::string scriptName;
bool operator==(const ScriptComponent&) const = default;
};
} // namespace Engine::ECS } // namespace Engine::ECS
#endif //HELLOVULKAN_COMPONENTS_H #endif //HELLOVULKAN_COMPONENTS_H
+11
View File
@@ -46,6 +46,7 @@ namespace Engine::ECS {
bool DrawSkybox = true; bool DrawSkybox = true;
}; };
Settings& GetSettings() { return m_Settings; } Settings& GetSettings() { return m_Settings; }
const Settings& GetSettings() const { return m_Settings; } // 序列化只读
// ---- 实体 ---- // ---- 实体 ----
@@ -54,6 +55,15 @@ namespace Engine::ECS {
void DestroyEntity(Entity e); // 延迟:帧尾统一销毁 void DestroyEntity(Entity e); // 延迟:帧尾统一销毁
void FlushDestroyQueue(); // 每帧末尾由 Application 调用 void FlushDestroyQueue(); // 每帧末尾由 Application 调用
// 整场景换血(序列化加载):立即销毁全部实体并清空延迟队列。
// 仅用于"校验通过后的原子替换"——调用方持有的 Entity/EntityHandle
// 全部悬空,必须自行重解析(GetEntityHandle 可从裸 entity 重建)
void Clear();
// 裸 entity → 句柄表皮(EntityHandle 构造私有 + friend Scene;
// 序列化加载后应用层重解析悬空句柄用)。无效 entity 返回空句柄
EntityHandle GetEntityHandle(Entity e);
// 帧循环统一重算脏 transform 的 world 矩阵(UE transform-update // 帧循环统一重算脏 transform 的 world 矩阵(UE transform-update
// 阶段模式)——由 Application 在 OnUpdate 之后、渲染之前调用, // 阶段模式)——由 Application 在 OnUpdate 之后、渲染之前调用,
// 用户代码永远不需要手动 Compose // 用户代码永远不需要手动 Compose
@@ -64,6 +74,7 @@ namespace Engine::ECS {
Entity FindEntityByUUID(UUID uuid); Entity FindEntityByUUID(UUID uuid);
Registry& GetRegistry() { return m_Registry; } Registry& GetRegistry() { return m_Registry; }
const Registry& GetRegistry() const { return m_Registry; } // 序列化等只读遍历
// ---- 组件便捷接口(转发 entt)---- // ---- 组件便捷接口(转发 entt)----
@@ -0,0 +1,74 @@
//
// CharacterController:单实体角色控制器(Jolt CharacterVirtual 后端)。
//
// 非刚体、扫掠运动学:CharacterVirtual 不注册进 PhysicsSystem,位置由
// 本类每步扫掠推进(撞墙滑动/斜坡限制/贴地/上台阶),其他物理对象
// "看不到"它(无碰撞解算,仅查询)。由 PhysicsWorld 对账创建/销毁并
// 在 PhysicsSystem::Update 之后驱动(Jolt 官方建议序)。
//
// 速度语义(游戏逻辑职责分工):
// - 水平移动:游戏逻辑每帧 SetCharacterVelocity 喂意图速度
// - 垂直分量:本类自动积分重力(着地清零向下分量);Jump 置垂直速度
// (Unity CharacterController.Move + gravity 手动积分同构)
//
// 实体原点 = 脚底(胶囊底端经 mShapeOffset 抬升到原点上方)。
//
#ifndef HELLOVULKAN_CHARACTERCONTROLLER_H
#define HELLOVULKAN_CHARACTERCONTROLLER_H
#include <Engine/Core/Types.h>
#include <Jolt/Jolt.h>
#include <Jolt/Physics/PhysicsSystem.h>
#include <Jolt/Physics/Character/CharacterVirtual.h>
#include <glm/glm.hpp>
namespace Engine {
class CharacterController {
public:
// 创建参数快照(变更检测 → 重建 CharacterVirtual)
struct Params {
f32 radius = 0.3f; // 胶囊半径
f32 halfHeight = 0.4f; // 圆柱段半高
f32 mass = 70.0f;
f32 maxSlopeAngle = 45.0f; // 度
f32 stepHeight = 0.4f; // 台阶攀爬高
bool operator==(const Params&) const = default;
};
CharacterController(JPH::PhysicsSystem& system, const Params& params,
const glm::vec3& position);
~CharacterController() = default; // Ref 引用计数自动释放
// 每物理步调用:重力积分 + ExtendedUpdate(扫掠滑动 + 贴地 +
// 上台阶)。allocator 与 PhysicsSystem::Update 串行复用同一池
void Update(f32 dt, const JPH::Vec3& gravity,
JPH::TempAllocator& allocator);
// ---- 游戏逻辑门面(PhysicsWorld 转发)----
void SetLinearVelocity(const glm::vec3& velocity);
glm::vec3 GetLinearVelocity() const;
void Jump(f32 speed); // 垂直速度置 speed(着地方向才有效)
bool IsGrounded() const;
glm::vec3 GetPosition() const;
void Teleport(const glm::vec3& position) const; // 编辑器拖动
const Params& GetParams() const { return m_Params; }
// 调试渲染:非 body,PhysicsSystem::DrawBodies 画不到,须手动
// (脚本在物理位姿/脚底原点处画胶囊线框)
void DebugDraw(JPH::DebugRenderer& renderer) const;
private:
Params m_Params;
JPH::Ref<JPH::CharacterVirtual> m_Character;
JPH::CharacterVirtual::ExtendedUpdateSettings m_Ext;
};
} // namespace Engine
#endif //HELLOVULKAN_CHARACTERCONTROLLER_H
+63 -3
View File
@@ -25,10 +25,13 @@
#include <Engine/Physics/PhysicsDebugDraw.h> #include <Engine/Physics/PhysicsDebugDraw.h>
#include <Engine/Physics/PhysicsEvents.h> #include <Engine/Physics/PhysicsEvents.h>
#include <Engine/Physics/PhysicsRay.h> #include <Engine/Physics/PhysicsRay.h>
#include <Engine/Physics/CharacterController.h>
#include <Engine/ECS/Components.h> // BodyRecord 快照存 CompoundChild(完整类型)
#include <Jolt/Jolt.h> #include <Jolt/Jolt.h>
#include <Jolt/Physics/PhysicsSystem.h> #include <Jolt/Physics/PhysicsSystem.h>
#include <Jolt/Physics/Body/BodyID.h> #include <Jolt/Physics/Body/BodyID.h>
#include <Jolt/Physics/Constraints/Constraint.h> // JPH::Ref<Constraint>
#include <Jolt/Core/JobSystemThreadPool.h> #include <Jolt/Core/JobSystemThreadPool.h>
#include <Jolt/Core/TempAllocator.h> #include <Jolt/Core/TempAllocator.h>
@@ -75,6 +78,27 @@ namespace Engine {
bool RayCast(const glm::vec3& origin, const glm::vec3& dir, bool RayCast(const glm::vec3& origin, const glm::vec3& dir,
f32 maxDist, RayHit& outHit) const; f32 maxDist, RayHit& outHit) const;
// ---- 刚体驱动(游戏逻辑/编辑器调试;按实体位模式寻址)----
// Jolt 力在每步模拟后清零:游戏逻辑每帧持续施加 = 正确用法;
// 冲量一次性改变动量(自动唤醒休眠体);速度直写会打断自然
// 模拟(Unity 同语义)。body 不存在或类型不符时静默忽略:
// 力/扭矩/冲量仅动态体,速度直写动态+运动学体
void AddForce(u32 entityBits, const glm::vec3& force);
void AddImpulse(u32 entityBits, const glm::vec3& impulse);
void AddTorque(u32 entityBits, const glm::vec3& torque);
void SetLinearVelocity(u32 entityBits, const glm::vec3& velocity);
void SetAngularVelocity(u32 entityBits, const glm::vec3& angularVelocity);
// 无 body / 静态体返回零向量
glm::vec3 GetLinearVelocity(u32 entityBits) const;
// ---- 角色控制器(游戏逻辑门面;实体无 CharacterControllerComponent
// 或未就绪时静默忽略)----
// 速度直写喂意图(水平移动);垂直分量由 Update 自动积分重力
void SetCharacterVelocity(u32 entityBits, const glm::vec3& velocity);
void Jump(u32 entityBits, f32 speed); // 垂直速度置 speed
bool IsCharacterGrounded(u32 entityBits) const;
glm::vec3 GetCharacterVelocity(u32 entityBits) const;
private: private:
// 创建时缓存的参数(变更检测 → 重建 body) // 创建时缓存的参数(变更检测 → 重建 body)
struct BodyRecord { struct BodyRecord {
@@ -82,9 +106,11 @@ namespace Engine {
u8 type = 0; // RigidBodyComponent::BodyType u8 type = 0; // RigidBodyComponent::BodyType
u8 collider = 0; // RigidBodyComponent::ColliderType u8 collider = 0; // RigidBodyComponent::ColliderType
glm::vec3 colliderSize{0.0f}; // 碰撞体尺寸缓存(box 半尺寸) glm::vec3 colliderSize{0.0f}; // 碰撞体尺寸缓存(box 半尺寸)
f32 radius = 0.0f; // 球半径缓存 f32 radius = 0.0f; // 球/Capsule/Cylinder 半径(collider 决定语义)
f32 halfHeight = 0.0f; // Capsule/Cylinder 圆柱段半高
std::vector<ECS::RigidBodyComponent::CompoundChild> compound; // 子形状快照
f32 mass = 0.0f; f32 mass = 0.0f;
bool isSensor = false; // 传感器标记(变更 → 重建 body) bool isTrigger = false; // 触发器标记(变更 → 重建 body)
glm::vec3 lastPos{0.0f}; // 静态/运动体编辑器拖动检测 glm::vec3 lastPos{0.0f}; // 静态/运动体编辑器拖动检测
glm::quat lastRot{1.0f, 0.0f, 0.0f, 0.0f}; // 同上(quat 直比,无欧拉往返) glm::quat lastRot{1.0f, 0.0f, 0.0f, 0.0f}; // 同上(quat 直比,无欧拉往返)
// 加速度箭头用:上一步速度(定步长差分)与平滑后的加速度 // 加速度箭头用:上一步速度(定步长差分)与平滑后的加速度
@@ -93,20 +119,54 @@ namespace Engine {
}; };
void SyncScene(); // 对账:孤儿清理 / 创建 / 参数同步 void SyncScene(); // 对账:孤儿清理 / 创建 / 参数同步
void Writeback() const; // 动态体 → TransformComponent(标脏) void Writeback(); // 动态体 → TransformComponent(标脏)+ lastPos/lastRot 对账基线
void UpdateAccels(f32 dt); // 速度差分 → BodyRecord.lastAccel(step 率) void UpdateAccels(f32 dt); // 速度差分 → BodyRecord.lastAccel(step 率)
void CollectDebugData(); // 按 m_DebugBits 收集调试图元(渲染帧率) void CollectDebugData(); // 按 m_DebugBits 收集调试图元(渲染帧率)
void CreateBody(u32 entityBits, ECS::RigidBodyComponent& rb, void CreateBody(u32 entityBits, ECS::RigidBodyComponent& rb,
ECS::TransformComponent& tr); ECS::TransformComponent& tr);
// 约束对账:owner 孤儿清理 / body 重建传染失效 / 建缺失与变更重建
// invalidated = 本帧 SyncScene 中被销毁或重建 body 的实体位模式
void SyncConstraints(const std::vector<u32>& invalidated);
void CreateConstraint(u32 ownerBits, u32 targetBits,
const ECS::JointComponent& jc);
void RemoveConstraint(u32 ownerBits);
// 角色对账:孤儿清理 / 参数变更重建 / 编辑器拖动传送。
// 角色非刚体(不进 PhysicsSystem),无 body 清理负担
void SyncCharacters();
// 每物理步(PhysicsSystem::Update 之后,Jolt 官方建议序):
// 重力积分 + 扫掠 → 写回 TransformComponent(标脏)
void UpdateCharacters(f32 dt);
// Mesh 碰撞体:实体视觉网格 → Jolt 形状(Static=MeshShape / // Mesh 碰撞体:实体视觉网格 → Jolt 形状(Static=MeshShape /
// Dynamic=ConvexHull)。无 MeshRenderer 或构建失败返回 null(回退 Box) // Dynamic=ConvexHull)。无 MeshRenderer 或构建失败返回 null(回退 Box)
JPH::RefConst<JPH::Shape> BuildMeshShape(u32 entityBits, JPH::RefConst<JPH::Shape> BuildMeshShape(u32 entityBits,
ECS::RigidBodyComponent& rb, ECS::RigidBodyComponent& rb,
ECS::TransformComponent& tr); ECS::TransformComponent& tr);
// Compound 碰撞体:子形状列表 → Jolt StaticCompoundShape。
// 空 children 或构建失败返回 null(回退 Box)
JPH::RefConst<JPH::Shape> BuildCompoundShape(ECS::RigidBodyComponent& rb,
ECS::TransformComponent& tr);
void RemoveBody(u32 entityBits); void RemoveBody(u32 entityBits);
const BodyRecord* FindRecord(u32 entityBits) const; // 刚体驱动寻址
CharacterController* FindCharacter(u32 entityBits); // 角色门面寻址
void QueueEvent(CollisionEvent&& ev); // 回调线程 append(多线程并发,加锁) void QueueEvent(CollisionEvent&& ev); // 回调线程 append(多线程并发,加锁)
bool TryBitsByBodyId(const JPH::BodyID& id, u32& outBits) const; bool TryBitsByBodyId(const JPH::BodyID& id, u32& outBits) const;
// ---- 约束(JointComponent → Jolt TwoBodyConstraint;owner 实体寻址)----
// target 惰性解析 UUID;body 重建/销毁 → 引用它的约束失效重建
struct ConstraintRecord {
JPH::Ref<JPH::Constraint> constraint;
ECS::JointComponent snapshot; // 变更检测 → 移除重建
u32 targetBits = 0;
};
std::unordered_map<u32, ConstraintRecord> m_Constraints;
// ---- 角色(entityBits → CharacterVirtual 封装)----
struct CharacterEntry {
std::unique_ptr<CharacterController> cc;
glm::vec3 lastPos{0.0f}; // 编辑器拖动检测
};
std::unordered_map<u32, CharacterEntry> m_Characters;
ECS::Scene& m_Scene; ECS::Scene& m_Scene;
Assets::AssetManager* m_Assets = nullptr; // Mesh 碰撞体解析 modelRef Assets::AssetManager* m_Assets = nullptr; // Mesh 碰撞体解析 modelRef
bool m_Enabled = true; bool m_Enabled = true;
+32 -8
View File
@@ -4,9 +4,18 @@
// Camera:独立的相机模块。 // Camera:独立的相机模块。
// //
// 设计(Transform 单一事实源):相机的位置与朝向完全由实体上的 // 设计(Transform 单一事实源):相机的位置与朝向完全由实体上的
// TransformComponent 承载——Update 把输入直接写回 transform(末尾 // TransformComponent 承载——Update 把输入直接写回 transform,视图
// Compose),视图矩阵 = 实体世界矩阵的逆(本地 -Z 为视线方向)。 // 矩阵 = 实体世界矩阵的逆(本地 -Z 为视线方向)。相机类自身只保存
// 相机类自身不保存任何位姿状态,杜绝"位置两套来源"的脱节。 // 焦点环绕(pivot orbit)状态(pivot/距离),杜绝位姿两套来源。
//
// 交互(UE Scene view 语义):
// RMB 按住 自由视角:鼠标转向 + WASD/Space/Shift 飞行
// (编辑器模式下 RMB 按住期间键盘归相机,W/E/R 不切
// Gizmo——键位冲突的解法与 UE 一致)
// MMB 拖拽 焦点环绕:绕 pivot 转向,距离不变
// Shift+MMB 屏幕平面平移(相机与 pivot 同步移动)
// 滚轮 推拉:改环绕距离(有 pivot 时指数步进)
// F(面板侧) 焦点选中实体(ViewportPanel 调 FocusOn)
// //
#ifndef HELLOVULKAN_CAMERA_H #ifndef HELLOVULKAN_CAMERA_H
@@ -22,9 +31,15 @@ namespace Engine {
class Camera { class Camera {
public: public:
// 输入驱动:WASD/Space/Shift 移动 + 按住右键鼠标视角。 // 输入驱动。flyRequiresRMB=true(编辑器模式):移动键仅在 RMB
// 全部写回传入的 transform,有变更才 Compose。 // 按住时生效——解决与 Gizmo W/E/R 工具热键的键位冲突(UE 语义:
void Update(Window& window, ECS::TransformComponent& transform, f32 deltaTime) const; // 飞行时键盘归相机)。全部写回传入的 transform
void Update(Window& window, ECS::TransformComponent& transform,
f32 deltaTime, bool flyRequiresRMB = false);
// 焦点选中(UE frame-selection):pivot 设为 point,相机沿当前
// 视线平移对准;环绕距离沿用当前值(初始 5m,滚轮可改)
void FocusOn(ECS::TransformComponent& transform, const glm::vec3& point);
// 相机视图 = 实体世界矩阵的逆(要求 transform 已 Compose) // 相机视图 = 实体世界矩阵的逆(要求 transform 已 Compose)
glm::mat4 GetView(const ECS::TransformComponent& transform) const; glm::mat4 GetView(const ECS::TransformComponent& transform) const;
@@ -35,15 +50,24 @@ namespace Engine {
void SetMoveSpeed(f32 speed) { m_MoveSpeed = speed; } void SetMoveSpeed(f32 speed) { m_MoveSpeed = speed; }
void SetMouseSensitivity(f32 sens) { m_MouseSensitivity = sens; } void SetMouseSensitivity(f32 sens) { m_MouseSensitivity = sens; }
// 鼠标视角总开关(默认开)。交互 = 按住右键自由视角(光标捕获), // 相机输入总开关(默认开)。关闭后转向/环绕/推拉/飞行全部静止
// 松开释放光标;右键落在 ImGui 面板上时归 UI 不触发相机
void SetMouseLookEnabled(const bool enabled) { m_MouseLookEnabled = enabled; } void SetMouseLookEnabled(const bool enabled) { m_MouseLookEnabled = enabled; }
bool IsMouseLookEnabled() const { return m_MouseLookEnabled; } bool IsMouseLookEnabled() const { return m_MouseLookEnabled; }
// 清除环绕 pivot(位姿被外部整体改写后 pivot 已失效——下一次
// MMB 环绕按新视线自动重建)
void ResetOrbit() { m_HasPivot = false; }
private: private:
f32 m_MoveSpeed = 2.5f; f32 m_MoveSpeed = 2.5f;
f32 m_MouseSensitivity = 0.1f; f32 m_MouseSensitivity = 0.1f;
bool m_MouseLookEnabled = true; bool m_MouseLookEnabled = true;
// 焦点环绕状态:pivot + 距离。首次环绕无 pivot 时自动取当前
// 视线上 m_OrbitDistance 处;FocusOn 显式设置
glm::vec3 m_OrbitPivot{0.0f};
f32 m_OrbitDistance = 5.0f;
bool m_HasPivot = false;
}; };
} // namespace Engine } // namespace Engine
@@ -0,0 +1,80 @@
//
// Created by atdunbg on 2026/10/3.
//
// EditorCamera:Scene view 专用相机(UE FEditorViewportClient 语义)。
//
// 与场景相机实体是两个东西——它是视口 UI 状态,不属于场景:
// - 位姿自有(内嵌 TransformComponent,无父层级),空场景/无相机
// 实体也可用,永在不可删
// - 交互复用 Camera 控制器(RMB 飞行 / MMB 环绕 / 滚轮推拉),输入
// 直接写回自有 transform——位姿单一事实源不变(transform 即位姿)
// - 渲染源分派在 SceneRenderer::BuildFrame:编辑器形态恒用本类,
// 游戏形态用场景 primary 相机实体(游戏逻辑所有,本类不参与,
// 不 Update 即静止)
//
#ifndef HELLOVULKAN_EDITORCAMERA_H
#define HELLOVULKAN_EDITORCAMERA_H
#include <Engine/Renderer/Camera.h>
#include <Engine/ECS/Components.h>
#include <glm/glm.hpp>
namespace Engine {
class Window;
class EditorCamera {
public:
EditorCamera()
{
// 出场位姿:俯视场中央(与旧沙盒 MainCamera 出场一致,
// 空场景也有可看的初始视角)
m_Transform.SetPosition({0.0f, 12.0f, 22.0f});
m_Transform.SetRotation({-25.0f, 0.0f, 0.0f});
m_Transform.Compose();
}
// 每帧输入驱动(编辑器形态由 Application 调用;游戏形态不调
// 即静止)。flyRequiresRMB 恒 true:编辑器移动键只在 RMB 按住
// 时生效(W/E/R 归 Gizmo,UE 语义)。暂停(timeScale=0)时
// 仍可飞行——调用方传 unscaled dt
void Update(Window& window, f32 deltaTime)
{
m_Controller.Update(window, m_Transform, deltaTime,
/*flyRequiresRMB=*/true);
m_Transform.Compose(); // World() 是渲染/Gizmo 读源,随写随算
}
// 跳转(对齐视口):把相机实体(或任意实体)位姿复制过来,
// Scene view 立即从该位置观看(UE "Pilot This Camera" 的
// 一次性对齐版)。环绕 pivot 已失效,清除待 MMB 重建
void AlignTo(const ECS::TransformComponent& target)
{
m_Transform.SetPosition(target.GetPosition());
m_Transform.SetRotationQuat(target.GetRotationQuat());
m_Transform.Compose();
m_Controller.ResetOrbit();
}
// 焦点选中(F 键):pivot 设为 point,沿当前视线平移对准
void FocusOn(const glm::vec3& point)
{
m_Controller.FocusOn(m_Transform, point);
}
// 视图 = 自有 transform 世界矩阵的逆(要求已 Compose——
// Update/AlignTo/ctor 三个写入口都收尾 Compose)
glm::mat4 GetView() const { return m_Controller.GetView(m_Transform); }
ECS::TransformComponent& GetTransform() { return m_Transform; }
Camera& GetController() { return m_Controller; }
private:
ECS::TransformComponent m_Transform; // 自有位姿(非场景实体)
Camera m_Controller; // 交互控制器(环绕状态在此)
};
} // namespace Engine
#endif //HELLOVULKAN_EDITORCAMERA_H
+4 -1
View File
@@ -28,10 +28,13 @@ namespace Engine
public: public:
// material 非拥有指针:Model 的材质缓存拥有(同贴图共享一份) // material 非拥有指针:Model 的材质缓存拥有(同贴图共享一份)
// lods[0] 必须为原始索引(与 vertices 对应),各级简化链由解析期生成 // lods[0] 必须为原始索引(与 vertices 对应),各级简化链由解析期生成
// cpuPositions 非空(蒙皮 mesh)= 物理数据源与包围球改用它
//(绑定姿态渲染空间,与视觉同源;vertices.position 是蒙皮原始空间)
Mesh(Device& device, MemoryAllocator& allocator, Mesh(Device& device, MemoryAllocator& allocator,
const std::vector<Vertex>& vertices, const std::vector<Vertex>& vertices,
const std::vector<std::vector<u32>>& lods, const std::vector<std::vector<u32>>& lods,
Material* material); Material* material,
const std::vector<glm::vec3>* cpuPositions = nullptr);
~Mesh(); ~Mesh();
Mesh(const Mesh&) = delete; Mesh(const Mesh&) = delete;
+7
View File
@@ -42,6 +42,13 @@ namespace Engine
std::array<std::string, Material::kSlotCount> texPaths; std::array<std::string, Material::kSlotCount> texPaths;
MaterialParams params; MaterialParams params;
bool transparent = false; // glTF alphaMode=BLEND(透明绘制队列) bool transparent = false; // glTF alphaMode=BLEND(透明绘制队列)
bool skinned = false; // 有骨骼(渲染走 palette 蒙皮)
// 蒙皮 mesh 专属:绑定姿态渲染空间的位置副本(palette(bind) 按
// 顶点权重混合,与 shader 同式)。非空 = Mesh 的物理数据源与
// 包围球改用它——FBX 单位换算折在骨骼链上(bind palette ≠ I),
// 直接用原始顶点会让物理凸包大一个数量级(M1911 实测 10×)
std::vector<glm::vec3> cpuPositions;
}; };
// ---- 骨骼动画数据(assimp aiNode/aiAnimation/aiBone 的引擎侧镜像)---- // ---- 骨骼动画数据(assimp aiNode/aiAnimation/aiBone 的引擎侧镜像)----
@@ -41,6 +41,7 @@ namespace Engine {
class CommandManager; class CommandManager;
class UniformBuffer; class UniformBuffer;
class SceneRenderer; class SceneRenderer;
class EditorCamera;
class VulkanAssetUploader; class VulkanAssetUploader;
class Cubemap; class Cubemap;
class IblGenerator; class IblGenerator;
@@ -141,6 +142,9 @@ namespace Engine {
// ---- 显示模式(Editor 视口 ↔ 全屏直出;帧首同步生效)---- // ---- 显示模式(Editor 视口 ↔ 全屏直出;帧首同步生效)----
int GetDisplayMode() const { return m_DesiredDisplayMode; } int GetDisplayMode() const { return m_DesiredDisplayMode; }
void SetDisplayMode(int mode); // cpp 内定义(SceneRenderer 前置声明) void SetDisplayMode(int mode); // cpp 内定义(SceneRenderer 前置声明)
// 编辑器相机(Scene view 恒用,与场景相机实体无关)。转发
// SceneRenderer 持有的实例:Application 驱动 Update,面板读写位姿
EditorCamera& GetEditorCamera(); // cpp 内定义(SceneRenderer 完整类型)
// 当帧场景数据(只读直通)。CPU 消费如视口拾取:反投影矩阵必须 // 当帧场景数据(只读直通)。CPU 消费如视口拾取:反投影矩阵必须
// 与渲染同一台相机同一投影(含 [0,1] 深度语义),禁止自行重算 // 与渲染同一台相机同一投影(含 [0,1] 深度语义),禁止自行重算
const FrameSceneData& GetFrameSceneData() const; // cpp 内定义 const FrameSceneData& GetFrameSceneData() const; // cpp 内定义
@@ -28,6 +28,8 @@
#include <array> #include <array>
#include <unordered_map> #include <unordered_map>
#include <Engine/Renderer/EditorCamera.h>
namespace Engine { namespace Engine {
namespace ECS { class Scene; } namespace ECS { class Scene; }
@@ -35,7 +37,8 @@ namespace Engine {
class SceneRenderer { class SceneRenderer {
public: public:
// 从场景构建当帧渲染数据。无 primary 相机返回 false(渲染跳帧)。 // 从场景构建当帧渲染数据。游戏形态无 primary 相机返回 false
//(渲染跳帧;编辑器形态恒用编辑器相机,不跳)。
// targetExtent 用于 TAA 子像素抖动的 NDC 偏移折算。 // targetExtent 用于 TAA 子像素抖动的 NDC 偏移折算。
// dt 驱动骨骼动画采样推进(crossfade 计时)。 // dt 驱动骨骼动画采样推进(crossfade 计时)。
bool BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets, bool BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets,
@@ -62,6 +65,9 @@ namespace Engine {
void SetLodEnabled(bool b) { m_LodEnabled = b; } void SetLodEnabled(bool b) { m_LodEnabled = b; }
void SetDebugView(int v) { m_DebugView = v; } void SetDebugView(int v) { m_DebugView = v; }
void SetDisplayMode(int m) { m_DisplayMode = m; } // kDisplayDirect / kDisplayViewport void SetDisplayMode(int m) { m_DisplayMode = m; } // kDisplayDirect / kDisplayViewport
// 编辑器相机(视口 UI 状态,SceneRenderer 持有 = 渲染源在此分派);
// Application 驱动 Update,ViewportPanel/Inspector 经 Renderer 门面访问
EditorCamera& GetEditorCamera() { return m_EditorCamera; }
// Editor 视口拾取请求(UI 段写入,下一次 BuildFrame 落帧后清除) // Editor 视口拾取请求(UI 段写入,下一次 BuildFrame 落帧后清除)
void SetPickRequest(const glm::vec2& ndc) void SetPickRequest(const glm::vec2& ndc)
{ {
@@ -83,6 +89,7 @@ namespace Engine {
static void PackIndirect(CullResult& view, bool byMaterial); static void PackIndirect(CullResult& view, bool byMaterial);
FrameSceneData m_Frame; FrameSceneData m_Frame;
EditorCamera m_EditorCamera; // 编辑器相机(视口 UI 状态,见 GetEditorCamera)
PickRequest m_PendingPick{}; // 拾取请求缓冲(BuildFrame 落帧后清除) PickRequest m_PendingPick{}; // 拾取请求缓冲(BuildFrame 落帧后清除)
u32 m_PendingSelected = 0; // 选中实体缓冲(描边) u32 m_PendingSelected = 0; // 选中实体缓冲(描边)
+133
View File
@@ -0,0 +1,133 @@
//
// Script.h:C++ 原生脚本设施(行为基类 + 按名注册表)。
//
// 设计参照 UE:脚本 = 挂在实体上的行为对象 + 统一基类生命周期
//(OnCreate/OnUpdate/OnDestroy,BeginPlay/Tick 语义)。引擎帧循环只认
// 基类接口——将来接 Lua/C# 时各实现一种 foreign behavior(内部持
// VM 状态),引擎零改动。
//
// 组件只存 scriptName 字符串(POD,序列化天然支持:Lua/C# 同样按名
// 解析);实例由 ScriptRuntime 对账式管理(照 PhysicsWorld 模式),
// 不进组件——组件可拷贝/可比较。
//
// 脚本作者注意:
// OnCreate/OnUpdate 阶段实体与组件已就位(假定所需组件存在);
// OnDestroy 只做自身清理——可能因实体销毁触发,句柄已失效。
//
#ifndef HELLOVULKAN_SCRIPT_H
#define HELLOVULKAN_SCRIPT_H
#include <Engine/Core/Types.h>
#include <Engine/ECS/Components.h>
#include <Engine/ECS/Scene.h>
#include <string>
#include <unordered_map>
#include <vector>
namespace Engine {
class Application; // 宿主门面(Window/Renderer/Physics 入口),实现见 ScriptRuntime.cpp
namespace Scripting {
class ScriptBehavior {
public:
virtual ~ScriptBehavior() = default;
// 注册表键(= 组件 scriptName),须与 ENGINE_REGISTER_SCRIPT 名一致
virtual const char* Name() const = 0;
// 生命周期(吃 scaled dt:编辑器暂停 timeScale=0 时自然停摆)
virtual void OnCreate() {}
virtual void OnUpdate(f32 dt) {}
virtual void OnDestroy() {}
protected:
// 便捷访问:绑定实体的 TransformComponent
ECS::TransformComponent& Transform() {
return m_Scene->GetRegistry().get<ECS::TransformComponent>(m_Entity);
}
template<typename T>
T& GetComponent() {
return m_Scene->GetRegistry().get<T>(m_Entity);
}
template<typename T>
T* TryGetComponent() {
return m_Scene->GetRegistry().try_get<T>(m_Entity);
}
// 绑定实体的 entityBits(物理门面按键值寻址)
u32 EntityBits() const {
return static_cast<u32>(entt::to_integral(m_Entity));
}
// 宿主应用(GetWindow/GetRenderer/GetPhysics/GetScene……)
Application& App() const;
bool Valid() const { return m_Scene != nullptr && m_Entity != entt::null; }
private:
friend class ScriptRuntime;
void Bind(ECS::Scene& scene, entt::entity e, Application* app) {
m_Scene = &scene;
m_Entity = e;
m_App = app;
}
ECS::Scene* m_Scene = nullptr;
entt::entity m_Entity = entt::null;
Application* m_App = nullptr;
};
// ---- 按名注册表:脚本类文件级自注册,运行时按名实例化 ----
class ScriptRegistry {
public:
using Factory = Scope<ScriptBehavior> (*)();
static void Register(const std::string& name, Factory f) {
Map()[name] = f;
Names().push_back(name); // Inspector 下拉用(保持注册序)
}
static Scope<ScriptBehavior> Create(const std::string& name) {
auto& m = Map();
const auto it = m.find(name);
return it != m.end() ? it->second() : nullptr;
}
static bool Contains(const std::string& name) {
return Map().count(name) != 0;
}
static std::vector<std::string>& Names() {
static std::vector<std::string> v;
return v;
}
private:
static std::unordered_map<std::string, Factory>& Map() {
static std::unordered_map<std::string, Factory> m;
return m;
}
};
} // namespace Scripting
} // namespace Engine
// 文件级自注册:脚本 cpp 里写一行即可,Inspector 下拉与序列化按
// 类名(字符串)解析。T 须默认可构造并继承 ScriptBehavior
#define ENGINE_REGISTER_SCRIPT(T) \
static const bool s_##T##_registered = [] { \
::Engine::Scripting::ScriptRegistry::Register( \
#T, []() -> ::Engine::Scope< \
::Engine::Scripting::ScriptBehavior> { \
return ::Engine::CreateScope<T>(); \
}); \
return true; \
}();
#endif //HELLOVULKAN_SCRIPT_H
@@ -0,0 +1,47 @@
//
// ScriptRuntime.h:脚本生命周期驱动。对账式管理(组件加/删/改名 ↔
// 实例建/毁),照 PhysicsWorld.SyncScene 模式——每帧对账,编辑器操作
// 即时生效,场景加载/实体销毁自动消化。
//
#ifndef HELLOVULKAN_SCRIPT_RUNTIME_H
#define HELLOVULKAN_SCRIPT_RUNTIME_H
#include <Engine/Core/Types.h>
#include <Engine/ECS/Scene.h>
#include <Engine/Scripting/Script.h>
#include <unordered_map>
namespace Engine {
class Application;
namespace Scripting {
// 实例簿记:行为对象 + OnCreate 是否已调
struct ScriptInstance {
Scope<ScriptBehavior> behavior;
bool created = false;
};
class ScriptRuntime {
public:
explicit ScriptRuntime(ECS::Scene& scene, Application* app = nullptr)
: m_Scene(scene), m_App(app) {}
// 每帧调用(吃 scaled dt):对账 → OnCreate(新实例)→ OnUpdate
void Update(f32 dt);
private:
void Sync();
ECS::Scene& m_Scene;
Application* m_App; // 注入脚本上下文
std::unordered_map<u32, ScriptInstance> m_Instances; // entityBits
};
} // namespace Scripting
} // namespace Engine
#endif //HELLOVULKAN_SCRIPT_RUNTIME_H
@@ -0,0 +1,62 @@
//
// Created by atdunbg on 2026/10/3.
//
// SceneSerializer:场景 → YAML 文件的双向转换(Unity 场景 YAML 同构简化)。
//
// 参照系(定案):Unity 的场景 YAML 结构 + UE AssetRegistry 式中央
// GUID 注册表(AssetManager 维护,存 assets/asset_registry.yaml)。
// 场景内资产引用双写 {Guid, Path}:GUID 是主键(重命名/移动资产不断
// 引用),Path 是人工可读提示 + 注册表缺失时的解析兜底。
//
// 引擎层身份:本类不感知编辑器(kEnvPaths 表)与渲染器——
// environmentPath 以普通字符串经参数进出,消费方(编辑器/游戏运行时)
// 自行解释。一套 API 服务两个消费者:编辑器 File 菜单 与
// 游戏运行时动态加载,不存在"另一套序列化"。
//
// 前向兼容(Prefab 路线预留):
// - 场景 = 实体节点列表,Prefab 复用同一套实体读写函数
// - Version 闸:高版本文件拒载并 WARN,低版本宽容
// - 未知组件名跳过(Unity 语义:新版场景在旧版引擎打开可玩)
// - UUID 重复 → 重新生成(即 Prefab 实例化的 ID 重映射)
//
#ifndef HELLOVULKAN_SCENESERIALIZER_H
#define HELLOVULKAN_SCENESERIALIZER_H
#include <string>
namespace Engine::ECS { class Scene; }
namespace Engine::Assets { class AssetManager; }
namespace Engine::Serialization {
// 当前场景文件格式版本(字段/结构变更时递增)
inline constexpr int kSceneVersion = 1;
class SceneSerializer {
public:
// ---- 保存 ----
// scene 序列化到 outPath(YAML)。environmentPath 可空(不写
// Environment 字段)。失败(无法写文件)返回 false。
static bool Serialize(const ECS::Scene& scene,
Assets::AssetManager& assets,
const std::string& outPath,
const std::string& environmentPath = {});
// ---- 加载 ----
// 原子换血:先解析校验到中间结构,全部成功才 scene.Clear() 重建
//(解析失败不动现场)。成功时 outEnvironmentPath 带回场景记录的
// 环境路径(空 = 场景未带环境,消费方保持当前环境即可)。
// 健壮性:单实体损坏跳过继续、缺字段取默认值、未知组件跳过、
// UUID 重复重新生成、YAML 异常优雅报错。
// 注意:调用方持有的 Entity/EntityHandle 在成功后全部悬空,
// 必须重解析(Scene::GetEntityHandle / 按 UUID 查询)。
static bool Deserialize(ECS::Scene& scene,
Assets::AssetManager& assets,
const std::string& inPath,
std::string& outEnvironmentPath);
};
} // namespace Engine::Serialization
#endif //HELLOVULKAN_SCENESERIALIZER_H
+94 -1
View File
@@ -4,13 +4,106 @@
#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 <yaml-cpp/yaml.h>
#include <fstream>
namespace Engine::Assets { namespace Engine::Assets {
namespace {
// GUID 注册表落盘位置(相对 CWD,与 assets/ 约定一致)
constexpr const char* kGuidRegistryPath = "assets/asset_registry.yaml";
// 伪路径前缀:参数确定性生成,无稳定身份需求,不进 GUID 注册表
constexpr std::string_view kProceduralPrefix = "procedural://";
bool IsProceduralPath(const std::string& path)
{
return path.rfind(kProceduralPrefix, 0) == 0;
}
}
AssetManager::AssetManager(IAssetUploader& uploader) AssetManager::AssetManager(IAssetUploader& uploader)
: m_Uploader(uploader) : m_Uploader(uploader)
{ {
ENGINE_INFO("AssetManager created"); LoadGUIDRegistry();
ENGINE_INFO("AssetManager created ({} guids registered)", m_GuidToPath.size());
}
// ---- 资产 GUID 注册表 ----
u64 AssetManager::GetOrCreateGUID(const std::string& path)
{
if (path.empty() || IsProceduralPath(path)) return 0;
if (auto it = m_PathToGuid.find(path); it != m_PathToGuid.end()) {
return it->second;
}
// 64 位随机 + 双 map 双向查重(UUID::New 撞已有 GUID 概率可忽略,
// 但注册表是持久身份,防御性重试)
u64 guid = 0;
do { guid = UUID::New().Value(); } while (m_GuidToPath.contains(guid));
m_PathToGuid.emplace(path, guid);
m_GuidToPath.emplace(guid, path);
SaveGUIDRegistry();
return guid;
}
std::string AssetManager::ResolveGUID(const u64 guid) const
{
if (auto it = m_GuidToPath.find(guid); it != m_GuidToPath.end()) {
return it->second;
}
return {};
}
void AssetManager::LoadGUIDRegistry()
{
try {
YAML::Node root = YAML::LoadFile(kGuidRegistryPath);
if (const YAML::Node assets = root["Assets"]; assets.IsSequence()) {
for (const auto& a : assets) {
const u64 guid = a["Guid"].as<u64>(0);
auto path = a["Path"].as<std::string>();
if (guid != 0 && !path.empty() &&
!m_GuidToPath.contains(guid)) {
m_GuidToPath.emplace(guid, path);
m_PathToGuid.emplace(std::move(path), guid);
}
}
}
}
catch (const YAML::Exception& e) {
// 注册表缺失/损坏不致命:场景里同时存了 Path 兜底
ENGINE_WARN("AssetManager: guid registry unavailable ({})", e.what());
}
catch (const std::exception& e) {
ENGINE_WARN("AssetManager: guid registry unreadable ({})", e.what());
}
}
void AssetManager::SaveGUIDRegistry() const
{
try {
YAML::Node root;
for (const auto& [guid, path] : m_GuidToPath) {
YAML::Node a;
a["Guid"] = guid;
a["Path"] = path;
root["Assets"].push_back(a);
}
std::ofstream out(kGuidRegistryPath, std::ios::trunc);
if (!out) {
ENGINE_WARN("AssetManager: cannot write {}", kGuidRegistryPath);
return;
}
out << root;
}
catch (const YAML::Exception& e) {
ENGINE_ERROR("AssetManager: guid registry save failed ({})", e.what());
}
} }
ModelRef AssetManager::LoadModel(const std::string& path) ModelRef AssetManager::LoadModel(const std::string& path)
+9
View File
@@ -7,6 +7,7 @@
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
#include <Engine/ECS/Scene.h> #include <Engine/ECS/Scene.h>
#include <Engine/Physics/PhysicsWorld.h> #include <Engine/Physics/PhysicsWorld.h>
#include <Engine/Scripting/ScriptRuntime.h>
#include <Engine/Renderer/Renderer.h> #include <Engine/Renderer/Renderer.h>
#include <Engine/Assets/AssetManager.h> #include <Engine/Assets/AssetManager.h>
#include <Engine/Vulkan/Device.h> #include <Engine/Vulkan/Device.h>
@@ -102,6 +103,10 @@ namespace Engine {
// 物理调试图元 → 渲染侧(PhysicsDebugPass 每帧上传绘制) // 物理调试图元 → 渲染侧(PhysicsDebugPass 每帧上传绘制)
m_Renderer->SetPhysicsDebugSource(&m_Physics->GetDebugFrame()); m_Renderer->SetPhysicsDebugSource(&m_Physics->GetDebugFrame());
// 脚本生命周期(对账式,组件增删/改名即时生效);this 注入脚本
// 上下文(Window/Renderer/Physics 门面)
m_Scripts = CreateScope<Scripting::ScriptRuntime>(*m_Scene, this);
while (m_Running) { while (m_Running) {
const auto frameStart = std::chrono::steady_clock::now(); const auto frameStart = std::chrono::steady_clock::now();
@@ -121,6 +126,10 @@ namespace Engine {
// SyncScene 对账仍跑(拖静态体照常同步) // SyncScene 对账仍跑(拖静态体照常同步)
m_Physics->Step(frame.Dt()); m_Physics->Step(frame.Dt());
// 脚本更新(物理之后、业务 OnUpdate 之前,Unity FixedUpdate→Update
// 语义;吃 scaled dt——timeScale=0 暂停时 dt=0 脚本自然停摆)
m_Scripts->Update(frame.Dt());
OnUpdate(frame); OnUpdate(frame);
// 延迟销毁在渲染前统一 flush,保证渲染遍历时池结构不变 // 延迟销毁在渲染前统一 flush,保证渲染遍历时池结构不变
+9
View File
@@ -72,6 +72,8 @@ namespace Engine
if (!self->m_EventCallback) return; if (!self->m_EventCallback) return;
if (self->m_MouseCapture && self->m_MouseCapture()) return; if (self->m_MouseCapture && self->m_MouseCapture()) return;
self->m_ScrollAccum += static_cast<f32>(yoffset); // 轮询消费通道
Event e; Event e;
e.type = EventType::MouseScroll; e.type = EventType::MouseScroll;
e.deltaX = static_cast<f32>(xoffset); e.deltaX = static_cast<f32>(xoffset);
@@ -245,4 +247,11 @@ namespace Engine
dx = dy = 0.0f; dx = dy = 0.0f;
} }
} }
float Window::ConsumeScrollDelta()
{
const float d = m_ScrollAccum;
m_ScrollAccum = 0.0f;
return d;
}
} // namespace Engine } // namespace Engine
+14 -1
View File
@@ -53,6 +53,19 @@ namespace Engine::ECS {
m_PendingDestroy.clear(); m_PendingDestroy.clear();
} }
void Scene::Clear()
{
// 先清延迟队列再销毁全部:避免 FlushDestroyQueue 语义混淆
//(registry.clear() 后队列里的句柄全部失效,直接丢弃即可)
m_PendingDestroy.clear();
m_Registry.clear();
}
EntityHandle Scene::GetEntityHandle(Entity e)
{
return m_Registry.valid(e) ? EntityHandle{this, e} : EntityHandle{};
}
void Scene::UpdateTransforms() void Scene::UpdateTransforms()
{ {
ZoneScoped; ZoneScoped;
@@ -68,7 +81,7 @@ namespace Engine::ECS {
CameraComponent* found = nullptr; CameraComponent* found = nullptr;
Entity foundEntity = entt::null; Entity foundEntity = entt::null;
m_Registry.view<CameraComponent>().each([&](Entity e, CameraComponent& cc) { m_Registry.view<CameraComponent>().each([&](Entity e, CameraComponent& cc) {
if (!found && cc.primary) { if (!found && cc.primary && cc.active) { // 禁用相机退出候选
found = &cc; found = &cc;
foundEntity = e; foundEntity = e;
} }
+103
View File
@@ -0,0 +1,103 @@
//
// CharacterController.cpp:Jolt CharacterVirtual 封装实现。
//
#include <Engine/Physics/CharacterController.h>
#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
#include <Jolt/Physics/PhysicsSystem.h>
#include <Jolt/Physics/Body/Body.h>
#include <Jolt/Physics/Collision/Shape/Shape.h>
#include <Jolt/Renderer/DebugRenderer.h>
namespace Engine {
CharacterController::CharacterController(JPH::PhysicsSystem &system,
const Params &params,
const glm::vec3 &position)
: m_Params(params) {
// 胶囊体以脚底为原点:CapsuleShape 原点在其中心,经 mShapeOffset
// 上抬 (halfHeight + radius) 使底端落在原点(Jolt 角色约定)
JPH::CharacterVirtualSettings s;
s.mUp = JPH::Vec3::sAxisY();
s.mMass = std::max(params.mass, 1e-3f);
s.mMaxSlopeAngle = glm::radians(params.maxSlopeAngle);
s.mShape = new JPH::CapsuleShape(
std::max(params.radius, 0.01f),
std::max(params.halfHeight, 0.0f));
s.mShapeOffset = JPH::Vec3(0.0f,
params.halfHeight + params.radius, 0.0f);
m_Character = new JPH::CharacterVirtual(
&s, JPH::RVec3(position.x, position.y, position.z),
JPH::Quat::sIdentity(), &system);
// 台阶攀爬:竖直抬升 = stepHeight(ExtendedUpdate 内部走
// WalkStairs 前向距离按速度取)
m_Ext.mWalkStairsStepUp = JPH::Vec3(0.0f, std::max(params.stepHeight, 0.0f), 0.0f);
}
void CharacterController::Update(const f32 dt, const JPH::Vec3 &gravity,
JPH::TempAllocator &allocator) {
// 重力积分:着地时清掉向下分量(防贴地累积穿透速度),悬空时累加
const JPH::Vec3 v = m_Character->GetLinearVelocity();
if (m_Character->IsSupported()) {
if (v.GetY() < 0.0f) {
m_Character->SetLinearVelocity(
JPH::Vec3(v.GetX(), 0.0f, v.GetZ()));
}
} else {
m_Character->SetLinearVelocity(v + gravity * dt);
}
// 扫掠推进(撞墙滑动 + 斜坡限制 + StickToFloor 贴地 + WalkStairs
// 上台阶)。过滤器默认全放行:角色与静/动层全碰
m_Character->ExtendedUpdate(dt, gravity, m_Ext,
JPH::BroadPhaseLayerFilter{},
JPH::ObjectLayerFilter{},
JPH::BodyFilter{}, JPH::ShapeFilter{},
allocator);
}
void CharacterController::SetLinearVelocity(const glm::vec3 &velocity) {
m_Character->SetLinearVelocity(
JPH::Vec3(velocity.x, velocity.y, velocity.z));
}
glm::vec3 CharacterController::GetLinearVelocity() const {
const JPH::Vec3 v = m_Character->GetLinearVelocity();
return {v.GetX(), v.GetY(), v.GetZ()};
}
void CharacterController::Jump(const f32 speed) {
const JPH::Vec3 v = m_Character->GetLinearVelocity();
m_Character->SetLinearVelocity(JPH::Vec3(v.GetX(), speed, v.GetZ()));
}
bool CharacterController::IsGrounded() const {
return m_Character->IsSupported();
}
glm::vec3 CharacterController::GetPosition() const {
const JPH::RVec3 p = m_Character->GetPosition();
return {static_cast<f32>(p.GetX()), static_cast<f32>(p.GetY()),
static_cast<f32>(p.GetZ())};
}
void CharacterController::Teleport(const glm::vec3 &position) const {
m_Character->SetPosition(
JPH::RVec3(position.x, position.y, position.z));
}
void CharacterController::DebugDraw(JPH::DebugRenderer &renderer) const {
// 胶囊中心 = 脚底原点 + shapeOffset(角色不旋转,纯平移)。
// CharacterVirtual 非 body,DrawBodies 永远画不到它——这是
// Jolt 层面的缺口(连 CharacterVirtual::DebugDraw 都没有)
const JPH::RMat44 t = JPH::RMat44::sTranslation(
m_Character->GetPosition() +
JPH::RVec3(m_Character->GetShapeOffset()));
m_Character->GetShape()->Draw(&renderer, t, JPH::Vec3::sOne(),
JPH::Color::sGreen, false, true);
}
} // namespace Engine
+477 -9
View File
@@ -16,6 +16,12 @@
#include <Jolt/Physics/Collision/Shape/SphereShape.h> #include <Jolt/Physics/Collision/Shape/SphereShape.h>
#include <Jolt/Physics/Collision/Shape/MeshShape.h> #include <Jolt/Physics/Collision/Shape/MeshShape.h>
#include <Jolt/Physics/Collision/Shape/ConvexHullShape.h> #include <Jolt/Physics/Collision/Shape/ConvexHullShape.h>
#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
#include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
#include <Jolt/Physics/Constraints/FixedConstraint.h>
#include <Jolt/Physics/Constraints/HingeConstraint.h>
#include <Jolt/Physics/Constraints/PointConstraint.h>
#include <Engine/Assets/AssetManager.h> #include <Engine/Assets/AssetManager.h>
#include <Engine/Renderer/Model.h> #include <Engine/Renderer/Model.h>
@@ -83,6 +89,26 @@ namespace Engine {
glm::quat ToGlm(const JPH::Quat &q) { glm::quat ToGlm(const JPH::Quat &q) {
return {q.GetW(), q.GetX(), q.GetY(), q.GetZ()}; return {q.GetW(), q.GetX(), q.GetY(), q.GetZ()};
} }
// 变更检测快照取值:collider 决定哪组参数生效(collider 本身变更
// 在外层先触发重建,到这里时语义已对齐)。Box/Mesh/Compound 不用
void ExtractShapeParams(const ECS::RigidBodyComponent &rb,
f32 &radius, f32 &halfHeight) {
using CT = ECS::RigidBodyComponent::ColliderType;
switch (rb.collider) {
case CT::Sphere: radius = rb.sphereRadius; break;
case CT::Capsule: radius = rb.capsuleRadius;
halfHeight = rb.capsuleHalfHeight; break;
case CT::Cylinder: radius = rb.cylinderRadius;
halfHeight = rb.cylinderHalfHeight; break;
default: break;
}
}
// 球/胶囊/圆柱类径向形状的缩放取 max 轴(不可各向异性)
f32 UniformScale(const glm::vec3 &s) {
return std::max({s.x, s.y, s.z});
}
} // namespace } // namespace
// ---- 碰撞事件监听:回调在 Update 内多线程并发,只读 body、加锁入队 ---- // ---- 碰撞事件监听:回调在 Update 内多线程并发,只读 body、加锁入队 ----
@@ -216,6 +242,7 @@ namespace Engine {
m_System->Update(kFixedDt, steps, m_TempAllocator.get(), m_JobSystem.get()); m_System->Update(kFixedDt, steps, m_TempAllocator.get(), m_JobSystem.get());
Writeback(); Writeback();
UpdateCharacters(kFixedDt); // 角色扫掠(Jolt 官方建议:物理更新后)
UpdateAccels(kFixedDt); // 速度差分 → 加速度(step 率采样一次) UpdateAccels(kFixedDt); // 速度差分 → 加速度(step 率采样一次)
} }
@@ -299,6 +326,14 @@ namespace Engine {
} }
} }
// 4) 角色控制器:CharacterVirtual 非 body,DrawBodies 画不到,
// 手动画胶囊线框(绿 = 与普通 collider 区分)
if ((m_DebugBits & kPhyDbgColliders) != 0) {
for (auto &[bits, entry] : m_Characters) {
entry.cc->DebugDraw(*m_DebugDraw);
}
}
// 释放上一帧缓存不再引用的通用几何 batch(DebugRenderer 内部池) // 释放上一帧缓存不再引用的通用几何 batch(DebugRenderer 内部池)
m_DebugDraw->NextFrame(); m_DebugDraw->NextFrame();
} }
@@ -308,6 +343,7 @@ namespace Engine {
void PhysicsWorld::SyncScene() { void PhysicsWorld::SyncScene() {
auto &reg = m_Scene.GetRegistry(); auto &reg = m_Scene.GetRegistry();
auto &bi = m_System->GetBodyInterface(); auto &bi = m_System->GetBodyInterface();
std::vector<u32> invalidated; // 本帧销毁/重建 body → 约束失效传染
// 1. 孤儿清理:实体销毁 / 组件被移除 → 移除 body // 1. 孤儿清理:实体销毁 / 组件被移除 → 移除 body
for (auto it = m_Bodies.begin(); it != m_Bodies.end();) { for (auto it = m_Bodies.begin(); it != m_Bodies.end();) {
@@ -316,6 +352,7 @@ namespace Engine {
!reg.all_of<ECS::RigidBodyComponent, ECS::TransformComponent>(e)) { !reg.all_of<ECS::RigidBodyComponent, ECS::TransformComponent>(e)) {
bi.RemoveBody(it->second.id); bi.RemoveBody(it->second.id);
bi.DestroyBody(it->second.id); bi.DestroyBody(it->second.id);
invalidated.push_back(it->first);
it = m_Bodies.erase(it); it = m_Bodies.erase(it);
} else { } else {
++it; ++it;
@@ -327,27 +364,46 @@ namespace Engine {
for (auto [e, rb, tr]: view.each()) { for (auto [e, rb, tr]: view.each()) {
const u32 bits = static_cast<u32>(entt::to_integral(e)); const u32 bits = static_cast<u32>(entt::to_integral(e));
const auto found = m_Bodies.find(bits); const auto found = m_Bodies.find(bits);
// 禁用 = 视为组件不存在:销毁 body(对账自然消化),重启用即重建
if (!rb.active) {
if (found != m_Bodies.end()) {
bi.RemoveBody(found->second.id);
bi.DestroyBody(found->second.id);
m_Bodies.erase(found);
invalidated.push_back(bits);
}
rb.bodyValid = false;
continue;
}
if (found == m_Bodies.end() || !rb.bodyValid) { if (found == m_Bodies.end() || !rb.bodyValid) {
if (found != m_Bodies.end()) { if (found != m_Bodies.end()) {
bi.RemoveBody(found->second.id); bi.RemoveBody(found->second.id);
bi.DestroyBody(found->second.id); bi.DestroyBody(found->second.id);
m_Bodies.erase(found); m_Bodies.erase(found);
invalidated.push_back(bits);
} }
CreateBody(bits, rb, tr); CreateBody(bits, rb, tr);
continue; continue;
} }
auto &rec = found->second; // 非 const:静态/运动体拖动时更新 lastPos/lastRot auto &rec = found->second; // 非 const:静态/运动体拖动时更新 lastPos/lastRot
f32 radius = 0.0f, halfHeight = 0.0f;
ExtractShapeParams(rb, radius, halfHeight);
if (rec.type != static_cast<u8>(rb.type) || if (rec.type != static_cast<u8>(rb.type) ||
rec.collider != static_cast<u8>(rb.collider) || rec.collider != static_cast<u8>(rb.collider) ||
rec.colliderSize != rb.boxHalfExtents || rec.colliderSize != rb.boxHalfExtents ||
rec.radius != rb.sphereRadius || rec.radius != radius ||
rec.halfHeight != halfHeight ||
rec.compound != rb.compound ||
rec.mass != rb.mass || rec.mass != rb.mass ||
rec.isSensor != rb.isSensor) { rec.isTrigger != rb.isTrigger) {
// 类型/碰撞体/质量变更 → 重建(简单正确,重置速度可接受) // 类型/碰撞体/质量变更 → 重建(简单正确,重置速度可接受)
bi.RemoveBody(rec.id); bi.RemoveBody(rec.id);
bi.DestroyBody(rec.id); bi.DestroyBody(rec.id);
m_Bodies.erase(found); m_Bodies.erase(found);
invalidated.push_back(bits);
rb.bodyValid = false; rb.bodyValid = false;
CreateBody(bits, rb, tr); CreateBody(bits, rb, tr);
continue; continue;
@@ -358,6 +414,22 @@ namespace Engine {
bi.SetRestitution(id, rb.restitution); bi.SetRestitution(id, rb.restitution);
if (rb.type == ECS::RigidBodyComponent::BodyType::Dynamic) { if (rb.type == ECS::RigidBodyComponent::BodyType::Dynamic) {
bi.SetGravityFactor(id, rb.gravityScale); bi.SetGravityFactor(id, rb.gravityScale);
// 编辑器拖动 Dynamic 体(暂停期/物理未步进的帧)→ 传送 +
// 清速度(UE 编辑器语义:编辑器操作权威于物理 body,拖动 =
// 重新放置)。物理模拟中 Writeback 每步同步 lastPos/lastRot
// → 本比对只在真正拖动帧为真,不会打断自由模拟
if (rec.lastPos != tr.GetPosition() ||
rec.lastRot != tr.GetRotationQuat()) {
bi.SetPositionAndRotation(
id,
JPH::RVec3(tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z),
ToJolt(tr.GetRotationQuat()),
JPH::EActivation::Activate);
bi.SetLinearVelocity(id, JPH::Vec3(0.0f, 0.0f, 0.0f));
bi.SetAngularVelocity(id, JPH::Vec3(0.0f, 0.0f, 0.0f));
rec.lastPos = tr.GetPosition();
rec.lastRot = tr.GetRotationQuat();
}
} else if (rec.lastPos != tr.GetPosition() || } else if (rec.lastPos != tr.GetPosition() ||
rec.lastRot != tr.GetRotationQuat()) { rec.lastRot != tr.GetRotationQuat()) {
// 编辑器拖动静态/运动学体 → 传送 body // 编辑器拖动静态/运动学体 → 传送 body
@@ -369,6 +441,241 @@ namespace Engine {
rec.lastRot = tr.GetRotationQuat(); rec.lastRot = tr.GetRotationQuat();
} }
} }
// 3. 约束对账(body 循环之后:依赖新建 body 的最终 ID)
SyncConstraints(invalidated);
// 4. 角色对账(非刚体,不依赖 body 生命周期)
SyncCharacters();
}
// ---- 约束对账 ----
void PhysicsWorld::SyncConstraints(const std::vector<u32> &invalidated) {
auto &reg = m_Scene.GetRegistry();
const auto tainted = [&invalidated](u32 bits) {
return std::find(invalidated.begin(), invalidated.end(), bits)
!= invalidated.end();
};
// 1. owner 实体/组件没了(或被禁用),或任一端 body 本帧被销毁/重建 → 移除
for (auto it = m_Constraints.begin(); it != m_Constraints.end();) {
const u32 owner = it->first;
const auto ownerEnt = static_cast<entt::entity>(owner);
const ECS::JointComponent* jc =
reg.valid(ownerEnt) ? reg.try_get<ECS::JointComponent>(ownerEnt)
: nullptr;
const bool ownerGone = jc == nullptr || !jc->active;
if (ownerGone || tainted(owner) || tainted(it->second.targetBits)) {
m_System->RemoveConstraint(it->second.constraint.GetPtr());
it = m_Constraints.erase(it);
} else {
++it;
}
}
// 2. 遍历组件:快照变更 → 重建;缺失 → 创建;禁用 → 跳过(上面已清)。
// target 惰性按 UUID 反查(UUID 失联/body 未就绪 → 下帧再试)
auto view = reg.view<ECS::JointComponent>();
for (auto [e, jc]: view.each()) {
if (!jc.active) {
continue;
}
const u32 bits = static_cast<u32>(entt::to_integral(e));
const auto found = m_Constraints.find(bits);
if (found != m_Constraints.end()) {
if (found->second.snapshot == jc) {
continue;
}
m_System->RemoveConstraint(found->second.constraint.GetPtr());
m_Constraints.erase(found);
}
const ECS::Entity target = m_Scene.FindEntityByUUID(jc.target);
if (target == entt::null) {
continue;
}
const u32 targetBits = static_cast<u32>(entt::to_integral(target));
if (bits == targetBits || !m_Bodies.contains(bits) ||
!m_Bodies.contains(targetBits)) {
continue;
}
CreateConstraint(bits, targetBits, jc);
}
}
void PhysicsWorld::CreateConstraint(u32 ownerBits, u32 targetBits,
const ECS::JointComponent &jc) {
auto &reg = m_Scene.GetRegistry();
const auto &b1 = m_Bodies.find(ownerBits)->second;
const auto &b2 = m_Bodies.find(targetBits)->second;
// 锚点(Transform 局部系)× 本体缩放 → COM 局部系
//(EConstraintSpace::LocalToBodyCOM 要求减 Shape::GetCenterOfMass,
// Compound 等形状 COM 会偏离原点;Box/Sphere/Capsule 为零)
const auto *tr1 = reg.try_get<ECS::TransformComponent>(
static_cast<entt::entity>(ownerBits));
const auto *tr2 = reg.try_get<ECS::TransformComponent>(
static_cast<entt::entity>(targetBits));
if (tr1 == nullptr || tr2 == nullptr) {
return;
}
const glm::vec3 s1 = tr1->GetScale();
const glm::vec3 s2 = tr2->GetScale();
// 双写锁读位姿/COM + 创建(SyncScene 在主线程,与 Update 不并发)
JPH::BodyLockWrite lock1(m_System->GetBodyLockInterface(), b1.id);
JPH::BodyLockWrite lock2(m_System->GetBodyLockInterface(), b2.id);
if (!lock1.Succeeded() || !lock2.Succeeded()) {
return;
}
JPH::Body &body1 = lock1.GetBody();
JPH::Body &body2 = lock2.GetBody();
const JPH::Quat r1 = body1.GetRotation();
const JPH::Quat r2 = body2.GetRotation();
const JPH::Vec3 com1 = body1.GetShape()->GetCenterOfMass();
const JPH::Vec3 com2 = body2.GetShape()->GetCenterOfMass();
const JPH::RVec3 p1(jc.anchorA.x * s1.x - com1.GetX(),
jc.anchorA.y * s1.y - com1.GetY(),
jc.anchorA.z * s1.z - com1.GetZ());
const JPH::RVec3 p2(jc.anchorB.x * s2.x - com2.GetX(),
jc.anchorB.y * s2.y - com2.GetY(),
jc.anchorB.z * s2.z - com2.GetZ());
JPH::Ref<JPH::Constraint> constraint;
if (jc.type == ECS::JointComponent::Type::Fixed) {
// 轴框架 = 同一世界姿态在各体局部系的表达 → 锁定创建时相对姿态
JPH::FixedConstraintSettings s;
s.mSpace = JPH::EConstraintSpace::LocalToBodyCOM;
s.mPoint1 = p1;
s.mPoint2 = p2;
s.mAxisX1 = r1.Conjugated() * JPH::Vec3::sAxisX();
s.mAxisY1 = r1.Conjugated() * JPH::Vec3::sAxisY();
s.mAxisX2 = r2.Conjugated() * JPH::Vec3::sAxisX();
s.mAxisY2 = r2.Conjugated() * JPH::Vec3::sAxisY();
constraint = s.Create(body1, body2);
} else if (jc.type == ECS::JointComponent::Type::Hinge) {
// 转轴:本体局部轴 → 世界(不吃缩放,归一化),再投到两体局部系;
// 法线任取与轴垂直的世界方向,保证两体框架共轴共面
JPH::HingeConstraintSettings s;
s.mSpace = JPH::EConstraintSpace::LocalToBodyCOM;
s.mPoint1 = p1;
s.mPoint2 = p2;
const JPH::Vec3 localAxis = JPH::Vec3(
jc.hingeAxis.x, jc.hingeAxis.y, jc.hingeAxis.z).Normalized();
const JPH::Vec3 worldAxis = r1 * localAxis;
JPH::Vec3 helper = std::abs(worldAxis.GetY()) < 0.99f
? JPH::Vec3::sAxisY()
: JPH::Vec3::sAxisX();
const JPH::Vec3 worldNormal = worldAxis.Cross(helper).Normalized();
s.mHingeAxis1 = r1.Conjugated() * worldAxis;
s.mNormalAxis1 = r1.Conjugated() * worldNormal;
s.mHingeAxis2 = r2.Conjugated() * worldAxis;
s.mNormalAxis2 = r2.Conjugated() * worldNormal;
if (jc.enableLimit) {
s.mLimitsMin = std::clamp(glm::radians(jc.limitMin),
-JPH::JPH_PI, 0.0f);
s.mLimitsMax = std::clamp(glm::radians(jc.limitMax),
0.0f, JPH::JPH_PI);
}
constraint = s.Create(body1, body2);
} else {
// Ball = PointConstraint(球窝:三旋转自由度全放开)
JPH::PointConstraintSettings s;
s.mSpace = JPH::EConstraintSpace::LocalToBodyCOM;
s.mPoint1 = p1;
s.mPoint2 = p2;
constraint = s.Create(body1, body2);
}
m_System->AddConstraint(constraint.GetPtr());
ConstraintRecord rec;
rec.constraint = std::move(constraint);
rec.snapshot = jc;
rec.targetBits = targetBits;
m_Constraints[ownerBits] = rec;
}
void PhysicsWorld::RemoveConstraint(const u32 ownerBits) {
const auto found = m_Constraints.find(ownerBits);
if (found == m_Constraints.end()) {
return;
}
m_System->RemoveConstraint(found->second.constraint.GetPtr());
m_Constraints.erase(found); // Ref 析构 → 引用计数释放
}
// ---- 角色控制器 ----
void PhysicsWorld::SyncCharacters() {
auto &reg = m_Scene.GetRegistry();
// 1. 孤儿清理:实体销毁 / 组件被移除或禁用(Ref 析构即清理,无 body 负担)
for (auto it = m_Characters.begin(); it != m_Characters.end();) {
const auto e = static_cast<entt::entity>(it->first);
const auto* cc = reg.valid(e)
? reg.try_get<ECS::CharacterControllerComponent>(e) : nullptr;
if (cc == nullptr || !cc->active ||
!reg.all_of<ECS::TransformComponent>(e)) {
it = m_Characters.erase(it);
} else {
++it;
}
}
// 2. 遍历:缺失 → 创建;参数变更 → 重建;编辑器拖动 → 传送;禁用 → 跳过
auto view = reg.view<ECS::CharacterControllerComponent,
ECS::TransformComponent>();
for (auto [e, cc, tr]: view.each()) {
if (!cc.active) {
continue;
}
const u32 bits = static_cast<u32>(entt::to_integral(e));
CharacterController::Params p;
p.radius = cc.radius;
p.halfHeight = cc.capsuleHalfHeight;
p.mass = cc.mass;
p.maxSlopeAngle = cc.maxSlopeAngle;
p.stepHeight = cc.stepHeight;
auto found = m_Characters.find(bits);
if (found == m_Characters.end()) {
CharacterEntry entry;
entry.cc = std::make_unique<CharacterController>(
*m_System, p, tr.GetPosition());
entry.lastPos = tr.GetPosition();
m_Characters.emplace(bits, std::move(entry));
continue;
}
if (found->second.cc->GetParams() != p) {
// 参数变更 → 重建(位置沿用当前 Transform)
found->second.cc = std::make_unique<CharacterController>(
*m_System, p, tr.GetPosition());
found->second.lastPos = tr.GetPosition();
continue;
}
if (found->second.lastPos != tr.GetPosition()) {
// 编辑器拖动实体 → 传送角色(角色是唯一位置源,反向覆盖)
found->second.cc->Teleport(tr.GetPosition());
found->second.lastPos = tr.GetPosition();
}
}
}
void PhysicsWorld::UpdateCharacters(const f32 dt) {
auto &reg = m_Scene.GetRegistry();
const JPH::Vec3 gravity = m_System->GetGravity();
for (auto &[bits, entry]: m_Characters) {
auto *tr = reg.try_get<ECS::TransformComponent>(
static_cast<entt::entity>(bits));
if (tr == nullptr) {
continue;
}
entry.cc->Update(dt, gravity, *m_TempAllocator);
const glm::vec3 p = entry.cc->GetPosition(); // 原点 = 脚底
tr->SetPosition(p); // 标脏 → UpdateTransforms 重算 world
entry.lastPos = p;
}
} }
// ---- Mesh 碰撞体:视觉网格 → Jolt 形状 ---- // ---- Mesh 碰撞体:视觉网格 → Jolt 形状 ----
@@ -444,16 +751,80 @@ namespace Engine {
return res.Get(); return res.Get();
} }
JPH::RefConst<JPH::Shape> PhysicsWorld::BuildCompoundShape(
ECS::RigidBodyComponent &rb, ECS::TransformComponent &tr) {
if (rb.compound.empty()) {
return nullptr;
}
// 缩放烘焙:child 局部位置按轴缩放,尺寸按 max 轴(球/胶囊径向
// 不可各向异性)。rotation 存数据与序列化(首版 Inspector 不编辑)
const glm::vec3 s = tr.GetScale();
JPH::StaticCompoundShapeSettings cs;
for (const auto &child: rb.compound) {
JPH::RefConst<JPH::Shape> sub;
const f32 us = UniformScale(s);
switch (child.shape) {
case ECS::RigidBodyComponent::CompoundChild::Shape::Sphere:
sub = new JPH::SphereShape(
std::max(child.radius * us, 0.01f));
break;
case ECS::RigidBodyComponent::CompoundChild::Shape::Capsule:
sub = new JPH::CapsuleShape(
std::max(child.radius * us, 0.01f),
std::max(child.capsuleHalfHeight * us, 0.01f));
break;
default: // Box
sub = new JPH::BoxShape(JPH::Vec3(
std::max(child.boxHalfExtents.x * s.x, 0.01f),
std::max(child.boxHalfExtents.y * s.y, 0.01f),
std::max(child.boxHalfExtents.z * s.z, 0.01f)));
break;
}
cs.AddShape(JPH::Vec3(child.position.x * s.x,
child.position.y * s.y,
child.position.z * s.z),
ToJolt(child.rotation), sub);
}
const JPH::Shape::ShapeResult res = cs.Create();
if (res.HasError()) {
ENGINE_WARN("[Physics] CompoundShape build failed: {}",
res.GetError().c_str());
return nullptr;
}
return res.Get();
}
void PhysicsWorld::CreateBody(u32 entityBits, ECS::RigidBodyComponent &rb, void PhysicsWorld::CreateBody(u32 entityBits, ECS::RigidBodyComponent &rb,
ECS::TransformComponent &tr) { ECS::TransformComponent &tr) {
auto &bi = m_System->GetBodyInterface(); auto &bi = m_System->GetBodyInterface();
// 碰撞体:组件显式尺寸 × Transform 缩放(形状按单位尺度烹饪 + // 碰撞体:组件显式尺寸 × Transform 缩放(形状按单位尺度烹饪 +
// ScaledShape 的优化留给 ShapeCache 阶段;MVP 直接带缩放建形状) // ScaledShape 的优化留给 ShapeCache 阶段;MVP 直接带缩放建形状)。
// 注意 Jolt 两 ctor 参数序相反:CapsuleShape(半径, 圆柱段半高)、
// CylinderShape(圆柱段半高, 半径)
JPH::RefConst<JPH::Shape> shape; JPH::RefConst<JPH::Shape> shape;
const f32 uniformScale = UniformScale(tr.GetScale());
if (rb.collider == ECS::RigidBodyComponent::ColliderType::Sphere) { if (rb.collider == ECS::RigidBodyComponent::ColliderType::Sphere) {
const f32 scale = std::max({tr.GetScale().x, tr.GetScale().y, tr.GetScale().z}); shape = new JPH::SphereShape(std::max(rb.sphereRadius * uniformScale, 0.01f));
shape = new JPH::SphereShape(std::max(rb.sphereRadius * scale, 0.01f)); } else if (rb.collider == ECS::RigidBodyComponent::ColliderType::Capsule) {
shape = new JPH::CapsuleShape(
std::max(rb.capsuleRadius * uniformScale, 0.01f),
std::max(rb.capsuleHalfHeight * uniformScale, 0.01f));
} else if (rb.collider == ECS::RigidBodyComponent::ColliderType::Cylinder) {
shape = new JPH::CylinderShape(
std::max(rb.cylinderHalfHeight * uniformScale, 0.01f),
std::max(rb.cylinderRadius * uniformScale, 0.01f));
} else if (rb.collider == ECS::RigidBodyComponent::ColliderType::Compound) {
// 子形状列表 → StaticCompoundShape;空列表/构建失败回退 Box
shape = BuildCompoundShape(rb, tr);
if (shape == nullptr) {
shape = new JPH::BoxShape(JPH::Vec3(
std::max(rb.boxHalfExtents.x * tr.GetScale().x, 0.01f),
std::max(rb.boxHalfExtents.y * tr.GetScale().y, 0.01f),
std::max(rb.boxHalfExtents.z * tr.GetScale().z, 0.01f)));
}
} else if (rb.collider == ECS::RigidBodyComponent::ColliderType::Mesh) { } else if (rb.collider == ECS::RigidBodyComponent::ColliderType::Mesh) {
// Mesh 碰撞体:实体 MeshRenderer 的视觉网格(Unity MeshCollider // Mesh 碰撞体:实体 MeshRenderer 的视觉网格(Unity MeshCollider
// 语义)。缩放烘进顶点(Jolt shape 无缩放);Static → MeshShape // 语义)。缩放烘进顶点(Jolt shape 无缩放);Static → MeshShape
@@ -497,7 +868,7 @@ namespace Engine {
settings.mFriction = rb.friction; settings.mFriction = rb.friction;
settings.mRestitution = rb.restitution; settings.mRestitution = rb.restitution;
settings.mUserData = entityBits; // 事件回调:Body → 实体位模式反查 settings.mUserData = entityBits; // 事件回调:Body → 实体位模式反查
settings.mIsSensor = rb.isSensor; // 传感器:只收 Added/Removed,不解算 settings.mIsSensor = rb.isTrigger; // 触发器:只收 Added/Removed,不解算
const JPH::BodyID id = bi.CreateAndAddBody( const JPH::BodyID id = bi.CreateAndAddBody(
settings, dynamic ? JPH::EActivation::Activate : JPH::EActivation::DontActivate); settings, dynamic ? JPH::EActivation::Activate : JPH::EActivation::DontActivate);
@@ -507,9 +878,10 @@ namespace Engine {
rec.type = static_cast<u8>(rb.type); rec.type = static_cast<u8>(rb.type);
rec.collider = static_cast<u8>(rb.collider); rec.collider = static_cast<u8>(rb.collider);
rec.colliderSize = rb.boxHalfExtents; rec.colliderSize = rb.boxHalfExtents;
rec.radius = rb.sphereRadius; ExtractShapeParams(rb, rec.radius, rec.halfHeight);
rec.compound = rb.compound;
rec.mass = rb.mass; rec.mass = rb.mass;
rec.isSensor = rb.isSensor; rec.isTrigger = rb.isTrigger;
rec.lastPos = tr.GetPosition(); rec.lastPos = tr.GetPosition();
rec.lastRot = tr.GetRotationQuat(); rec.lastRot = tr.GetRotationQuat();
m_Bodies[entityBits] = rec; m_Bodies[entityBits] = rec;
@@ -578,9 +950,97 @@ namespace Engine {
return true; return true;
} }
// ---- 刚体驱动(游戏逻辑/编辑器调试;见头文件语义注释)----
const PhysicsWorld::BodyRecord *PhysicsWorld::FindRecord(const u32 entityBits) const {
const auto found = m_Bodies.find(entityBits);
return found != m_Bodies.end() ? &found->second : nullptr;
}
// 力/扭矩/冲量:仅动态体(Jolt 对静态/运动学体施加力无意义)
void PhysicsWorld::AddForce(const u32 entityBits, const glm::vec3 &force) {
const auto *rec = FindRecord(entityBits);
if (!rec || rec->type != static_cast<u8>(ECS::RigidBodyComponent::BodyType::Dynamic))
return;
m_System->GetBodyInterface().AddForce(
rec->id, JPH::Vec3(force.x, force.y, force.z));
}
void PhysicsWorld::AddImpulse(const u32 entityBits, const glm::vec3 &impulse) {
const auto *rec = FindRecord(entityBits);
if (!rec || rec->type != static_cast<u8>(ECS::RigidBodyComponent::BodyType::Dynamic))
return;
m_System->GetBodyInterface().AddImpulse(
rec->id, JPH::Vec3(impulse.x, impulse.y, impulse.z));
}
void PhysicsWorld::AddTorque(const u32 entityBits, const glm::vec3 &torque) {
const auto *rec = FindRecord(entityBits);
if (!rec || rec->type != static_cast<u8>(ECS::RigidBodyComponent::BodyType::Dynamic))
return;
m_System->GetBodyInterface().AddTorque(
rec->id, JPH::Vec3(torque.x, torque.y, torque.z));
}
// 速度直写:动态+运动学体都有效(运动学体速度即其运动方式)
void PhysicsWorld::SetLinearVelocity(const u32 entityBits, const glm::vec3 &velocity) {
const auto *rec = FindRecord(entityBits);
if (!rec || rec->type == static_cast<u8>(ECS::RigidBodyComponent::BodyType::Static))
return;
m_System->GetBodyInterface().SetLinearVelocity(
rec->id, JPH::Vec3(velocity.x, velocity.y, velocity.z));
}
void PhysicsWorld::SetAngularVelocity(const u32 entityBits, const glm::vec3 &angularVelocity) {
const auto *rec = FindRecord(entityBits);
if (!rec || rec->type == static_cast<u8>(ECS::RigidBodyComponent::BodyType::Static))
return;
m_System->GetBodyInterface().SetAngularVelocity(
rec->id, JPH::Vec3(angularVelocity.x, angularVelocity.y, angularVelocity.z));
}
glm::vec3 PhysicsWorld::GetLinearVelocity(const u32 entityBits) const {
const auto *rec = FindRecord(entityBits);
if (!rec || rec->type == static_cast<u8>(ECS::RigidBodyComponent::BodyType::Static))
return {0.0f, 0.0f, 0.0f};
const JPH::Vec3 v = m_System->GetBodyInterface().GetLinearVelocity(rec->id);
return {v.GetX(), v.GetY(), v.GetZ()};
}
// ---- 角色控制器门面 ----
CharacterController *PhysicsWorld::FindCharacter(const u32 entityBits) {
const auto found = m_Characters.find(entityBits);
return found != m_Characters.end() ? found->second.cc.get() : nullptr;
}
void PhysicsWorld::SetCharacterVelocity(const u32 entityBits,
const glm::vec3 &velocity) {
if (CharacterController *cc = FindCharacter(entityBits)) {
cc->SetLinearVelocity(velocity);
}
}
void PhysicsWorld::Jump(const u32 entityBits, const f32 speed) {
if (CharacterController *cc = FindCharacter(entityBits)) {
cc->Jump(speed);
}
}
bool PhysicsWorld::IsCharacterGrounded(const u32 entityBits) const {
const auto found = m_Characters.find(entityBits);
return found != m_Characters.end() && found->second.cc->IsGrounded();
}
glm::vec3 PhysicsWorld::GetCharacterVelocity(const u32 entityBits) const {
const auto found = m_Characters.find(entityBits);
return found != m_Characters.end() ? found->second.cc->GetLinearVelocity()
: glm::vec3{0.0f};
}
// ---- 写回:动态体姿态 → Transform(标脏,帧循环统一 Compose)---- // ---- 写回:动态体姿态 → Transform(标脏,帧循环统一 Compose)----
void PhysicsWorld::Writeback() const { void PhysicsWorld::Writeback() {
auto &reg = m_Scene.GetRegistry(); auto &reg = m_Scene.GetRegistry();
const auto &bi = m_System->GetBodyInterface(); const auto &bi = m_System->GetBodyInterface();
@@ -598,6 +1058,14 @@ namespace Engine {
const JPH::Vec3 vel = bi.GetLinearVelocity(id); const JPH::Vec3 vel = bi.GetLinearVelocity(id);
rb.velocity = glm::vec3(vel.GetX(), vel.GetY(), vel.GetZ()); rb.velocity = glm::vec3(vel.GetX(), vel.GetY(), vel.GetZ());
// lastPos/lastRot 同步到 body 姿态:否则下帧 SyncScene 把物理
// 位移误判为编辑器拖动 → teleport 回旧位置死循环
const u32 bits = static_cast<u32>(entt::to_integral(e));
if (auto it = m_Bodies.find(bits); it != m_Bodies.end()) {
it->second.lastPos = tr.GetPosition();
it->second.lastRot = tr.GetRotationQuat();
}
} }
} }
} // namespace Engine } // namespace Engine
+103 -19
View File
@@ -8,16 +8,38 @@
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
#include <cmath>
#include "GLFW/glfw3.h" #include "GLFW/glfw3.h"
namespace Engine { namespace Engine {
void Camera::Update(Window& window, ECS::TransformComponent& transform, const f32 deltaTime) const { namespace {
// 鼠标视角(编辑器习惯):按住右键捕获光标进入自由视角, // 欧拉(度)→ 旋转矩阵:应用序 Y→X,与 TransformComponent::
// 松开释放光标(ImGui 面板可正常交互)。m_MouseLookEnabled 总开关 // Compose 一致(roll 不参与 FPS/编辑器相机)
glm::mat3 EulerRotMat(const glm::vec3& rotDeg)
{
return static_cast<glm::mat3>(
glm::rotate(glm::mat4(1.0f), glm::radians(rotDeg.y),
glm::vec3(0, 1, 0)) *
glm::rotate(glm::mat4(1.0f), glm::radians(rotDeg.x),
glm::vec3(1, 0, 0)));
}
}
void Camera::Update(Window& window, ECS::TransformComponent& transform,
const f32 deltaTime, const bool flyRequiresRMB)
{
// ---- 输入模式(UE Scene view 语义):RMB=自由视角(鼠标转向
// + 飞行),MMB=焦点环绕(Shift+MMB=平移),滚轮=推拉。三种
// 光标型交互互斥共用捕获;编辑器模式 flyRequiresRMB=true 时
// 移动键仅 RMB 按住生效(否则与 Gizmo W/E/R 热键冲突)----
const bool wantLook = m_MouseLookEnabled && const bool wantLook = m_MouseLookEnabled &&
window.IsMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT); window.IsMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT);
if (wantLook) const bool wantOrbit = m_MouseLookEnabled &&
window.IsMouseButtonPressed(GLFW_MOUSE_BUTTON_MIDDLE);
if (wantLook || wantOrbit)
{ {
if (!window.IsCursorCaptured()) if (!window.IsCursorCaptured())
{ {
@@ -37,6 +59,42 @@ namespace Engine {
rot.x = glm::clamp( rot.x = glm::clamp(
rot.x - mouseDY * m_MouseSensitivity, -89.0f, 89.0f); rot.x - mouseDY * m_MouseSensitivity, -89.0f, 89.0f);
transform.SetRotation(rot); transform.SetRotation(rot);
if (wantOrbit)
{
if (window.IsKeyPressed(GLFW_KEY_LEFT_SHIFT))
{
// ---- 屏幕平面平移:内容跟手(拖右 → 相机左移、
// 拖下 → 相机上移),pivot 同步平移保持环绕点的
// 屏幕相对位置 ----
const glm::mat3 r = EulerRotMat(rot);
const glm::vec3 delta =
(-r * glm::vec3(1, 0, 0)) * mouseDX +
(r * glm::vec3(0, 1, 0)) * mouseDY;
const f32 k = m_OrbitDistance * 0.002f; // m/px
transform.SetPosition(transform.GetPosition() + delta * k);
if (m_HasPivot) m_OrbitPivot += delta * k;
}
else
{
// ---- 焦点环绕:转向后保持到 pivot 的距离。首次
// 环绕无 pivot:取当前视线上 m_OrbitDistance 处
if (!m_HasPivot)
{
m_OrbitPivot = transform.GetPosition() +
EulerRotMat(transform.GetRotation()) *
glm::vec3(0, 0, -1) *
m_OrbitDistance;
m_HasPivot = true;
}
m_OrbitDistance = glm::length(
transform.GetPosition() - m_OrbitPivot);
const glm::vec3 orbitFwd =
EulerRotMat(rot) * glm::vec3(0, 0, -1);
transform.SetPosition(m_OrbitPivot -
orbitFwd * m_OrbitDistance);
}
}
} }
} }
else if (window.IsCursorCaptured()) else if (window.IsCursorCaptured())
@@ -44,24 +102,50 @@ namespace Engine {
window.SetCursorCaptured(false); window.SetCursorCaptured(false);
} }
// 基向量由实体旋转决定(本地 -Z 为视线方向;应用顺序 Y→X, // 基向量由(可能已更新的)实体旋转决定(本地 -Z 为视线方向)
// 与 TransformComponent::Compose 一致;roll 不参与 FPS 相机) const glm::mat3 r = EulerRotMat(transform.GetRotation());
const glm::vec3 rot = transform.GetRotation(); // 派生欧拉(度)
const glm::mat3 r = static_cast<glm::mat3>(
glm::rotate(glm::mat4(1.0f), glm::radians(rot.y),
glm::vec3(0, 1, 0)) *
glm::rotate(glm::mat4(1.0f), glm::radians(rot.x),
glm::vec3(1, 0, 0)));
const glm::vec3 forward = r * glm::vec3(0, 0, -1); const glm::vec3 forward = r * glm::vec3(0, 0, -1);
const glm::vec3 right = r * glm::vec3(1, 0, 0); const glm::vec3 right = r * glm::vec3(1, 0, 0);
const f32 speed = m_MoveSpeed * deltaTime; // ---- 飞行移动:编辑器模式仅 RMB(wantLook)时生效;游戏模式
if (window.IsKeyPressed(GLFW_KEY_W)) { transform.SetPosition(transform.GetPosition() + forward * speed); } // 常开(原行为)----
if (window.IsKeyPressed(GLFW_KEY_S)) { transform.SetPosition(transform.GetPosition() - forward * speed); } if (wantLook || !flyRequiresRMB)
if (window.IsKeyPressed(GLFW_KEY_A)) { transform.SetPosition(transform.GetPosition() - right * speed); } {
if (window.IsKeyPressed(GLFW_KEY_D)) { transform.SetPosition(transform.GetPosition() + right * speed); } const f32 speed = m_MoveSpeed * deltaTime;
if (window.IsKeyPressed(GLFW_KEY_SPACE)) { transform.SetPosition(transform.GetPosition() + glm::vec3(0, speed, 0)); } if (window.IsKeyPressed(GLFW_KEY_W)) { transform.SetPosition(transform.GetPosition() + forward * speed); }
if (window.IsKeyPressed(GLFW_KEY_LEFT_SHIFT)) { transform.SetPosition(transform.GetPosition() - glm::vec3(0, speed, 0)); } if (window.IsKeyPressed(GLFW_KEY_S)) { transform.SetPosition(transform.GetPosition() - forward * speed); }
if (window.IsKeyPressed(GLFW_KEY_A)) { transform.SetPosition(transform.GetPosition() - right * speed); }
if (window.IsKeyPressed(GLFW_KEY_D)) { transform.SetPosition(transform.GetPosition() + right * speed); }
if (window.IsKeyPressed(GLFW_KEY_SPACE)) { transform.SetPosition(transform.GetPosition() + glm::vec3(0, speed, 0)); }
if (window.IsKeyPressed(GLFW_KEY_LEFT_SHIFT)) { transform.SetPosition(transform.GetPosition() - glm::vec3(0, speed, 0)); }
}
// ---- 滚轮推拉:有 pivot 时改环绕距离(指数步进,近处精细/
// 远处大步);无 pivot 时沿视线平移 ----
const f32 wheel = window.ConsumeScrollDelta();
if (m_MouseLookEnabled && wheel != 0.0f)
{
if (m_HasPivot)
{
m_OrbitDistance = glm::clamp(
m_OrbitDistance * std::pow(0.9f, wheel), 0.5f, 500.0f);
transform.SetPosition(m_OrbitPivot - forward * m_OrbitDistance);
}
else
{
transform.SetPosition(transform.GetPosition() +
forward * wheel * m_MoveSpeed * deltaTime);
}
}
}
void Camera::FocusOn(ECS::TransformComponent& transform, const glm::vec3& point)
{
const glm::vec3 fwd =
EulerRotMat(transform.GetRotation()) * glm::vec3(0, 0, -1);
m_OrbitPivot = point;
m_HasPivot = true;
transform.SetPosition(point - fwd * m_OrbitDistance);
} }
glm::mat4 Camera::GetView(const ECS::TransformComponent& t) const glm::mat4 Camera::GetView(const ECS::TransformComponent& t) const
+18 -8
View File
@@ -25,7 +25,8 @@ namespace Engine
Mesh::Mesh(Device& device, MemoryAllocator& allocator, Mesh::Mesh(Device& device, MemoryAllocator& allocator,
const std::vector<Vertex>& vertices, const std::vector<Vertex>& vertices,
const std::vector<std::vector<u32>>& lods, const std::vector<std::vector<u32>>& lods,
Material* material) Material* material,
const std::vector<glm::vec3>* cpuPositions)
: m_Material(material) : m_Material(material)
, m_Slot(s_nextMeshSlot.fetch_add(1, std::memory_order_relaxed)) , m_Slot(s_nextMeshSlot.fetch_add(1, std::memory_order_relaxed))
{ {
@@ -35,11 +36,19 @@ namespace Engine
return; return;
} }
// 位置取数:蒙皮 mesh 用绑定姿态渲染空间副本(与视觉同源),
// 否则用原始顶点位置
const bool useBind = cpuPositions != nullptr &&
cpuPositions->size() == vertices.size();
auto posAt = [&](size_t i) -> const glm::vec3& {
return useBind ? (*cpuPositions)[i] : vertices[i].position;
};
// CPU 位置副本:物理 MeshShape/ConvexHull 数据源(GPU 上传后 // CPU 位置副本:物理 MeshShape/ConvexHull 数据源(GPU 上传后
// 完整 Vertex 不再保留,见 Mesh.h) // 完整 Vertex 不再保留,见 Mesh.h)
m_CpuPositions.reserve(vertices.size()); m_CpuPositions.reserve(vertices.size());
for (const auto& v : vertices) { for (size_t i = 0; i < vertices.size(); ++i) {
m_CpuPositions.push_back(v.position); m_CpuPositions.push_back(posAt(i));
} }
m_CpuIndices = lods[0]; m_CpuIndices = lods[0];
@@ -78,18 +87,19 @@ namespace Engine
} }
// 局部空间包围球:顶点包围盒 → 中心 + 最大半径(视锥剔除用) // 局部空间包围球:顶点包围盒 → 中心 + 最大半径(视锥剔除用)
// 蒙皮 mesh 用绑定姿态副本(原始空间包围球大一个数量级 = 剔除/拾取失真)
{ {
glm::vec3 mn(std::numeric_limits<f32>::max()); glm::vec3 mn(std::numeric_limits<f32>::max());
glm::vec3 mx(std::numeric_limits<f32>::lowest()); glm::vec3 mx(std::numeric_limits<f32>::lowest());
for (const auto& v : vertices) { for (size_t i = 0; i < vertices.size(); ++i) {
mn = glm::min(mn, v.position); mn = glm::min(mn, posAt(i));
mx = glm::max(mx, v.position); mx = glm::max(mx, posAt(i));
} }
m_Bounds.center = (mn + mx) * 0.5f; m_Bounds.center = (mn + mx) * 0.5f;
m_Bounds.radius = 0.0f; m_Bounds.radius = 0.0f;
for (const auto& v : vertices) { for (size_t i = 0; i < vertices.size(); ++i) {
m_Bounds.radius = glm::max(m_Bounds.radius, m_Bounds.radius = glm::max(m_Bounds.radius,
glm::length(v.position - m_Bounds.center)); glm::length(posAt(i) - m_Bounds.center));
} }
} }
+41 -2
View File
@@ -238,6 +238,42 @@ ParsedModel Model::ParseFromDisk(const std::string& path)
bone.name); bone.name);
} }
} }
// 蒙皮 mesh 的 CPU 位置副本换算到渲染空间:渲染 = palette(t)×raw
//(palette = global(bone)×invBind,与 shader 同式),物理/包围球取
// 绑定姿态。FBX 的单位换算折在骨骼链上(bind palette ≠ I),直接
// 用 raw 会让物理凸包大一个数量级(M1911 实测 10×)
if (!out.bones.empty()) {
std::vector<glm::mat4> globals(out.nodes.size());
for (size_t i = 0; i < out.nodes.size(); ++i) {
globals[i] = out.nodes[i].parent < 0
? out.nodes[i].localTransform
: globals[out.nodes[i].parent] * out.nodes[i].localTransform;
}
std::vector<glm::mat4> bindPalette(out.bones.size());
for (size_t b = 0; b < out.bones.size(); ++b) {
bindPalette[b] = globals[out.bones[b].nodeIdx]
* out.bones[b].invBind;
}
for (auto& pm : out.meshes) {
if (!pm.skinned) continue;
pm.cpuPositions.resize(pm.vertices.size());
for (size_t v = 0; v < pm.vertices.size(); ++v) {
const Vertex& vert = pm.vertices[v];
glm::vec3 p(0.0f);
f32 wsum = 0.0f;
for (int k = 0; k < 4; ++k) {
const f32 w = vert.weights[k];
if (w <= 0.0f) continue;
p += w * glm::vec3(bindPalette[vert.boneIds[k]]
* glm::vec4(vert.position, 1.0f));
wsum += w;
}
// 零权重顶点(shader 侧塌到原点)物理侧保留原始位置兜底
pm.cpuPositions[v] = wsum > 1e-6f ? p : vert.position;
}
}
}
} }
ENGINE_INFO("Model parsed: {} ({} meshes, {} bones, {} anims)", ENGINE_INFO("Model parsed: {} ({} meshes, {} bones, {} anims)",
@@ -383,7 +419,8 @@ Scope<Model> Model::FromParsed(Device& device, MemoryAllocator& allocator,
for (auto& pm : parsed.meshes) { for (auto& pm : parsed.meshes) {
Material* mat = model->GetOrCreateMaterial(pm.texPaths.data(), pm.params); Material* mat = model->GetOrCreateMaterial(pm.texPaths.data(), pm.params);
model->m_Meshes.push_back(CreateScope<Mesh>( model->m_Meshes.push_back(CreateScope<Mesh>(
device, allocator, pm.vertices, pm.lods, mat)); device, allocator, pm.vertices, pm.lods, mat,
pm.cpuPositions.empty() ? nullptr : &pm.cpuPositions));
} }
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)", ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
parsed.path, model->m_Meshes.size(), model->m_TextureCache.size()); parsed.path, model->m_Meshes.size(), model->m_TextureCache.size());
@@ -406,7 +443,8 @@ void Model::LoadModel(const std::string& path)
for (auto& pm : parsed.meshes) { for (auto& pm : parsed.meshes) {
Material* mat = GetOrCreateMaterial(pm.texPaths.data(), pm.params); Material* mat = GetOrCreateMaterial(pm.texPaths.data(), pm.params);
m_Meshes.push_back(CreateScope<Mesh>( m_Meshes.push_back(CreateScope<Mesh>(
m_Device, m_Allocator, pm.vertices, pm.lods, mat)); m_Device, m_Allocator, pm.vertices, pm.lods, mat,
pm.cpuPositions.empty() ? nullptr : &pm.cpuPositions));
} }
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)", ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
path, m_Meshes.size(), m_TextureCache.size()); path, m_Meshes.size(), m_TextureCache.size());
@@ -577,6 +615,7 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
// 骨骼权重灌顶点(无骨骼 mesh 顶点保持默认 idx0/w1 → identity 直通) // 骨骼权重灌顶点(无骨骼 mesh 顶点保持默认 idx0/w1 → identity 直通)
CollectMeshBones(mesh, vertices, bones, boneSlots); CollectMeshBones(mesh, vertices, bones, boneSlots);
out.skinned = hasBones;
for (u32 i = 0; i < mesh->mNumFaces; ++i) { for (u32 i = 0; i < mesh->mNumFaces; ++i) {
aiFace face = mesh->mFaces[i]; aiFace face = mesh->mFaces[i];
+6
View File
@@ -8,6 +8,7 @@
#include <Engine/Renderer/Renderer.h> #include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/SceneRenderer.h> #include <Engine/Renderer/SceneRenderer.h>
#include <Engine/Renderer/EditorCamera.h>
#include <Engine/Renderer/VulkanAssetUploader.h> #include <Engine/Renderer/VulkanAssetUploader.h>
#include <Engine/Renderer/Passes/SceneGpu.h> #include <Engine/Renderer/Passes/SceneGpu.h>
#include <Engine/Renderer/Passes/FullscreenGpu.h> #include <Engine/Renderer/Passes/FullscreenGpu.h>
@@ -233,6 +234,11 @@ namespace Engine
m_SceneRenderer->SetDisplayMode(mode); m_SceneRenderer->SetDisplayMode(mode);
} }
EditorCamera& Renderer::GetEditorCamera()
{
return m_SceneRenderer->GetEditorCamera();
}
const FrameSceneData &Renderer::GetFrameSceneData() const const FrameSceneData &Renderer::GetFrameSceneData() const
{ {
return m_SceneRenderer->Frame(); return m_SceneRenderer->Frame();
+19 -8
View File
@@ -6,6 +6,7 @@
#include <Engine/ECS/Scene.h> #include <Engine/ECS/Scene.h>
#include <Engine/ECS/Components.h> #include <Engine/ECS/Components.h>
#include <Engine/Renderer/Camera.h> #include <Engine/Renderer/Camera.h>
#include <Engine/Renderer/EditorCamera.h>
#include <Engine/Renderer/Frustum.h> #include <Engine/Renderer/Frustum.h>
#include <Engine/Renderer/Model.h> #include <Engine/Renderer/Model.h>
#include <Engine/Renderer/Mesh.h> #include <Engine/Renderer/Mesh.h>
@@ -258,14 +259,21 @@ namespace Engine
ubo.prevView = m_LastView; ubo.prevView = m_LastView;
ubo.prevProj = m_LastProjNoJitter; ubo.prevProj = m_LastProjNoJitter;
// ---- 1. 相机(位姿单一事实源 = 相机实体的 Transform)---- // ---- 1. 相机:渲染源按显示模式分派(UE 语义)。编辑器形态
TransformComponent* camTransform = nullptr; // Scene view 恒用编辑器相机(视口 UI 状态,空场景可用);
auto* cameraComp = scene.GetPrimaryCamera(&camTransform); // 游戏形态用场景 primary 相机实体(游戏逻辑所有)----
if (!cameraComp || !camTransform) { ECS::TransformComponent* camTransform = nullptr;
// 无相机:本帧跳过(正常场景必有相机,安全网) const Camera* camera = nullptr;
if (m_DisplayMode == kDisplayViewport) {
camTransform = &m_EditorCamera.GetTransform();
camera = &m_EditorCamera.GetController();
} else if (auto* cameraComp = scene.GetPrimaryCamera(&camTransform)) {
camera = &cameraComp->camera;
}
if (!camera || !camTransform) {
// 游戏形态无 primary 相机:本帧跳过(安全网)
return false; // 拾取请求未消费,留待下一帧重试 return false; // 拾取请求未消费,留待下一帧重试
} }
const Camera& camera = cameraComp->camera;
// 拾取请求落帧(一次性消费:本帧 HitProxy 渲染 + 像素拷贝)。 // 拾取请求落帧(一次性消费:本帧 HitProxy 渲染 + 像素拷贝)。
// 放在相机校验之后:跳帧时请求保留,不丢点击 // 放在相机校验之后:跳帧时请求保留,不丢点击
@@ -276,8 +284,8 @@ namespace Engine
const f32 aspect = static_cast<f32>(targetExtent.width) / const f32 aspect = static_cast<f32>(targetExtent.width) /
static_cast<f32>(targetExtent.height); static_cast<f32>(targetExtent.height);
ubo.view = camera.GetView(*camTransform); ubo.view = camera->GetView(*camTransform);
ubo.proj = camera.GetProjection(aspect, 0.1f, 100.0f); ubo.proj = camera->GetProjection(aspect, 0.1f, 100.0f);
// 未抖动投影:velocity 的当前帧 clip 位置用它(抖动不能进 velocity) // 未抖动投影:velocity 的当前帧 clip 位置用它(抖动不能进 velocity)
ubo.projNoJitter = ubo.proj; ubo.projNoJitter = ubo.proj;
@@ -320,6 +328,7 @@ namespace Engine
u32 collectedCount = 0; u32 collectedCount = 0;
scene.Each<TransformComponent, LightComponent>( scene.Each<TransformComponent, LightComponent>(
[&](Entity, TransformComponent& t, LightComponent& light) { [&](Entity, TransformComponent& t, LightComponent& light) {
if (!light.active) return; // 组件禁用 = 不参与光照
if (collectedCount >= kMaxLights) return; if (collectedCount >= kMaxLights) return;
auto& dst = collected[collectedCount++]; auto& dst = collected[collectedCount++];
@@ -384,6 +393,7 @@ namespace Engine
m_EntitySkinBase.clear(); m_EntitySkinBase.clear();
scene.Each<MeshRendererComponent, AnimationComponent>( scene.Each<MeshRendererComponent, AnimationComponent>(
[&](Entity entity, MeshRendererComponent& mr, AnimationComponent& anim) { [&](Entity entity, MeshRendererComponent& mr, AnimationComponent& anim) {
if (!anim.active) 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;
const auto& clips = model->GetClips(); const auto& clips = model->GetClips();
@@ -505,6 +515,7 @@ namespace Engine
scene.Each<TransformComponent, MeshRendererComponent>( scene.Each<TransformComponent, MeshRendererComponent>(
[&](Entity entity, TransformComponent& t, MeshRendererComponent& mr) { [&](Entity entity, TransformComponent& t, MeshRendererComponent& mr) {
if (!mr.active) return; // 组件禁用 = 不渲染不拾取
Model* model = assets.GetModel(mr.model); Model* model = assets.GetModel(mr.model);
if (!model) return; if (!model) return;
+86
View File
@@ -0,0 +1,86 @@
//
// ScriptRuntime.cpp:脚本对账与生命周期驱动。
//
#include <Engine/Scripting/ScriptRuntime.h>
#include <Engine/Core/Log.h>
#include <Engine/ECS/Components.h>
namespace Engine {
namespace Scripting {
// 宿主门面访问器:Application 完整类型只在脚本 cpp 里需要,
// 这里仅回引成员(避免引擎头循环依赖)
Application& ScriptBehavior::App() const {
return *m_App;
}
void ScriptRuntime::Update(f32 dt) {
Sync();
auto& reg = m_Scene.GetRegistry();
for (auto& [bits, inst] : m_Instances) {
const auto e = static_cast<entt::entity>(bits);
const ECS::ScriptComponent* sc = reg.valid(e)
? reg.try_get<ECS::ScriptComponent>(e) : nullptr;
if (!sc) {
continue; // 延迟销毁队列里,下帧 Sync 清理
}
if (!sc->active) {
continue; // 禁用 = 保留实例停 tick(Unity disable 语义)
}
if (!inst.created) {
inst.behavior->OnCreate();
inst.created = true;
}
inst.behavior->OnUpdate(dt);
}
}
void ScriptRuntime::Sync() {
auto& reg = m_Scene.GetRegistry();
// 1. 孤儿/改名清理:实体或组件没了、脚本名变了 → OnDestroy + 删除
for (auto it = m_Instances.begin(); it != m_Instances.end();) {
const auto e = static_cast<entt::entity>(it->first);
const ECS::ScriptComponent* sc =
reg.valid(e) ? reg.try_get<ECS::ScriptComponent>(e) : nullptr;
const bool stale = !sc || !it->second.behavior ||
it->second.behavior->Name() != sc->scriptName;
if (stale) {
if (it->second.behavior) {
it->second.behavior->OnDestroy();
}
it = m_Instances.erase(it);
} else {
++it;
}
}
// 2. 缺失实例化:按名查注册表(未知名警告——场景含未注册脚本时
// 弱前向兼容,跳过不影响其余实体)
const auto view = reg.view<ECS::ScriptComponent>();
for (auto [e, sc] : view.each()) {
const u32 bits = static_cast<u32>(entt::to_integral(e));
if (m_Instances.contains(bits) || sc.scriptName.empty()) {
continue;
}
auto behavior = ScriptRegistry::Create(sc.scriptName);
if (!behavior) {
ENGINE_WARN("Script '{}' not registered (entity {})",
sc.scriptName, bits);
continue;
}
behavior->Bind(m_Scene, e, m_App);
ScriptInstance inst;
inst.behavior = std::move(behavior);
inst.behavior->OnCreate(); // Unity Awake 语义:实例化即回调
inst.created = true;
m_Instances.emplace(bits, std::move(inst));
}
}
} // namespace Scripting
} // namespace Engine
@@ -0,0 +1,675 @@
//
// Created by atdunbg on 2026/10/3.
//
// SceneSerializer:场景 ↔ YAML。结构见头文件注释。
//
// 写端:手动 Emitter 逐键输出(短集合用 Flow 单行数组/内联 map,
// 浮点 9 位精度保证 float32 无损往返)。
// 读端:yaml-cpp 新版对"僵尸节点"(缺失键的返回值)硬化——operator=、
// size()、Type()、链式 operator[] 一律抛 InvalidNode,只有 IsDefined/
// as<T>(fallback)/begin() 安全。因此读侧铁律:每层节点先
// `const Node n = parent["Key"]` 拷贝初始化 + `if (n)` 判空再访问,
// 绝不对可能缺失的键做赋值或链式下标。
//
// 原子性:Deserialize 先解析到中间结构(逐实体容错),全部成功才
// scene.Clear() 换血重建——解析失败不动现场。
//
#include <Engine/Serialization/SceneSerializer.h>
#include <Engine/ECS/Scene.h>
#include <Engine/ECS/EntityHandle.h>
#include <Engine/ECS/Components.h>
#include <Engine/Assets/AssetManager.h>
#include <Engine/Core/Log.h>
#include <Engine/Core/UUID.h>
#include <yaml-cpp/yaml.h>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <string>
#include <string_view>
#include <unordered_set>
#include <vector>
namespace Engine::Serialization {
namespace {
// ---- 写端辅助:Flow 单行数组(Position: [0, 1.2, -6])----
void EmitVec3(YAML::Emitter& e, const glm::vec3& v)
{
e << YAML::Flow << YAML::BeginSeq << v.x << v.y << v.z
<< YAML::EndSeq;
}
void EmitVec4(YAML::Emitter& e, const glm::vec4& v)
{
e << YAML::Flow << YAML::BeginSeq << v.x << v.y << v.z << v.w
<< YAML::EndSeq;
}
void EmitQuat(YAML::Emitter& e, const glm::quat& q)
{
e << YAML::Flow << YAML::BeginSeq << q.w << q.x << q.y << q.z
<< YAML::EndSeq;
}
// ---- 读端辅助:缺字段/类型错/僵尸节点 → 默认值 ----
glm::vec3 ReadVec3(const YAML::Node& n, const glm::vec3& def)
{
if (!n || !n.IsSequence() || n.size() < 3) return def;
return {n[0].as<f32>(def.x), n[1].as<f32>(def.y),
n[2].as<f32>(def.z)};
}
glm::vec4 ReadVec4(const YAML::Node& n, const glm::vec4& def)
{
if (!n || !n.IsSequence() || n.size() < 4) return def;
return {n[0].as<f32>(def.x), n[1].as<f32>(def.y),
n[2].as<f32>(def.z), n[3].as<f32>(def.w)};
}
// [w,x,y,z] → 归一化 quat;非法/缺失回退单位四元数(防手改档出 NaN)
glm::quat ReadQuat(const YAML::Node& n)
{
if (!n || !n.IsSequence() || n.size() < 4) return {};
const f32 w = n[0].as<f32>(1.0f), x = n[1].as<f32>(0.0f);
const f32 y = n[2].as<f32>(0.0f), z = n[3].as<f32>(0.0f);
const f32 len = std::sqrt(x * x + y * y + z * z + w * w);
return len > 1e-8f ? glm::quat(w, x, y, z) / len : glm::quat{};
}
// "a,b,c[,d]" → N 个 int(procedural 伪路径参数解析)
template <size_t N>
bool ParseCsvInts(const std::string& s, int (&out)[N])
{
size_t pos = 0;
for (size_t i = 0; i < N; ++i) {
const size_t next =
(i + 1 < N) ? s.find(',', pos) : std::string::npos;
if (i + 1 < N && next == std::string::npos) return false;
try {
out[i] = std::stoi(s.substr(pos, next - pos));
}
catch (const std::exception&) { return false; }
pos = (next == std::string::npos) ? s.size() : next + 1;
}
return true;
}
// procedural:// 伪路径 → 参数反解重建(确定性键必然命中缓存;
// 绝不对伪路径调 LoadModel——那会走 assimp 必炸)
ModelRef ResolveProcedural(Assets::AssetManager& assets,
const std::string& path)
{
constexpr std::string_view kPlane = "procedural://plane/";
constexpr std::string_view kTPlane =
"procedural://transparent-plane/";
if (path.rfind(kTPlane, 0) == 0) {
// "{halfSize}/{r},{g},{b},{a}"
const std::string rest = path.substr(kTPlane.size());
const auto slash = rest.find('/');
if (slash == std::string::npos) return {};
int c[4];
if (!ParseCsvInts(rest.substr(slash + 1), c)) return {};
try {
return assets.CreateTransparentPlane(
std::stof(rest.substr(0, slash)),
static_cast<u8>(c[0]), static_cast<u8>(c[1]),
static_cast<u8>(c[2]), static_cast<u8>(c[3]));
}
catch (const std::exception&) { return {}; }
}
if (path.rfind(kPlane, 0) == 0) {
// "{halfSize}/{r},{g},{b}"
const std::string rest = path.substr(kPlane.size());
const auto slash = rest.find('/');
if (slash == std::string::npos) return {};
int c[3];
if (!ParseCsvInts(rest.substr(slash + 1), c)) return {};
try {
return assets.CreatePlane(
std::stof(rest.substr(0, slash)),
static_cast<u8>(c[0]), static_cast<u8>(c[1]),
static_cast<u8>(c[2]));
}
catch (const std::exception&) { return {}; }
}
return {}; // 未知伪路径
}
// 模型引用解析降级链:procedural 反解 → GUID 主键 → Path 兜底。
// 加载成功后 GetOrCreateGUID 回写注册表(注册表丢档自愈)
ModelRef ResolveModel(Assets::AssetManager& assets, u64 guid,
const std::string& path)
{
if (ModelRef ref = ResolveProcedural(assets, path); ref.Valid()) {
return ref;
}
std::string resolved;
if (guid != 0) resolved = assets.ResolveGUID(guid);
if (resolved.empty()) resolved = path;
if (resolved.empty()) return {};
ModelRef ref = assets.LoadModel(resolved);
if (ref.Valid()) assets.GetOrCreateGUID(resolved);
return ref;
}
// ---- 中间结构:解析全部成功才碰 registry ----
// comps 持 Components 节点(仅在键存在且为 map 时赋值——
// yaml-cpp 新版对僵尸节点做 operator= 会抛异常)
struct ParsedEntity
{
u64 uuid = 0;
std::string name = "entity";
YAML::Node comps; // 缺省 = Null(无组件)
};
// 写端:单实体 → Emitter(调用方已进入 Entities 序列)
void EmitEntity(YAML::Emitter& out, Assets::AssetManager& assets,
const ECS::Registry& reg, entt::entity e)
{
const auto& id = reg.get<ECS::IdComponent>(e);
out << YAML::BeginMap;
out << YAML::Key << "Entity" << YAML::Value << id.uuid.Value();
if (const auto* name = reg.try_get<ECS::NameComponent>(e)) {
out << YAML::Key << "Name" << YAML::Value << name->name;
}
out << YAML::Key << "Components" << YAML::Value << YAML::BeginMap;
if (const auto* t = reg.try_get<ECS::TransformComponent>(e)) {
out << YAML::Key << "Transform" << YAML::Value
<< YAML::BeginMap;
out << YAML::Key << "Position" << YAML::Value;
EmitVec3(out, t->GetPosition());
out << YAML::Key << "Rotation" << YAML::Value;
EmitQuat(out, t->GetRotationQuat()); // (w,x,y,z) 无损
out << YAML::Key << "Scale" << YAML::Value;
EmitVec3(out, t->GetScale());
out << YAML::EndMap;
}
if (const auto* mr = reg.try_get<ECS::MeshRendererComponent>(e)) {
// ModelRef → {Guid, Path}:Path 反查为空 = 无效引用,跳过
const std::string& path = assets.Models().Path(mr->model);
if (!path.empty()) {
out << YAML::Key << "MeshRenderer" << YAML::Value
<< YAML::BeginMap;
out << YAML::Key << "Model" << YAML::Value << YAML::Flow
<< YAML::BeginMap;
if (const u64 guid = assets.GetOrCreateGUID(path);
guid != 0) {
out << YAML::Key << "Guid" << YAML::Value << guid;
}
out << YAML::Key << "Path" << YAML::Value << path;
out << YAML::EndMap; // Model(flow)
out << YAML::Key << "Active" << YAML::Value << mr->active;
out << YAML::EndMap; // MeshRenderer
}
else {
ENGINE_WARN("SceneSerializer: entity has invalid model "
"ref, MeshRenderer skipped");
}
}
if (const auto* a = reg.try_get<ECS::AnimationComponent>(e)) {
out << YAML::Key << "Animation" << YAML::Value << YAML::Flow
<< YAML::BeginMap;
out << YAML::Key << "ClipIndex" << YAML::Value << a->clipIndex;
out << YAML::Key << "Playing" << YAML::Value << a->playing;
out << YAML::Key << "Loop" << YAML::Value << a->loop;
out << YAML::Key << "Speed" << YAML::Value << a->speed;
out << YAML::Key << "Active" << YAML::Value << a->active;
out << YAML::EndMap;
}
if (const auto* c = reg.try_get<ECS::CameraComponent>(e)) {
out << YAML::Key << "Camera" << YAML::Value << YAML::Flow
<< YAML::BeginMap;
out << YAML::Key << "Primary" << YAML::Value << c->primary;
out << YAML::Key << "Active" << YAML::Value << c->active;
out << YAML::EndMap;
}
if (const auto* l = reg.try_get<ECS::LightComponent>(e)) {
out << YAML::Key << "Light" << YAML::Value << YAML::BeginMap;
out << YAML::Key << "Type" << YAML::Value
<< static_cast<int>(l->type);
out << YAML::Key << "Direction" << YAML::Value;
EmitVec3(out, l->direction);
out << YAML::Key << "Color" << YAML::Value;
EmitVec4(out, l->color);
out << YAML::Key << "Radius" << YAML::Value << l->radius;
out << YAML::Key << "CastShadow" << YAML::Value
<< l->castShadow;
out << YAML::Key << "Active" << YAML::Value << l->active;
out << YAML::EndMap;
}
if (const auto* rb = reg.try_get<ECS::RigidBodyComponent>(e)) {
out << YAML::Key << "RigidBody" << YAML::Value
<< YAML::BeginMap;
out << YAML::Key << "Type" << YAML::Value
<< static_cast<int>(rb->type);
out << YAML::Key << "Collider" << YAML::Value
<< static_cast<int>(rb->collider);
out << YAML::Key << "BoxHalfExtents" << YAML::Value;
EmitVec3(out, rb->boxHalfExtents);
out << YAML::Key << "SphereRadius" << YAML::Value
<< rb->sphereRadius;
out << YAML::Key << "CapsuleRadius" << YAML::Value
<< rb->capsuleRadius;
out << YAML::Key << "CapsuleHalfHeight" << YAML::Value
<< rb->capsuleHalfHeight;
out << YAML::Key << "CylinderRadius" << YAML::Value
<< rb->cylinderRadius;
out << YAML::Key << "CylinderHalfHeight" << YAML::Value
<< rb->cylinderHalfHeight;
if (!rb->compound.empty()) {
out << YAML::Key << "Compound" << YAML::Value
<< YAML::BeginSeq;
for (const auto& c : rb->compound) {
out << YAML::BeginMap;
out << YAML::Key << "Shape" << YAML::Value
<< static_cast<int>(c.shape);
out << YAML::Key << "BoxHalfExtents" << YAML::Value;
EmitVec3(out, c.boxHalfExtents);
out << YAML::Key << "Radius" << YAML::Value << c.radius;
out << YAML::Key << "CapsuleHalfHeight" << YAML::Value
<< c.capsuleHalfHeight;
out << YAML::Key << "Position" << YAML::Value;
EmitVec3(out, c.position);
out << YAML::Key << "Rotation" << YAML::Value;
EmitQuat(out, c.rotation); // (w,x,y,z)
out << YAML::EndMap;
}
out << YAML::EndSeq;
}
out << YAML::Key << "Mass" << YAML::Value << rb->mass;
out << YAML::Key << "GravityScale" << YAML::Value
<< rb->gravityScale;
out << YAML::Key << "Friction" << YAML::Value << rb->friction;
out << YAML::Key << "Restitution" << YAML::Value
<< rb->restitution;
out << YAML::Key << "IsTrigger" << YAML::Value << rb->isTrigger;
// bodyId/bodyValid/velocity 由 PhysicsWorld 对账重建,不存
out << YAML::Key << "Active" << YAML::Value << rb->active;
out << YAML::EndMap;
}
if (const auto* jc = reg.try_get<ECS::JointComponent>(e)) {
out << YAML::Key << "Joint" << YAML::Value
<< YAML::BeginMap;
out << YAML::Key << "Type" << YAML::Value
<< static_cast<int>(jc->type);
// 实体引用存 UUID(跨存档稳定;target=0 未设置)
out << YAML::Key << "Target" << YAML::Value
<< jc->target.Value();
out << YAML::Key << "AnchorA" << YAML::Value;
EmitVec3(out, jc->anchorA);
out << YAML::Key << "AnchorB" << YAML::Value;
EmitVec3(out, jc->anchorB);
out << YAML::Key << "HingeAxis" << YAML::Value;
EmitVec3(out, jc->hingeAxis);
out << YAML::Key << "EnableLimit" << YAML::Value
<< jc->enableLimit;
out << YAML::Key << "LimitMin" << YAML::Value << jc->limitMin;
out << YAML::Key << "LimitMax" << YAML::Value << jc->limitMax;
out << YAML::Key << "Active" << YAML::Value << jc->active;
out << YAML::EndMap;
}
// 角色只存参数(速度/着地等状态不序列化)
if (const auto* cc = reg.try_get<ECS::CharacterControllerComponent>(e)) {
out << YAML::Key << "CharacterController" << YAML::Value
<< YAML::BeginMap;
out << YAML::Key << "Radius" << YAML::Value << cc->radius;
out << YAML::Key << "CapsuleHalfHeight" << YAML::Value
<< cc->capsuleHalfHeight;
out << YAML::Key << "Mass" << YAML::Value << cc->mass;
out << YAML::Key << "MaxSlopeAngle" << YAML::Value
<< cc->maxSlopeAngle;
out << YAML::Key << "StepHeight" << YAML::Value
<< cc->stepHeight;
out << YAML::Key << "Active" << YAML::Value << cc->active;
out << YAML::EndMap;
}
// 脚本只存类名(注册表键);未注册名字加载后运行时警告跳过
if (const auto* sc = reg.try_get<ECS::ScriptComponent>(e)) {
out << YAML::Key << "Script" << YAML::Value << YAML::BeginMap;
out << YAML::Key << "ScriptName" << YAML::Value
<< sc->scriptName;
out << YAML::Key << "Active" << YAML::Value << sc->active;
out << YAML::EndMap;
}
out << YAML::EndMap; // Components
out << YAML::EndMap; // Entity
}
} // namespace
// ---- 保存 ----
bool SceneSerializer::Serialize(const ECS::Scene& scene,
Assets::AssetManager& assets,
const std::string& outPath,
const std::string& environmentPath)
{
try {
// float32 无损往返需 9 位有效数字(std::numeric_limits
// <float>::max_digits10)
YAML::Emitter out;
out.SetFloatPrecision(9);
out << YAML::BeginMap;
out << YAML::Key << "Scene" << YAML::Value
<< std::filesystem::path(outPath).stem().string();
out << YAML::Key << "Version" << YAML::Value << kSceneVersion;
if (!environmentPath.empty()) {
out << YAML::Key << "Environment" << YAML::Value
<< environmentPath;
}
const auto& s = scene.GetSettings();
out << YAML::Key << "Settings" << YAML::Value << YAML::BeginMap;
out << YAML::Key << "ClearColor" << YAML::Value;
EmitVec4(out, s.ClearColor);
out << YAML::Key << "AmbientColor" << YAML::Value;
EmitVec4(out, s.AmbientColor);
out << YAML::Key << "DrawSkybox" << YAML::Value << s.DrawSkybox;
out << YAML::EndMap;
out << YAML::Key << "Entities" << YAML::Value << YAML::BeginSeq;
const ECS::Registry& reg = scene.GetRegistry();
reg.view<ECS::IdComponent>().each(
[&](entt::entity e, const ECS::IdComponent&) {
EmitEntity(out, assets, reg, e);
});
out << YAML::EndSeq;
out << YAML::EndMap;
if (!out.good()) {
ENGINE_ERROR("SceneSerializer: emit failed: {}", out.GetLastError());
return false;
}
std::ofstream file(outPath, std::ios::trunc);
if (!file) {
ENGINE_ERROR("SceneSerializer: cannot open '{}' for write",
outPath);
return false;
}
file << out.c_str();
return true;
}
catch (const YAML::Exception& e) {
ENGINE_ERROR("SceneSerializer: save failed ({})", e.what());
return false;
}
catch (const std::exception& e) {
ENGINE_ERROR("SceneSerializer: save failed ({})", e.what());
return false;
}
}
// ---- 加载 ----
bool SceneSerializer::Deserialize(ECS::Scene& scene,
Assets::AssetManager& assets,
const std::string& inPath,
std::string& outEnvironmentPath)
{
outEnvironmentPath.clear();
YAML::Node root;
try {
root = YAML::LoadFile(inPath);
}
catch (const YAML::Exception& e) {
ENGINE_ERROR("SceneSerializer: cannot parse '{}' ({})", inPath,
e.what());
return false;
}
catch (const std::exception& e) {
ENGINE_ERROR("SceneSerializer: cannot read '{}' ({})", inPath,
e.what());
return false;
}
// Version 闸:高版本拒载(结构未知),缺失按 0 宽容旧档
if (const int version = root["Version"].as<int>(0);
version > kSceneVersion) {
ENGINE_ERROR("SceneSerializer: '{}' version {} > supported {}",
inPath, version, kSceneVersion);
return false;
}
// 僵尸节点安全:as<T>(fallback) 对缺失键返回 fallback
outEnvironmentPath = root["Environment"].as<std::string>("");
if (const YAML::Node st = root["Settings"]; st && st.IsMap()) {
auto& s = scene.GetSettings();
s.ClearColor = ReadVec4(st["ClearColor"], s.ClearColor);
s.AmbientColor = ReadVec4(st["AmbientColor"], s.AmbientColor);
s.DrawSkybox = st["DrawSkybox"].as<bool>(s.DrawSkybox);
}
// ---- 阶段 1:解析到中间结构(逐实体容错,不动 registry)----
std::vector<ParsedEntity> parsed;
const YAML::Node entities = root["Entities"];
if (entities && entities.IsSequence()) {
parsed.reserve(entities.size());
for (const auto& e : entities) {
try {
ParsedEntity pe;
pe.uuid = e["Entity"].as<u64>(0);
pe.name = e["Name"].as<std::string>(pe.name);
if (const YAML::Node comps = e["Components"];
comps && comps.IsMap()) {
// 仅在 rhs 有效时赋值(僵尸节点 operator= 会抛)
pe.comps = comps;
}
parsed.push_back(std::move(pe));
}
catch (const std::exception& err) {
// 单实体损坏不炸全场:跳过继续
ENGINE_WARN("SceneSerializer: entity skipped ({})",
err.what());
}
}
}
// ---- 阶段 2:原子换血重建 ----
scene.Clear();
std::unordered_set<u64> seenUuids;
for (ParsedEntity& pe : parsed) {
auto handle = scene.CreateEntity(pe.name);
// UUID 重复/无效 → 重新生成(Prefab 实例化的 ID 重映射语义)
u64 uuid = pe.uuid;
if (uuid == 0 || !seenUuids.insert(uuid).second) {
uuid = UUID::New().Value();
}
handle.GetComponent<ECS::IdComponent>().uuid = UUID{uuid};
const YAML::Node comps = pe.comps; // 拷贝初始化,缺省 Null
if (const YAML::Node n = comps["Transform"]) {
auto& t = handle.GetComponent<ECS::TransformComponent>();
t.SetPosition(ReadVec3(n["Position"], t.GetPosition()));
t.SetRotationQuat(ReadQuat(n["Rotation"]));
t.SetScale(ReadVec3(n["Scale"], t.GetScale()));
}
if (const YAML::Node n = comps["MeshRenderer"]) {
if (const YAML::Node m = n["Model"]) {
const u64 guid = m["Guid"].as<u64>(0);
const auto path = m["Path"].as<std::string>("");
if (ModelRef ref = ResolveModel(assets, guid, path);
ref.Valid()) {
handle.AddComponent<ECS::MeshRendererComponent>(
ECS::MeshRendererComponent{ref});
handle.GetComponent<ECS::MeshRendererComponent>().active =
n["Active"].as<bool>(true); // 缺省 true 向前兼容
}
else {
ENGINE_WARN("SceneSerializer: model unresolved "
"(guid={}, path='{}'), MeshRenderer "
"skipped",
guid, path);
}
}
}
if (const YAML::Node n = comps["Animation"]) {
auto& a = handle.AddComponent<ECS::AnimationComponent>();
a.clipIndex = n["ClipIndex"].as<int>(a.clipIndex);
a.playing = n["Playing"].as<bool>(a.playing);
a.loop = n["Loop"].as<bool>(a.loop);
a.speed = n["Speed"].as<f32>(a.speed);
a.active = n["Active"].as<bool>(true);
}
if (const YAML::Node n = comps["Camera"]) {
auto& c = handle.AddComponent<ECS::CameraComponent>();
c.primary = n["Primary"].as<bool>(false);
c.active = n["Active"].as<bool>(true);
}
if (const YAML::Node n = comps["Light"]) {
auto& l = handle.AddComponent<ECS::LightComponent>();
if (const int type = n["Type"].as<int>(0); type >= 0 &&
type <= 1) {
l.type = static_cast<ECS::LightComponent::Type>(type);
}
l.direction = ReadVec3(n["Direction"], l.direction);
l.color = ReadVec4(n["Color"], l.color);
l.radius = n["Radius"].as<f32>(l.radius);
l.castShadow = n["CastShadow"].as<bool>(l.castShadow);
l.active = n["Active"].as<bool>(true);
}
if (const YAML::Node n = comps["RigidBody"]) {
auto& rb = handle.AddComponent<ECS::RigidBodyComponent>();
if (const int type = n["Type"].as<int>(0);
type >= 0 && type <= 2) {
rb.type =
static_cast<ECS::RigidBodyComponent::BodyType>(type);
}
if (const int col = n["Collider"].as<int>(0);
col >= 0 && col <= 5) {
rb.collider =
static_cast<ECS::RigidBodyComponent::ColliderType>(
col);
}
rb.boxHalfExtents = ReadVec3(n["BoxHalfExtents"],
rb.boxHalfExtents);
rb.sphereRadius = n["SphereRadius"].as<f32>(rb.sphereRadius);
rb.capsuleRadius = n["CapsuleRadius"].as<f32>(rb.capsuleRadius);
rb.capsuleHalfHeight =
n["CapsuleHalfHeight"].as<f32>(rb.capsuleHalfHeight);
rb.cylinderRadius =
n["CylinderRadius"].as<f32>(rb.cylinderRadius);
rb.cylinderHalfHeight =
n["CylinderHalfHeight"].as<f32>(rb.cylinderHalfHeight);
// 僵尸节点铁律:拷贝初始化 + 判空再遍历(缺 Compound 键安全)
if (const YAML::Node cs = n["Compound"]) {
for (const auto& item : cs) {
ECS::RigidBodyComponent::CompoundChild c;
if (const int sh = item["Shape"].as<int>(0);
sh >= 0 && sh <= 2) {
c.shape = static_cast<
ECS::RigidBodyComponent::CompoundChild::Shape>(
sh);
}
c.boxHalfExtents = ReadVec3(item["BoxHalfExtents"],
c.boxHalfExtents);
c.radius = item["Radius"].as<f32>(c.radius);
c.capsuleHalfHeight =
item["CapsuleHalfHeight"].as<f32>(
c.capsuleHalfHeight);
c.position = ReadVec3(item["Position"], c.position);
c.rotation = ReadQuat(item["Rotation"]);
rb.compound.push_back(std::move(c));
}
}
rb.mass = n["Mass"].as<f32>(rb.mass);
rb.gravityScale = n["GravityScale"].as<f32>(rb.gravityScale);
rb.friction = n["Friction"].as<f32>(rb.friction);
rb.restitution = n["Restitution"].as<f32>(rb.restitution);
// 新键 IsTrigger;旧场景 IsSensor 兼容读(缺键 as 返回 fallback 安全)
rb.isTrigger = n["IsTrigger"].as<bool>(
n["IsSensor"].as<bool>(rb.isTrigger));
rb.active = n["Active"].as<bool>(true);
// bodyId/bodyValid/velocity 保持默认(对账重建)
}
// Joint:target 只存 UUID,加载后由 PhysicsWorld 惰性反查
//(两遍解析不需要——UUID 再生/加载顺序均由逐帧解析消化)
if (const YAML::Node n = comps["Joint"]) {
auto& jc = handle.AddComponent<ECS::JointComponent>();
if (const int type = n["Type"].as<int>(0);
type >= 0 && type <= 2) {
jc.type =
static_cast<ECS::JointComponent::Type>(type);
}
jc.target = UUID{n["Target"].as<u64>(0)};
jc.anchorA = ReadVec3(n["AnchorA"], jc.anchorA);
jc.anchorB = ReadVec3(n["AnchorB"], jc.anchorB);
jc.hingeAxis = ReadVec3(n["HingeAxis"], jc.hingeAxis);
jc.enableLimit = n["EnableLimit"].as<bool>(jc.enableLimit);
jc.limitMin = n["LimitMin"].as<f32>(jc.limitMin);
jc.limitMax = n["LimitMax"].as<f32>(jc.limitMax);
jc.active = n["Active"].as<bool>(true);
}
if (const YAML::Node n = comps["CharacterController"]) {
auto& cc =
handle.AddComponent<ECS::CharacterControllerComponent>();
cc.radius = n["Radius"].as<f32>(cc.radius);
cc.capsuleHalfHeight =
n["CapsuleHalfHeight"].as<f32>(cc.capsuleHalfHeight);
cc.mass = n["Mass"].as<f32>(cc.mass);
cc.maxSlopeAngle = n["MaxSlopeAngle"].as<f32>(cc.maxSlopeAngle);
cc.stepHeight = n["StepHeight"].as<f32>(cc.stepHeight);
cc.active = n["Active"].as<bool>(true);
// 状态(速度/着地)由 PhysicsWorld 对账重建,不存
}
// 脚本:只存类名;未注册名字由 ScriptRuntime 运行时警告跳过
if (const YAML::Node n = comps["Script"]) {
auto& sc = handle.AddComponent<ECS::ScriptComponent>();
sc.scriptName = n["ScriptName"].as<std::string>("");
sc.active = n["Active"].as<bool>(true);
}
}
// 加载在 OnImGuiRender 段,错过本帧 UpdateTransforms:
// 显式补算,否则首帧 World = 单位阵
scene.UpdateTransforms();
ENGINE_INFO("SceneSerializer: loaded {} entities from '{}'",
parsed.size(), inPath);
return true;
}
} // namespace Engine::Serialization
Submodule
+1
Submodule Lib/yaml-cpp added at 1e0876c671