< wrapper_element_attributes= >

Camera tracking

How to get two moving objects on camera at once in Unity

Each game with 3rd person player controls and the ability to focus enemies or other objects needs a camera tracking method which follows both the player and the focused object at once. This tutorial shows you a very simple and easy to implement solution of that problem. At first we’ll implement simple 3rd person controls and after that we’ll show you the camera tracking trick 🙂

Thanks for reading and I hope you’ll enjoy the article!

Download Files

Simple Player Controls

In order to be able to test the enemy focus strategy we need to implement simple player controls. Soon we’ll release another tutorial about the implementation of more complex player controls. For now the player should only be able to move and look around. The control system we are going to implement should fulfill the following requirements:

  • Rotation

    The player can freely rotate the camera around the protagonist by moving the mouse.

  • Zoom

    By scrolling the mouse wheel the camera zooms in and out.

  • Movement

    By pressing the arrow keys or WASD the protagonist moves in the camera’s viewing direction

Camera Tracking - Simple camera controler and player movement

Camera Controller

The camera controller is responsible for zooming in and out and for the rotation around the so called target (in our case the protagonist). The camera should also follow the protagonist’s movements. To do so, create a new script called “CameraController.cs” within your scripts-directory and insert the following code snippet. The code comments describe each line in detail.

using UnityEngine;
using System.Collections;

namespace TFGames
{
    // This is a very simple camera controller. It follows a target-object,
    // rotates around it and is able to zoom in and out
    //
    // Attach the script to your main camera
    public class CameraController : MonoBehaviour
    {
        [Tooltip("The target is the object the camera rotates around")]
        public Transform target;
        [Tooltip("Sensitivity of the camera rotation. Higher values mean a faster rotation")]
        public float rotationSpeed = 10;
        [Tooltip("Boundaries for X-rotation of the camera")]
        public Interval tiltBoundaries;
        [Tooltip("Maximum and minimum zoom level")]
        public Interval zoomLevelBoundaries;
        [Tooltip("Sensitivity of the camera zoom")]
        public float zoomSpeed;


        // The direction from the camera position to the target
        private Vector3 lookDirection;
        // Input variables for rotation and zoom
        private float cameraXInput, cameraYInput, zoomInput;
        // Current vertical rotation of the camera
        private float tilt;

        // Linear zoom level. Will be transformed into an exponential value
        private float zoomLevel = 0.0f;
        // Transformed zoom level
        private float zoomValue = 1.0f;

        // Hides the transformation of the zoom level into the zoom value
        // When the zoom level is set, the zoom value is automatically calculated
        public float ZoomLevel
        {
            get { return zoomLevel; }
            set
            {
                // Recalculate only if zoom level changed
                if (zoomLevel != value)
                {
                    zoomLevel = value;

                    // Consider zoom level boundaries and calculate zoom value
                    zoomLevel = Mathf.Clamp(zoomLevel, zoomLevelBoundaries.Min, zoomLevelBoundaries.Max);
                    zoomValue = Mathf.Pow(1.1f, -zoomLevel);
                }
            }
        }

        private void Start()
        {
            // In the beginning calculate the look direction to prevent
            // the Vector3 to be zero
            lookDirection = target.position - transform.position;
        }

        private void Update()
        {
            // Ask and store user input at each frame
            cameraXInput = Input.GetAxisRaw("Mouse X") * rotationSpeed;
            cameraYInput = Input.GetAxisRaw("Mouse Y") * rotationSpeed;
            zoomInput = Input.GetAxisRaw("Mouse ScrollWheel") * zoomSpeed;
        }

        private void FixedUpdate()
        {
            // Set position and rotation of the camera
            // Quaternion.LookRotation transforms a direction Vector3 into a rotation
            transform.position = CalcCameraPos();
            transform.rotation = Quaternion.LookRotation(target.position - transform.position);
        }

        // Calculates the new camera position. Considers mouse- and zoom-input
        private Vector3 CalcCameraPos()
        {
            // New look direction is based on the input camera rotation
            lookDirection = Quaternion.Euler(0.0f, cameraXInput, 0.0f) * lookDirection;
            // Normalize look direction
            lookDirection.Normalize();

            // Update camera tilt and check tilt-boundaries
            tilt -= cameraYInput;
            tilt = Mathf.Clamp(tilt, tiltBoundaries.Min, tiltBoundaries.Max);

            // Recalculate zoom value
            ZoomLevel += zoomInput;

            // Calculate the new camera position. Steps:
            //  - Place it at the target's position
            //  - Rotate it towards the target and consider camera tilting
            //  - then move the camera backwards by the zoom value
            return target.position - Quaternion.LookRotation(new Vector3(lookDirection.x, 0.0f, lookDirection.z))
                * Quaternion.Euler(tilt, 0.0f, 0.0f) * Vector3.forward * 4.0f * zoomValue;
        }
    }
}

Now attach the script to the “MainCamera” in the hierarchy. Set the player in your scene as the target of the camera and configure the rest of the settings as you like. As a result your component’s settings should be something like this:

Camera Tracking - Camera controler Unity setup

Now by moving the mouse the camera rotates around the protagonist and by scrolling the mouse wheel the camera zooms in and out.

Player Movement

The next step is to let the player move around in the scene. The camera should follow. To do so create a new script called “PlayerMovement.cs” and insert the code snippet below. Again the code comments describe each line in detail.

using UnityEngine;
using System.Collections;

namespace TFGames
{
    // This is a rudimentary player movement controller, which moves the player
    // "away" from the camera. Therefore if the player presses the forward-button,
    // the protagonist moves to the look direction of the camera.
    [RequireComponent(typeof(Rigidbody))]
    public class PlayerMovement : MonoBehaviour
    {
        [Tooltip("Speed of the player movement")]
        public float speed;

        // Contains current camera position and rotation
        private Transform camera;
        // A reference to the player's rigidbody
        private Rigidbody rigidBody;
        // A vector storing the current velocity of the player
        private Vector3 velocityInput;

        void Start()
        {
            // Find the main camera and get a reference to the player's rigidbody
            camera = Camera.main.transform;
            rigidBody = GetComponent

Attach the script to the "Player" in the hierarchy of your scene. The script automatically builds a connection to the camera. That's why you don't have to drag the "Main Camera" from your scene into the script-component. Make sure that the player has a Rigidbody attached and the rotation in all directions is freezed. Lastly define the protagonist's speed. As a result the configuration should look like this:

Camera Tracking - Player movement Unity setup

Now you can move the player around in the scene by using the arrow keys or WASD together with your mouse. Keep in mind, that collisions and other advanced functionalities are not considered in this tutorial. A tutorial about all of that will be released soon!

Building the framework

If the protagonist in your game should be able to focus nearby enemies or other objects, it is crucial that the camera tracks both the player and the other object at once. If a focused object isn't visible, the camera should zoom out until the object appears on the screen. The difficult part of tracking two objects at once is that it doesn't only depend on the distance between the player and the other object, but also on the camera's viewing angle. For example if the player and his enemy are far away from each other but the camera films in a sharp angle, both are still rendered and no correction of the camera's zoom value is required.

Camera Viewing Angle

Script Interactions

This section may be a bit complicated, because it requires the interaction between multiple scripts. But the following list and picture give a great overview of the scripts and their purposes:

  • IFocusable

    Besides enemies, in games it is often possible to also focus objects like a sign, a harmless NPC or a specific item. Therefore an interface "IFocusable.cs" abstracts the ability to focus objects of all kind.

  • Enemy

    To make things more representative we will implement a simple enemy class which has no effect (in the next tutorial we'll implement a simple enemy AI). In order to make it focusable the enemy script realizes the IFocusable-interface.

  • FocusSelectionClient

    To keep things simple, we directly implement the 'selection' of an enemy in the "FocusSelectionClient". Later in the article we'll use the strategy pattern to make it possible to easily switch between different selection strategies.

  • CameraController

    In the previous chapter we already created the "CameraController"-script. Now we'll add some more code. The script always keeps the selected enemy and the player on screen at once. Therefore the camera controller asks the FocusSelectionClient if there is a focusable object somewhere nearby.

Camera Tracking - Camera Tracking Script Overview

Focusable Interface

IFocusable is an interface which defines an object as focusable. This may be an enemy, a sign, NPCs or some specific items. If we wouldn't create this interface, the same code needed to be written for each focusable object again and again.

using UnityEngine;
using System.Collections;

namespace TFGames
{
    // Since not only enemies may be able to be focused, an IFocusable
    // interface is defined which can be realized by either enemies or
    // other focusable objects
    public interface IFocusable
    {
        // The focus point of the object may not always be exactly the position of
        // that object. That's why each focusable object can specify its own
        // focus point
        Vector3 FocusPoint
        { get; }

        // A reference to the GameObject of the focusable object
        GameObject GameObject
        { get; }
    }
}

Enemy

This class is left empty in this chapter. It should only be a representative to demonstrate the concept. We'll implement a simple AI in the next tutorial. For now it is enough to realize the IFocusable-interface:

using UnityEngine;
using System.Collections;

namespace TFGames
{
    // In this example enemies realize the IFocusable-interface. In practice
    // every arbitrary object can be made focusable by realizing the interface
    public class Enemy : MonoBehaviour, IFocusable
    {
        // In this case the focus point of the enemy is equal to its position
        public Vector3 FocusPoint
        {
            get { return transform.position; }
        }

        // Return the reference to the gameObject
        public GameObject GameObject
        {
            get { return gameObject; }
        }

        // ...
        // Enemy AI implementation
        // ...
    }
}

Focus Selection Client

Later the FocusSelectionClient will choose a focus selection strategy. This means that it chooses from a set of heuristics. Each heuristic selects a specific focusable object in the scene based on different parameters like the player's and camera's position. For now we manually choose the selected enemy via the Unity editor. So in this chapter we wont implement a heuristic at all, because we want to demonstrate the camera tracking algorithm at frist.

using UnityEngine;
using System.Collections;

namespace TFGames
{
    // The FocusSelectionClient-class selects a specific enemy out of a horde of enemies
    // based on a selection strategy.
    public class FocusSelectionClient : MonoBehaviour
    {
        // To demonstrate how two objects can be tracked at once by the camera,
        // the enemy is not selected by an algorithm but manually within the Unity editor
        public Enemy enemy;

        // Returns the currently focused object (may be null!)
        public IFocusable Focus
        { get { return enemy; } }
    }
}

Tracking two objects at once

In the normal case the camera directly films the player. Now when the player focuses an object the camera should film the point directly in the middle between the player and the other object as shown in the image below.

Camera Tracking - Track two Objects at once

The problem which may occur is, that only one or even none of the two objects are visible on the screen anymore. In that case the camera needs to zoom out to extend the visible area. Look at the following illustration to get a better understanding of the problem:

Camera Tracking - Principle of Tracking two Objects

The Problem’s Solution

The solution of the problem is easier than expected. We simply use the objects' viewport distance (check out the information block about viewport points down below) to approximately estimate if both objects are on screen or not. For example if the viewport distance is bigger than 1 you can be pretty sure that the objects are only partly visible. The answer to this issue is to zoom out till such time as the viewport distance reaches a specific value, 0.85 for instance. The following illustration should demonstrate that:

Camera Tracking - Use Viewport Distance to track two Objects

To implement the explained solution, only add a few more lines in the "CameraController.cs". We marked the changed lines in the following code snipped:

using UnityEngine;
using System.Collections;

namespace TFGames
{
    // This is a very simple camera controler. It follows a target-object,
    // rotates around it and is able to zoom in and out
    //
    // Attach the script to your main camera
    public class CameraController : MonoBehaviour
    {
        [Tooltip("The object towards the camera rotates")]
        public Transform target;
        [Tooltip("Sensitivity of the camera rotation. Higher values mean a faster rotation")]
        public float rotationSpeed = 10;
        [Tooltip("Boundaries for X-rotation of the camera")]
        public Interval tiltBoundaries;
        [Tooltip("Maximum and minimum zoom level")]
        public Interval zoomLevelBoundaries;
        [Tooltip("Sensitivity of the camera zoom")]
        public float zoomSpeed;


        [Tooltip("If the viewport distance between the player and the focused enemy \n" +
                 "is bigger than the Max Viewport Distance, the camera zooms out.")]
        public float maxViewportDistance = 0.6f;
        
        [Tooltip("If the camera automatically zooms out, add 'Auto Zoom Step' to the \n"+
                 "'Zoom Level' per frame")]
        public float autoZoomStep = 0.2f;

        // There are two possible target positions:
        //  - The player's position, if the camera follows the player in normal mode
        //  - The position between the player and the focused enemy. In this situation
        //        the camera films an imaginary point directly in the middle between the player
        //        and the focused enemy
        private Vector3 targetPosition;
        // A variable needed for Unity's Mathf.SmoothDamp - function
        private float zoomVelocity;
        // The reference to the player's focus script
        private FocusSelectionClient focusSelection;


        // The direction from the camera position to the target
        private Vector3 lookDirection;
        // Input variables for rotation and zoom
        private float cameraXInput, cameraYInput, zoomInput;
        // Current vertical rotation of the camera
        private float tilt;

        // Linear zoom level. Will be transformed into an exponential value
        private float zoomLevel = 0.0f;
        // Transformed zoom level
        private float zoomValue = 1.0f;

        // Hides transformation of the zoom level into the zoom value
        // When the zoom level is set, the zoom value is automatically calculated
        public float ZoomLevel
        {
            get { return zoomLevel; }
            set
            {
                // Recalculate only if zoom level changed
                if (zoomLevel != value)
                {
                    zoomLevel = value;

                    // Consider zoom level boundaries and calculate zoom value
                    zoomLevel = Mathf.Clamp(zoomLevel, zoomLevelBoundaries.Min, zoomLevelBoundaries.Max);
                    zoomValue = Mathf.Pow(1.1f, -zoomLevel);
                }
            }
        }

        private void Start()
        {
            // In the beginning calculate the look direction to prevent
            // the Vector3 to be zero
            lookDirection = target.position - transform.position;

            // Assume that the target is the player object. Therefore,
            // ask for the PlayerFocus-component attached to the player
            focusSelection = target.GetComponent();
        }

        private void Update()
        {
            // Ask and store user input in each frame
            cameraXInput = Input.GetAxisRaw("Mouse X") * rotationSpeed;
            cameraYInput = Input.GetAxisRaw("Mouse Y") * rotationSpeed;
            zoomInput = Input.GetAxisRaw("Mouse ScrollWheel") * zoomSpeed;
        }

        private void FixedUpdate()
        {
            // Since the target position changes while the player focuses an enemy,
            // recalculate and store the target position.
            targetPosition = CalcTargetPosition();

            // Set position and rotation of the camera
            // Quatenion.LookRotation transforms a direction Vector3 into a rotation
            transform.position = CalcCameraPos();
            transform.rotation = Quaternion.LookRotation(targetPosition - transform.position);
        }

        // Calculates the new camera position. Considers mouse- and zoom-input
        private Vector3 CalcCameraPos()
        {
            // New look direction is based on the input camera rotation
            lookDirection = Quaternion.Euler(0.0f, cameraXInput, 0.0f) * lookDirection;
            // Normalize look direction
            lookDirection.Normalize();

            // Update camera tilt and check tilt-boundaries
            tilt -= cameraYInput;
            tilt = Mathf.Clamp(tilt, tiltBoundaries.Min, tiltBoundaries.Max);

            // Recalculate zoom value
            ZoomLevel += zoomInput;


            // Perform only if the target has a PlayerFocus-component attached
            if (focusSelection != null)
            {
                // Ask for a focus point and perform only if the player has selected one
                IFocusable focus = focusSelection.Focus;
                if (focus != null)
                {
                    // Let's say the screen size is 1920 x 1080 pixels. Then if a point is rendered at pixel
                    //  - (0, 0): the viewport point is also (0, 0)
                    //  - (1920, 0): the viewport point is (1, 0)
                    //  - (960, 540): the viewport point is (0.5, 0.5)
                    //
                    // Now calculate the viewport distance between the player's and the enemy's positions
                    Vector2 playerViewport = Camera.main.WorldToViewportPoint(target.position);
                    Vector2 focusViewport = Camera.main.WorldToViewportPoint(focus.FocusPoint);
                    float viewportDistance = Vector2.Distance(playerViewport, focusViewport);

                    // If the viewport distance between the player and the enemy is too big, zoom out a little bit
                    if (viewportDistance > maxViewportDistance)
                    {
                        ZoomLevel = Mathf.SmoothDamp(ZoomLevel, ZoomLevel - autoZoomStep, ref zoomVelocity, Time.deltaTime);
                    }
                }
            }

            // Calculate the new camera position. Steps:
            //  - Position it at the target's position
            //  - Rotate it towards the target and consider camera tilting
            //  - Given this rotation move the camera backwards by the zoom value
            return targetPosition - Quaternion.LookRotation(new Vector3(lookDirection.x, 0.0f, lookDirection.z))
                * Quaternion.Euler(tilt, 0.0f, 0.0f) * Vector3.forward * 4.0f * zoomValue;
        }


        // Calculates the target position, which can either be the player's position or
        // the point exactly in the middle of the player and the enemy
        private Vector3 CalcTargetPosition()
        {
            // Perform only if the target has a PlayerFocus-component attached
            if (focusSelection != null)
            {
                // Ask for a focus point and perform only if the player has selected one
                IFocusable focusable = focusSelection.Focus;
                if (focusable != null)
                {
                    // Calculate the distance bewteen the player and the focused enemy and
                    // return the point which is exactly in the middle of the player and the enemy
                    Vector3 dist = focusable.FocusPoint - target.position;
                    return target.position + (dist * 0.5f);
                }
            }

            // If no enemy is focused, return the player's position
            return target.position;
        }
    }
}

Viewport points

The function WorldToViewportPoint(Vector3 point) transforms a position from world space into viewport space (Unity-Reference: "Viewport space is normalized and relative to the camera. The bottom-left of the camera is (0,0); the top-right is (1,1). The z position is in world units from the camera"). Simply put, if a transformed point from world space into viewport space is a vector between (0,0) and (1,1), the point is visible on screen. Hopefully the image below makes it even clearer:

Viewport Points Explanation

Thank you…

...for reading this article. We hope you enjoyed it 🙂

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

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

*