Monthly Archives: October 2015

Implementing an orbital camera in Unity

Currently I am working at a company on a project where it is very important that the player is able to easily inspect and interact with certain objects from various angles by focusing on, zoom and rotating around them. This also required constraints to limit the camera to predefined areas, to guide the player and to prevent clipping. Nothing I could find really satisfied me, so I wrote my own variant.

Orbital camera systems by themselves are quite simple to implement, but things can get a bit tricky when you start to add constraints as a requirement. I noticed many people having problems with this sort of thing, so I finally took the time to explain the way I decided to approach this problem. It’s also to help me remember this stuff if I need something similar at a later date. Included is a simplified piece of example code.

Defining the problem

  • We want our camera (or any object really) to orbit around another object, responding to user actions and keeping at a certain distance from the target object. In other words, we want the camera to be driven by some type of a Spherical Coordinate System.
  • We want the ability to control both Pitch (up/down or the local x-axis) and Yaw. (left/right or the local y-axis) We’ll ignore the Roll axis.
  • For each point that the camera will focus on, we want the ability to define constraints that determine where the camera is allowed to go.

Implementation

Let’s start with defining our focus points and constraints. When people think about rotations, many of them think in degrees or radians. That’s why it might seem natural to define the constraints simply as a set of angles like MinPitch, MaxPitch, etc.

There is a different way to do it, which may seem a bit of a weird approach at first, but it makes the entire constraint problem much easier to solve. Instead of setting explicit boundary numbers on each side, we define the constraint as a maximum deviation from a center rotation. The constraint is then defined by the angles YawLimit, PitchLimit and the center rotation. (the center rotation in this case being simply the rotation of the focus point object)

Doing it this way means we can avoid the mess of trying to figure out where we are in relation to the constraints and whether we need to rotate left or right to satisfy them. We can simply calculate the difference between the angles and then use linear interpolation to move the required amount back towards the target rotation without needing to know or care which direction that is. We also don’t have to deal with issues such as wrapping around from weird angles like at the 0-360 degrees boundary.

For that reason, instead of storing most of our angles as floats, we store them as Quaternions. Quaternions seem complicated, but they really make some calculations much easier. And if we want to work in degree angles, we simply convert between them.

Finally, we multiply our current pitch and yaw rotations together to get our desired rotation. We could stop at this point, and then we would simply have a camera that rotates on the spot. Useful for e.g. a security camera. To turn it into an orbit camera, we calculate our offset by taking our forward vector and multiplying it with our target rotation, then adding that to the position of our target object. Lastly, we can add some damping for smoother movement.

Example

Note that this implementation may not be suitable for everyone, as it largely depends on your use case, but if not then at least it may give you some ideas on how to write your own, suited for your purposes. The example code is also very basic and not optimized so you may want to add a few improvements if you decide to use it.


using UnityEngine;
using System.Collections;
/// <summary>
/// Defines angle limits as the maximum deviation away from the rotation of this object.
/// (in other words: if the yawlimit is 45, then you can only move up to 45 degrees away from this rotation in both directions.
/// This means the total angle available would be an angle of 90 degrees)
/// An angle of 180 allows complete freedom of movement on that axis.
/// </summary>
public class FocusPoint : MonoBehaviour
{
[SerializeField]
private float _yawLimit = 45f;
[SerializeField]
private float _pitchLimit = 45;
public float YawLimit { get { return _yawLimit; } }
public float PitchLimit { get { return _pitchLimit; } }
}
/// <summary>
/// A basic orbital camera.
/// </summary>
public class OrbitCamera : MonoBehaviour
{
// This is the target we'll orbit around
[SerializeField]
private FocusPoint _target;
// Our desired distance from the target object.
[SerializeField]
private float _distance = 5;
[SerializeField]
private float _damping = 2;
// These will store our currently desired angles
private Quaternion _pitch;
private Quaternion _yaw;
// this is where we want to go.
private Quaternion _targetRotation;
private Vector3 _targetPosition;
public FocusPoint Target
{
get { return _target; }
set { _target = value; }
}
public float Yaw
{
get { return _yaw.eulerAngles.y; }
private set { _yaw = Quaternion.Euler(0, value, 0); }
}
public float Pitch
{
get { return _pitch.eulerAngles.x; }
private set { _pitch = Quaternion.Euler(value, 0, 0); }
}
public void Move(float yawDelta, float pitchDelta)
{
_yaw = _yaw * Quaternion.Euler(0, yawDelta, 0);
_pitch = _pitch * Quaternion.Euler(pitchDelta, 0, 0);
ApplyConstraints();
}
private void ApplyConstraints()
{
Quaternion targetYaw = Quaternion.Euler(0, _target.transform.rotation.eulerAngles.y, 0);
Quaternion targetPitch = Quaternion.Euler(_target.transform.rotation.eulerAngles.x, 0, 0);
float yawDifference = Quaternion.Angle(_yaw, targetYaw);
float pitchDifference = Quaternion.Angle(_pitch, targetPitch);
float yawOverflow = yawDifference – _target.YawLimit;
float pitchOverflow = pitchDifference – _target.PitchLimit;
// We'll simply use lerp to move a bit towards the focus target's orientation. Just enough to get back within the constraints.
// This way we don't need to worry about wether we need to move left or right, up or down.
if (yawOverflow > 0) { _yaw = Quaternion.Slerp(_yaw, targetYaw, yawOverflow / yawDifference); }
if (pitchOverflow > 0) { _pitch = Quaternion.Slerp(_pitch, targetPitch, pitchOverflow / pitchDifference); }
}
void Awake()
{
// initialise our pitch and yaw settings to our current orientation.
_pitch = Quaternion.Euler(this.transform.rotation.eulerAngles.x, 0, 0);
_yaw = Quaternion.Euler(0, this.transform.rotation.eulerAngles.y, 0);
}
void Update()
{
// calculate target positions
_targetRotation = _yaw * _pitch;
_targetPosition = _target.transform.position + _targetRotation * (-Vector3.forward * _distance);
// apply movement damping
// (Yeah I know this is not a mathematically correct use of Lerp. We'll never reach destination. Sue me!)
// (It doesn't matter because we are damping. We Do Not Need to arrive at our exact destination, we just want to move smoothly and get really, really close to it.)
this.transform.rotation = Quaternion.Lerp(this.transform.rotation, _targetRotation, Mathf.Clamp01(Time.smoothDeltaTime * _damping));
// offset the camera at distance from the target position.
Vector3 offset = this.transform.rotation * (-Vector3.forward * _distance);
this.transform.position = _target.transform.position + offset;
// alternatively, if we desire a slightly different behaviour, we could also add damping to the target position. But this can lead to awkward behaviour if the user rotates quickly or the damping is low.
//this.transform.position = Vector3.Lerp(this.transform.position, _targetPosition, Mathf.Clamp01(Time.smoothDeltaTime * _damping));
}
}
public class CameraMouseInput : MonoBehaviour
{
[SerializeField]
private OrbitCamera _cam;
private Vector3 _prevMousePos;
void Update()
{
const int LeftButton = 0;
if (Input.GetMouseButton(LeftButton))
{
// mouse movement in pixels this frame
Vector3 mouseDelta = Input.mousePosition – _prevMousePos;
// adjust to screen size
Vector3 moveDelta = mouseDelta * (360f / Screen.height);
_cam.Move(moveDelta.x, -moveDelta.y);
}
_prevMousePos = Input.mousePosition;
}
}

view raw

OrbitCamera.cs

hosted with ❤ by GitHub

There’s many ways this example can be improved upon. The following suggestions are left as an exercise for the reader:

  • Zooming. (hint: you’ll want to look at logarithmic or exponential functions instead of just linearly increasing/decreasing the distance)
  • A fancy editor that allows you to see the constraint angles in the scene view. (hint: Handles.DrawWireArc)
  • Can you spot the minor bug? (Well. It may or may not be considered a bug depending on your perspective.)
  • Using local space instead of world space allows different camera angles, but also makes your camera more dependent on the transform hierarchy. Useful e.g. if your target object is a spaceship and you want the camera to keep a certain orientation in relation to the ship instead of the world.
  • Smooth switching from one target object to another.