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
+37
View File
@@ -121,6 +121,11 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
{
reg.emplace<Engine::ECS::JointComponent>(selected);
}
if (!reg.all_of<Engine::ECS::CharacterControllerComponent>(selected) &&
ImGui::MenuItem("Character Controller"))
{
reg.emplace<Engine::ECS::CharacterControllerComponent>(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<Engine::ECS::CharacterControllerComponent>(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<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);
}
}
}
}
// ---- Animation ----
if (auto* anim = reg.try_get<Engine::ECS::AnimationComponent>(selected))
{