Zoom using multiple cinemachine cameras
Outline
This article desribes how I manage multiple cinemachine cameras using a custom finite state machine. Giving the player control over different cameras while also zooming in and out.
Use case
When dealing with multiple cameras, we need a way to change between these. Cameras can be player controlled and/or logic controlled. This can be done by simply changing the priority of each of the cameras via a button input, or like I propose here. In this article I will describe how I set up multiple cinemachine cameras that are controlled by the players interaction with the mouse scroll wheel ie the camera zoom level is changed by scrolling up and down on the mouse.
The cameras
In this case we have 6 cameras, 5 of which are controllable by the player, 1 is an aim cam which is triggered by right mouse button, the 4 others are controlled by the mouse scroll wheel which zooms the cameras in and out while also changing which camera is currently active.
Minimum zoom level provides the player with an FPV camera, with camera behaviour as would be expected in an FPV style game. Scrolling out decreases the zoom level which will initially blend to a freelook camera that looks at- and follows the player. This camera, as opposed to the fpv camera, has a larger zoom range. while zooming out, the rig of the camera is extended, thus moving the camera further out while still providing horizontal and vertical movement, similar to what we have in many mmorpgs. Beyond a threshold, the camera state is changed seamlessly to an orbital transposer, removing the ability to control the camera in the vertical plane, but allows the player to pan around itself at a distance. This camera is, similarily to the freelook camera, able to zoom between thresholds, minimum being the freelook camera furthest zoom level, and maximum being the threshold to the last camera, which is a top down style camera, following the player at a great distance.
State machines and states
I set up two state machines. One to handle which camera is active and which are not, which we refer to as CameraState. There is a CameraState foreach of the cinemachine cameras in the game, both player controlled and the game logic controlled.
public enum CameraState
{
FPV,
Orbital,
FreeLook,
Pause,
SpyGlass,
Map,
TopCam,
}
Changes to the state could be implemented in several ways, I chose to run a check in the Update() loop for comparison between a public State and a private state, if theyre not equal, an UpdateState method is called.
void Update(){
if (input.scroll != 0)
{
zoomLevel -= input.scroll * scrollSensitivty;
UpdateZoom();
}
if (State != state)
{
UpdateState();
}
}
The UpdateState method simply sets the priority of each camera to 0 if it’s not already, and changes the camera priority higher of the cinemachine camera associated with what the new state is in a switch statement.
The second state machine handles the current zoomLevel.
This enum can be omitted and handled by defining zoom limtations otherwise, but I really like enums since they increase readability of my code and are easy to work with. There are a ZoomState for each of the player controlled cameras except the aim camera, thus 4 states.
Similarily to the CameraStates they are controlled by an UpdateZoomState method. The switch statement evaluates which camera to update depending on a float variable cameraZoom, which is calculated on Update depending on player input multiplied by sensitivity, which the player can adjust in the game settings.
Since enums are inherently integers, we can define the extend of each cameras zoom when defining the ZoomState.
public enum ZoomLevel
{
Fpv = 0,
FreeLook = 1,
Orbit = 150,
TopCam = 500
}
In the UpdateZoomState method we can evaluate the zoomLevel to the ZoomStates integer value, thus changing zoom parameters on the revelant cameras as well as changing CameraState State.
We could easily UpdateState directly, however in this case we want the player to be able to snap to another camera by pressing a button, and not only by zooming in and out. Thus we change the State of the camera and let the UpdateState do its work.
Zoom on different cinemachine cameras
Traditionally on cameras, changing the POV gives the player experiencing of zooming in on whatever the camera is pointing at. However this is done slightly different depending on what cinemachine camera you are using. The FPV camera only has 1 zoom level ie the first person view, zoomLevel > 1 blends to the FreeLook camera, the parameter relevant to zoom is the rig associated with the camera, by modifying the top, middle and bottom rig height and radius, we move the camera further away from the player, depending on the zoomLevel. To achieve a seemless transition between the freelook and orbital transposer camera, we modify the orbital transposer Body-Offset-Vector3 Y and Z, by setting its value to the freelook top Rig height and -radius. The top down virtual camera is an orthographic camera, thus we change the orthographicSize parameter of the lens to change the zoom level.
//Define each relevant camera
[SerializeField] private Cinemachine.CinemachineVirtualCamera orbitalCam;
[SerializeField] private Cinemachine.CinemachineVirtualCamera fpvCam;
[SerializeField] private Cinemachine.CinemachineFreeLook freeLookCamera;
[SerializeField] private Cinemachine.CinemachineVirtualCamera topCam;
//Define the relevant parameters for zoom
private CinemachineFreeLook.Orbit[] baseOrbits;
private CinemachineOrbitalTransposer orbitalTransposer;
private float zoomLevel = 0.0f;
//Input controller as implmenented in game
[SerializeField] private InputController input;
public float scrollSensitivity = 0.01f; //modify by player in controlsettings
//Record the presets for use when resetting the cameras
baseOrbits = new CinemachineFreeLook.Orbit[freeLookCamera.m_Orbits.Length];
for (int i = 0; i < baseOrbits.Length; i++)
{
baseOrbits[i].m_Height = freeLookCamera.m_Orbits[i].m_Height;
baseOrbits[i].m_Radius = freeLookCamera.m_Orbits[i].m_Radius;
}
var orbitalBody = orbitalCam.GetCinemachineComponent(CinemachineCore.Stage.Body);
orbitalTransposer = orbitalBody as CinemachineOrbitalTransposer;
baseFollowOffsets = orbitalTransposer.m_FollowOffset;
orbitalTransposer.m_FollowOffset = new Vector3(0, baseOrbits[0].m_Height, -baseOrbits[0].m_Radius);
baseTopCamOrthographic = topCam.m_Lens.OrthographicSize;
void Update(){
if (input.scroll != 0)
{
zoomLevel -= input.scroll * scrollSensitivty;
UpdateZoom();
}
if (State != state)
{
UpdateState();
}
}
//Update the zoom parameters for the FreeLook and Orbital Cams
void UpdateCamOffsets()
{
for (int i = 0; i < baseOrbits.Length; i++)
{
freeLookCamera.m_Orbits[i].m_Height = Mathf.Lerp(freeLookCamera.m_Orbits[i].m_Height, freeLookCamera.m_Orbits[i].m_Height - input.scroll * scrollSensitivty, 100);
freeLookCamera.m_Orbits[i].m_Radius = Mathf.Lerp(freeLookCamera.m_Orbits[i].m_Radius, freeLookCamera.m_Orbits[i].m_Radius - input.scroll * scrollSensitivty, 100);
}
orbitalTransposer.m_FollowOffset = new Vector3(0, freeLookCamera.m_Orbits[0].m_Height, -freeLookCamera.m_Orbits[0].m_Radius);
}
void UpdateZoom()
{
switch (zoomLevel)
{
case var x when x < (int)ZoomLevel.FreeLook:
State = CameraState.FPV;
break;
case var x when x > (int)ZoomLevel.FreeLook && x < (int)ZoomLevel.Orbit:
State = CameraState.FreeLook;
UpdateCamOffsets();
break;
case var x when x > (int)ZoomLevel.Orbit && x < (int)ZoomLevel.TopCam:
State = CameraState.Orbital;
UpdateCamOffsets();
break;
case var x when x > (int)ZoomLevel.TopCam:
topCam.m_Lens.OrthographicSize -= input.scroll * scrollSensitivty;
State = CameraState.TopCam;
break;
}
}
void UpdateState()
{
state = State;
ZeroPriorities();
switch (state)
{
case CameraState.FPV:
fpvCam.Priority = 10;
ResetCameraOffsets();
break;
case CameraState.Orbital:
orbitalCam.Priority = 10;
break;
case CameraState.Pause:
extCam.Priority = 10;
break;
case CameraState.SpyGlass:
spyglassCam.Priority = 10;
ResetCameraOffsets();
break;
case CameraState.Map:
ResetCameraOffsets();
mapCam.Priority = 10;
break;
case CameraState.FreeLook:
freeLookCamera.Priority = 10;
break;
case CameraState.TopCam:
topCam.Priority = 10;
break;
}
}
Blending between cameras
On the CinemachineBrain component we create a new custom blend asset. In this asset, we define which blending to use and the extend of time it should take. I have chosen to EaseInOut over 2 seconds for each camera except for the aim camera, which we Cut directly to and from.
I check the Inherit position on the Orbital and FreeLook cameras. As well as setting the X and Y dampening on the body Follow Offset to 0, which will otherwise cause a sort of rubberbanding when transitioning between cameras.
Final notes
This way we attain zoom functionality on each relevant cinemachine camera, while also transitioning seamlessly or smoothly between each camera.
Thanks for taking your time to read this article, I hope you find it helpful in your endevours with Unity and cinemachine. Feel free to reach out to me if you have comments or want to discuss aspects of game development.