This camera script provides constrained first-person rotation in both the spin and pitch axes.
Note: This is not a replacement for the regular first-person camera - do not attach this to the Player GameObject. It is meant for situations where the player is e.g. sitting down, and can only rotate a certain amount.
Steps to use:
- Paste the code below into a C# script named ConstrainedFirstPersonCamera.cs
- Add a new Unity Camera into the scene (GameObject -> Camera in the top toolbar)
- Attach the "Constrained First Person Camera" component to the new Camera
- Configure the fields as desired
- Assign the camera in the normal way, i.e. through the "Camera: Switch" Action or as the scene's default
ConstrainedFirstPersonCamera.cs:
using UnityEngine;
using UnityEngine.Events;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace AC
{
public class ConstrainedFirstPersonCamera : _Camera
{
#region Variables
[Header ("Input")]
[SerializeField] private float inputSensitivity = 2f;
[SerializeField] private string xAxisName = "CursorHorizontal";
[SerializeField] private string yAxisName = "CursorVertical";
[Header ("Limits")]
[SerializeField] protected Vector2 verticalLimits = new Vector2 (-50f, 50f);
[SerializeField] protected Vector2 horizontalLimits = new Vector2 (-50f, 50f);
[SerializeField] [Range(0f,1f)] float edgeDampingFactor = 0.2f;
[Header ("Misc")]
[SerializeField] private bool snapForwardOnTransition = true;
protected Transform cameraTransform;
private float lerpDuration;
private float lerpStartDuration;
private LerpUtils.Vector3Lerp forwardLerp = new LerpUtils.Vector3Lerp ();
private Transform lookAtTransform;
private float lookAtSpeed = 1f;
#endregion
#region UnityStandards
protected override void Awake ()
{
cameraTransform = GetComponent <_Camera>().Camera.transform;
base.Awake ();
}
private void Update ()
{
if (lookAtTransform != null)
{
UpdateLookAt ();
}
else
{
Vector2 inputVector = Vector2.zero;
if (isDragControlled)
{
if (KickStarter.playerInput.GetDragState () == DragState._Camera)
{
if (KickStarter.playerInput.IsCursorLocked ())
{
inputVector = KickStarter.playerInput.GetFreeAim ();
}
else
{
inputVector = KickStarter.playerInput.GetDragVector () / 500f;
}
}
}
else
{
inputVector = new Vector2 (Input.GetAxis (xAxisName), Input.GetAxis (yAxisName));
}
AddInput (inputVector * inputSensitivity);
UpdateLerpForward ();
}
}
protected override void OnEnable ()
{
EventManager.OnSwitchCamera += OnSwitchCamera;
base.OnEnable ();
}
protected override void OnDisable ()
{
EventManager.OnSwitchCamera -= OnSwitchCamera;
base.OnDisable ();
}
#endregion
#region PublicFunctions
public void SnapForward ()
{
ClearLookAt ();
cameraTransform.localRotation = Quaternion.identity;
}
public void LerpForward (float transitionTime)
{
ClearLookAt ();
if (transitionTime > 0f)
{
lerpDuration = transitionTime;
lerpStartDuration = transitionTime;
}
else
{
SnapForward ();
}
}
public float SetPitch (float pitchAngle)
{
ClearLookAt ();
pitchAngle = Mathf.Clamp (pitchAngle, verticalLimits.x, verticalLimits.y);
cameraTransform.localEulerAngles = new Vector3 (-pitchAngle, cameraTransform.localEulerAngles.y, cameraTransform.localEulerAngles.z);
return pitchAngle;
}
public float SetSpin (float spinAngle)
{
ClearLookAt ();
spinAngle = Mathf.Clamp (spinAngle, horizontalLimits.x, horizontalLimits.y);
cameraTransform.localEulerAngles = new Vector3 (cameraTransform.localEulerAngles.x, spinAngle, cameraTransform.localEulerAngles.z);
return spinAngle;
}
public Vector2 SetAngle (Vector2 input)
{
input.y = Mathf.Clamp (input.y, verticalLimits.x, verticalLimits.y);
input.x = Mathf.Clamp (input.x, horizontalLimits.x, horizontalLimits.y);
cameraTransform.localEulerAngles = new Vector3 (-input.y, input.x, cameraTransform.localEulerAngles.z);
return input;
}
public bool SetLookAt (Transform lookAtTransform, float lookAtSpeed = 1f)
{
if (this.lookAtTransform != lookAtTransform)
{
this.lookAtTransform = lookAtTransform;
forwardLerp.Update (Vector3.zero, Vector3.zero, 100f);
}
this.lookAtSpeed = lookAtSpeed;
Vector3 newForward = (lookAtTransform.position - cameraTransform.position).normalized;
float dot = Vector3.Dot (cameraTransform.forward, newForward);
return (dot > 0.99f);
}
public float GetPitch ()
{
float pitchAngle = -cameraTransform.localEulerAngles.x;
pitchAngle = Mathf.Clamp (pitchAngle, verticalLimits.x, verticalLimits.y);
return pitchAngle;
}
public void SnapToRotation (Quaternion rotation)
{
cameraTransform.rotation = rotation;
}
public void ClearLookAt ()
{
if (lookAtTransform != null)
{
lookAtTransform = null;
}
}
#if UNITY_EDITOR
public void ShowGUI ()
{
EditorGUILayout.LabelField ("Input", EditorStyles.boldLabel);
isDragControlled = EditorGUILayout.Toggle ("Is drag controlled?", isDragControlled);
if (!isDragControlled)
{
xAxisName = EditorGUILayout.TextField ("X-axis input:", xAxisName);
yAxisName = EditorGUILayout.TextField ("Y-axis input:", yAxisName);
}
inputSensitivity = EditorGUILayout.FloatField ("Sensitivity", inputSensitivity);
EditorGUILayout.Space ();
EditorGUILayout.LabelField ("Limits", EditorStyles.boldLabel);
horizontalLimits = EditorGUILayout.Vector2Field ("Horizontal:", horizontalLimits);
verticalLimits = EditorGUILayout.Vector2Field ("Vertical", verticalLimits);
edgeDampingFactor = EditorGUILayout.Slider ("Edge damping:", edgeDampingFactor, 0f, 1f);
EditorGUILayout.Space ();
EditorGUILayout.LabelField ("Misc", EditorStyles.boldLabel);
snapForwardOnTransition = EditorGUILayout.Toggle ("Snap on transition?", snapForwardOnTransition);
}
#endif
#endregion
#region PrivateFunctions
private void AddInput (Vector2 lookInput)
{
Vector2 lookRotation = new Vector2 (cameraTransform.localEulerAngles.y, -cameraTransform.localEulerAngles.x);
if (lookRotation.y < -180f)
{
lookRotation.y += 360f;
}
if (lookRotation.x > 180f)
{
lookRotation.x -= 360f;
}
lookInput = AddEdgeDamping (lookRotation + lookInput, lookInput);
SetAngle (lookRotation + lookInput);
}
private void UpdateLookAt ()
{
Vector3 newForward = (lookAtTransform.position - cameraTransform.position).normalized;
cameraTransform.forward = forwardLerp.Update (cameraTransform.forward, newForward, lookAtSpeed);
}
private void UpdateLerpForward ()
{
if (lerpDuration > 0f)
{
float t = 1f - (lerpDuration / lerpStartDuration);
Quaternion newRotation = Quaternion.Slerp (cameraTransform.localRotation, Quaternion.identity, t);
cameraTransform.localRotation = newRotation;
lerpDuration -= Time.deltaTime;
if (lerpDuration <= 0f)
{
lerpDuration = 0f;
}
}
}
private Vector2 AddEdgeDamping (Vector2 newLookRotation, Vector2 lookInput)
{
if (edgeDampingFactor <= 0f) return lookInput;
float inverseDamping = 1f - edgeDampingFactor;
// Horizontal
float xAmount = newLookRotation.x / horizontalLimits.y;
if (xAmount > inverseDamping && lookInput.x > 0f)
{
lookInput.x = Mathf.Lerp (lookInput.x, 0f, xAmount);
}
xAmount = -newLookRotation.x / horizontalLimits.x;
if (xAmount < -inverseDamping && lookInput.x < -0f)
{
lookInput.x = Mathf.Lerp (lookInput.x, 0f, -xAmount);
}
// Vertical
float yAmount = newLookRotation.y / verticalLimits.y;
if (yAmount > inverseDamping && lookInput.y > 0f)
{
lookInput.y = Mathf.Lerp (lookInput.y, 0f, yAmount);
}
yAmount = -newLookRotation.y / verticalLimits.x;
if (yAmount < -inverseDamping && lookInput.y < -0f)
{
lookInput.y = Mathf.Lerp (lookInput.y, 0f, -yAmount);
}
return lookInput;
}
private void OnSwitchCamera (_Camera fromCamera, _Camera toCamera, float transitionTime)
{
if (toCamera.transform == cameraTransform && snapForwardOnTransition)
{
SnapForward ();
}
}
#endregion
#region GetSet
public Vector3 FlatForward
{
get
{
return new Vector3 (transform.forward.x, 0f, transform.forward.z).normalized;
}
}
#endregion
}
#if UNITY_EDITOR
[CustomEditor(typeof(ConstrainedFirstPersonCamera))]
public class ConstrainedFirstPersonCameraEditor : Editor
{
public override void OnInspectorGUI ()
{
ConstrainedFirstPersonCamera _target = (ConstrainedFirstPersonCamera) target;
_target.ShowGUI ();
UnityVersionHandler.CustomSetDirty (_target);
}
}
#endif
}