Hello there lovely people.
im working on a character controller of mine.
and i cannot figure out how to limit movement on slopes.
My Character controller uses a rigidbody to move.
ive tried to modify the calculateDesiredMoveDirection function to limit the movement in the direction but that didnt work correctly. i could jump into a wall and the player would still accelerate up the slope.
Here would be the character controller script i wrote.
i hope you could somehow help me out. <3
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.Events;
public class characterController : MonoBehaviour
{
[Header("General data")]
public Camera camera;
[Header("Movement Parameters")]
public float maxSpeed = 8f;
public float acceleration = 200f;
public AnimationCurve accelerationFactorFromDotCurve;
public float deceleration = 200f;
public float accelMod = 1f;
public float decelMod = 1f;
public float turnSpeed = 10f;
public AnimationCurve rotationSpeedByMoveSpeed;
public float maxGroundAngle = 45f;
[Header("Jump Parameters")]
public float targetJumpHeight = 5.0f;
public float cyoteTime = 0.1f;
public float bufferedJumpTime = 0.1f;
public float deccendGravityMultiplier = 2f;
[Header("Float Parameters")]
public float rideHeight = 0.8f;
public float maxRideHeight = 1f;
public float groundedDistance = 1f;
public float rideSpringStrength = 1f;
public float rideSpringDamper = 1f;
public LayerMask groundLayerMask;
[Header("state info")]
public state currentState = state.baseState;
public bool isMoving;
public bool isJumping = false;
public bool cancelJump = false;
public bool isGrounded = true;
public enum state
{
baseState,
slideSlope
};
public RaycastHit groundHit;
//private fields
private Rigidbody rb;
private float goalSpeed;
private Vector2 inputDir;
private Vector3 moveDirection;
private Vector3 lookDirection;
private bool wasGrounded;
private float lastGroundTime;
private float lastJumpInputTime;
private void Awake()
{
rb = GetComponent<Rigidbody>();
}
private void Start()
{
lastJumpInputTime = -Mathf.Infinity;
lookDirection = transform.forward;
}
private void FixedUpdate()
{
isGrounded = CheckGrounded(out groundHit);
//state switcher
switch (currentState)
{
case state.baseState:
break;
}
//state logic
switch (currentState)
{
case state.baseState:
calculateDesiredMoveDirection();
isMoving = moveAndRotatePlayer();
if (Time.time - lastJumpInputTime <= bufferedJumpTime)
{
doJump();
}
if (isGrounded)
{
hoverAboveGround(groundHit);
}
else
{
if (wasGrounded && !isGrounded)
{
rb.velocity = new Vector3(rb.velocity.x, 0, rb.velocity.z);
}
if (rb.velocity.y < 0 || cancelJump)
{
updateGravityMultiplier(deccendGravityMultiplier);
}
}
break;
}
if(moveDirection != Vector3.zero)
{
lookDirection = moveDirection;
}
wasGrounded = isGrounded;
}
public void getMovementInput(InputAction.CallbackContext context)
{
inputDir.x = context.ReadValue<Vector2>().x;
inputDir.y = context.ReadValue<Vector2>().y;
}
public void calculateDesiredMoveDirection()
{
float moveHorizontal = inputDir.x;
float moveVertical = inputDir.y;
Vector3 camForward = camera.transform.forward;
camForward.y = 0f;
camForward.Normalize();
Vector3 desiredMoveDirection = (camForward * moveVertical) + (camera.transform.right * moveHorizontal);
moveDirection = desiredMoveDirection.normalized;
if (Vector3.Angle(Vector3.up, groundHit.normal) > maxGroundAngle)
{
Vector3 slopeDir = Vector3.ProjectOnPlane(-getGroundSlope(), Vector3.up);
moveDirection -= slopeDir.normalized;
}
moveDirection.Normalize();
}
private float calculateRotationSpeedFactor()
{
float currentSpeed = rb.velocity.magnitude;
float normalizedSpeed = Mathf.Clamp01(currentSpeed / maxSpeed);
return normalizedSpeed;
}
private bool moveAndRotatePlayer()
{
bool isMoving = false;
float idealSpeed = moveDirection.magnitude * maxSpeed;
float speedDifference = idealSpeed - goalSpeed;
Vector3 unitVel = rb.velocity.normalized;
Vector3 unitGoal = moveDirection.normalized;
float velDot = Vector3.Dot(unitGoal, unitVel);
if (speedDifference > 0)
{
goalSpeed = Mathf.Min(goalSpeed + ((acceleration * accelMod) * accelerationFactorFromDotCurve.Evaluate(velDot)) * Time.deltaTime, idealSpeed);
}
else if(speedDifference < 0)
{
if (!isJumping)
{
goalSpeed = Mathf.Max(goalSpeed - (deceleration * decelMod) * Time.deltaTime, idealSpeed);
}
}
Quaternion targetRotation = Quaternion.LookRotation(lookDirection);
float turnSpeedFactor = rotationSpeedByMoveSpeed.Evaluate(calculateRotationSpeedFactor());
rb.rotation = Quaternion.Lerp(rb.rotation, targetRotation, Time.fixedDeltaTime * ((turnSpeed * accelMod) * turnSpeedFactor));
if (goalSpeed > Mathf.Epsilon)
{
isMoving = true;
Vector3 idealVel = transform.forward * goalSpeed;
Vector3 neededAccel = (idealVel - rb.velocity) / Time.fixedDeltaTime;
rb.AddForce(new Vector3(neededAccel.x, 0, neededAccel.z), ForceMode.Acceleration);
}
else
{
Vector3 velocity = rb.velocity;
velocity.x = 0;
velocity.z = 0;
rb.velocity = velocity;
}
return isMoving;
}
private void hoverAboveGround(RaycastHit groundHit)
{
if (groundHit.distance <= maxRideHeight)
{
float distanceToGround = groundHit.distance;
Vector3 upForce = Vector3.up * (rideHeight - distanceToGround) * rideSpringStrength;
Vector3 dampingForce = new Vector3(0f, -rb.velocity.y, 0f) * rideSpringDamper;
rb.AddForce(upForce + dampingForce);
if(groundHit.rigidbody != null)
{
Vector3 groundVel = groundHit.rigidbody.velocity;
groundVel.y = 0;
Vector3 platformAngularVelocity = groundHit.rigidbody.angularVelocity;
// Calculate player's position relative to rotation axis (assuming axis is centered)
Vector3 relativePosition = transform.position - groundHit.rigidbody.position;
// Calculate tangential velocity based on angular velocity and relative position
Vector3 tangentialVelocity = Vector3.Cross(platformAngularVelocity, relativePosition);
// Add tangential velocity to player's velocity, scaled by a factor
rb.velocity += tangentialVelocity + groundVel;
groundHit.rigidbody.AddForceAtPosition(-(upForce / 2) + dampingForce, groundHit.point);
}
}
}
public void triggerJump(InputAction.CallbackContext context)
{
if (context.performed)
{
lastJumpInputTime = Time.time;
}
else if (context.canceled)
{
if (isJumping)
{
cancelJump = true;
}
}
}
public void doJump()
{
if (!(isGrounded || Time.time - lastGroundTime <= cyoteTime) || isJumping)
{
return;
}
isGrounded = false;
isJumping = true;
float jumpForce = calculateJumpForce();
rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
}
private float calculateJumpForce()
{
float initialVelocity = Mathf.Sqrt(2 * Mathf.Abs(Physics.gravity.y) * targetJumpHeight);
return initialVelocity * rb.mass;
}
private bool CheckGrounded(out RaycastHit groundHit)
{
RaycastHit hit;
if (Physics.Raycast(transform.position, Vector3.down, out hit, Mathf.Infinity, groundLayerMask))
{
groundHit = hit;
if (isGrounded)
{
isJumping = false;
cancelJump = false;
return hit.distance <= maxRideHeight;
}
else
{
lastGroundTime = Time.time;
return hit.distance <= groundedDistance;
}
}
groundHit = hit;
return false;
}
private Vector3 getGroundSlope()
{
Vector3 gravityDirection = Physics.gravity.normalized;
Vector3 slopeDirection = Vector3.ProjectOnPlane(gravityDirection, groundHit.normal).normalized;
return slopeDirection;
}
private void updateGravityMultiplier(float multiplier)
{
rb.AddForce(Physics.gravity * multiplier, ForceMode.Acceleration);
}
}