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