Third Person Camera - primary orbit script logic keeps overriding other position logic

I have a third person camera script that allows the player to orbit the character, sort of like an RPG. I’m trying to add a convenience command (called hOrbitSnapInput) to allow the player to press a button to have the camera “reset” behind the player.

My problem is when the player tries to execute the hOrbitSnapInput. It will change the position of the camera for a split second (probably just one frame?) and then return right back to its previous position.

Here’s a video of what’s going on: https://youtu.be/uP91hr_FeNc.

  • At about the 0:11 second mark you see
    the camera change position and switch
    right back to it’s original positon.
  • At 0:21 I add logic to stop executing the Update() method after I send the orbit snap input to make sure that my orbit snap input logic did work at 0:31.

Here is my camera script:

public class CameraOrbit : MonoBehaviour
{
    public Transform player;
    public float distance = 5.0f;
    private float distanceFromGround = 1.5f;
    float xAxis;
    float yAxis;
    bool hOrbitSnapInput;
    bool snapped = false;
    const float speed = 5.0f;
    public float minVerticalAngle = -15f;
    public float maxVerticalAngle = 60f;

    void Update()
    {
        //if (hOrbitSnapInput)
        //    return;

        var behindPlayerPosition = player.position + player.forward * -distance;
        hOrbitSnapInput = Input.GetButtonDown("X360_RightStickClick");
        Debug.DrawLine(player.position, behindPlayerPosition, Color.red);

        if (hOrbitSnapInput)
        {
            xAxis = 0;
            yAxis = 0;

            // Camera position
            transform.position = 
                behindPlayerPosition + new Vector3(0.0f, distanceFromGround, 0.0f);

            transform.LookAt(player);
        }
        else
        {
            xAxis += Input.GetAxis("X360_RightStickX") * speed;
            yAxis -= Input.GetAxis("X360_RightStickY") * speed;

            // Restrict camera vertical rotation movement
            yAxis = Mathf.Clamp(yAxis, minVerticalAngle, maxVerticalAngle);

            var rotation = Quaternion.Euler(yAxis, xAxis, 0.0f);

            // Camera position
            transform.position = 
                player.position + rotation * new Vector3(0.0f, distanceFromGround, -distance);

            // Camera lookat rotation
            transform.rotation = rotation;
        }
    }
}

Here is my camera setup:

Here is my player setup:

Well from looking at it, I see when you reset the camera you do 2 things:

  • You set the xAxis and yAxis values to 0
  • You force the camera to a specific location

However you then continue as normal with some different maths in the normal case the following frames.

This would be fine if the act of setting ‘xAxis’ and ‘yAxis’ to 0 had exactly the same result as you forcing the camera to the ‘behind’ position. If that were true, then when the normal orbit took over it’d look exactly right.

The problem is that I doubt very much if that’s the case. I can’t tell you exactly what to write from looking at it, but I would recommend you structure your code more like this:

void Update()
{

	//first we will do our 'controls' logic, which modifies our xAxis and yAxis values based on what the player does
	hOrbitSnapInput = Input.GetButtonDown("X360_RightStickClick");
	if (hOrbitSnapInput)
	{
		//you will need to work out what the right values are for xAxis and yAxis to 'reset' nicely
		xAxis = 0;
		yAxis = 0;
	}
	else
	{
		//update xAxis and yAxis based on input
		xAxis += Input.GetAxis("X360_RightStickX") * speed;
		yAxis -= Input.GetAxis("X360_RightStickY") * speed;
		yAxis = Mathf.Clamp(yAxis, minVerticalAngle, maxVerticalAngle);
	}

	//now that xAxis and yAxis are updated, we will use them to generate a new transform
	var rotation = Quaternion.Euler(yAxis, xAxis, 0.0f);
 
	// Camera position
	transform.position = player.position + rotation * new Vector3(0.0f, distanceFromGround, -distance);
 
	// Camera lookat rotation
	transform.rotation = rotation;
}

The reason I structure it this way is that I have divided your code into 2 sections:

  • The bit that does the controls
  • The math that turns the results of the controls into a camera position

By doing this I’ve made it impossible for your controls to inadvertently use different maths in different scenarios. You will still need to work out what a nice pair of values to reset xAxis and yAxis to are, but you can at least now be sure that when you do reset them, the controls logic will continue on correctly from whatever you reset them to.

Here’s what I ended up doing, it seems to work pretty well.

I wish I could understand why the other script wouldn’t work, I think I’m missing out on some fundamental understanding on what/how properties can and can’t be assigned.

using UnityEngine;

public class CameraOrbit : MonoBehaviour
{
    public Transform player;
    public float distance = 5.0f;
    public float minVerticalAngle = -15f;
    public float maxVerticalAngle = 60f;
    float distanceFromGround = 1.5f;
    float xAxis;
    float yAxis;
    const float speed = 5.0f;
    Vector3 originAngle;

    void Start()
    {
        originAngle = GetOrthographicTop(player.forward);
    }

    void LateUpdate()
    {
        var hOrbitSnapInput = Input.GetButtonDown("X360_RightStickClick");

        if (hOrbitSnapInput)
        {
            float cameraAngle = SignedAngle(originAngle, GetOrthographicTop(transform.forward)) * -1;
            float playerAngle = SignedAngle(originAngle, GetOrthographicTop(player.forward));

            xAxis = xAxis + playerAngle + cameraAngle;         
            yAxis = 0;
        }
        else
        {
            xAxis += Input.GetAxis("X360_RightStickX") * speed;
            yAxis -= Input.GetAxis("X360_RightStickY") * speed;

            // Restrict camera vertical rotation movement
            yAxis = Mathf.Clamp(yAxis, minVerticalAngle, maxVerticalAngle);
        }

        var rotation = Quaternion.Euler(yAxis, xAxis, 0.0f);

        // Camera "lookat" rotation
        transform.rotation = rotation;

        // Camera position
        transform.position = player.position + rotation * new Vector3(0.0f, distanceFromGround, -distance);
    }

    float SignedAngle(Vector3 a, Vector3 b)
    {
        var angle = Vector3.Angle(a, b);

        // assume the sign of the cross product's Y component:
        return angle * Mathf.Sign(Vector3.Cross(a, b).y);
    }

    Vector3 GetOrthographicTop(Vector3 vector)
    {
        return new Vector3(vector.x, 0, vector.z);
    }
}