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 the targetRotation.
  • transform.rotation = Quaternion.Euler(targetEulerAngles.x, targetEulerAngles.y, targetEulerAngles.z): Here, we set the camera’s rotation using the Euler angles extracted from targetEulerAngles.

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 and objectRotation 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 variables objectPosition and objectRotation 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

Your email address will not be published. Required fields are marked *

Recent Posts


Archive


Social Links