Camera System
Set up a smooth, professional camera that follows the player using Cinemachine — with camera shake for impacts.
Why Not Just Parent the Camera to the Player?
The simplest approach is to make the camera a child of the player object. The camera then automatically follows wherever the player goes. It works — but it feels terrible. Here is why:
- No damping. The camera snaps to the player's position instantly. Every tiny bump, every frame of jump movement, the camera follows perfectly. This creates a jittery, nauseating experience.
- No smoothing. When the player switches lanes, the camera jerks sideways. When the player lands from a jump, the camera jolts downward. Professional games have a slight delay where the camera "catches up" smoothly.
- No flexibility. Want to do a camera shake when the player dies? A zoom effect when hitting a speed boost? A look-ahead when switching lanes? All of this is extremely difficult with a parented camera.
Instead, we will use Cinemachine — Unity's professional camera system. It handles smooth following, damping, look-ahead, camera shake, and much more, all configurable in the Inspector with zero code for basic setups.
Cinemachine is a free Unity package that controls your main camera procedurally. You create "virtual cameras" that define how the camera should behave (what to follow, from what angle, with what smoothing). Cinemachine then drives the actual Main Camera to match the active virtual camera. It is used in AAA games, films, and almost every professional Unity project.
Installing Cinemachine
- Open Window > Package Manager.
- In the top-left dropdown, select "Unity Registry".
- Search for "Cinemachine".
- Select the Cinemachine package (version 3.x or later) and click Install.
- Wait for the installation to complete. No restart is required.
Cinemachine 3.x (released with Unity 2023+) has a different API from Cinemachine 2.x. The component names changed: CinemachineVirtualCamera became CinemachineCamera, CinemachineTransposer became CinemachineFollow, and so on. This tutorial uses Cinemachine 3.x naming. If you are on Unity 2022 or earlier, you may have Cinemachine 2.x — the concepts are the same, but the component names differ slightly.
Creating a Cinemachine Virtual Camera
A virtual camera is a lightweight object that says "the camera should be HERE, looking at THAT, with THESE settings." Cinemachine uses the active virtual camera to drive the real Main Camera.
- In the top menu, go to GameObject > Cinemachine > Cinemachine Camera. This creates a new virtual camera in your scene.
- Name it
CM_FollowCamera. - Notice that Unity also added a
CinemachineBraincomponent to your Main Camera automatically. This component is the "driver" that reads virtual cameras and moves the Main Camera accordingly.
Prefix Cinemachine cameras with CM_ to make them easy to identify in the Hierarchy. In complex games, you might have CM_FollowCamera, CM_MenuCamera, CM_DeathCamera, etc.
Setting Up Follow and Look At Targets
Select the CM_FollowCamera in the Hierarchy. In the Inspector, you will see the CinemachineCamera component with several fields:
-
Tracking Target: Drag the
PlayerGameObject into this field. This tells the virtual camera "follow this object." -
Look At Target: Also drag the
Playerhere (or leave it set to the same Tracking Target). This tells the camera "always face toward this object."
At this point, if you press Play, the camera will follow the player — but it will be sitting right on top of them. We need to configure an offset.
Configuring the Follow Offset (Transposer)
We need the camera to be behind and above the player, looking down slightly. In Cinemachine 3.x, this is handled by the CinemachineFollow component (in 2.x, it was called CinemachineTransposer).
- Select
CM_FollowCamera. - In the CinemachineCamera component, look for the Body section. It should show Cinemachine Follow (or you can add it via the "Add Extension" or "Body" dropdown).
- Set the Follow Offset to:
- X:
0(centered behind the player, not to the side) - Y:
7(7 units above the player) - Z:
-10(10 units behind the player)
- X:
Side View:
Camera
/ (Y=7, Z=-10)
/
/ ← Looking down at player
/
─────────[Player]─────────────────────────────
(0,0,0) → Running direction (Z+)
Top-Down View:
Camera
|
| Z = -10
|
──────────[Player]────────────────────────────
→ Z+
The camera is directly behind the player, offset 10 units back
and 7 units up. This creates the classic runner perspective.
These values are starting points. With Cinemachine, you can tweak the offset in the Inspector and see the result in real-time in the Game view (even during Play mode!). Try different Y and Z values until the perspective feels right. Subway Surfers uses a higher angle; Temple Run uses a lower angle. It is an artistic choice.
Damping Settings for Smooth Camera Feel
Damping is the secret ingredient that makes cameras feel professional. It controls how quickly the camera catches up to the target. Without damping, the camera is rigidly locked to the player (jittery). With damping, the camera smoothly glides to where it should be.
In the CinemachineFollow (Body) section, find the Damping settings:
| Axis | Value | What It Controls |
|---|---|---|
| X Damping | 0.5 | How quickly the camera follows left/right lane switches. Lower = snappier. Higher = more lag. |
| Y Damping | 0.3 | How quickly the camera follows vertical movement (jumps). Lower = camera follows jumps closely. Higher = camera barely moves during jumps. |
| Z Damping | 0.1 | How quickly the camera follows forward movement. Keep this low so the camera stays close behind. |
0 = no damping (camera rigidly follows, no smoothing). 1 = heavy damping (camera takes about 1 second to catch up). 0.5 = moderate damping (camera takes about half a second). For an infinite runner, you want: LOW Z damping (camera must keep up with forward movement), MODERATE X damping (smooth lane switches), and MODERATE Y damping (gentle response to jumps without making the player feel disconnected).
Aim Settings
In the Aim section of the CinemachineCamera, you may see CinemachineRotationComposer (or Composer in 2.x). This controls how the camera rotates to keep the player in frame. For a runner, we want simple settings:
- Tracked Object Offset Y:
1— Look at the player's chest, not their feet. - Lookahead Time:
0— Do not look ahead of the player. (In some runners you might want a small value here, but start with 0.) - Damping:
0.5— Smooth rotation.
Camera Shake on Death
When the player hits an obstacle and dies, a quick camera shake adds satisfying impact. Cinemachine has a built-in system for this called Impulse.
How Cinemachine Impulse Works
- An Impulse Source generates a shake signal (attached to whatever causes the impact).
- An Impulse Listener receives the signal and shakes the camera (attached to the virtual camera).
Step 1: Add an Impulse Listener to the Camera
- Select
CM_FollowCamera. - Click Add Component and add CinemachineImpulseListener.
- Leave the default settings. The Gain controls how strongly this camera responds to impulses (1 = full strength).
Step 2: Add an Impulse Source to the Player
- Select the
PlayerGameObject. - Click Add Component and add CinemachineImpulseSource.
- Configure the impulse:
- Raw Signal: Select a built-in noise profile (e.g.,
6D Shake). This defines the shake pattern. - Amplitude Gain:
1(how strong the shake is) - Frequency Gain:
1(how fast the shake vibrates) - Time Envelope > Sustain:
0.1seconds - Time Envelope > Decay:
0.3seconds
- Raw Signal: Select a built-in noise profile (e.g.,
Step 3: Create the CameraShake Utility Script
We need a simple script that triggers the impulse when called. This bridges our game code with Cinemachine.
using UnityEngine;
using Unity.Cinemachine;
namespace InfiniteRunner.Camera
{
/// <summary>
/// Utility script for triggering camera shake effects.
/// Attach to the same GameObject as a CinemachineImpulseSource.
///
/// Usage: Call Shake() from any script to trigger the camera shake.
/// The PlayerController calls this on death.
/// </summary>
[RequireComponent(typeof(CinemachineImpulseSource))]
public class CameraShake : MonoBehaviour
{
[Header("Shake Profiles")]
[Tooltip("Force of the death shake. Higher = more violent.")]
[SerializeField] private float deathShakeForce = 3f;
[Tooltip("Force of a minor hit shake (e.g., stumble).")]
[SerializeField] private float minorShakeForce = 0.5f;
// The Cinemachine impulse source component
private CinemachineImpulseSource _impulseSource;
// Singleton-like static reference for easy access
// (We could also use the event system from Chapter 7)
private static CameraShake _instance;
/// <summary>
/// Static accessor so any script can trigger a shake.
/// Usage: CameraShake.Instance.Shake();
/// </summary>
public static CameraShake Instance => _instance;
private void Awake()
{
_instance = this;
_impulseSource = GetComponent<CinemachineImpulseSource>();
if (_impulseSource == null)
{
Debug.LogError(
"[CameraShake] No CinemachineImpulseSource found! " +
"Add one to this GameObject.");
}
}
/// <summary>
/// Triggers a strong camera shake (used for death/big impacts).
/// </summary>
public void ShakeDeath()
{
if (_impulseSource != null)
{
_impulseSource.GenerateImpulse(deathShakeForce);
Debug.Log($"[CameraShake] Death shake (force: {deathShakeForce})");
}
}
/// <summary>
/// Triggers a mild camera shake (used for stumbles, minor hits).
/// </summary>
public void ShakeMinor()
{
if (_impulseSource != null)
{
_impulseSource.GenerateImpulse(minorShakeForce);
}
}
/// <summary>
/// Triggers a camera shake with a custom force.
/// </summary>
/// <param name="force">Shake intensity. 0.5 = mild, 3 = strong.</param>
public void Shake(float force)
{
if (_impulseSource != null)
{
_impulseSource.GenerateImpulse(force);
}
}
/// <summary>
/// Triggers a directional camera shake.
/// The camera shakes in the specified direction.
/// </summary>
/// <param name="direction">Direction and magnitude of the shake.</param>
public void ShakeDirectional(Vector3 direction)
{
if (_impulseSource != null)
{
_impulseSource.GenerateImpulse(direction);
}
}
}
}
Save this at Assets/Scripts/Camera/CameraShake.cs.
Step 4: Connect Camera Shake to Player Death
Update the Die() method in PlayerController.cs to trigger the camera shake:
private void Die()
{
if (_currentState == PlayerState.Dead) return;
_currentState = PlayerState.Dead;
// Stop all movement
_rb.linearVelocity = Vector3.zero;
forwardSpeed = 0f;
Debug.Log("[Player] DIED!");
// Trigger camera shake for impact feel
if (InfiniteRunner.Camera.CameraShake.Instance != null)
{
InfiniteRunner.Camera.CameraShake.Instance.ShakeDeath();
}
// Notify listeners
OnPlayerDied?.Invoke();
// Tell the GameManager the game is over
if (GameManager.Instance != null)
{
GameManager.Instance.EndGame();
}
}
Complete Setup Checklist
Here is everything you should have in the scene after this chapter:
-
Main Camera — Has a
CinemachineBraincomponent (added automatically). Do NOT add any follow scripts to this — Cinemachine controls it entirely. -
CM_FollowCamera (GameObject) — Has:
CinemachineCameracomponent with Tracking Target set to Player.CinemachineFollow(Body) with Follow Offset (0, 7, -10) and damping values.CinemachineRotationComposer(Aim) for smooth look-at behavior.CinemachineImpulseListenerfor receiving camera shake signals.
-
Player (GameObject) — Has:
PlayerController(from Chapter 9)CinemachineImpulseSourcefor generating shake signals.CameraShakescript (from this chapter).RigidbodyandCapsuleCollider(from Chapter 9).
Testing and Tweaking
- Press Play.
- Forward movement: The camera should smoothly follow the player as they run forward. The camera should never jitter or snap.
- Lane switching: Press A/D. The camera should follow with a slight delay (the X damping). The player switches lanes, and the camera gently slides over to keep them centered.
- Jumping: Press Space. The camera should barely move vertically (or move just a little). The player jumps, but the camera stays relatively stable. This keeps the player's view of upcoming obstacles consistent.
- Death shake: Place a cube in the player's path, tag it as "Obstacle" (with a trigger collider). When the player hits it, the camera should shake violently for a brief moment.
Recommended Tweaking Process
One of Cinemachine's best features: you can change values in the Inspector while the game is running and see the effect immediately. Enter Play mode, select CM_FollowCamera, and adjust damping values, offset, etc. When you find values you like, write them down, then exit Play mode and enter them again (changes during Play mode are lost when you stop).
| If the camera feels... | Adjust this... |
|---|---|
| Too jittery / rigid | Increase damping values (try 0.5 - 1.0) |
| Too laggy / floaty | Decrease damping values (try 0.1 - 0.3) |
| Too close to the player | Increase Z offset (more negative, e.g., -12) |
| Too far from the player | Decrease Z offset (less negative, e.g., -8) |
| Too high / birds-eye | Decrease Y offset (e.g., 5) |
| Too low / behind | Increase Y offset (e.g., 10) |
| Camera moves too much on jump | Increase Y damping (e.g., 1.0) |
| Camera shake too strong | Decrease deathShakeForce or Impulse Gain |
| Camera shake too weak | Increase deathShakeForce |
Advanced: Multiple Camera States
In a polished game, you might want different camera behaviors for different game states. For example:
- Menu camera: A slow orbit around a scene, or a static angle showing the character.
- Gameplay camera: The follow camera we just built.
- Death camera: Zooms in on the player, or does a slow-motion pan.
Cinemachine makes this easy with priority-based camera blending. You can have multiple virtual cameras and switch between them by changing their priority:
using UnityEngine;
using Unity.Cinemachine;
using InfiniteRunner.Core;
namespace InfiniteRunner.Camera
{
/// <summary>
/// Manages which Cinemachine virtual camera is active
/// based on the current game state.
///
/// Higher priority = this camera is used.
/// Cinemachine automatically blends between cameras.
/// </summary>
public class CameraManager : MonoBehaviour
{
[Header("Virtual Cameras")]
[SerializeField] private CinemachineCamera menuCamera;
[SerializeField] private CinemachineCamera gameplayCamera;
[SerializeField] private CinemachineCamera deathCamera;
[Header("Settings")]
[Tooltip("Priority value for the active camera.")]
[SerializeField] private int activePriority = 20;
[Tooltip("Priority value for inactive cameras.")]
[SerializeField] private int inactivePriority = 10;
private void OnEnable()
{
if (GameManager.Instance != null)
{
GameManager.Instance.OnGameStateChanged += HandleStateChanged;
}
}
private void OnDisable()
{
if (GameManager.Instance != null)
{
GameManager.Instance.OnGameStateChanged -= HandleStateChanged;
}
}
private void HandleStateChanged(GameState newState)
{
// Reset all cameras to low priority
SetAllInactive();
// Activate the camera for the current state
switch (newState)
{
case GameState.Menu:
if (menuCamera != null)
menuCamera.Priority = activePriority;
break;
case GameState.Playing:
if (gameplayCamera != null)
gameplayCamera.Priority = activePriority;
break;
case GameState.GameOver:
if (deathCamera != null)
deathCamera.Priority = activePriority;
break;
case GameState.Paused:
// Keep gameplay camera during pause
if (gameplayCamera != null)
gameplayCamera.Priority = activePriority;
break;
}
}
private void SetAllInactive()
{
if (menuCamera != null) menuCamera.Priority = inactivePriority;
if (gameplayCamera != null) gameplayCamera.Priority = inactivePriority;
if (deathCamera != null) deathCamera.Priority = inactivePriority;
}
}
}
Save this at Assets/Scripts/Camera/CameraManager.cs.
When you switch the active camera (by changing priorities), Cinemachine does not cut instantly. It blends between them over a configurable duration. You can set the blend time on the CinemachineBrain component on the Main Camera. The default is 2 seconds, which you may want to reduce to 0.5 seconds for a snappier feel. You can also configure custom blend times for specific camera transitions.
Project Files After This Chapter
Assets/
Scripts/
Camera/
CameraShake.cs <-- Triggers Cinemachine Impulse for screen shake
CameraManager.cs <-- Switches cameras based on game state (advanced)
Player/
PlayerController.cs <-- Updated Die() method calls CameraShake
PlayerState.cs <-- Player state enum
Input/
InputReader.cs <-- Input wrapper
Core/
GameManager.cs <-- Game state manager
Events/
GameEvent.cs <-- Event system
Scene Hierarchy:
Main Camera <-- CinemachineBrain (auto-added)
CM_FollowCamera <-- CinemachineCamera + Follow + Aim + ImpulseListener
Player <-- PlayerController + ImpulseSource + CameraShake
PlayerModel <-- Visual capsule/model
[GameManager] <-- GameManager singleton
Ground <-- Plane with Ground layer
Common Issues and Solutions
Check that: (1) The Main Camera has a CinemachineBrain component. (2) The CM_FollowCamera has a CinemachineCamera component with a Tracking Target assigned. (3) There are no other virtual cameras with higher priority. (4) The Player object exists in the scene and is not destroyed.
This usually means the camera and the player are updating at different rates. The player moves in FixedUpdate() (physics), but the camera updates in LateUpdate() (rendering). Cinemachine handles this automatically in most cases. If you still see jitter, try setting the CinemachineBrain Update Method to "Fixed Update" or "Smart Update".
Make sure: (1) The virtual camera has a CinemachineImpulseListener. (2) The Player has a CinemachineImpulseSource with a noise profile assigned. (3) The CameraShake script is on the same object as the ImpulseSource. (4) The shake force is high enough to see (try 5 for testing).
What We Built
- Understood why a parented camera is not good enough for a professional game.
- Installed Cinemachine and created a virtual camera that follows the player.
- Configured the Follow Offset to position the camera behind and above the player.
- Set up damping for smooth camera movement on all three axes.
- Implemented camera shake on death using Cinemachine Impulse (ImpulseSource and ImpulseListener).
- Created a CameraShake utility script for triggering shake effects.
- Built an advanced CameraManager for switching cameras based on game state.
This completes Part 2: Core Systems. You now have a Game Manager, Event System, Input System, Player Controller, and Camera System — the five pillars that every game needs. In Part 3, we will build the World Generation system: chunks, object pooling, procedural generation, and origin shifting to create an infinite world.
Camera system complete with follow and shake. Commit it.
git status
git add .
git commit -m "Add Cinemachine camera with follow and screen shake"
You've now completed Part 2. Run git log --oneline — you should have about 7 commits. That's 7 safe points you can return to if anything breaks.