Player speed changes based on camera rotation

I’m currently converting the unity 3d 3rd person controller starter assets into my own game, and I’ve made a lot of changes to the code but for the most part (vast exaggeration) its been going smoothly. However, I’ve run into this problem where for some reason whenever the camera is looking up or down, the character moves slowly, but when it looks straight, the character moves faster. It also affects jump height the same way, where if you look up or down you have lower jumpheight than if you were looking straight

I’ve narrowed it down to this line of code

Vector3 inputDirection = new Vector3(_input.moveInput.x, 0.0f, _input.moveInput.y).normalized;
        inputDirection = inputDirection.x * cameraTransform.right.normalized + inputDirection.z * cameraTransform.forward.normalized;

Specifically the inputdirection.z * cameratransform.forward.normalized, because whenever I take that out the character doesnt change speed when the camera moves up or down, but they can only move left or right.

Here’s the rest of the code if you’re interested:

    private void Move()
    {
        // set target speed based on move speed, sprint speed and if sprint is pressed
        float targetSpeed = playerStats.currentSpeed;
        // if there is no input, set the target speed to 0
        if (_input.moveInput == Vector2.zero)
            targetSpeed = 0.0f;

        // a reference to the players current horizontal velocity
        float currentHorizontalSpeed = new Vector3(_controller.velocity.x, 0.0f, _controller.velocity.z).magnitude;

        float speedOffset = 0.1f;
        float inputMagnitude = _input.analogMovement ? _input.moveInput.magnitude : 1f;

        // accelerate or decelerate to target speed
        if (currentHorizontalSpeed < targetSpeed - speedOffset || currentHorizontalSpeed > targetSpeed + speedOffset)
        {
            // creates curved result rather than a linear one giving a more organic speed change
            // note T in Lerp is clamped, so we don't need to clamp our speed
            _speed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed * inputMagnitude, Time.deltaTime * SpeedChangeRate);

            // round speed to 3 decimal places
            _speed = Mathf.Round(_speed * 1000f) / 1000f;
        }
        else
        {
            _speed = targetSpeed;
        }

        // normalise input direction
        Vector3 inputDirection = new Vector3(_input.moveInput.x, 0.0f, _input.moveInput.y).normalized;
        inputDirection = inputDirection.x * cameraTransform.right.normalized + inputDirection.z * cameraTransform.forward.normalized;

        Quaternion targetRotation = Quaternion.Euler(0, cameraTransform.eulerAngles.y, 0);
        transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, 5 * Time.deltaTime);

        // move the player
        _controller.Move(inputDirection.normalized * (_speed * Time.deltaTime) + new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime); //Cinemago?
    }

And here’s the _input.moveInput:

    public void OnMove(InputValue value)
    {
        MoveInput(value.Get<Vector2>());
    }
    public void MoveInput(Vector2 newMoveDirection)
    {
        moveInput = newMoveDirection;
    }

And lastly here’s a video:

If anyone could help with this I would really appreciate that. Thanks in advance!

funny, just yesterday I answered a question with the opposite effect where looking up and down sped everything up. He/she didn’t make the code framerate independent, but calculated the direction vectors correctly.

The problem with your code is that when you look down, cameraTransform.forward also points down and you’ll only go forward with a projection of that vector onto the plane. I’m just gonna steal the lines from the other thread ( Perfect movement towards camera forward )

var verticalAxis = new Vector3(mainCamera.transform.forward.x, 0, mainCamera.transform.forward.z).normalized;
var horizontalAxis = new Vector3(mainCamera.transform.right.x, 0, mainCamera.transform.right.z).normalized;

// We'll use this in the lines you posted above:
Vector3 inputDirection = new Vector3(_input.moveInput.x, 0.0f, _input.moveInput.y).normalized;
        inputDirection = inputDirection.x * horizontalAxis + inputDirection.z * verticalAxis;

Maybe you have to switch around the two if something is awkward. Please note that .normalized is not necessary on the forward, right and up vectors because they’re already normalized.

tjmaul, thanks so much! This works really well, and while I was looking for a solution I actually found the post you were talking about and tried using that, but to no avail. Thanks alot for the reply, and if it isn’t too much, do you think you could explain to me what this code does compared to my old code? I’m still learning so I’d like to understand as much as possible. Thanks alot anyways though!

I can! Let’s go through the code in question and I’ll point out some problems:

Vector3 inputDirection = new Vector3(_input.moveInput.x, 0.0f, _input.moveInput.y).normalized;

This is somewhat dangerous because “.normalized” tries to scale the vector3 to a length of 1. This is not defined for a Vector3(0,0,0), but I think the Vector3 class just filters out very small values and returns Vector3.zero. That means, that even for very small inputs, for example when using a Gamepad with analog sticks, inputDirection will always snap to “full throttle” in whatever direction you press the stick.

Better use Vector3.ClampMagnitude (and you should probably do that wherever “moveInput” is written, not in the Move-Code):

Vector3 inputDirection = Vector3.ClampMagnitude(new Vector3(_input.moveInput.x, 0.0f, _input.moveInput.y), 1);

Now to the next line, which is the actual culprit here:

inputDirection = inputDirection.x * cameraTransform.right + inputDirection.z * cameraTransform.forward;

First of all, I think this code is hard to understand because inputDirection is set twice and is referencing itself. Just use an extra variable. Now what causes the issue is that you’re using cameraTransform.forward, which, when you look up or down, points into or out of the X/Z plane. In the scene view, select the camera GameObject, switch to “Local” to see in which direction the blue (forward) arrow is pointing. Imagine the blue arrow casting a shadow onto the ground (with an imaginary sun exactly from above). This shadow will shrink the more you look up or down and will continue to shrink to a point when you look exactly up or down. This projection is what the CharacterController does, too, because the character can only move in the X/Z-plane. (link for reference: https://en.wikipedia.org/wiki/Vector_projection)

So my approach - optimized for readability - would be:

Vector3 projectedForward = Vector3.ProjectOnPlane(cameraTransform.forward, Vector3.up);

Vector3 forward = projectedForward.normalized;
Vector3 right = cameraTransform.right;

Vector3 moveDirection = right * input.x + forward * input.y;

(for reference: https://docs.unity3d.com/ScriptReference/Vector3.ProjectOnPlane.html)

Thanks for the reply, sorry I couldn’t get back to you sooner. This definitely helps me understand more anyways, and that’s all I could ask for considering I’m still learning.

1 Like