From e1cef53158e973cfe01c79be535870096185a3ee Mon Sep 17 00:00:00 2001 From: atdunbg Date: Sat, 3 Oct 2026 16:34:20 +0800 Subject: [PATCH] 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 --- App/editor/InspectorPanel.cpp | 37 +++++++ Engine/include/Engine/ECS/Components.h | 15 +++ .../Engine/Physics/CharacterController.h | 70 ++++++++++++ Engine/include/Engine/Physics/PhysicsWorld.h | 23 ++++ Engine/src/Physics/CharacterController.cpp | 88 +++++++++++++++ Engine/src/Physics/PhysicsWorld.cpp | 104 ++++++++++++++++++ Engine/src/Serialization/SceneSerializer.cpp | 27 +++++ 7 files changed, 364 insertions(+) create mode 100644 Engine/include/Engine/Physics/CharacterController.h create mode 100644 Engine/src/Physics/CharacterController.cpp diff --git a/App/editor/InspectorPanel.cpp b/App/editor/InspectorPanel.cpp index 17e9481..90b0c3a 100644 --- a/App/editor/InspectorPanel.cpp +++ b/App/editor/InspectorPanel.cpp @@ -121,6 +121,11 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) { reg.emplace(selected); } + if (!reg.all_of(selected) && + ImGui::MenuItem("Character Controller")) + { + reg.emplace(selected); + } ImGui::EndPopup(); } ImGui::Separator(); @@ -391,6 +396,38 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) } } + // ---- CharacterController(参数编辑;状态在 PhysicsWorld 运行时)---- + if (auto* cc = reg.try_get(selected)) + { + if (ImGui::CollapsingHeader("Character Controller", + ImGuiTreeNodeFlags_DefaultOpen)) + { + // 实体原点 = 脚底(胶囊底端),总高 = 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(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); + } + } + } + } + // ---- Animation ---- if (auto* anim = reg.try_get(selected)) { diff --git a/Engine/include/Engine/ECS/Components.h b/Engine/include/Engine/ECS/Components.h index abe3dbc..e08a674 100644 --- a/Engine/include/Engine/ECS/Components.h +++ b/Engine/include/Engine/ECS/Components.h @@ -259,6 +259,21 @@ namespace Engine::ECS { bool operator==(const JointComponent&) const = default; }; + // 角色控制器参数(Jolt CharacterVirtual 扫掠运动学,非刚体——不受 + // 物理模拟驱动,游戏逻辑经 PhysicsWorld 门面喂速度/跳跃)。本组件 + // 只存参数(序列化:存参数不存状态),速度/着地等运行时状态在 + // PhysicsWorld 的 CharacterController 里(对账重建) + struct CharacterControllerComponent { + 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 习惯 + }; + } // namespace Engine::ECS #endif //HELLOVULKAN_COMPONENTS_H diff --git a/Engine/include/Engine/Physics/CharacterController.h b/Engine/include/Engine/Physics/CharacterController.h new file mode 100644 index 0000000..11786a2 --- /dev/null +++ b/Engine/include/Engine/Physics/CharacterController.h @@ -0,0 +1,70 @@ +// +// 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 + +#include +#include +#include + +#include + +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 Params& GetParams() const { return m_Params; } + + private: + Params m_Params; + JPH::Ref m_Character; + JPH::CharacterVirtual::ExtendedUpdateSettings m_Ext; + }; + +} // namespace Engine + +#endif //HELLOVULKAN_CHARACTERCONTROLLER_H diff --git a/Engine/include/Engine/Physics/PhysicsWorld.h b/Engine/include/Engine/Physics/PhysicsWorld.h index bba3732..9a7c76f 100644 --- a/Engine/include/Engine/Physics/PhysicsWorld.h +++ b/Engine/include/Engine/Physics/PhysicsWorld.h @@ -25,6 +25,7 @@ #include #include #include +#include #include // BodyRecord 快照存 CompoundChild(完整类型) #include @@ -90,6 +91,14 @@ namespace Engine { // 无 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: // 创建时缓存的参数(变更检测 → 重建 body) struct BodyRecord { @@ -121,6 +130,12 @@ namespace Engine { 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 / // Dynamic=ConvexHull)。无 MeshRenderer 或构建失败返回 null(回退 Box) JPH::RefConst BuildMeshShape(u32 entityBits, @@ -132,6 +147,7 @@ namespace Engine { ECS::TransformComponent& tr); void RemoveBody(u32 entityBits); const BodyRecord* FindRecord(u32 entityBits) const; // 刚体驱动寻址 + CharacterController* FindCharacter(u32 entityBits); // 角色门面寻址 void QueueEvent(CollisionEvent&& ev); // 回调线程 append(多线程并发,加锁) bool TryBitsByBodyId(const JPH::BodyID& id, u32& outBits) const; @@ -144,6 +160,13 @@ namespace Engine { }; std::unordered_map m_Constraints; + // ---- 角色(entityBits → CharacterVirtual 封装)---- + struct CharacterEntry { + std::unique_ptr cc; + glm::vec3 lastPos{0.0f}; // 编辑器拖动检测 + }; + std::unordered_map m_Characters; + ECS::Scene& m_Scene; Assets::AssetManager* m_Assets = nullptr; // Mesh 碰撞体解析 modelRef bool m_Enabled = true; diff --git a/Engine/src/Physics/CharacterController.cpp b/Engine/src/Physics/CharacterController.cpp new file mode 100644 index 0000000..ac71dbb --- /dev/null +++ b/Engine/src/Physics/CharacterController.cpp @@ -0,0 +1,88 @@ +// +// CharacterController.cpp:Jolt CharacterVirtual 封装实现。 +// + +#include + +#include + +namespace Engine { + + CharacterController::CharacterController(JPH::PhysicsSystem &system, + const Params ¶ms, + 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(p.GetX()), static_cast(p.GetY()), + static_cast(p.GetZ())}; + } + + void CharacterController::Teleport(const glm::vec3 &position) { + m_Character->SetPosition( + JPH::RVec3(position.x, position.y, position.z)); + } + +} // namespace Engine diff --git a/Engine/src/Physics/PhysicsWorld.cpp b/Engine/src/Physics/PhysicsWorld.cpp index 83a8046..d488f73 100644 --- a/Engine/src/Physics/PhysicsWorld.cpp +++ b/Engine/src/Physics/PhysicsWorld.cpp @@ -242,6 +242,7 @@ namespace Engine { m_System->Update(kFixedDt, steps, m_TempAllocator.get(), m_JobSystem.get()); Writeback(); + UpdateCharacters(kFixedDt); // 角色扫掠(Jolt 官方建议:物理更新后) UpdateAccels(kFixedDt); // 速度差分 → 加速度(step 率采样一次) } @@ -406,6 +407,9 @@ namespace Engine { // 3. 约束对账(body 循环之后:依赖新建 body 的最终 ID) SyncConstraints(invalidated); + + // 4. 角色对账(非刚体,不依赖 body 生命周期) + SyncCharacters(); } // ---- 约束对账 ---- @@ -559,6 +563,75 @@ namespace Engine { m_Constraints.erase(found); // Ref 析构 → 引用计数释放 } + // ---- 角色控制器 ---- + + void PhysicsWorld::SyncCharacters() { + auto ® = m_Scene.GetRegistry(); + + // 1. 孤儿清理:实体销毁 / 组件被移除(Ref 析构即清理,无 body 负担) + for (auto it = m_Characters.begin(); it != m_Characters.end();) { + const auto e = static_cast(it->first); + if (!reg.valid(e) || + !reg.all_of(e)) { + it = m_Characters.erase(it); + } else { + ++it; + } + } + + // 2. 遍历:缺失 → 创建;参数变更 → 重建;编辑器拖动 → 传送 + auto view = reg.view(); + for (auto [e, cc, tr]: view.each()) { + const u32 bits = static_cast(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( + *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( + *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 ® = m_Scene.GetRegistry(); + const JPH::Vec3 gravity = m_System->GetGravity(); + for (auto &[bits, entry]: m_Characters) { + auto *tr = reg.try_get( + static_cast(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 形状 ---- JPH::RefConst PhysicsWorld::BuildMeshShape( @@ -888,6 +961,37 @@ namespace Engine { 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)---- void PhysicsWorld::Writeback() const { diff --git a/Engine/src/Serialization/SceneSerializer.cpp b/Engine/src/Serialization/SceneSerializer.cpp index de25e94..afa45ff 100644 --- a/Engine/src/Serialization/SceneSerializer.cpp +++ b/Engine/src/Serialization/SceneSerializer.cpp @@ -328,6 +328,21 @@ namespace Engine::Serialization { out << YAML::EndMap; } + // 角色只存参数(速度/着地等状态不序列化) + if (const auto* cc = reg.try_get(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::EndMap; + } + out << YAML::EndMap; // Components out << YAML::EndMap; // Entity } @@ -601,6 +616,18 @@ namespace Engine::Serialization { jc.limitMin = n["LimitMin"].as(jc.limitMin); jc.limitMax = n["LimitMax"].as(jc.limitMax); } + + if (const YAML::Node n = comps["CharacterController"]) { + auto& cc = + handle.AddComponent(); + cc.radius = n["Radius"].as(cc.radius); + cc.capsuleHalfHeight = + n["CapsuleHalfHeight"].as(cc.capsuleHalfHeight); + cc.mass = n["Mass"].as(cc.mass); + cc.maxSlopeAngle = n["MaxSlopeAngle"].as(cc.maxSlopeAngle); + cc.stepHeight = n["StepHeight"].as(cc.stepHeight); + // 状态(速度/着地)由 PhysicsWorld 对账重建,不存 + } } // 加载在 OnImGuiRender 段,错过本帧 UpdateTransforms: