I had created a very simple camera controller for the city hall interior scene. But it was not very usable in other scenes. So I rewrote the code and made it universal and a lot easier to use!
SimpleCameraFollow.cs (final code)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using AC;
public class SimpleCameraFollow : MonoBehaviour
{
//maybe in the future we need to set up support for the camera to pan left, right and up / down
//[SerializeField]
//private bool PanX = true;
//[SerializeField]
//private bool PanY = false;
[Header("Pan Settings")]
[SerializeField]
[Tooltip("The pan influence. The smaller the value, the less the player position affects the pan move. Can be used to make larger, sweeping motions. Use negative values for counter motion.")]
private float panAmount = 0.1f;
[SerializeField]
[Tooltip("The maximum amount the camera can pan left.")]
private float panMin = -5.0f;
[SerializeField]
[Tooltip("The maximum amount the camera can pan right.")]
private float panMax = 5.0f;
[Header("Truck Settings")]
[SerializeField]
[Tooltip("A multiplier for how much the player movement affects the camera movement. Use to make slower, more elegant camera trucking. Use negative value for counter motion.")]
private float truckAmount = 0.1f;
[SerializeField]
[Tooltip("The maximum amount the camera can move left.")]
private float truckMin = -1.0f;
[SerializeField]
[Tooltip("The maximum amount the camera can move right.")]
private float truckMax = 1.0f;
[SerializeField]
[Tooltip("The width of a dead zone at the 0 position for the camera, when target movement does not affect trucking at all.")]
private float truckDeadZone = 0.0f;
[Header("Time Settings")]
[SerializeField]
[Tooltip("The smoothnes of the camera movements. Slower value means faster movement.")]
private float smoothTime = 0.3F;
private GameObject followTarget;
private Vector3 originalPosition;
private Vector3 targetPosition;
private Vector3 lookDirection;
private Vector3 originalLookDirection;
private Quaternion originalRotation;
private Vector3 velocity = Vector3.zero;
private float velocityX = 0.0f;
private float distance;
private Vector2 distanceVector;
[Header("Debug values")]
[SerializeField]
private bool DrawDebug = true;
[SerializeField]
[Tooltip("The pan value the system is trying to reach. Look at this alue to inform you on Pan Settings.")]
private float panDebug;
[SerializeField]
[Tooltip("The truck value the system is trying to reach. Look at this alue to inform you on Truck Settings.")]
private float truckDebug;
private Quaternion minRotationDebug;
private Quaternion maxRotationDebug;
private float TruckMinDebug;
private float TruckMaxDebug;
private float flipPankDebug = 1.0f;
// Method to transform a local position to the world space
Vector3 LocalToWorld(Vector3 localPosition)
{
// Transform the local position to world space using the stored object's position and rotation
Vector3 worldPosition = originalPosition + (originalRotation * localPosition);
return worldPosition;
}
private bool firstRun = true;
// Start is called before the first frame update
void Start()
{
//save original camera position, rotation and look direction for setting the zero point for the camera
originalPosition = this.transform.position;
originalRotation = this.transform.rotation;
originalLookDirection = originalRotation.eulerAngles;
}
// Update is called once per frame
void Update()
{
if (DrawDebug == true)
{
if (truckAmount < 0)
{
TruckMaxDebug = truckMax;
TruckMinDebug = truckMin;
}
else
{
TruckMaxDebug = truckMin;
TruckMinDebug = truckMax;
}
if (panAmount < 0)
{
flipPankDebug = -1.0f;
}
else
{
flipPankDebug = 1.0f;
}
minRotationDebug = Quaternion.Euler(originalLookDirection.x, originalLookDirection.y - panMin * flipPankDebug, originalLookDirection.z);
maxRotationDebug = Quaternion.Euler(originalLookDirection.x, originalLookDirection.y - panMax * flipPankDebug, originalLookDirection.z);
//draw centerline
Debug.DrawLine(originalPosition, (originalPosition + (originalRotation * Vector3.forward * 1)));
//draw min angle
Debug.DrawLine(targetPosition = LocalToWorld(new Vector3(TruckMinDebug, 0, 0)), (targetPosition = LocalToWorld(new Vector3(TruckMinDebug, 0, 0)) + (minRotationDebug * Vector3.forward * 5)), new Color(1.0f, 0.0f, 0.0f));
//draw max angle
Debug.DrawLine(targetPosition = LocalToWorld(new Vector3(TruckMaxDebug, 0, 0)), (targetPosition = LocalToWorld(new Vector3(TruckMaxDebug, 0, 0)) + (maxRotationDebug * Vector3.forward * 5)), new Color(0.0f, 0.0f, 1.0f));
//Draw truck path
Debug.DrawLine(targetPosition = LocalToWorld(new Vector3(TruckMaxDebug, 0, 0)), LocalToWorld(new Vector3(TruckMinDebug, 0, 0)), new Color(0.0f, 1.0f, 0.0f));
//draw current heading
Debug.DrawLine(this.transform.position, (this.transform.position + (this.transform.rotation * Vector3.forward * 5)));
}
if (followTarget == null)
{
followTarget = KickStarter.player.gameObject;
}
//Figure out lookDirection for the camera.
lookDirection = Quaternion.LookRotation(followTarget.transform.position - originalPosition).eulerAngles;
//Calculate the offset vector of the camera target from the center point.
distanceVector = new Vector2(originalLookDirection[1] - lookDirection[1], originalLookDirection[0] - lookDirection[0]);
//Get new camera target position in world space.
targetPosition = LocalToWorld(new Vector3(ApplyDeadZone(Mathf.Clamp(distanceVector[0] * -truckAmount, truckMin, truckMax), truckDeadZone), 0, 0));
//after the first frame smoothly transition the camera to the target position and rotation:
if (firstRun == false)
{
//Calculate the pan distance for the camera with SmoothDamp
distance = Mathf.SmoothDamp(distance, Mathf.Clamp((distanceVector[0] * panAmount), panMin, panMax), ref velocityX, smoothTime);
//Apply camera position with SmoothDamp
this.transform.position = Vector3.SmoothDamp(this.transform.position, targetPosition, ref velocity, smoothTime);
}
//if the code runs for the first time, do instant camera transition to targer position and rotation:
else
{
//Calculate the pan distance for the camera instantly
distance = Mathf.Clamp((distanceVector[0] * panAmount), panMin, panMax);
//Apply camera position instantly
this.transform.position = targetPosition;
//mark first run as complete
firstRun = false;
}
//apply camera rotation:
this.transform.eulerAngles = new Vector3(this.transform.eulerAngles.x, originalRotation.eulerAngles.y - distance, this.transform.eulerAngles.z
);
//return debug values
truckDebug = distanceVector[0] * -truckAmount;
panDebug = distanceVector[0] * panAmount;
}
float ApplyDeadZone(float input, float threshold)
{
// If the input is within the dead zone range, set it to 0
if (Mathf.Abs(input) < threshold)
{
return 0f;
}
// Otherwise, return the original input
return input;
}
}
The original camera script worked in world space. Only moving the camera along a set world space axis. But what if the camera was angled to begin with, what if the axis I wanted it to move along was diagonal, or slanted? I needed to move the camera left / right regardless of its orientation. I knew I had to take my code and turn it into local space.
This was also my first script that I actually spent time time with the UX. Thinking of ways to make it as easy to set up as possible.
Using chatGPT for code
But in order to make it work, I first, I needed to get direction difference towards the character from the camera forward vector. I decided to ask ChatGPT about it.
Me
I need a c# script that makes a camera point at a target. In Euler space
Sure! Below is a simple C# script for Unity that makes a camera point at a target.
If you need the direction in Euler angles rather than as a quaternion, you can convert the quaternion to Euler angles using Quaternion.eulerAngles
. Here’s the script:
CameraController
using UnityEngine;
public class CameraController : MonoBehaviour
{
public Transform target; // The target to look at
void Update()
{
// Check if target is assigned
if (target != null)
{
// Calculate the direction from the camera to the target
Vector3 lookDirection = target.position - transform.position;
// Get the rotation that points the Z axis forward, and the Y axis upwards
Quaternion targetRotation = Quaternion.LookRotation(lookDirection);
// Convert the rotation to Euler angles
Vector3 targetEulerAngles = targetRotation.eulerAngles;
// Apply the rotation to the camera
transform.rotation = Quaternion.Euler(targetEulerAngles.x, targetEulerAngles.y, targetEulerAngles.z);
}
else
{
Debug.LogWarning("No target assigned to CameraController.");
}
}
}
In this script:
targetRotation
: This variable holds the rotation that points the camera toward the target.targetEulerAngles
: This variable holds the Euler angles of thetargetRotation
.transform.rotation = Quaternion.Euler(targetEulerAngles.x, targetEulerAngles.y, targetEulerAngles.z)
: Here, we set the camera’s rotation using the Euler angles extracted fromtargetEulerAngles
.
With this script, the camera will still continuously point at the assigned target, but the rotation will be represented in Euler angles.
With this information I could simply subtract the point at Euler.x from the stored camera rotation.x to get the difference. I could then use this value to move the camera sideways as needed and make the camera pan sideways as well. (I captured the offset in a vector2 so that I would be prepared for eventually moving the camera up and down as well.)
Me
how about local position to world position using stored transform data
Certainly! Here’s how you can transform a local position to the world space using the stored transform data of the object:
LocalToWorld
using UnityEngine;
public class LocalToWorld : MonoBehaviour
{
// Variables to store the position and rotation of the object
public Vector3 objectPosition;
public Quaternion objectRotation;
void Start()
{
// Store the object's position and rotation
objectPosition = transform.position;
objectRotation = transform.rotation;
// Example: Transforming a local position to the world space
Vector3 localPosition = new Vector3(2f, 3f, 1f); // Example local position
Vector3 worldPosition = LocalToWorld(localPosition);
Debug.Log("World Position: " + worldPosition);
}
// Method to transform a local position to the world space
Vector3 LocalToWorld(Vector3 localPosition)
{
// Transform the local position to world space using the stored object's position and rotation
Vector3 worldPosition = objectPosition + (objectRotation * localPosition);
return worldPosition;
}
}
In this script:
objectPosition
andobjectRotation
are variables that store the position and rotation of the object. You can set these variables to the desired values in the Unity Editor or through script.Start()
method initializes the variablesobjectPosition
andobjectRotation
with the object’s current position and rotation.LocalToWorld(Vector3 localPosition)
method takes a local position as input and returns the equivalent world position. It calculates the world position by transforming the local position into world space using the stored object’s position and rotation.
You can adjust the localPosition
variable in the Start()
method to test different local positions and see how they are transformed into the world space.
The little snippets of code ChatGPT was able to provide me helped immensely! I did not need to google for anything. I simply copy pasted these functions into my own code and they just worked and did exactly what I wanted them to do! Yay!
// Method to transform a local position to the world space
Vector3 LocalToWorld(Vector3 localPosition)
{
// Transform the local position to world space using the stored object's position and rotation
Vector3 worldPosition = objectPosition + (objectRotation * localPosition);
return worldPosition;
}
UX and debug
Once I had a working camera setup I wanted to spend some time making it easy to set-up. This part had very little help from the AI, but I already had most of the building blocks I needed.
Tooltips
Basic stuff, but still a thing I do not usually bother to make. But this time I felt like I wanted to make the tool a bit easier for me to use. I added some headers and tooltips. It is was a good exercise if nothing more!
The code for the headers and tooltips is very simple. just a few lines of additional code.
[Header("Truck Settings")]
[SerializeField]
[Tooltip("A multiplier for how much the player movement affects the camera movement. Use to make slower, more elegant camera trucking. Use negative value for counter motion.")]
private float truckAmount = 0.1f;
After I had gone the extra step of adding tooltips, I got to thinking, what other things could I add to make using the tool nicer?
Debug lines
The easy winner was debug lines. Lines that explain the function of the camera feature easily and make it easy to see what is going on and why.
This was a lot more difficult to program than the tooltips. But nothing too hard. I just had to calculate the start and end points for all of the lines. for the pan angles it was a tad more difficult, but I managed to figure it out.
The blue line in the image is the farthest the camera can truck left, and the red line is the right extend. The Green line is the path the camera will take.
The blue and red lines are tilted to show the angle the camera will land at the line. The short white line from around the center of the green line visualises the original position of the camera in the scene. The long while line from the camera shows the current heading.
These do not seem like much, but thy are a massive help in visualising what the camera will do when it is moving in the scene! After these were added, tweaking the camera motion per scene became a breeze!
Leave a Reply