Add option to change Character Controller capsule origin to start at feet location

This commit is contained in:
Wojtek Figat
2025-06-05 17:48:07 +02:00
parent e982a23ed3
commit c2cbaeed30
4 changed files with 129 additions and 33 deletions

View File

@@ -21,6 +21,7 @@ CharacterController::CharacterController(const SpawnParams& params)
, _upDirection(Vector3::Up) , _upDirection(Vector3::Up)
, _gravityDisplacement(Vector3::Zero) , _gravityDisplacement(Vector3::Zero)
, _nonWalkableMode(NonWalkableModes::PreventClimbing) , _nonWalkableMode(NonWalkableModes::PreventClimbing)
, _originMode(OriginModes::CapsuleCenter)
, _lastFlags(CollisionFlags::None) , _lastFlags(CollisionFlags::None)
{ {
_contactOffset = 10.0f; _contactOffset = 10.0f;
@@ -35,9 +36,7 @@ void CharacterController::SetRadius(const float value)
{ {
if (value == _radius) if (value == _radius)
return; return;
_radius = value; _radius = value;
UpdateSize(); UpdateSize();
UpdateBounds(); UpdateBounds();
} }
@@ -51,9 +50,7 @@ void CharacterController::SetHeight(const float value)
{ {
if (value == _height) if (value == _height)
return; return;
_height = value; _height = value;
UpdateSize(); UpdateSize();
UpdateBounds(); UpdateBounds();
} }
@@ -87,6 +84,23 @@ void CharacterController::SetNonWalkableMode(NonWalkableModes value)
PhysicsBackend::SetControllerNonWalkableMode(_controller, (int32)value); PhysicsBackend::SetControllerNonWalkableMode(_controller, (int32)value);
} }
CharacterController::OriginModes CharacterController::GetOriginMode() const
{
return _originMode;
}
void CharacterController::SetOriginMode(OriginModes value)
{
if (_originMode == value)
return;
_originMode = value;
if (_controller)
{
DeleteController();
CreateController();
}
}
float CharacterController::GetStepOffset() const float CharacterController::GetStepOffset() const
{ {
return _stepOffset; return _stepOffset;
@@ -165,13 +179,23 @@ CharacterController::CollisionFlags CharacterController::SimpleMove(const Vector
CharacterController::CollisionFlags CharacterController::Move(const Vector3& displacement) CharacterController::CollisionFlags CharacterController::Move(const Vector3& displacement)
{ {
CollisionFlags result = CollisionFlags::None; CollisionFlags result = CollisionFlags::None;
if (_controller) if (_controller && !_isUpdatingTransform)
{ {
// Perform move
const float deltaTime = Time::GetCurrentSafe()->DeltaTime.GetTotalSeconds(); const float deltaTime = Time::GetCurrentSafe()->DeltaTime.GetTotalSeconds();
result = (CollisionFlags)PhysicsBackend::MoveController(_controller, _shape, displacement, _minMoveDistance, deltaTime); result = (CollisionFlags)PhysicsBackend::MoveController(_controller, _shape, displacement, _minMoveDistance, deltaTime);
_lastFlags = result; _lastFlags = result;
Vector3 position = PhysicsBackend::GetControllerPosition(_controller) - _center;
// Update position
Vector3 position;
if (_originMode == OriginModes::Base)
position = PhysicsBackend::GetControllerBasePosition(_controller);
else
position = PhysicsBackend::GetControllerPosition(_controller);
position -= _center;
_isUpdatingTransform = true;
SetPosition(position); SetPosition(position);
_isUpdatingTransform = false;
} }
return result; return result;
} }
@@ -194,10 +218,21 @@ void CharacterController::Resize(float height, float radius)
Vector3 positionDelta = _upDirection * centerDiff; Vector3 positionDelta = _upDirection * centerDiff;
// Change physics position to maintain feet placement (base) // Change physics position to maintain feet placement (base)
Vector3 position = PhysicsBackend::GetControllerPosition(_controller); Vector3 position;
position += positionDelta; switch (_originMode)
_center += positionDelta; {
PhysicsBackend::SetControllerPosition(_controller, position); case OriginModes::CapsuleCenter:
position = PhysicsBackend::GetControllerPosition(_controller);
position += positionDelta;
_center += positionDelta;
PhysicsBackend::SetControllerPosition(_controller, position);
break;
case OriginModes::Base:
position = PhysicsBackend::GetControllerBasePosition(_controller);
position += positionDelta;
PhysicsBackend::SetControllerBasePosition(_controller, position);
break;
}
// Change actor position // Change actor position
_isUpdatingTransform = true; _isUpdatingTransform = true;
@@ -215,7 +250,7 @@ void CharacterController::Resize(float height, float radius)
void CharacterController::DrawPhysicsDebug(RenderView& view) void CharacterController::DrawPhysicsDebug(RenderView& view)
{ {
Quaternion rotation = Quaternion::Euler(90, 0, 0); Quaternion rotation = Quaternion::Euler(90, 0, 0);
const Vector3 position = _transform.LocalToWorld(_center); const Vector3 position = GetControllerPosition();
if (view.Mode == ViewMode::PhysicsColliders) if (view.Mode == ViewMode::PhysicsColliders)
DEBUG_DRAW_CAPSULE(position, rotation, _radius, _height, Color::LightYellow, 0, true); DEBUG_DRAW_CAPSULE(position, rotation, _radius, _height, Color::LightYellow, 0, true);
else else
@@ -225,23 +260,25 @@ void CharacterController::DrawPhysicsDebug(RenderView& view)
void CharacterController::OnDebugDrawSelected() void CharacterController::OnDebugDrawSelected()
{ {
Quaternion rotation = Quaternion::Euler(90, 0, 0); Quaternion rotation = Quaternion::Euler(90, 0, 0);
const Vector3 position = _transform.LocalToWorld(_center); const Vector3 position = GetControllerPosition();
DEBUG_DRAW_WIRE_CAPSULE(position, rotation, _radius, _height, Color::GreenYellow, 0, false); DEBUG_DRAW_WIRE_CAPSULE(position, rotation, _radius, _height, Color::GreenYellow, 0, false);
if (_contactOffset > 0) if (_contactOffset > 0)
DEBUG_DRAW_WIRE_CAPSULE(position, rotation, _radius - _contactOffset, _height, Color::Blue.AlphaMultiplied(0.4f), 0, false); DEBUG_DRAW_WIRE_CAPSULE(position, rotation, _radius - _contactOffset, _height, Color::Blue.AlphaMultiplied(0.4f), 0, false);
#if 0 #if 1
// More technical visuals debugging // More technical visuals debugging
if (_controller) if (_controller)
{ {
float height, radius; float height, radius;
GetControllerSize(height, radius); GetControllerSize(height, radius);
DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(PhysicsBackend::GetControllerBasePosition(_controller), 5.0f), Color::Red, 0, false); Vector3 base = PhysicsBackend::GetControllerBasePosition(_controller);
DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(PhysicsBackend::GetControllerPosition(_controller), 4.0f), Color::Red, 0, false); Vector3 pos = PhysicsBackend::GetControllerPosition(_controller);
DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(PhysicsBackend::GetControllerPosition(_controller) + Vector3(0, height * 0.5f, 0), 2.0f), Color::Red, 0, false); DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(base, 5.0f), Color::Red, 0, false);
DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(PhysicsBackend::GetControllerPosition(_controller) - Vector3(0, height * 0.5f, 0), 2.0f), Color::Red, 0, false); DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(pos, 4.0f), Color::Red, 0, false);
DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(PhysicsBackend::GetControllerPosition(_controller) + Vector3(0, height * 0.5f, 0), radius), Color::Red.AlphaMultiplied(0.5f), 0, false); DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(pos + Vector3(0, height * 0.5f, 0), 2.0f), Color::Red, 0, false);
DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(PhysicsBackend::GetControllerPosition(_controller) - Vector3(0, height * 0.5f, 0), radius), Color::Red.AlphaMultiplied(0.5f), 0, false); DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(pos - Vector3(0, height * 0.5f, 0), 2.0f), Color::Red, 0, false);
DEBUG_DRAW_WIRE_CYLINDER(PhysicsBackend::GetControllerPosition(_controller), Quaternion::Identity, radius, height, Color::Red.AlphaMultiplied(0.2f), 0, false); DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(pos + Vector3(0, height * 0.5f, 0), radius), Color::Red.AlphaMultiplied(0.5f), 0, false);
DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(pos - Vector3(0, height * 0.5f, 0), radius), Color::Red.AlphaMultiplied(0.5f), 0, false);
DEBUG_DRAW_WIRE_CYLINDER(pos, Quaternion::Identity, radius, height, Color::Red.AlphaMultiplied(0.2f), 0, false);
} }
DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(position, 3.0f), Color::GreenYellow, 0, false); DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(position, 3.0f), Color::GreenYellow, 0, false);
#else #else
@@ -267,7 +304,10 @@ void CharacterController::CreateController()
_cachedScale = GetScale().GetAbsolute().MaxValue(); _cachedScale = GetScale().GetAbsolute().MaxValue();
float height, radius; float height, radius;
GetControllerSize(height, radius); GetControllerSize(height, radius);
const Vector3 position = _transform.LocalToWorld(_center); Vector3 position = _center;
if (_originMode == OriginModes::Base)
position += _upDirection * (_height * 0.5f + _radius);
position = _transform.LocalToWorld(position);
_controller = PhysicsBackend::CreateController(GetPhysicsScene()->GetPhysicsScene(), this, this, _contactOffset, position, _slopeLimit, (int32)_nonWalkableMode, Material, radius, height, _stepOffset, _shape); _controller = PhysicsBackend::CreateController(GetPhysicsScene()->GetPhysicsScene(), this, this, _contactOffset, position, _slopeLimit, (int32)_nonWalkableMode, Material, radius, height, _stepOffset, _shape);
// Setup // Setup
@@ -297,14 +337,25 @@ void CharacterController::UpdateSize() const
} }
} }
Vector3 CharacterController::GetControllerPosition() const
{
Vector3 position = _center;
if (_originMode == OriginModes::Base)
position += _upDirection * (_height * 0.5f + _radius);
position = _transform.LocalToWorld(position);
return position;
}
void CharacterController::GetControllerSize(float& height, float& radius) const void CharacterController::GetControllerSize(float& height, float& radius) const
{ {
// Use absolute values including scale
height = Math::Abs(_height) * _cachedScale; height = Math::Abs(_height) * _cachedScale;
radius = Math::Abs(_radius) * _cachedScale; radius = Math::Abs(_radius) * _cachedScale;
{
// Exclude contact offset around the capsule (otherwise character floats in the air) // Exclude contact offset around the capsule (otherwise character floats in the air)
radius = radius - Math::Max(_contactOffset, 0.0f); radius = radius - Math::Max(_contactOffset, 0.0f);
}
// Prevent too small controllers
height = Math::Max(height, CC_MIN_SIZE); height = Math::Max(height, CC_MIN_SIZE);
radius = Math::Max(radius, CC_MIN_SIZE); radius = Math::Max(radius, CC_MIN_SIZE);
} }
@@ -319,7 +370,7 @@ void CharacterController::UpdateBounds()
_cachedScale = GetScale().GetAbsolute().MaxValue(); _cachedScale = GetScale().GetAbsolute().MaxValue();
float height, radius; float height, radius;
GetControllerSize(height, radius); GetControllerSize(height, radius);
const Vector3 position = _transform.LocalToWorld(_center); const Vector3 position = GetControllerPosition();
const Vector3 extent(radius, height * 0.5f + radius, radius); const Vector3 extent(radius, height * 0.5f + radius, radius);
_box = BoundingBox(position - extent, position + extent); _box = BoundingBox(position - extent, position + extent);
BoundingSphere::FromBox(_box, _sphere); BoundingSphere::FromBox(_box, _sphere);
@@ -376,7 +427,12 @@ void CharacterController::OnActiveTransformChanged()
// Change actor transform (but with locking) // Change actor transform (but with locking)
ASSERT(!_isUpdatingTransform); ASSERT(!_isUpdatingTransform);
_isUpdatingTransform = true; _isUpdatingTransform = true;
const Vector3 position = PhysicsBackend::GetControllerPosition(_controller) - _center; Vector3 position;
if (_originMode == OriginModes::Base)
position = PhysicsBackend::GetControllerBasePosition(_controller);
else
position = PhysicsBackend::GetControllerPosition(_controller);
position -= _center;
SetPosition(position); SetPosition(position);
_isUpdatingTransform = false; _isUpdatingTransform = false;
@@ -458,7 +514,10 @@ void CharacterController::OnTransformChanged()
const Vector3 position = _transform.LocalToWorld(_center); const Vector3 position = _transform.LocalToWorld(_center);
if (!_isUpdatingTransform && _controller) if (!_isUpdatingTransform && _controller)
{ {
PhysicsBackend::SetControllerPosition(_controller, position); if (_originMode == OriginModes::Base)
PhysicsBackend::SetControllerBasePosition(_controller, position);
else
PhysicsBackend::SetControllerPosition(_controller, position);
const float scale = GetScale().GetAbsolute().MaxValue(); const float scale = GetScale().GetAbsolute().MaxValue();
if (_cachedScale != scale) if (_cachedScale != scale)
UpdateGeometry(); UpdateGeometry();

View File

@@ -41,6 +41,22 @@ public:
Below = 1 << 2, Below = 1 << 2,
}; };
/// <summary>
/// Specifies how a character controller capsule placement.
/// </summary>
API_ENUM() enum class OriginModes
{
/// <summary>
/// Character origin starts at capsule center (including Center offset properly).
/// </summary>
CapsuleCenter,
/// <summary>
/// Character origin starts at capsule base position aka character feet placement.
/// </summary>
Base,
};
/// <summary> /// <summary>
/// Specifies how a character controller interacts with non-walkable parts. /// Specifies how a character controller interacts with non-walkable parts.
/// </summary> /// </summary>
@@ -69,6 +85,7 @@ private:
Vector3 _upDirection; Vector3 _upDirection;
Vector3 _gravityDisplacement; Vector3 _gravityDisplacement;
NonWalkableModes _nonWalkableMode; NonWalkableModes _nonWalkableMode;
OriginModes _originMode;
CollisionFlags _lastFlags; CollisionFlags _lastFlags;
public: public:
@@ -116,6 +133,17 @@ public:
/// </summary> /// </summary>
API_PROPERTY() void SetNonWalkableMode(NonWalkableModes value); API_PROPERTY() void SetNonWalkableMode(NonWalkableModes value);
/// <summary>
/// Gets the position origin placement mode.
/// </summary>
API_PROPERTY(Attributes="EditorOrder(216), DefaultValue(OriginModes.CapsuleCenter), EditorDisplay(\"Character Controller\")")
OriginModes GetOriginMode() const;
/// <summary>
/// Sets the position origin placement mode.
/// </summary>
API_PROPERTY() void SetOriginMode(OriginModes value);
/// <summary> /// <summary>
/// Gets the step height. The character will step up a stair only if it is closer to the ground than the indicated value. This should not be greater than the Character Controllers height or it will generate an error. /// Gets the step height. The character will step up a stair only if it is closer to the ground than the indicated value. This should not be greater than the Character Controllers height or it will generate an error.
/// </summary> /// </summary>
@@ -218,6 +246,7 @@ protected:
void UpdateSize() const; void UpdateSize() const;
private: private:
Vector3 GetControllerPosition() const;
void GetControllerSize(float& height, float& radius) const; void GetControllerSize(float& height, float& radius) const;
public: public:

View File

@@ -3155,10 +3155,9 @@ void* PhysicsBackend::CreateController(void* scene, IPhysicsActor* actor, Physic
desc.material = (PxMaterial*)((PhysicalMaterial*)material->Instance)->GetPhysicsMaterial(); desc.material = (PxMaterial*)((PhysicalMaterial*)material->Instance)->GetPhysicsMaterial();
else else
desc.material = DefaultMaterial; desc.material = DefaultMaterial;
const float minSize = 0.001f; desc.height = height;
desc.height = Math::Max(height, minSize); desc.radius = radius;
desc.radius = Math::Max(radius, minSize); desc.stepOffset = Math::Min(stepOffset, desc.height + desc.radius * 2.0f - 0.001f);
desc.stepOffset = Math::Min(stepOffset, desc.height + desc.radius * 2.0f - minSize);
auto controllerPhysX = (PxCapsuleController*)scenePhysX->ControllerManager->createController(desc); auto controllerPhysX = (PxCapsuleController*)scenePhysX->ControllerManager->createController(desc);
PxRigidActor* actorPhysX = controllerPhysX->getActor(); PxRigidActor* actorPhysX = controllerPhysX->getActor();
ASSERT(actorPhysX && actorPhysX->getNbShapes() == 1); ASSERT(actorPhysX && actorPhysX->getNbShapes() == 1);
@@ -3207,7 +3206,15 @@ void PhysicsBackend::SetControllerStepOffset(void* controller, float value)
Vector3 PhysicsBackend::GetControllerBasePosition(void* controller) Vector3 PhysicsBackend::GetControllerBasePosition(void* controller)
{ {
auto controllerPhysX = (PxCapsuleController*)controller; auto controllerPhysX = (PxCapsuleController*)controller;
return P2C(controllerPhysX->getFootPosition()); const Vector3 origin = SceneOrigins[controllerPhysX->getScene()];
return P2C(controllerPhysX->getFootPosition()) + origin;
}
void PhysicsBackend::SetControllerBasePosition(void* controller, const Vector3& value)
{
auto controllerPhysX = (PxCapsuleController*)controller;
const Vector3 sceneOrigin = SceneOrigins[controllerPhysX->getScene()];
controllerPhysX->setFootPosition(PxExtendedVec3(value.X - sceneOrigin.X, value.Y - sceneOrigin.Y, value.Z - sceneOrigin.Z));
} }
Vector3 PhysicsBackend::GetControllerUpDirection(void* controller) Vector3 PhysicsBackend::GetControllerUpDirection(void* controller)

View File

@@ -249,6 +249,7 @@ public:
static void SetControllerNonWalkableMode(void* controller, int32 value); static void SetControllerNonWalkableMode(void* controller, int32 value);
static void SetControllerStepOffset(void* controller, float value); static void SetControllerStepOffset(void* controller, float value);
static Vector3 GetControllerBasePosition(void* controller); static Vector3 GetControllerBasePosition(void* controller);
static void SetControllerBasePosition(void* controller, const Vector3& value);
static Vector3 GetControllerUpDirection(void* controller); static Vector3 GetControllerUpDirection(void* controller);
static void SetControllerUpDirection(void* controller, const Vector3& value); static void SetControllerUpDirection(void* controller, const Vector3& value);
static Vector3 GetControllerPosition(void* controller); static Vector3 GetControllerPosition(void* controller);