VG1, Quest 8 - Cameras

Download Files

Supporting files for VG1 quests are part of a single archive that you can download here.

Set Up Assets

Using the Lit 2D (URP) scene template, create a SplitScreen scene for this tutorial.

Import zelda1.gif from the course files into the /Assets/Textures/Adventure/ folder. Configure it with the following settings:

Sprite Mode = Multiple
PPU = 16
Filter Mode = Point (no filter)

Sprite Editor → Splice → Automatic → Slice → Apply

Apply all settings after changes and as prompted.

Turn Off Gravity

So far, we've seen how gravity can be altered per-object using a Rigidbody component. Gravity can also be configured in the Project Settings. Be mindful that these settings will also affect the entire project including other scenes. The best approach to use depends on the context of your project.

This tutorial creates a top-down perspective game that does not use gravity. Go to Edit → Project Settings → Physics 2D → General Settings and set Gravity X and Y to 0.

Create Player

Create a Player game object.

Attach a SpriteRenderer and assign the #0 graphic.

Add a Rigidbody2D component for physics and a CircleCollider2D component for collisions. Note the high Linear Damping and Freeze Rotation settings to keep our player from sliding away or spinning out of control.

We use a circle-shaped collider to help prevent the player from getting snagged on sharp boundaries when moving in any direction.

Level Obstacle

Create an Obstacle object with a Sprite component. Use sprite number 42 which is the fire. Add a BoxCollider2D component. This obstacle will prevent passage by players.

PlayerController

Create PlayerController.cs in /Assets/Scripts/SplitScreen/ and attach it to the Player scene object. Notice how the SplitScreen namespace will differentiate this file from all the other PlayerControllers in the project.

The Rigidbody2D outlet helps PlayerController affect the object's physics. The key varibles will configure player input instead of hardcoding specific keyboard keys in the code.

PlayerController.cs

using UnityEngine;
using UnityEngine.InputSystem;

namespace SplitScreen {
	public class PlayerController : MonoBehaviour
	{
		// Outlets
		Rigidbody2D _rigidbody;

		// Configuration
		public Key keyUp;
		public Key keyDown;
		public Key keyLeft;
		public Key keyRight;
		public float moveSpeed;
	}
}

Fill in the public configuration values in the Unity inspector.

Set up the Rigidbody2D reference in the Start event and use the FixedUpdate loop to send physics forces. We use FixedUpdate instead of Update to synchronize the timing of our force commands with the physics simulation and prevent movement stuttering. Also notice how this code uses fixedDeltaTime instead of deltaTime to match the FixedUpdate event.

		// Methods
		void Start() {
			_rigidbody = GetComponent<Rigidbody2D>();
		}

		void FixedUpdate() {
			if(Keyboard.current[keyUp].isPressed) {
				_rigidbody.AddForce(moveSpeed * Time.fixedDeltaTime * Vector2.up, ForceMode2D.Impulse);
			}
			if(Keyboard.current[keyDown].isPressed) {
				_rigidbody.AddForce(moveSpeed * Time.fixedDeltaTime * Vector2.down, ForceMode2D.Impulse);
			}
			if(Keyboard.current[keyLeft].isPressed) {
				_rigidbody.AddForce(moveSpeed * Time.fixedDeltaTime * Vector2.left, ForceMode2D.Impulse);
			}
			if(Keyboard.current[keyRight].isPressed) {
				_rigidbody.AddForce(moveSpeed * Time.fixedDeltaTime * Vector2.right, ForceMode2D.Impulse);
			}
		}

Playtest to ensure the character moves and cannot pass through the fire.

Prefab the Player game object in /Assets/Prefabs/SplitScreen/ because we will be making a variant of it in the next step for player two.

Player Two

Duplicate the Player object in the SplitScreen scene so that there are now Player and Player2 game objects. Both should be blue to signify that they originate from a prefab.

Tint Player2 slightly using the Color property on its SpriteRenderer, so that we can differentiate them.

For Player2, use the IJKL keys for movement.

Playtest to ensure both players can move independently using separate keyboard keys and that they both cannot pass through the fire.

Follow Camera (Smoothdamp Approach)

There are many ways to set up a camera that follows a target. Some options include using a mix of parenting, programmatic tracking, and cinematic packages. We will demo a few in class and implement a couple through this exercise.

Create a CameraController.cs file in /Assets/Code/SplitScreen/ and attach it to the MainCamera game object.

The target outlet specifies what object the camera will follow. The offset represents how far the camera will stay away from the target in all dimensions. (At a minimum you want the camera to stay away from the target in the z-axis so the camera has room to look at the target, rather than the camera following the target and being inside of it.) Smoothness specifies how long it should take the camera to catch up with the target (in seconds). The smoothing function requires a variable to store velocity (even though we will not use the variable in this assignment).

CameraController.cs

using UnityEngine;

namespace SplitScreen {
	public class CameraController : MonoBehaviour
	{
		// Outlets
		public Transform target;
		
		// Configuration
		public Vector3 offset;
		public float smoothness;
		
		// State Tracking
		Vector3 _velocity;
	}
}

In the Start event, we compute the distance between the camera and the target (if there is one) and use this as the offset. This means if your camera is misaligned in the scene, the game will maintain that misaligned offset. Make sure your camera is perfectly centered atop the player.

CameraController.cs

		// Methods
		void Start() {
			if(target) {
				offset = transform.position - target.position;
			}
		}

In the FixedUpdate loop, we calculate an updated camera position that is a smooth progression between the location of the camera toward the position of the target. The smoothness is measured in seconds.

Balancing the camera smoothness is a matter of determining how much time does the camera have to move as slow and as smoothly as possible toward the target while not being so slow that the target escapes the view of gameplay. If the target and camera do not use the same type of update loop (Update vs. FixedUpdate), the desync between the loops will be visible as stuttering movement.

CameraController.cs

		void FixedUpdate() {
			if(target) {
				transform.position = Vector3.SmoothDamp(
					transform.position, 
					target.position + offset, 
					ref _velocity, 
					smoothness
				);
			}
		}

With these settings the Camera will smoothly follow the player with an approximate delay of 0.5 seconds to catch up. Notice the offset is set to 0 because it's not computed until the Start event.

Split Screen

All of our assignments so far have required only one camera. It is possible to divide up screen space to view multiple cameras at the same time.

Create a new Camera object named Camera2.

It is important that both cameras use the same sizing and perspective. Click the triple dots to the right of the Camera Component on Main Camera and select Copy Component.

On Camera2, click the triple dots to the right of the Camera Component and select Paste Component Values.

Position Camera2 so that it is above Player2, while MainCamera remains positioned over Player. Camera2 should have a Z position of -10, so that Player2 is in front of the camera. If your character is invisible in the Game preview, then the camera is too close. You should have two players each with their own camera.

Remove the AudioListener on Camera2. There is already an AudioListener on the MainCamera, and you are only allowed one "set of ears" in a scene.

You can specify that Camera2 occupies only the bottom half of the game view by altering its Viewport Rectangle in the Camera component. Because shrinking the viewport causes content to appear smaller, we compensate by adjusting the Projection Size of the camera. Change the background color to a grass-like green to help visualize the camera boundary.

Previewing the game now should show that Camera2 (rendered with green) occupies the bottom half of the screen and still shows content at the same size as the Main Camera.

Inspect MainCamera. Alter its Viewport Rectangle so that it occupies the top half of the game view. Notice that we are also changing the Projection Size to 2.5 to match the smaller viewport.

Playtest and ensure that the top half of the screen still follows AND CENTERS ON Player1. Camera2 does not yet have follow functionality.

Environment

Create an enclosed environment using duplicates of the level Obstacle objects. The environment must be larger than the camera view rectangle of a 16:9 screen as shown by the grey outline.

Playtest and notice that while players are confined to the room, the camera pans past level boundaries. This could be awkward for some level designs where you're in an enclosed room or dungeon and don’t want the camera to show a bunch of empty space. In the next steps, we will implement a Cinemachine camera, which has advanced logic for handling camera framing.

Cinemachine

We are going to implement a more advanced follow technique for the bottom view (Camera2). You MUST still use the Smoothdamp technique for the top-half camera in order for that approach to be be graded.

To install Cinemachine, open the Package Manager from Window → Package Manager. Search for Cinemachine within the Unity Registry and click Install.

Cinemachine uses "Cinemachine Camera" components in coordination with the standard Camera component. The Cinemachine Camera can be used to implement cinematic techniques, while the Camera component still renders the scene.

Once Cinemachine is installed, create a 2D Camera.

This creates a Cinemachine Camera object, which we will configure soon. Check your Cameras. Creating a Cinemachine Camera will also automatically add a Cinemachine Brain to one of your Cameras (signified by the Gear Camera icon).

You may need to move the Cinemachine Brain from Main Camera to Camera 2, if Cinemachine picked the wrong camera.

The top camera will be graded for the smoothdamp technique. You will lose points if the project doesn't showcase both camera techniques in their appropriate halves. Make sure Main Camera is still centered on the player with a Z-position of -10 and a Projection Size of 2.5.

The Cinemachine Brain coordinates the Camera and Cinemachine Camera components. Similar to how we had to match the update events of Camera Controller with Player Controller, we should also select Fixed Update as the Update Method for Cinemachine Brain.

With the Cinemachine Brain on Camera2, we can inspect Cinemachine Camera to configure its options.

Similar to before, we use a Lens Size of 2.5 and tell the Camera to follow Player2.

Set Position Control to Position Composer. Doing do adds another component with even more in-depth composition options. Enable Dead Zone and Hard Limits. Specify both Dead Zone and Hard Limits Sizes as diagrammed.

Instead of following and smoothing based on time, Cinemachine uses visual regions instead. These composition settings are much more customizable than SmoothDamp's single smoothness variable.

If the CinemachineCamera scene object is selected in Hierarchy, the Game view will show a visualization of the settings. In this diagram, the camera will follow the player such that the player never ends up in the OUTER RED portion of the frame. Meanwhile, the MIDDLE BLUE ring of the frame will trigger a smooth camera follow until the character rests in the CENTER portion. In this central deadzone, the character can move freely without any effect on the camera. These regions provide more flexibility in handling camera stability and following behaviors.

Playtest your game to ensure the bottom camera actually follows these constraints, while the top camera still maintains smoothdamp functionality.

Level Boundaries

While playtesting, you may have noticed the Cinemachine camera still goes past the level bounds just like the Smoothdamp camera.

Cinemachine has additional settings to make it aware of environmental boundaries. We need to create a trigger zone to represent the valid camera coverage area. This must be a trigger because a solid collider will shove the characters into the fire. Create an empty game object called LevelBounds and attach a PolygonCollider2D to it.

Use the Edit Collider button to shape the polygon so that it encloses your level design with a slight margin. The polygon collider allows you to enclose levels of any shape. Click anywhere on a segment to add a new vertex. You can remove a line segment by holding CTRL/CMD before clicking the segment.

Inspect the Cinemachine Camera. At the bottom of the Cinemachine Camera component is an Add Extension menu. Add a CinemachineConfiner2D extension.

The Cinemachine Confiner 2D appears as an addition component at the bottom of the inspector. Set the Bounding Shape 2D to the LevelBounds object.

Playtest to see that Camera2 no longer moves beyond the level boundaries. (The smoothdamp technique on Main Camera does not have room boundary functionality.)

Save and Test

Playtest to ensure all interactions work as expected and that the addition of any new features hasn’t broken any earlier interactions.

Submit Assignment

SAVE any open files or scenes.

Submit your assignment for grading following the instructions supplied for your particular classroom.