Files
FlaxEngine/Source/Engine/Physics/Colliders/CharacterController.cpp

481 lines
14 KiB
C++

// Copyright (c) Wojciech Figat. All rights reserved.
#include "CharacterController.h"
#include "Engine/Physics/Colliders/Collider.h"
#include "Engine/Physics/Physics.h"
#include "Engine/Physics/PhysicsBackend.h"
#include "Engine/Physics/PhysicsScene.h"
#include "Engine/Engine/Time.h"
#define CC_MIN_SIZE 0.001f
CharacterController::CharacterController(const SpawnParams& params)
: Collider(params)
, _controller(nullptr)
, _stepOffset(30.0f)
, _slopeLimit(45.0f)
, _radius(50.0f)
, _height(150.0f)
, _minMoveDistance(0.0f)
, _isUpdatingTransform(false)
, _upDirection(Vector3::Up)
, _gravityDisplacement(Vector3::Zero)
, _nonWalkableMode(NonWalkableModes::PreventClimbing)
, _lastFlags(CollisionFlags::None)
{
_contactOffset = 10.0f;
}
float CharacterController::GetRadius() const
{
return _radius;
}
void CharacterController::SetRadius(const float value)
{
if (value == _radius)
return;
_radius = value;
UpdateSize();
UpdateBounds();
}
float CharacterController::GetHeight() const
{
return _height;
}
void CharacterController::SetHeight(const float value)
{
if (value == _height)
return;
_height = value;
UpdateSize();
UpdateBounds();
}
float CharacterController::GetSlopeLimit() const
{
return _slopeLimit;
}
void CharacterController::SetSlopeLimit(float value)
{
value = Math::Clamp(value, 0.0f, 89.0f);
if (value == _slopeLimit)
return;
_slopeLimit = value;
if (_controller)
PhysicsBackend::SetControllerSlopeLimit(_controller, value);
}
CharacterController::NonWalkableModes CharacterController::GetNonWalkableMode() const
{
return _nonWalkableMode;
}
void CharacterController::SetNonWalkableMode(NonWalkableModes value)
{
if (_nonWalkableMode == value)
return;
_nonWalkableMode = value;
if (_controller)
PhysicsBackend::SetControllerNonWalkableMode(_controller, (int32)value);
}
float CharacterController::GetStepOffset() const
{
return _stepOffset;
}
void CharacterController::SetStepOffset(float value)
{
if (value == _stepOffset)
return;
_stepOffset = value;
if (_controller)
{
float height, radius;
GetControllerSize(height, radius);
PhysicsBackend::SetControllerStepOffset(_controller, Math::Min(value, height + radius * 2.0f - CC_MIN_SIZE));
}
}
void CharacterController::SetUpDirection(const Vector3& up)
{
_upDirection = up;
if (_controller)
PhysicsBackend::SetControllerUpDirection(_controller, up);
}
float CharacterController::GetMinMoveDistance() const
{
return _minMoveDistance;
}
Vector3 CharacterController::GetUpDirection() const
{
return _controller ? PhysicsBackend::GetControllerUpDirection(_controller) : _upDirection;
}
void CharacterController::SetMinMoveDistance(float value)
{
_minMoveDistance = Math::Max(value, 0.0f);
}
void CharacterController::SetAutoGravity(bool value)
{
_autoGravity = value;
}
Vector3 CharacterController::GetVelocity() const
{
return _controller ? PhysicsBackend::GetRigidDynamicActorLinearVelocity(PhysicsBackend::GetControllerRigidDynamicActor(_controller)) : Vector3::Zero;
}
bool CharacterController::IsGrounded() const
{
return (static_cast<int>(_lastFlags) & static_cast<int>(CollisionFlags::Below)) != 0;
}
CharacterController::CollisionFlags CharacterController::GetFlags() const
{
return _lastFlags;
}
CharacterController::CollisionFlags CharacterController::SimpleMove(const Vector3& speed)
{
const float deltaTime = Time::GetCurrentSafe()->DeltaTime.GetTotalSeconds();
Vector3 displacement = speed + _gravityDisplacement;
CollisionFlags result = Move(displacement * deltaTime);
if ((static_cast<int>(result) & static_cast<int>(CollisionFlags::Below)) != 0)
{
// Reset accumulated gravity acceleration when we touch the ground
_gravityDisplacement = Vector3::Zero;
}
else
_gravityDisplacement += GetPhysicsScene()->GetGravity() * deltaTime;
return result;
}
CharacterController::CollisionFlags CharacterController::Move(const Vector3& displacement)
{
CollisionFlags result = CollisionFlags::None;
if (_controller)
{
const float deltaTime = Time::GetCurrentSafe()->DeltaTime.GetTotalSeconds();
result = (CollisionFlags)PhysicsBackend::MoveController(_controller, _shape, displacement, _minMoveDistance, deltaTime);
_lastFlags = result;
Vector3 position = PhysicsBackend::GetControllerPosition(_controller) - _center;
SetPosition(position);
}
return result;
}
void CharacterController::Resize(float height, float radius)
{
const float heightDiff = height - _height;
const float radiusDiff = radius - _radius;
if (Math::IsZero(heightDiff) && Math::IsZero(radiusDiff))
return;
_height = height;
_radius = radius;
if (_controller)
{
float centerDiff = heightDiff * 0.5f + radiusDiff;
// Change physics size
GetControllerSize(height, radius);
PhysicsBackend::SetControllerSize(_controller, radius, height);
Vector3 positionDelta = _upDirection * centerDiff;
// Change physics position to maintain feet placement (base)
Vector3 position = PhysicsBackend::GetControllerPosition(_controller);
position += positionDelta;
_center += positionDelta;
PhysicsBackend::SetControllerPosition(_controller, position);
// Change actor position
_isUpdatingTransform = true;
SetPosition(position - _center);
_isUpdatingTransform = false;
}
UpdateBounds();
}
#if USE_EDITOR
#include "Engine/Debug/DebugDraw.h"
#include "Engine/Graphics/RenderView.h"
void CharacterController::DrawPhysicsDebug(RenderView& view)
{
Quaternion rotation = Quaternion::Euler(90, 0, 0);
const Vector3 position = _transform.LocalToWorld(_center);
if (view.Mode == ViewMode::PhysicsColliders)
DEBUG_DRAW_CAPSULE(position, rotation, _radius, _height, Color::LightYellow, 0, true);
else
DEBUG_DRAW_WIRE_CAPSULE(position, rotation, _radius, _height, Color::GreenYellow * 0.8f, 0, true);
}
void CharacterController::OnDebugDrawSelected()
{
Quaternion rotation = Quaternion::Euler(90, 0, 0);
const Vector3 position = _transform.LocalToWorld(_center);
DEBUG_DRAW_WIRE_CAPSULE(position, rotation, _radius, _height, Color::GreenYellow, 0, false);
if (_contactOffset > 0)
DEBUG_DRAW_WIRE_CAPSULE(position, rotation, _radius - _contactOffset, _height, Color::Blue.AlphaMultiplied(0.4f), 0, false);
#if 0
// More technical visuals debugging
if (_controller)
{
float height, radius;
GetControllerSize(height, radius);
DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(PhysicsBackend::GetControllerBasePosition(_controller), 5.0f), Color::Red, 0, false);
DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(PhysicsBackend::GetControllerPosition(_controller), 4.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(PhysicsBackend::GetControllerPosition(_controller) - 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(PhysicsBackend::GetControllerPosition(_controller) - Vector3(0, height * 0.5f, 0), radius), Color::Red.AlphaMultiplied(0.5f), 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(position, 3.0f), Color::GreenYellow, 0, false);
#else
if (_controller)
{
// Physics backend capsule shape
float height, radius;
GetControllerSize(height, radius);
DEBUG_DRAW_WIRE_CAPSULE(PhysicsBackend::GetControllerPosition(_controller), rotation, radius, height, Color::Blue.AlphaMultiplied(0.2f), 0, false);
}
#endif
// Base
Collider::OnDebugDrawSelected();
}
#endif
void CharacterController::CreateController()
{
// Create controller
ASSERT(_controller == nullptr && _shape == nullptr);
_cachedScale = GetScale().GetAbsolute().MaxValue();
float height, radius;
GetControllerSize(height, radius);
const Vector3 position = _transform.LocalToWorld(_center);
_controller = PhysicsBackend::CreateController(GetPhysicsScene()->GetPhysicsScene(), this, this, _contactOffset, position, _slopeLimit, (int32)_nonWalkableMode, Material, radius, height, _stepOffset, _shape);
// Setup
PhysicsBackend::SetControllerUpDirection(_controller, _upDirection);
PhysicsBackend::SetShapeLocalPose(_shape, Vector3::Zero, Quaternion::Identity);
UpdateLayerBits();
UpdateBounds();
}
void CharacterController::DeleteController()
{
if (_controller)
{
PhysicsBackend::DestroyController(_controller);
_controller = nullptr;
_shape = nullptr;
}
}
void CharacterController::UpdateSize() const
{
if (_controller)
{
float height, radius;
GetControllerSize(height, radius);
PhysicsBackend::SetControllerSize(_controller, radius, height);
}
}
void CharacterController::GetControllerSize(float& height, float& radius) const
{
height = Math::Abs(_height) * _cachedScale;
radius = Math::Abs(_radius) * _cachedScale;
{
// Exclude contact offset around the capsule (otherwise character floats in the air)
radius = radius - Math::Max(_contactOffset, 0.0f);
}
height = Math::Max(height, CC_MIN_SIZE);
radius = Math::Max(radius, CC_MIN_SIZE);
}
void CharacterController::CreateShape()
{
// Not used
}
void CharacterController::UpdateBounds()
{
_cachedScale = GetScale().GetAbsolute().MaxValue();
float height, radius;
GetControllerSize(height, radius);
const Vector3 position = _transform.LocalToWorld(_center);
const Vector3 extent(radius, height * 0.5f + radius, radius);
_box = BoundingBox(position - extent, position + extent);
BoundingSphere::FromBox(_box, _sphere);
}
void CharacterController::AddMovement(const Vector3& translation, const Quaternion& rotation)
{
Vector3 displacement = translation;
if (_autoGravity)
{
// Apply gravity
const float deltaTime = Time::GetCurrentSafe()->DeltaTime.GetTotalSeconds();
displacement += GetPhysicsScene()->GetGravity() * deltaTime;
}
Move(displacement);
if (!rotation.IsIdentity())
{
SetOrientation(GetOrientation() * rotation);
}
}
bool CharacterController::CanAttach(RigidBody* rigidBody) const
{
return false;
}
RigidBody* CharacterController::GetAttachedRigidBody() const
{
return nullptr;
}
void CharacterController::SetCenter(const Vector3& value)
{
if (value == _center)
return;
Vector3 delta = value - _center;
_center = value;
if (_controller)
{
// Change physics position while maintaining actor placement
Vector3 position = PhysicsBackend::GetControllerPosition(_controller);
position += _upDirection * delta;
PhysicsBackend::SetControllerPosition(_controller, position);
}
}
void CharacterController::OnActiveTransformChanged()
{
if (!_shape)
return;
// Change actor transform (but with locking)
ASSERT(!_isUpdatingTransform);
_isUpdatingTransform = true;
const Vector3 position = PhysicsBackend::GetControllerPosition(_controller) - _center;
SetPosition(position);
_isUpdatingTransform = false;
UpdateBounds();
}
void* CharacterController::GetPhysicsActor() const
{
return _shape ? PhysicsBackend::GetShapeActor(_shape) : nullptr;
}
void CharacterController::UpdateGeometry()
{
// Check if has no character created
if (_shape == nullptr)
return;
// Setup shape geometry
_cachedScale = GetScale().GetAbsolute().MaxValue();
UpdateSize();
}
void CharacterController::GetGeometry(CollisionShape& collision)
{
// Unused
}
void CharacterController::BeginPlay(SceneBeginData* data)
{
if (IsActiveInHierarchy())
CreateController();
// Skip collider base
Actor::BeginPlay(data);
}
void CharacterController::EndPlay()
{
// Skip collider base
Actor::EndPlay();
// Remove controller
DeleteController();
}
void CharacterController::OnActiveInTreeChanged()
{
// Skip collider base
Actor::OnActiveInTreeChanged();
}
void CharacterController::OnEnable()
{
if (_controller == nullptr)
CreateController();
Collider::OnEnable();
}
void CharacterController::OnDisable()
{
Collider::OnDisable();
DeleteController();
}
void CharacterController::OnParentChanged()
{
// Skip collider base
Actor::OnParentChanged();
}
void CharacterController::OnTransformChanged()
{
// Skip collider base
Actor::OnTransformChanged();
// Update physics
const Vector3 position = _transform.LocalToWorld(_center);
if (!_isUpdatingTransform && _controller)
{
PhysicsBackend::SetControllerPosition(_controller, position);
const float scale = GetScale().GetAbsolute().MaxValue();
if (_cachedScale != scale)
UpdateGeometry();
UpdateBounds();
}
else if (!_controller)
{
_box = BoundingBox(position);
BoundingSphere::FromBox(_box, _sphere);
}
}
void CharacterController::OnPhysicsSceneChanged(PhysicsScene* previous)
{
Collider::OnPhysicsSceneChanged(previous);
DeleteController();
CreateController();
}