Tuesday, September 6, 2016

Mastering 2D Cameras in Unity: A Tutorial for Game Developers

For a developer, the camera is one of the cornerstones of the game development process. From just showing your game view in a chess app to masterfully directing camera movement in a 3D AAA game to obtain cinematic effects, cameras are basically used in any video game ever made, even before actually being called “cameras”.
In this article I’m going to explain how to design a camera system for 2D games, and I’m also going to explain some points on how to go about implementing it in one of the most popular game engines out there, Unity.

From 2D to 2.5D: An Extensible Camera System

The camera system we are going to design together is modular and extensible. It has a basic core consisting of several components which will ensure the basic functionality, and then various components/effects that can be optionally used, depending on the situation at hand.
The camera system we are building here is targeted at 2D platform games, but can easily extended to other types of 2D games, 2.5D games or even 3D games.
Mastering 2D Camera in Unity: A Tutorial for Game DevelopersMastering 2D Camera in Unity: A Tutorial for Game Developers
I am going to split the camera functionality into two main groups: camera tracking and camera effects.

Tracking

Most of the camera movement we’ll do here will be based on tracking. That is the ability of an object, in this case the camera, to track other objects as they move about in the game scene. The types of tracking that we’ll be implementing are going to solve some common scenarios encountered in 2d platform games, but they can be extended with new types of tracking for other particular scenarios you might have.

Effects

We will be implementing some cool effects like camera shake, camera zoom, camera fade, and color overlay.

Getting Started

Create a new 2D project in Unity and import standard assets, especially the RobotBoy character. Next, create a ground box and add a character instance. You should be able to walk and jump with your character in your current scene. Make sure the camera is set to Orthographic mode (it is set to Perspective by default).

Tracking a Target

The following script will add basic tracking behavior to our main camera. The script must be attached as a component to the main camera in your scene and it exposes a field for assigning a target object to track. Then the script ensures the x and y coordinates of the camera are the same with the object it tracks. All this processing is done during the Update step.
[SerializeField]
protected Transform trackingTarget;

// ...

void Update()
{
    transform.position = new Vector3(trackingTarget.position.x,
         trackingTarget.position.y, transform.position.z);
}
Drag the RobotBoy character from your scene hierarchy over the “Tracking Target” field exposed by our following behavior in order to enable tracking of the main character.

Adding Offset

All good, but we can see a limitation straight off the bat: the character is always in the center of our scene. We can see a lot behind the character, which is usually stuff we are not interested in, and we are seeing too little of what is ahead of our character, which might be detrimental to the gameplay.
To solve this, we are adding some new fields to the script that will allow the positioning of the camera at an offset from its target.
[SerializeField]
float xOffset;

[SerializeField]
float yOffset;

// ...

void Update()
{
    transform.position = new Vector3(trackingTarget.position.x + xOffset, 
        trackingTarget.position.y + yOffset, transform.position.z);
}
Below you can see a possible configuration for the two new fields:

Smoothing Things Out

The camera movement is pretty stiff and will also produce dizziness in some players from the constant perceived movement of the environment. In order to fix this we’ll be adding some delay in camera tracking using linear interpolation, and a new field to control how fast the camera gets into place after the character starts changing its position.
[SerializeField]
protected float followSpeed;

// ...

protected override void Update()
{
    float xTarget = trackingTarget.position.x + xOffset;
    float yTarget = trackingTarget.position.y + yOffset;

    float xNew = Mathf.Lerp(transform.position.x, xTarget, Time.deltaTime * followSpeed);
    float yNew = Mathf.Lerp(transform.position.y, yTarget, Time.deltaTime * followSpeed);

    transform.position = new Vector3(xNew, yNew, transform.position.z);
}

Stop the Dizziness: Axis Locking

Since it is not pleasant for your brain to watch the camera going up and down all the time along with the character, we are introducing axis locking. This means we can limit the tracking to only one axis. Then we’ll separate our tracking code into axis independent tracking, and we’ll take the new locking flags into account.
[SerializeField]
protected bool isXLocked = false;

[SerializeField]
protected bool isYLocked = false;

// ...

float xNew = transform.position.x;
if (!isXLocked)
{
    xNew = Mathf.Lerp(transform.position.x, xTarget, Time.deltaTime * followSpeed);
}

float yNew = transform.position.y;
if (!isYLocked)
{
     yNew = Mathf.Lerp(transform.position.y, yTarget, Time.deltaTime * followSpeed);
}

Lane System

Now that the camera only tracks the player horizontally, we are limited to the height of one screen. If the character climbs some ladder or jumps higher than this, we have to follow. The way we are doing this is by using a lane system.
Imagine the following scenario:
The character is initially on the lower lane. While the character remains within the boundaries of this lane the camera will be moving only horizontally on lane specific height offset we can set.
As soon as the character enters another lane, the camera will transition to that lane and continue to move horizontally from there on until the next lane change occurs.
Care must be taken on lane design in order to prevent fast lane switching during actions like jumps, which can create confusion for the player. A lane should be changed only if the player’s character is going to stay on it for a while.
Lanes’ levels can change throughout the game level based on the designer’s specific needs, or can be interrupted altogether and another camera tracking system can take their place. Therefore, we need some limiters for specifying lane zones.

Implementation

A possible implementation is to add lanes as simple objects in the scene. We will use their Y position coordinate paired with Y offset in the tracking script above to implement the system. Therefore, their positioning on the X and Z coordinates does not matter.
Add the LaneSystem class to camera, along with the tracking class, and assign the lane objects to the provided array. Also assign the player character to the Reference field. As the reference is positioned between a lane and another lane, the lower one of the two will be used to position the camera.
And the LaneSystem class takes care of moving the camera between lanes, based on reference position. The followSpeed is used here again for position interpolation, to prevent lane switching from being too abrupt:
[SerializeField]
Transform reference;

[SerializeField]
List<Transform> lanes;

[SerializeField]
float followSpeed = 5f;

// ...

void Update()
{
  float targetYCoord = transform.position.y;
  if (lanes.Count > 1)
  {
   int i = 0;
   for (i = 0; i < lanes.Count - 1; ++i)
   {
    if ((reference.position.y > lanes[i].position.y) &&
     (reference.position.y <= lanes[i + 1].position.y))
    {
     targetYCoord = lanes[i].position.y;
     break;
    }
   }

   if (i == lanes.Count - 1) 
    targetYCoord = lanes[lanes.Count - 1].position.y;
  }
  else
  {
   targetYCoord = lanes[0].position.y;
  }
  float yCoord = Mathf.Lerp(transform.position.y, targetYCoord, Time.deltaTime * followSpeed);
  transform.position = new Vector3(transform.position.x, yCoord, transform.position.z);
}
This implementation is not a WYSIWYG one, and is left as such as an exercise for the reader.

Lock Node System

Having the camera move on lanes is great, but sometimes we need the camera to be locked on to something, a point of interest (POI) in the game scene.
This can be achieved by configuring such POI in the scene and attaching a trigger collider to them. Whenever the character enters that trigger collider, we move the camera and stay on the POI. As the character moves and then leaves the POI’s trigger collider, we get back to another type of tracking, usually the standard follow behavior.
The switching of the camera tracking to a lock node and back can be done either by a simple switch or by a stack system, on which tracking modes are pushed and popped.

Implementation

In order to configure a lock node, just create an object (can be empty or like in the screenshot below, a sprite) and attach a large Circle Collider 2D component to it so it marks the area the player will be in when the camera will focus the node. You can choose any type of collider, I’m choosing Circle as an example here. Also create a tag you can easily check for, like “CameraNode” and assign it to this object.
Add the following property to the tracking script on your camera:
public Transform TrackingTarget
{
    get
    {
        return trackingTarget;
    }
    set
    {
        trackingTarget = value;
    }
}
Then attach the following script to the player, that will allow it to temporarily switch the camera’s target to the lock node you’ve set. The script will also remember its previous target so it can get back to it when the player is out of the trigger area. You can go ahead and transform this in a full stack if you need that, but for our purpose since we don’t overlap multiple lock nodes this will do. Also please be aware that you can tweak the position of the Circle Collider 2D, or again add any other kind of collider to trigger the camera lock, this is just a mere example.
public class LockBehavior : MonoBehaviour
{
 #region Public Fields

 [SerializeField]
 Camera camera;

 [SerializeField]
 string tag;

 #endregion

 #region Private

 private Transform previousTarget;

 private TrackingBehavior trackingBehavior;

 private bool isLocked = false;

 #endregion

 // Use this for initialization
 void Start()
 {
  trackingBehavior = camera.GetComponent<TrackingBehavior>();
 }

 void OnTriggerEnter2D(Collider2D other)
 {
  if (other.tag == tag && !isLocked)
  {
   isLocked = true;
   PushTarget(other.transform);
  }
 }

 void OnTriggerExit2D(Collider2D other)
 {
  if (other.tag == tag && isLocked)
  {
   isLocked = false;
   PopTarget();
  }
 }

 private void PushTarget(Transform newTarget)
 {
  previousTarget = trackingBehavior.TrackingTarget;
  trackingBehavior.TrackingTarget = newTarget;
 }

 private void PopTarget()
 {
  trackingBehavior.TrackingTarget = previousTarget;
 }

}

Camera Zoom

Camera zoom can be executed either on user input or as an animation when we want to focus on something like a POI or a tighter area within a level.
2D camera zoom in Unity 3D can be achieved by manipulating the orthographicSize of the camera. Attaching the next script as a component to a camera and using the SetZoom method to change the zoom factor will produce the desired effect. 1.0 means no zoom, 0.5 means zoom in twice, 2 means zoom out twice, and so on.
[SerializeField]
float zoomFactor = 1.0f;

[SerializeField]
float zoomSpeed = 5.0f;

private float originalSize = 0f;

private Camera thisCamera;

// Use this for initialization
void Start()
{
    thisCamera = GetComponent<Camera>();
    originalSize = thisCamera.orthographicSize;
}

// Update is called once per frame
void Update()
{
    float targetSize = originalSize * zoomFactor;
    if (targetSize != thisCamera.orthographicSize)
    {
        thisCamera.orthographicSize = Mathf.Lerp(thisCamera.orthographicSize, 
targetSize, Time.deltaTime * zoomSpeed);
    }
}

void SetZoom(float zoomFactor)
{
    this.zoomFactor = zoomFactor;
}

Screen Shake

Whenever we need to show an earthquake, some explosion or any other effect in our game, a camera shake effect comes in handy.
An example implementation of how to do that is available on GitHub: gist.github.com/ftvs/5822103. The implementation is fairly straightforward. Unlike the other effects we have covered so far, it relies on a little randomness.

Fade & Overlay

When our level starts or ends, a fade-in or out-effect is nice. We can implement this by adding a non-interactable UI texture in a panel stretching all over our screen. Initially transparent, we can fill this with any color and opacity, or animate that to achieve the effect we want.
Here is an example of that configuration, please note the UI Panel object being assigned to “Camera Overlay” child of the Main Camera Object. Camera Overlay exposes a script called Overlay that features the following:
[SerializeField]
Image overlay;

// ...

public void SetOverlayColor(Color color)
{
    overlay.color = color;
}
In order to have a fade-in effect, change your Overlay script by adding an interpolation to a target color you set with SetOverlayColor like in the next script, and set the initial color of our Panel to Black (or White) and the target color to the final color of your overlay. You can change the fadeSpeed to whatever suits your needs, I think 0.8 is a good one for starters. The value of fadeSpeed works as a time modifier. 1.0 means it will happen over multiple frames, but within a 1 second time frame. 0.8 means it will actually take 1/0.8 = 1.25 seconds to complete.
public class Overlay : MonoBehaviour
{
    #region Fields

    [SerializeField]
    Image overlay;

    [SerializeField]
    float fadeSpeed = 5f;

    [SerializeField]
    Color targetColor;

    #endregion

    void Update()
    {
        if (overlay.color != targetColor)
        {
            overlay.color = Color.Lerp(overlay.color, targetColor, 
                 Time.deltaTime * fadeSpeed);
        }
    }

    #region Public

    public void SetOverlayColor(Color color)
    {
        targetColor = color;
    }

    #endregion
}

Wrap Up

In this article I have tried to demonstrate the basic components needed to have a modular 2D camera system in place for your game, and also what the required mind set is for designing it. Naturally, all games have their particular needs, but with the basic tracking and simple effects described here you can get a long way and also have a blueprint for implementing your own effects. Then you can go even further and pack up everything into a reusable Unity 3D package which you can transfer to other projects as well.
Camera systems are very important in conveying the right atmosphere for your players. A good comparison I like to use is when you think of the difference between classical theatre and movies. The cameras and film themselves brought so many possibilities to the scene that it eventually evolved into an art on its own, so if you are not planning to implement another “Pong” game, advanced cameras should be your tool of choice inany game project you’ll undertake from now on.
This article was written by Mihai Cozma, a Toptal freelance developer.

No comments:

Post a Comment