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
This commit is contained in:
2026-10-03 16:34:20 +08:00
parent 82910c41e8
commit e1cef53158
7 changed files with 364 additions and 0 deletions
@@ -0,0 +1,88 @@
//
// CharacterController.cpp:Jolt CharacterVirtual 封装实现。
//
#include <Engine/Physics/CharacterController.h>
#include <Jolt/Physics/Collision/Shape/CapsuleShape.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) {
m_Character->SetPosition(
JPH::RVec3(position.x, position.y, position.z));
}
} // namespace Engine
+104
View File
@@ -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 &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);
if (!reg.valid(e) ||
!reg.all_of<ECS::CharacterControllerComponent,
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()) {
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 形状 ----
JPH::RefConst<JPH::Shape> 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 {
@@ -328,6 +328,21 @@ namespace Engine::Serialization {
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::EndMap;
}
out << YAML::EndMap; // Components
out << YAML::EndMap; // Entity
}
@@ -601,6 +616,18 @@ namespace Engine::Serialization {
jc.limitMin = n["LimitMin"].as<f32>(jc.limitMin);
jc.limitMax = n["LimitMax"].as<f32>(jc.limitMax);
}
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);
// 状态(速度/着地)由 PhysicsWorld 对账重建,不存
}
}
// 加载在 OnImGuiRender 段,错过本帧 UpdateTransforms: