Using Vector3.Dot to move at different speeds on slopes

I am using the in-built character controller and I have a SphereCast for slopes and if a slope is too steep, then my player slides down it. But when I am within the slope limit, I am trying to make my player move at a slower speed if moving up the slope which I can’t get quite right.

private Vector3 hitPointNormal;
public float radius;
RaycastHit slopeHit;
float slope;

public bool isSliding
    {
        get
        {
            if (cc.isGrounded && Physics.SphereCast(transform.position, radius, -transform.up, out slopeHit, 2f))
            {
                hitPointNormal = slopeHit.normal;
                return Vector3.Angle(hitPointNormal, Vector3.up) > cc.slopeLimit;
            }
            else
            {
                return false;
            }
        }
    }

For movement I just use standard controller movement:

Vector3 move = transform.forward * z + transform.right * x;
move *= speed;
controller.Move(velocity * Time.deltaTime);

if (Input.GetKey(KeyCode.LeftShift))
{
    speed = sprintSpeed;
}
else
{
    speed = walkSpeed;
}

I have been trying to use Vector3.Dot and then using that to alter my speed, but there has been no results even with debugging.

void FixedUpdate()
    {
        RaycastHit hit;
        if (Physics.SphereCast(transform.position, .25f, Vector3.down, out hit, 3f))
        {
            slope = Vector3.Dot(transform.forward, hit.normal);
        }
    }
  1. Recognize slope angle in front of your character. Raycast, spherecast or capsulecast, then take a signed angle (in relation to player X axis) from the hit normal.
  2. Adjust speed in relation to angle

I think I figured it out. Late reply but I was doing some learning on SignedAngle.

void FixedUpdate()
    {
        RaycastHit hit;

        if (Physics.SphereCast(transform.position, .5f, Vector3.down, out hit, 2f))
        {
            float angle = Vector3.SignedAngle(transform.position, transform.forward, Vector3.up);
            if (angle > 0.1f)
            {
              
                Debug.Log("Moving downwards");
                walkSpeed = 4f;
                sprintSpeed = 7f;
            }
            else if (angle < 0.1f)
            {
                Debug.Log("Moving upwards");
                walkSpeed = 1f;
                sprintSpeed = 1f;
            }
        } 
    }

While I am standing on a slope, this seems to work better than Vector3.Dot. However my issue is that even if I am on a flat surface (like a standard plane object), my speed constantly changes between the values above despite not being on a slope. I’ve run debugging but I don’t see anything wrong with my SphereCast.

Take the signed angle between hit.normal (the normal of the ground at that point) and Vector3.up, using playertransform.right as the axis component. This will output how much the ground normal is tilted (rotated around the character’s X) in relation to your character’s walking direction.

Yeah I realised that I should have had transform.right instead of forward and I believe I understand what you mean. I replaced that and I also swapped my SphereCast for a Raycast and I used the hit.normal instead of just the transform.position, but even though it all works fine when on a slope, from the very start of runtime it is still acting as if I’m moving upwards/downwards even when I am idle and not moving my player.

if (Physics.Raycast(transform.position, Vector3.down, out hit, 2f))
        {
            float angle = Vector3.SignedAngle(hit.normal, transform.right, Vector3.up);

I’m taking the signed angle between hit.normal and Vector3.up and am now using my player transform.right but I’m stumped as to why that one issue is still persisting. I’m actively looking at different applications of SignedAngle online but to me I can’t spot the problem even after updating my code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Platformer : MonoBehaviour
{
    CharacterController cc;

    Vector3 playerVelocity;

    void Start()
    {
        cc=GetComponent<CharacterController>();
    }

    void Update()
    {
        Vector3 moveDirection=new Vector3(Input.GetAxis("Horizontal"),0,Input.GetAxis("Vertical"));
        if (Input.GetKey(KeyCode.LeftShift))
            moveDirection*=2;
        playerVelocity+=moveDirection;
        if (cc.isGrounded==false)
            playerVelocity.y-=9.8f;    // gravity
        else
        {
            RaycastHit hit;
            if (Physics.SphereCast(transform.position,0.5f,Vector3.down,out hit,5f))
            {
                if (Vector3.Dot(hit.normal,Vector3.up)<0.99f)   // slope?
                    playerVelocity=Vector3.ProjectOnPlane(playerVelocity,hit.normal);
            }
        }

        playerVelocity*=0.8f;   // basic friction
        cc.Move(playerVelocity*Time.deltaTime);
    }
}

I don’t think this can work even in theory because you are taking a dot product of hit.normal and Vector3.up and the player direction is not a part of the formula. Then you can’t distinguish between upward and downward slope, because this is the same unless you count in which way the player is facing.

Read again SignedAngle API. Your second and third parameter are the wrong way around. You need to get the angle between UP and the ground normal, expressed as an angle using the player.right axis.

(Btw I’m sure of this solution because it’s a part of a pro character controller library I am using, and I just coded this a few days back :))

Yeah I was looking through the docs and I realised where I went wrong. Thanks a lot for the continued help I appreciate it and it’s helped me know a little bit more about SignedAngle.

The issue was also how I was applying the speed penalty so I fixed that too.

1 Like

Sorry this is a bit of a late reply. I’ve tweaked a few values and everything works great, but is there a way to only have that Raycast SignedAngle check activate only when I’m on a slope and within the controller slope limit?

Currently when I begin runtime, the Raycast is constantly checking the SignedAngle and it’s disrupting certain mechanics/movements I have. For example I have a flat object (basically a small ground area) that slows the player via trigger, but due to the SignedAngle raycast, that trigger is not working properly.

Edit: I managed to fix it by adding a bool to prevent constant angle checks while on a flat surface.

The SignedAngle is the slope check. On a flat surface the value is 0 because ground normal is 0. The raycast doesn’t have any side effects. It’s up to you what you want to do with this value.