Jerky camera?

Hi! Can I get some help with this script? When I use it and move the player around, in any axis, the camera is weirdly jerky. It isn’t jerky relative to the player, only relative to the scenery/background. Thanks!

using UnityEngine;
using System.Collections;

public class PrototypeCameraController : MonoBehaviour {

    public Transform player; //player position to track
    public Camera cam; // the camera following the player
    public float ratio; // how following speed scales
    public float smoothf; //how fast the camera follows
    public float looky; // how high up the camera looks at the player
    public float distancey; // how far above the player the camera follows
    public float distancex; // how far behind the payer the camera follows
    float x; // gap, x, y, and z are storage variables
    float y;
    float z;
    float gap;

    // Use this for initialization
    void Start () {
   
    }
   
    // Update is called once per frame
    void LateUpdate () {
        if (y != player.position.y + distancey){
            y = Vector3.MoveTowards(cam.transform.position, player.position + new Vector3 (0, distancey, 0), Time.deltaTime).y;
        }
        else y = player.position.y + distancey;
        if (Vector3.Distance(cam.transform.position - new Vector3 (0, cam.transform.position.y,0), player.position - new Vector3 (0, player.position.y,0) ) > distancex){
            gap = Vector3.Distance(cam.transform.position - new Vector3 (0, cam.transform.position.y,0), player.position - new Vector3 (0, player.position.y,0)) - distancex;
            x = Vector3.MoveTowards(cam.transform.position, player.position, Mathf.Pow(smoothf, (gap*ratio))*Time.deltaTime).x;
            z = Vector3.MoveTowards(cam.transform.position, player.position, Mathf.Pow(smoothf, (gap*ratio))*Time.deltaTime).z;
            cam.transform.position = new Vector3(x,y,z);
        }
        else{
            cam.transform.position = new Vector3(cam.transform.position.x, y, cam.transform.position.z);
        }
        cam.transform.LookAt(player.position + new Vector3(0,looky,0));
    }
}

Change MoveTowards to Lerp.

Just did that, still appears to be jerky.

I’ll also note that the camera isn’t jerky when it’s still and just rotating to look at the player, or when the player (and therefore the camera) is moving fast enough.

  gap = Vector3.Distance(cam.transform.position - new Vector3 (0, cam.transform.position.y,0), player.position - new Vector3 (0, player.position.y,0)) - distancex;
Mathf.Pow(smoothf, (gap*ratio));

That can give wildly different values. frame to frame; anything to a power of 0 (for when Gap becomes 0) would return 1, while a power ~0 would return a number close to 0. this is because of the special rules of powers. as the exponent approaches 0 the result approaches 0 faster. N^0 =1, but also 0^N = 0 as well.

This is why 0^0 is considered NAN or undefined (because its both 0 and 1). In general either answer can be accepted, whichever is more convenient for you, but in general (including most power functions outside of statistics applications) its commonly agreed that 0^0 = 1

since it appears you want the camera to maintain a ring around the player you can try and use SmoothDamp

public Transform player;
    public float cameraXzSmooth = 0.85f;
    public float cameraYSmooth = 0.15f;
    public float maxXySpeed = 15f;
    public float maxYSpeed = 50f;
    public float XYdistance = 3f;
    public float heightOffset = 2;
    public Vector3 targetLookOffset = new Vector3(0,1,0);


    private Vector3 XZVelocity;
    private Vector3 YVelocity;

    void LateUpdate ()
    {
        Vector3 playerLookOffset = player.position + targetLookOffset;

        //initalize without the y data, so that XZ movement is simplified and consistent
        Vector3 playerXZposition = Vector3.Scale(player.position, new Vector3(1,0,1));
        Vector3 cameraTarget = Vector3.Scale(transform.position, new Vector3(1,0,1));

        //holds the xz direction and thus also distance the camera is from player
        Vector3 playerDirection = playerXZposition - cameraTarget;

        //ensures the camera gravitates to a position along a XZ ring around the player
        //too far camera moves in, too close camera moves out
        Vector3 cameraTargetXZPosition = playerXZposition - playerDirection.normalized * XYdistance;

        //xz movement
        cameraTarget = Vector3.SmoothDamp(cameraTarget,cameraTargetXZPosition,ref XZVelocity,cameraXzSmooth,maxXySpeed);

        // y movement: overwrite the y data from the previous smoothdamp
        cameraTarget.y = Mathf.SmoothDamp(transform.position.y,player.position.y + heightOffset,ref YVelocity,cameraYSmooth, maxYSpeed);


        transform.position = cameraTarget;
        transform.LookAt(playerLookOffset);
    }

Huh! I’m not exactly certain how this code works, but it does seem to do what I want! I’d like to know how exactly each bit works in detail, though, because just having the code handed to me feels like cheating. Oh, also, I had to slightly modify it to get it to compile - instead of

cameraTarget.y = Mathf.SmoothDamp(transform.position.y,player.position.y + heightOffset,ref YVelocity,cameraYSmooth, maxYSpeed);

I had to change the Vector 3 YVelocity to a float, YVelocity.y.

I guess the first and most important thing to understand how this code works is understanding that Vector3 can represent different concepts of data It may represent a position, a distance/offset (meters), a direction (northwest), a velocity (speed), or or even a force/impulse (acceleration). there are a few math rules you can use to convert from one concept to another. I’m sure you’re familiar with most of these rules as you have some in your own code, but I’ll cover ones used just in case.

  • positionB - positionA = offsetAB (i.e. where B is in relation to A)
  • offsetAB.nomalize = directionAB. an offset of any non-zero length gets set to a total offset of 1. specifically for when you just want to know which direction something is, not how far away.
  • directionAB * float = offsetAB with a magnitude of “float”. so if float is 5 and direction is east it means you’ll get a vector that will offset a postion by 5 units to the east
  • positionA + offsetAB = postionB. likewise positionB - offsetAB = positionA

these rules are simple to understand and easy to memorize but are massively important if you want to do anything in unity’s coordinate system. In this code I use most of these and use them interchangibly. And that’s all this code does: mess with different types of vectors through these rules.

with this rules in mind here how the code is broken up

Vector3 playerLookOffset = player.position + targetLookOffset;

simply calculates a point in space where the camera will be looking at in relation to the player. its not going to look directly at the player but at a specific point offset from its pivot. “playerLookOffset” is not the best name in regards to the rules I pointed out, as this is actually a position, not an offset as it follows Rule 4.

 //initalize without the y data, so that XZ movement is simplified and consistent
        Vector3 playerXZposition = Vector3.Scale(player.position, new Vector3(1,0,1));
        Vector3 cameraTarget = Vector3.Scale(transform.position, new Vector3(1,0,1));

to keep the math simple (since the Y motion is independent of the xz motion) I toss out the Y values by setting them to 0 so that they don’t muddy the calculation SmoothDamp may try to do later on. If you didn’t the camera will move much slower sometimes since most of its motion could be in the Y which would get overwritten later. The way I did this is by using scale to practically zero out the values of y (so that as far as the math is concerned its working on a 2d plane). explicitly setting the y to zero does the same thing. Looking back, “cameraTarget” is probably not the best name either as its simply stores where calculations for the camera current position for this frame, not where the camera is trying to go to.

//holds the xz direction and thus also distance the camera is from player
        Vector3 playerDirection = playerXZposition - cameraTarget;

here I calculate the offset (on the xz plane) the camera would have to go to match the player’s position (this is using Rule 1). if I take the negative of this value (as I do on the next line) it gets me the inverse direction. or the direction the player would have to go to reach the camera. notice that the vector has not been normalized so it also contains the distance between the player and camera. at the time of writing I wasn’t sure if I would need the offset (finding the direction from offset is easy, but you can’t get an offset with just a direction), so I kept it as an offset.

//ensures the camera gravitates to a position along a XZ ring around the player
        //too far camera moves in, too close camera moves out
        Vector3 cameraTargetXZPosition = playerXZposition - playerDirection.normalized * XYdistance;

here is where we figure out where the camera wants to be along the XZ plane. this single line of code is actually using the rules 2, 3, and 4 (in that order) to find the target position for the camera.

The camera could be hundreds of units away from the player. we want it to be 3 units away, so we normalize the playerdirection (Rule 2) giving a distance of one and then multiply it by the distance we want (Rule 3). now remember playerDirection is from the camera to the player, and since we want to move from the player to the camera we make the playerDirection negative. and then add that to the player’s XZ position (read it as position plus a negative direction or Rule 4).

Looking over this code I noticed theres a potential error that can happen with this math which I’ll talk about later after i finish this code break down. anyway we now have the xz position we want the camera to be at, now its simply the animation to take it there.

//xz movement
        cameraTarget = Vector3.SmoothDamp(cameraTarget,cameraTargetXZPosition,ref XZVelocity,cameraXzSmooth,maxXySpeed);

SmoothDamp is an easing method that speeds up towards a target (with an optional max speed which is being used here) and slows down when it gets near. its best used when its not important for the target to be reached in an explicit time, especially if the target is constantly changing. so Smooth damp is perfect for camera following for this reason. theres a another optional field which its not being used here (where you can set the deltaTime) which I find to be much more powerful at controlling the acceleration than smooth time so if you need more control over the acceleration you can change that field just remember to also adjust the maxspeed to compensate since it’ll also be affected. Anyway for this line its simply doing a smooth damp for the XZ motion of the camera.

 // y movement: overwrite the y data from the previous smoothdamp
        cameraTarget.y = Mathf.SmoothDamp(transform.position.y,player.position.y + heightOffset,ref YVelocity,cameraYSmooth, maxYSpeed);

with the XZ motion done I then do the y motion independently. doing so allows the XZ motion and Y motion to travel at independent velocities.

in regards to that one potential error.

its playerDirection.normalized. if the Camera and the player are on the same position then that math will fail since it will have a vector3 of (0,0,0); you can’t normalize a zero vector because you don’t know which direction it was looking.

basically normalization of a vector does this:

float xyzSum = abs(x) + abs(y) + abs(z);
newVector.x = x/xyzSum;
newVector.y = y/xyzSum;
newVector.z = z/xyzSum;
return newVector;

but since xyzSum is 0 you’d get a divide by 0 error (or the error that unity would throw which is something along the lines of “can’t normalize a zero vector”). so there needs to be a graceful degradation in the event that playerDirection is zero. so we’ll provide a fallback. We’ll use the Camera’s current forward (converted to the XZ plane) as a fallback, and if the camer’a direction is also invalid we’ll default to a world direction

        //holds the xz direction and thus also distance the camera is from player
        Vector3 playerDirection = playerXZposition - cameraTarget;
        //if cameraTarget is too close to playerXZposition normalization will have issues
        if(playerDirection.sqrMagnitude<0.01f)
        {
            playerDirection = Vector3.Scale(transform.forward,new Vector3(1,0,1);

            //if the camera itself is looking mostly up/down then default to a world direction
            if(playerDirection.sqrMagnitude<0.01f)
            {
                playerDirection = Vector3.forward;
            }
        }

if you get the world default a lot you could expand further by storing a “lastValidDirection”. but at that point you’d probably be better off tweaking any speeds or code you currently have thats somehow causing the camera to consitently sit on top of the player position

Extremely helpful! Thank you so very much. I didn’t know about the Smoothdamp function, that’s essentially what I did. And the camera doesn’t ever really sit on top of the player, but I’ll be sure to have that failsafe just in case it ever gets there somehow. Thanks much! <3