So, I’ve pretty much finished the script I was working on. I’d like opinions to see if this code is too similar to CMP. For reference, I read though the CMP code, completely reverse-engineered how it works, and remade it using some similar methods in Flax Engine. I fully understand how all of the code works, and I directly copied 0 lines of code. I plan to release this code under the Zlib license. I’m hoping to get an opinion from someone who works at Epic.
(Note: It’s not 100% completed, but this gives an idea of what the script looks like)
float CharacterControllerPro::GetCurrentBrakingForce() const
{
if (!_useSeperateBrakingForce)
{
return _brakingForce;
}
switch (_movementMode)
{
case MovementMode::Walking:
return _walkingBrakingForce;
case MovementMode::Falling:
return _walkingBrakingForce;
default:
break;
}
return _brakingForce;
}
bool CharacterControllerPro::CanJump() const
{
bool wasRecentlyOnGround = _coyoteTimer > 0;
bool canJumpInAir = _airJumpCount < _airJumpsAllowed;
bool canJump = _jumpingEnabled && (wasRecentlyOnGround || canJumpInAir);
return canJump || _jumpingOverrideEnabled;
}
void CharacterControllerPro::CalculateVelocity()
{
// Add any extra displacement we did to the last position
Vector3 lastRelevantPosition = _lastPosition + _additionalDisplacement;
// Calculate velocity based on our previous position
_linearVelocity = (GetActor()->GetPosition() - lastRelevantPosition) / Time::GetDeltaTime();
// Subtract the floor's velocity
_linearVelocity -= _appliedFloorDelta / Time::GetDeltaTime();
_lastPosition = GetActor()->GetPosition();
_additionalDisplacement = Vector3::Zero;
}
void CharacterControllerPro::ApplyPlatformVelocity()
{
// Only apply platform leave velocity if that setting is enabled
if (_applyVelocityOnPlatformLeave && !_platformLeaveDelta.IsZero())
{
_characterController->Move(_platformLeaveDelta);
}
}
void CharacterControllerPro::UpdateFloorInfo()
{
_isFloorDetected = false;
_floorInfo = RayCastHit();
RayCastHit _hitResult;
Vector3 sphereCenter = GetActor()->GetPosition();
sphereCenter.Y -= (_characterController->GetHeight() / 2);
sphereCenter.Y += _characterController->GetRadius() - 1;
_isFloorDetected = Physics::SphereCast(sphereCenter, _characterController->GetRadius(), -_characterController->GetUpDirection(), _hitResult, _characterController->GetRadius() + _characterController->GetStepOffset(), _groundLayer);
if (_isFloorDetected)
{
//_isOnFloor = true;
_floorInfo = _hitResult;
return;
}
_tryingToDetachFromGorund = false;
}
void CharacterControllerPro::UpdateFloorAnchor()
{
if (!_stickToMovingPlatforms)
{
_floorAnchor = nullptr;
_appliedFloorDelta = Vector3::Zero;
return;
}
_platformLeaveDelta = _appliedFloorDelta;
if (!_isOnFloor)
{
_floorAnchor = nullptr;
_appliedFloorDelta = Vector3::Zero;
return;
}
Actor* oldFloorAnchor = _floorAnchor;
if (_floorInfo.Collider != nullptr)
{
_floorAnchor = _floorInfo.Collider;
}
// If we're now on a new surface, reset our floor transform cache
if (!oldFloorAnchor || _floorInfo.Collider != oldFloorAnchor)
{
_floorAnchorLastPosition = _floorAnchor->GetPosition();
_floorAnchorLastOrientation = _floorAnchor->GetOrientation();
}
Transform currentFloorTransform = Transform(_floorAnchor->GetPosition(), _floorAnchor->GetOrientation());
Transform lastFloorTransform = Transform(_floorAnchorLastPosition, _floorAnchorLastOrientation);
bool floorOrientationChanged = !Quaternion::NearEqual(_floorAnchorLastOrientation, _floorAnchor->GetOrientation());
_floorAnchorLastPosition = _floorAnchor->GetPosition();
_floorAnchorLastOrientation = _floorAnchor->GetOrientation();
if (floorOrientationChanged && _rotateWithMovingPlatforms)
{
// Get the difference between the floor's current orientation and last orientation
Vector3 deltaEuler = (currentFloorTransform.Orientation * Quaternion::Invert(lastFloorTransform.Orientation)).GetEuler();
// Prevent the floor from causing us to rotate using pitch or roll
deltaEuler.X = 0;
deltaEuler.Z = 0;
// Apply the rotation
GetActor()->SetOrientation(GetActor()->GetOrientation() * Quaternion::Euler(deltaEuler));
// Update our look rotation to reflect the rotation of the floor
Vector3 newLookEuler = GetLookOrientation().GetEuler() + deltaEuler;
SetLookOrientation(Quaternion::Euler(newLookEuler));
}
// Get our current world position relative to the floor's old transform
Vector3 oldFloorPos = lastFloorTransform.WorldToLocal(GetActor()->GetPosition());
// Determine what our world position would be if we were on the same spot relative to the floor's current transform
Vector3 newFloorPos = currentFloorTransform.LocalToWorld(oldFloorPos);
// Find out how much we need to move to get o that new relative position
Vector3 floorAnchorPositionDelta = (newFloorPos - GetActor()->GetPosition());
Vector3 previousPos = GetActor()->GetPosition();
_characterController->Move(floorAnchorPositionDelta);
// Our linear velocity shouldn't take into account any velocity from floor
_appliedFloorDelta = (GetActor()->GetPosition() - previousPos);
}
void CharacterControllerPro::StopJump()
{
_isJumping = false;
}
bool CharacterControllerPro::ShouldStopJump() const
{
if (_maxJumpHoldTime == 0)
{
return true;
}
if (_jumpTime >= _maxJumpHoldTime)
{
return true;
}
return false;
}
void CharacterControllerPro::MoveJumping()
{
// Only jump if the player recently pressed the jump button
bool wantsToJump = _jumpBufferTimer > 0 || _justPressedJump;
if (wantsToJump && CanJump() && !_isJumping)
{
// Reset our jump timers
_jumpBufferTimer = 0;
_jumpTime = 0;
_isJumping = true;
// If we were on the ground, apply platform velocity
if (_coyoteTimer > 0 || _isOnFloor)
{
ApplyPlatformVelocity();
_coyoteTimer = 0;
}
else
{
// We were in the air, so increment our air jump count
_airJumpCount += 1;
}
}
_justPressedJump = false;
if (_isJumping)
{
// Prevent floor snappping from keeping us on the ground
_tryingToDetachFromGorund = true;
// Reset our falling velocity
_linearVelocity.Y = 0;
_movementMode = MovementMode::Falling;
_isOnFloor = false;
_jumpTime += Time::GetDeltaTime();
// Apply jump force
Vector3 jumpForce = Vector3(0, _jumpForce, 0);
_characterController->Move(jumpForce * Time::GetDeltaTime());
if (ShouldStopJump())
{
StopJump();
}
}
_jumpBufferTimer = Math::Max(_jumpBufferTimer - Time::GetDeltaTime(), 0.0f);
if (_isOnFloor)
{
_coyoteTimer = _coyoteTime;
_airJumpCount = 0;
}
else
{
_coyoteTimer = Math::Max(_coyoteTimer - Time::GetDeltaTime(), 0.0f);
}
}
Vector3 CharacterControllerPro::GetDesiredVelocity() const
{
return Vector3::ClampLength(_movementInputVector * _maxSpeed, _maxSpeed);
}
Vector3 CharacterControllerPro::AccelerateToVelocity(const Vector3& currentVelocity, const Vector3& desiredVelocity, float maxAccel)
{
// Find out how much acceleration we need to reach our desired velocity in a single fixed update
Vector3 requiredAcceleration = (desiredVelocity - currentVelocity) / Time::GetDeltaTime();
// Clamp it to the supplied max accel
requiredAcceleration = Vector3::ClampLength(requiredAcceleration, maxAccel);
return requiredAcceleration * Time::GetDeltaTime();
}
Vector3 CharacterControllerPro::CalculateFrictionVector(const Vector3& velocity, float friction, const Vector3& desiredMoveDirection)
{
Vector3 frictionAmount = velocity;
// Calculate friction direction from desired movement direction
if (!desiredMoveDirection.IsZero())
{
// If a desired direction is supplied, use it as our friction amount
Vector3 frictionDirection = desiredMoveDirection.GetNormalized() * velocity.Length();
frictionAmount = (velocity - frictionDirection);
}
// Limit friction amount so it doesn't move us backward
float frictionMultiplier = Math::Clamp(Time::GetDeltaTime() * friction, 0.f, 1.f);
// Subtract our new friction vector from velocity
return velocity - (frictionAmount * frictionMultiplier);
}
Vector3 CharacterControllerPro::CalculateNewMovement(const Vector3& currentVelocity, const Vector3& desiredVelocity, float friction, float maxBrakingForce)
{
// Brake only if the character wants to stop or if we're moving faster than our MaxSpeed
bool shouldBrake = _movementInputVector.IsZero() || currentVelocity.Length() > _maxSpeed;
Vector3 desiredDirection = shouldBrake ? Vector3::Zero : desiredVelocity.GetNormalized();
float accel = shouldBrake ? maxBrakingForce : _maxAcceleration;
// Apply friction
Vector3 newVelocity = CalculateFrictionVector(currentVelocity, friction * _frictionMultiplier, desiredDirection);
// Apply it to our new velocity
newVelocity += AccelerateToVelocity(currentVelocity, desiredVelocity, accel);
if (!shouldBrake)
{
return newVelocity;
}
// Don't allow deceleration to lower our speed below 0
if (Vector3::Dot(newVelocity, currentVelocity) < 0)
{
newVelocity = Vector3::Zero;
}
// If we started above max speed, don't allow deceleration to lower us below max speed
else if (currentVelocity.Length() > _maxSpeed && Vector3::Dot(currentVelocity, desiredDirection) > 0 && newVelocity.Length() < _maxSpeed)
{
newVelocity = currentVelocity.GetNormalized() * _maxSpeed;
}
return newVelocity;
}
void CharacterControllerPro::HandleWalkOffFloor()
{
_movementMode = MovementMode::Falling;
ApplyPlatformVelocity();
if (_tryingToDetachFromGorund)
{
return;
}
// Don't include the floor snapping in our velocity
Vector3 floorSnapDisplacement = _lastFloorSnapDisplacement;
_additionalDisplacement += floorSnapDisplacement;
}
void CharacterControllerPro::MoveWalking()
{
// Calculate movement velocity
Vector3 movementDelta = CalculateNewMovement(_linearVelocity, GetDesiredVelocity(), _walkingFriction, GetCurrentBrakingForce());
// Turn our new velocity vector into a displacement vector
movementDelta *= Time::GetDeltaTime();
// Apply our movement
_characterController->Move(movementDelta);
_isOnFloor = _isFloorDetected;
if (!_isOnFloor)
{
HandleWalkOffFloor();
return;
}
// Snap character to floor
if (!_tryingToDetachFromGorund)
{
Vector3 floorSnapDisplacement = Vector3::Up * -250.f * Time::GetDeltaTime();
Vector3 oldPosition = GetActor()->GetPosition();
_characterController->Move(floorSnapDisplacement);
_lastFloorSnapDisplacement = GetActor()->GetPosition() - oldPosition;
}
}
void CharacterControllerPro::MoveFalling()
{
Vector3 lateralVelocity = _linearVelocity;
lateralVelocity.Y = 0;
// Calculate lateral movement velocity
Vector3 movementDelta = CalculateNewMovement(lateralVelocity, GetDesiredVelocity(), _fallingFriction, GetBrakingForce());
// Turn our new velocity vector into a displacement vector
movementDelta *= Time::GetDeltaTime();
//movementDelta += new Vector3(0, -50 * Time.DeltaTime, 0);
_characterController->Move(movementDelta);
Vector3 gravityVelocity = Vector3::Zero;
gravityVelocity.Y = GetVelocity().Y;
if (!(_isJumping && _disableGravityWhileHoldingJump))
{
// Sync our gravity with the physics system's gravity
Vector3 gravityAmount = Physics::GetGravity() * _gravityScale;
bool shouldUseFalloff = _useJumpGravityFalloff && GetVelocity().Y > _gravityFalloff;
if (shouldUseFalloff)
{
gravityAmount *= _gravityFalloffMultiplier;
}
gravityVelocity.Y += gravityAmount.Y * Time::GetDeltaTime();
}
// Apply gravity
_characterController->Move(gravityVelocity * Time::GetDeltaTime());
_isOnFloor = _characterController->IsGrounded();
if (_isOnFloor)
{
_movementMode = MovementMode::Walking;
}
}
void CharacterControllerPro::RotateCharacter()
{
if (_rotateTowards == RotationMode::NoRotation)
{
return;
}
float desiredYaw;
switch (_rotateTowards)
{
case RotationMode::RotateTowardsDesiredVelocity:
if (_lastMovementInputVector == Vector3::Zero)
{
return;
}
Vector3 lateralVelocity = Vector3(_lastMovementInputVector.X, 0, _lastMovementInputVector.Z);
desiredYaw = Quaternion::LookRotation(lateralVelocity.GetNormalized(), Vector3::Up).GetEuler().Y;
break;
case RotationMode::RotateTowardsLookOrientation:
desiredYaw = GetLookOrientation().GetEuler().Y;
break;
default:
return;
}
if (_characterRotationVelocity == 0)
{
GetActor()->SetOrientation(Quaternion::Euler(0, desiredYaw, 0));
return;
}
float newYaw = Math::MoveTowardsAngle(GetActor()->GetOrientation().GetEuler().Y, desiredYaw, _characterRotationVelocity * Time::GetDeltaTime());
GetActor()->SetOrientation(Quaternion::Euler(GetActor()->GetOrientation().GetEuler().X, newYaw, GetActor()->GetOrientation().GetEuler().Z));
}
bool CharacterControllerPro::CheckIfActorDataIsValid(bool logMessage) const
{
if (_characterController == nullptr)
{
if (GetActor() != nullptr)
{
if (logMessage)
{
auto errorMsg = TEXT("ChacterControllerPro script must be attached to a CharacterController actor, instead attached to: {}. (Of type {})");
DebugLog::LogError(String::Format(errorMsg, GetActor()->GetName(), GetActor()->GetType().ToString()));
}
return false;
}
if (logMessage)
{
DebugLog::LogError(TEXT("CharacterControllerPro is unable to find a valid CharacterController."));
}
return false;
}
return true;
}
void CharacterControllerPro::OnAwake()
{
}
void CharacterControllerPro::OnStart()
{
// Here you can add code that needs to be called when script is enabled (eg. register for events)
}
void CharacterControllerPro::OnEnable()
{
Actor* actor = GetActor();
_characterController = Cast<CharacterController>(actor);
CheckIfActorDataIsValid(true);
}
void CharacterControllerPro::OnDisable()
{
_characterController = nullptr;
_customMovementFunction.Unbind();
// Reset velocity
_movementInputVector = Vector3::Zero;
_lastMovementInputVector = Vector3::Zero;
_lastPosition = GetActor()->GetPosition();
_linearVelocity = Vector3::Zero;
_additionalDisplacement = Vector3::Zero;
_appliedFloorDelta = Vector3::Zero;
_platformLeaveDelta = Vector3::Zero;
// Reset jump
_isJumping = false;
_jumpTime = 0;
_coyoteTimer = 0;
_jumpBufferTimer = 0;
_justPressedJump = false;
_airJumpCount = 0;
_floorAnchor = nullptr;
}
void CharacterControllerPro::OnFixedUpdate()
{
if (!CheckIfActorDataIsValid())
{
return;
}
MoveJumping();
UpdateFloorInfo();
UpdateFloorAnchor();
// Calculate movement based on our movement mode
switch (_movementMode)
{
case MovementMode::Stopped:
break;
case MovementMode::Walking:
MoveWalking();
break;
case MovementMode::Falling:
MoveFalling();
break;
case MovementMode::Custom:
if (_customMovementMode == nullptr)
{
DebugLog::LogWarning(TEXT("No custom movement mode specified in CharacterControllerPro."));
break;
}
_customMovementMode->MoveCustom();
break;
}
CalculateVelocity();
// Reset inputs
_lastMovementInputVector = _movementInputVector;
_movementInputVector = Vector3::Zero;
RotateCharacter();
}