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!
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:
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:
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:
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.
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:
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.
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:
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:
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:
Thank you…
...for reading this article. We hope you enjoyed it 🙂
Leave a Reply
Want to join the discussion?Feel free to contribute!