using UnityEngine;
using System.Collections;
// Sci-Fi Ship Controller. Copyright (c) 2018-2023 SCSM Pty Ltd. All rights reserved.
namespace SciFiShipController
{
[AddComponentMenu("Sci-Fi Ship Controller/Misc/Ship Camera Module")]
[HelpURL("http://scsmmedia.com/ssc-documentation")]
[RequireComponent(typeof(Camera))]
[RequireComponent(typeof(Rigidbody))]
public class ShipCameraModule : MonoBehaviour
{
#region Public Enumerations
public enum CameraUpdateType
{
///
/// The update occurs during FixedUpdate. Recommended for rigidbodies with Interpolation set to None.
///
FixedUpdate = 0,
///
/// The update occurs during LateUpdate. Recommended for rigidbodies with Interpolation set to Interpolate.
///
LateUpdate = 1,
///
/// When the update occurs is automatically determined.
///
Automatic = 2
}
public enum CameraRotationMode
{
///
/// The camera rotates to face in the direction the ship is moving in.
///
FollowVelocity = 0,
///
/// The camera rotates to face the direction the ship is facing in.
///
FollowTargetRotation = 1,
///
/// The camera rotates to face towards the ship itself.
///
AimAtTarget = 2,
///
/// The camera faces downwards and rotates so that the top of the screen is in the direction
/// the ship is moving in.
///
TopDownFollowVelocity = 5,
///
/// The camera faces downwards and rotates so that the top of the screen is in the direction
/// the ship is facing in.
///
TopDownFollowTargetRotation = 6,
///
/// The camera rotation is fixed.
///
Fixed = 20
}
public enum TargetOffsetCoordinates
{
///
/// The target offset is relative to the rotation of the camera.
///
CameraRotation = 0,
///
/// The target offset is relative to the rotation of the target.
///
TargetRotation = 1,
///
/// The target offset is relative to the flat rotation of the target.
///
TargetRotationFlat = 2,
///
/// The target offset is relative to the world coordinate system.
///
World = 10
}
#endregion
#region Public Variables - General
///
/// Start the camera rendering when it is initialised.
/// See also StartCamera() and StopCamera()
///
public bool startOnInitialise = true;
///
/// Enable camera movement when it is initialised and startOnInitialise is true.
/// See also EnableCamera() and DisableCamera()
///
public bool enableOnInitialise = true;
///
/// The target ship for this camera to follow. At runtime call GetTarget() or SetTarget(newTarget)
///
public ShipControlModule target;
///
/// The offset from the target (in local space) for the camera to aim for.
///
public Vector3 targetOffset = Vector3.zero;
///
/// The coordinate system used to interpret the target offset.
/// CameraRotation: The target offset is relative to the rotation of the camera.
/// TargetRotation: The target offset is relative to the rotation of the target.
/// TargetRotationFlat: The target offset is relative to the flat rotation of the target.
/// World: The target offset is relative to the world coordinate system.
///
public TargetOffsetCoordinates targetOffsetCoordinates = TargetOffsetCoordinates.CameraRotation;
///
/// If enabled, the camera will stay locked to the optimal camera position.
///
public bool lockToTargetPosition = false;
///
/// How quickly the camera moves towards the optimal camera position. Only relevant when lockToTargetPosition is disabled.
///
[Range(1f, 100f)] public float moveSpeed = 15f;
///
/// Damp or modify the target position offset based upon the ship pitch and yaw inputs
///
public bool targetOffsetDamping = false;
///
/// The rate at which Target Offset Y is modified by ship pitch input. Higher values are more responsive.
///
[Range(0.01f, 1f)] public float dampingPitchRate = 0.25f;
///
/// The rate at which the Target Offset Y returns to normal when there is no ship pitch input. Higher values are more responsive.
///
[Range(0.01f, 1f)] public float dampingPitchGravity = 0.25f;
///
/// The rate at which Target Offset X is modified by ship yaw input. Higher values are more responsive.
///
[Range(0.01f, 1f)] public float dampingYawRate = 0.25f;
///
/// The rate at which the Target Offset X returns to normal when there is no ship yaw input. Higher values are more responsive.
///
[Range(0.01f, 1f)] public float dampingYawGravity = 0.25f;
///
/// The damping maximum pitch Target Offset Up (y-axis)
///
public float dampingMaxPitchOffsetUp = 2f;
///
/// The damping maximum pitch Target Offset Down (y-axis)
///
public float dampingMaxPitchOffsetDown = -2f;
///
/// The damping maximum yaw Target Offset right (x-axis)
///
public float dampingMaxYawOffsetRight = 2f;
///
/// The damping maximum yaw Target Offset left (x-axis)
///
public float dampingMaxYawOffsetLeft = -2f;
///
/// If enabled, the camera will stay locked to the optimal camera rotation.
///
public bool lockToTargetRotation = false;
///
/// How quickly the camera turns towards the optimal camera rotation. Only relevant when lockToTargetRotation is disabled.
///
[Range(1f, 100f)] public float turnSpeed = 15f;
///
/// When cameraRotationMode is Aim At Target, enabling this will enable the camera to track the target
/// without moving in the scene.
///
public bool lockCameraPosition = false;
///
/// How the camera rotation is determined.
/// FollowVelocity: The camera rotates to face in the direction the ship is moving in.
/// FollowTargetRotation: The camera rotates to face the direction the ship is facing in.
/// AimAtTarget: The camera rotates to face towards the ship itself.
///
public CameraRotationMode cameraRotationMode = CameraRotationMode.FollowTargetRotation;
///
/// Below this velocity (in metres per second) the forwards direction of the target will be followed instead of the velocity.
/// Only relevant when cameraRotationMode is set to FollowVelocity or TopDownFollowVelocity.
///
public float followVelocityThreshold = 10f;
///
/// If enabled, the camera will orient with respect to the world up direction rather than the target's up direction.
///
public bool orientUpwards = false;
///
/// The rotation of the camera. Only relevant when cameraRotationMode is set to Fixed.
///
public Vector3 cameraFixedRotation = Vector3.zero;
///
/// When the camera position/rotation is updated.
/// FixedUpdate: The update occurs during FixedUpdate. Recommended for rigidbodies with Interpolation set to None.
/// LateUpdate: The update occurs during LateUpdate. Recommended for rigidbodies with Interpolation set to Interpolate.
/// Automatic: When the update occurs is automatically determined.
///
public CameraUpdateType updateType = CameraUpdateType.Automatic;
///
/// The maximum strength of the camera shake. Smaller numbers are better.
/// This can be overridden by calling ShakeCamera(duration,strength)
/// If modifying at runtime, you must call ReinitialiseTargetVariables().
///
[Range(0f,0.5f)] public float maxShakeStrength = 0f;
///
/// The maximum duration (in seconds) the camera will shake per incident.
/// This can be overridden by calling ShakeCamera(duration,strength).
/// If modifying at runtime, you must call ReinitialiseTargetVariables().
///
[Range(0.1f, 5f)] public float maxShakeDuration = 0.2f;
///
/// [INTERNAL ONLY]
///
[HideInInspector] public bool allowRepaint = false;
#endregion
#region Public Variables - Object Clipping
///
/// Adjust the camera position to attempt to avoid the camera flying through objects between the ship and the camera.
///
public bool clipObjects = false;
///
/// The minimum speed the camera will move to avoid flying through objects between the ship and the camera.
/// High values make clipping more effective. Lower values will make it smoother.
/// Currently this has no effect if Lock to Target Position is enabled.
///
[Range(1f, 100f)] public float minClipMoveSpeed = 10f;
///
/// When clipObjects is true, the minimum distance the camera can be from the Ship (target) position.
/// Typically this is the spheric radius of the ship. If the ship has colliders that do not overlay the
/// target position, this value should be set, else set to 0 to improve performance.
///
[Range(0f,1000f)] public float clipMinDistance = 0f;
///
/// The minimum offset on the x-axis, in metres, the camera can be from the Ship (target) when object clipping. This should be less than or equal to the Target Offset X value.
///
[Range(0f, 50f)] public float clipMinOffsetX = 0f;
///
/// The minimum offset on the y-axis, in metres, the camera can be from the Ship (target) when object clipping. This should be less than or equal to the Target Offset Y value.
///
[Range(0f, 50)] public float clipMinOffsetY = 0f;
///
/// Clip objects in the selected Unity Layers.
/// Start with Nothing (0) and call ResetClipObjectSettings()
///
public LayerMask clipObjectMask = 0;
#endregion
#region Public Variables - Zoom
///
/// The time, in seconds, to zoom fully in or out
///
[Range(0.1f, 20f)] public float zoomDuration = 3f;
///
/// The delay, in seconds, before zoom starts to return to the non-zoomed position
///
[Range(0f, 3600f)] public float unzoomDelay = 0f;
///
/// The camera field-of-view when no zoom is applied
///
[Range(20f, 85f)] public float unzoomedFoV = 60f;
///
/// The camera field-of-view when the camera is fully zoomed in.
///
[Range(1f, 50f)] public float zoomedInFoV = 10f;
///
/// The camera field-of-view when the camera is fully zoomed out.
///
[Range(40f, 150f)] public float zoomedOutFoV = 90f;
///
/// The amount of damping applied when starting or stopping camera zoom
///
[Range(0f, 1f)] public float zoomDamping = 0.1f;
#endregion
#region Public Member Properties
///
/// Is the camera being moved using FixedUpdate()?
///
public bool IsCameraInFixedUpdate { get; private set; }
///
/// Is camera started and rendering?
///
public bool IsCameraStarted { get { return camera1 == null ? false : camera1.enabled; } }
///
/// Is the camera enabled for movement?
///
public bool IsCameraEnabled { get { return cameraIsEnabled; } }
///
/// Get the camera being used by this module
///
public Camera GetCamera1 { get { return camera1; } }
#endregion
#region Public Static Properties
///
/// Everything, except TransparentFX (1), IgnoreRaycast (2), UI (5)
///
public static LayerMask DefaultClipObjectMask { get { return ~((1 << 1) | (1 << 2) | (1 << 5)); } }
#endregion
#region Private Variables
private Rigidbody rBody;
private Camera camera1;
private Transform targetTrfm;
private Rigidbody targetRBody;
private Vector3 currentPosition;
private Vector3 targetPosition;
private Quaternion targetRotation;
private Vector3 targetTrfmUp;
private Vector3 targetTrfmFwd;
private Vector3 targetOffsetDamped = Vector3.zero;
private float dampingOffsetAdjPitch = 0f, dampingOffsetAdjYaw = 0f;
private Vector3 optimalCameraPosition;
private Vector3 previousOptimalCameraPosition = Vector3.zero;
private Quaternion optimalCameraRotation;
private Vector3 optimalCameraForward;
private Vector3 optimalCameraUp;
//private Vector3 currentMoveDampVelocity = Vector3.zero;
private Vector3 currentTurnDampVelocity = Vector3.zero;
private Vector3 optimalCameraRotationEulerAngles;
private Vector3 currentCameraRotationEulerAngles;
private bool cameraIsEnabled = true;
// Camera shake variables - See MoveCamera() and ShakeCamera(..).
private bool isShaking = false;
private float shakeStrength = 1f;
private float shakeTimer = 0f;
// When initialised, was there an audioListener component present and enabled?
// This may not be the case when it is a point-of-interest camera in the scene,
// like in TechDemo2.
private bool isAudioListenerConfigured = false;
private AudioListener audioListener = null;
// Object clipping
private Vector3 viewHalfExtents = Vector3.zero;
// Zoom
private float currentZoomInput = 0f;
private float previousZoomInput = 0f;
private float zoomFactor = 0f;
private float unzoomTimer = 0f;
private bool isZoomInputProcessed = false;
[SerializeField] private bool isZoomEnabled = false;
#endregion
#region Awake Method
// Use this for initialization
void Awake()
{
// Get the rigidbody attached to the camera
// Caters for the situation when script was already attached, but without a rigidbody
if (!TryGetComponent(out rBody))
{
rBody = gameObject.AddComponent();
#if UNITY_EDITOR
Debug.Log("ShipCameraModule - Adding missing Rigidbody");
#endif
}
// Set up the camera rigidbody
rBody.mass = 1f;
rBody.isKinematic = true;
rBody.interpolation = RigidbodyInterpolation.None;
// Initialise target variables
if (target != null) { ReinitialiseTargetVariables(); }
isShaking = false;
audioListener = GetComponent();
isAudioListenerConfigured = audioListener == null ? false : audioListener.enabled;
ReinitialiseCameraSettings();
if (startOnInitialise)
{
StartCamera();
if (!enableOnInitialise) { DisableCamera(); }
}
else { StopCamera(); }
previousOptimalCameraPosition = transform.position;
}
#endregion
#region Update Methods
// FixedUpdate is called once per physics update
void FixedUpdate ()
{
// Move camera in FixedUpdate if either:
// - We are in FixedUpdate mode
// - We are in Automatic mode and the target rigidbody has interpolation disabled
if (cameraIsEnabled && target != null && targetRBody != null && (updateType == CameraUpdateType.FixedUpdate || (updateType == CameraUpdateType.Automatic &&
targetRBody.interpolation == RigidbodyInterpolation.None)))
{
IsCameraInFixedUpdate = true;
MoveCamera();
}
}
// LateUpdate is called once per frame, after all update functions have been called
void LateUpdate ()
{
// Move camera in LateUpdate if either:
// - We are in LateUpdate mode
// - We are in Automatic mode and the target rigidbody has interpolation enabled
if (cameraIsEnabled && target != null && targetRBody != null && (updateType == CameraUpdateType.LateUpdate || (updateType == CameraUpdateType.Automatic &&
targetRBody.interpolation != RigidbodyInterpolation.None)))
{
IsCameraInFixedUpdate = false;
MoveCamera();
}
if (cameraIsEnabled && isZoomEnabled)
{
CalcZoomFactor();
ZoomCamera();
}
}
#endregion
#region Private and Internal Non-Static Methods
///
/// Calculate the zoom factor based on zoom input and any unzoom delay.
///
private void CalcZoomFactor()
{
// Is the user (or program) attempting to manually zoom in or out
if (currentZoomInput != 0f)
{
zoomFactor += currentZoomInput * Time.deltaTime / zoomDuration;
if (zoomFactor > 1f) { zoomFactor = 1f; }
else if (zoomFactor < -1f) { zoomFactor = -1f; }
unzoomTimer = unzoomDelay;
// Reset zoom input after use
isZoomInputProcessed = true;
currentZoomInput = 0f;
}
else if (unzoomTimer <= 0f)
{
previousZoomInput = 0f;
// +ve zoom, start reducing zoom towards no zoom
if (zoomFactor > 0f)
{
// If it has almost returned to 0, stop zooming
if (zoomFactor < Vector3.kEpsilon) { zoomFactor = 0f; }
else
{
zoomFactor -= Time.deltaTime / zoomDuration;
if (zoomFactor < 0f) { zoomFactor = 0f; }
}
}
// -ve zoom, start increasing zoom towards no zoom
else
{
// If it has almost returned to 0, stop zooming
if (-zoomFactor < Vector3.kEpsilon) { zoomFactor = 0f; }
else
{
zoomFactor += Time.deltaTime / zoomDuration;
if (zoomFactor > 0f) { zoomFactor = 0f; }
}
}
}
else
{
unzoomTimer -= Time.deltaTime;
if (unzoomTimer < 0f) { unzoomTimer = 0f; }
}
}
///
/// Enable or Disable the zoom feature
///
///
private void EnableOrDisableZoom(bool isEnabled)
{
if (isEnabled)
{
SetZoomAmount(0f);
currentZoomInput = 0f;
previousZoomInput = 0f;
}
else
{
SetZoomAmount(0f);
ZoomCamera();
}
isZoomEnabled = isEnabled;
}
private Vector3 SmoothFollowPosition(Vector3 currentPosition, Vector3 previousTargetPosition, Vector3 currentTargetPosition, float deltaTime, float speed)
{
// Avoid NaN when speed = 0.
if (speed < 0.001f) { return currentPosition; }
else
{
float speedTimeProduct = deltaTime * speed;
Vector3 v = (currentTargetPosition - previousTargetPosition) / speedTimeProduct;
Vector3 f = currentPosition - previousTargetPosition + v;
return currentTargetPosition - v + f * Mathf.Exp(-speedTimeProduct);
}
}
///
/// This is called by a ship when it is hit.
/// The shakeAmount should be proportion of the maximum duration and strength as a
/// normalised value (between 0.0 and 1.0).
///
/// Normalised amount between 0.0 and 1.0
internal void ShakeCameraInternal(float shakeAmountNormalised)
{
ShakeCamera(maxShakeDuration * shakeAmountNormalised, maxShakeStrength * shakeAmountNormalised);
}
///
/// Attempt to not fly through objects in the scene that are in the clipObjectMask layer(s)
///
private bool ObjectClipping(Vector3 aimAtPosition, Quaternion targetRot, ref Vector3 cameraPosition, ref Quaternion cameraRotation)
{
// Get the direction looking from the target to the camera.
Vector3 clipLookDirection = (cameraPosition - aimAtPosition).normalized;
Vector3 clipStartPosition = clipMinDistance > 0f ? aimAtPosition + (clipLookDirection * clipMinDistance) : aimAtPosition;
// The distance between the ship (or ship + clipMinDistance) and the camera LESS the camera near clip plane (don't need
// to check what camera does not render).
float clipCheckDistance = Vector3.Distance(clipStartPosition, cameraPosition) - camera1.nearClipPlane;
RaycastHit clipHit;
if (Physics.BoxCast(clipStartPosition, viewHalfExtents, clipLookDirection, out clipHit,
cameraRotation, clipCheckDistance, clipObjectMask, QueryTriggerInteraction.Ignore))
{
// The near clip plane should be placed in front of the obstacle. By not adding the nearClipPlane
// onto the hit distance, it should cater for situations where the obstacle normal is not aligned with the clipLookDirection.
cameraPosition = clipStartPosition + (clipLookDirection * clipHit.distance);
if (clipMinOffsetX > 0f || clipMinOffsetY > 0f)
{
// Get local space offset
Vector3 currentOffset = Quaternion.Inverse(targetRot) * (cameraPosition - aimAtPosition);
if (currentOffset.y < clipMinOffsetY || currentOffset.x < clipMinOffsetX)
{
cameraPosition = aimAtPosition + (targetRot * new Vector3(currentOffset.x < clipMinOffsetX ? clipMinOffsetX : currentOffset.x, currentOffset.y < clipMinOffsetY ? clipMinOffsetY : currentOffset.y, currentOffset.z));
}
}
return true;
}
else { return false; }
}
//Vector3 clipStartPosition = Vector3.zero, clipLookDirection = Vector3.zero;
//float clipCheckDistance;
//private void OnDrawGizmos()
//{
// if (camera1 != null)
// {
// // Test Object Clipping
// //Gizmos.color = Color.yellow;
// //Gizmos.DrawRay(optimalCameraPosition, clipLookDirection * 20f);
// Gizmos.color = Color.blue;
// Gizmos.DrawRay(clipStartPosition, clipLookDirection * (Vector3.Distance(clipStartPosition, optimalCameraPosition) - camera1.nearClipPlane));
// Gizmos.DrawWireCube(clipStartPosition, viewHalfExtents * 2f);
// }
//}
///
/// Adjust the target position based on the player or AI input.
/// This should only be called when target is not null and
/// isLockCameraPosition is not true.
///
private void TargetOffsetPositionDamping()
{
// Get pitch and yaw input from the ship
Vector3 momentInput = target.pilotMomentInput;
float _dTFactor = Time.deltaTime * 10f;
// Pitch - ship goes toward bottom of screen while pointing up
if (momentInput.x > 0f)
{
dampingOffsetAdjPitch += -momentInput.x * dampingPitchRate * _dTFactor;
if (dampingOffsetAdjPitch < dampingMaxPitchOffsetDown)
{
dampingOffsetAdjPitch = dampingMaxPitchOffsetDown;
}
}
// Pitch - ship goes toward top of screen while pointing down
else if (momentInput.x < 0f)
{
dampingOffsetAdjPitch += -momentInput.x * dampingPitchRate * _dTFactor;
if (dampingOffsetAdjPitch > dampingMaxPitchOffsetUp)
{
dampingOffsetAdjPitch = dampingMaxPitchOffsetUp;
}
}
else
{
if (dampingOffsetAdjPitch > targetOffset.y)
{
dampingOffsetAdjPitch -= dampingPitchGravity * _dTFactor;
if (dampingOffsetAdjPitch < targetOffset.y)
{
dampingOffsetAdjPitch = targetOffset.y;
}
}
else if (dampingOffsetAdjPitch < targetOffset.y)
{
dampingOffsetAdjPitch += dampingPitchGravity * _dTFactor;
if (dampingOffsetAdjPitch > targetOffset.y)
{
dampingOffsetAdjPitch = targetOffset.y;
}
}
}
//dampingOffsetAdjPitch = targetOffset.y;
// Yaw - ship goes right, camera goes left
if (momentInput.y > 0f)
{
dampingOffsetAdjYaw += momentInput.y * dampingYawRate * _dTFactor;
if (dampingOffsetAdjYaw > dampingMaxYawOffsetRight)
{
dampingOffsetAdjYaw = dampingMaxYawOffsetRight;
}
}
// Yaw - ship goes left, camera goes right
else if (momentInput.y < 0f)
{
dampingOffsetAdjYaw += momentInput.y * dampingYawRate * _dTFactor;
if (dampingOffsetAdjYaw < dampingMaxYawOffsetLeft)
{
dampingOffsetAdjYaw = dampingMaxYawOffsetLeft;
}
}
else
{
if (dampingOffsetAdjYaw > targetOffset.x)
{
dampingOffsetAdjYaw -= dampingYawGravity * _dTFactor;
if (dampingOffsetAdjYaw < targetOffset.x)
{
dampingOffsetAdjYaw = targetOffset.x;
}
}
else if (dampingOffsetAdjYaw < targetOffset.x)
{
dampingOffsetAdjYaw += dampingYawGravity * _dTFactor;
if (dampingOffsetAdjYaw > targetOffset.x)
{
dampingOffsetAdjYaw = targetOffset.x;
}
}
}
targetOffsetDamped.x = dampingOffsetAdjYaw;
targetOffsetDamped.y = dampingOffsetAdjPitch;
targetOffsetDamped.z = targetOffset.z;
//Debug.Log("[DEBUG] targetOffsetDamped: " + targetOffsetDamped + " T:" + Time.time);
}
#endregion
#region Public API Non-Static Methods
///
/// Attempt to apply camera settings from a ScriptableObject to this ShipCameraModule.
/// See also ExportCameraSettings(..)
///
///
public virtual void ApplyCameraSettings (ShipCameraSettings shipCameraSettings)
{
if (shipCameraSettings != null)
{
cameraFixedRotation = shipCameraSettings.cameraFixedRotation;
cameraRotationMode = shipCameraSettings.cameraRotationMode;
clipMinDistance = shipCameraSettings.clipMinDistance;
clipMinOffsetX = shipCameraSettings.clipMinOffsetX;
clipMinOffsetY = shipCameraSettings.clipMinOffsetY;
clipObjectMask = shipCameraSettings.clipObjectMask;
clipObjects = shipCameraSettings.clipObjects;
dampingMaxPitchOffsetDown = shipCameraSettings.dampingMaxPitchOffsetDown;
dampingMaxPitchOffsetUp = shipCameraSettings.dampingMaxPitchOffsetUp;
dampingMaxYawOffsetLeft = shipCameraSettings.dampingMaxYawOffsetLeft;
dampingMaxYawOffsetRight = shipCameraSettings.dampingMaxYawOffsetRight;
dampingPitchGravity = shipCameraSettings.dampingPitchGravity;
dampingPitchRate = shipCameraSettings.dampingPitchRate;
dampingYawGravity = shipCameraSettings.dampingYawGravity;
dampingYawRate = shipCameraSettings.dampingYawRate;
followVelocityThreshold = shipCameraSettings.followVelocityThreshold;
isZoomEnabled = shipCameraSettings.isZoomEnabled;
lockCameraPosition = shipCameraSettings.lockCameraPosition;
lockToTargetPosition = shipCameraSettings.lockToTargetPosition;
lockToTargetRotation = shipCameraSettings.lockToTargetRotation;
maxShakeDuration = shipCameraSettings.maxShakeDuration;
maxShakeStrength = shipCameraSettings.maxShakeStrength;
minClipMoveSpeed = shipCameraSettings.minClipMoveSpeed;
moveSpeed = shipCameraSettings.moveSpeed;
orientUpwards = shipCameraSettings.orientUpwards;
targetOffset = shipCameraSettings.targetOffset;
targetOffsetCoordinates = shipCameraSettings.targetOffsetCoordinates;
targetOffsetDamping = shipCameraSettings.targetOffsetDamping;
turnSpeed = shipCameraSettings.turnSpeed;
unzoomDelay = shipCameraSettings.unzoomDelay;
unzoomedFoV = shipCameraSettings.unzoomedFoV;
updateType = shipCameraSettings.updateType;
zoomDamping = shipCameraSettings.zoomDamping;
zoomDuration = shipCameraSettings.zoomDuration;
zoomedInFoV = shipCameraSettings.zoomedInFoV;
zoomedOutFoV = shipCameraSettings.zoomedOutFoV;
}
}
///
/// Disables the camera, preventing it from moving (does not stop the camera rendering).
///
public void DisableCamera()
{
cameraIsEnabled = false;
}
///
/// Turn off the zoom feature and reset the camera field of view to the default setting.
///
public void DisableZoom()
{
EnableOrDisableZoom(false);
}
///
/// Enables the camera, allowing it to move.
/// See StartCamera() to allow it to render.
///
public void EnableCamera()
{
cameraIsEnabled = true;
}
///
/// Turn on the zoom feature
///
public void EnableZoom()
{
EnableOrDisableZoom(true);
}
///
/// Attempt to export the camera setings from this ShipCameraModule to a pre-created ShipCameraSettings ScriptableObject.
/// See also ApplyCameraSettings(..)
///
///
public virtual void ExportCameraSettings (ref ShipCameraSettings shipCameraSettings)
{
if (shipCameraSettings != null)
{
shipCameraSettings.cameraFixedRotation = cameraFixedRotation;
shipCameraSettings.cameraRotationMode = cameraRotationMode;
shipCameraSettings.clipMinDistance = clipMinDistance;
shipCameraSettings.clipMinOffsetX = clipMinOffsetX;
shipCameraSettings.clipMinOffsetY = clipMinOffsetY;
shipCameraSettings.clipObjectMask = clipObjectMask;
shipCameraSettings.clipObjects = clipObjects;
shipCameraSettings.dampingMaxPitchOffsetDown = dampingMaxPitchOffsetDown;
shipCameraSettings.dampingMaxPitchOffsetUp = dampingMaxPitchOffsetUp;
shipCameraSettings.dampingMaxYawOffsetLeft = dampingMaxYawOffsetLeft;
shipCameraSettings.dampingMaxYawOffsetRight = dampingMaxYawOffsetRight;
shipCameraSettings.dampingPitchGravity = dampingPitchGravity;
shipCameraSettings.dampingPitchRate = dampingPitchRate;
shipCameraSettings.dampingYawGravity = dampingYawGravity;
shipCameraSettings.dampingYawRate = dampingYawRate;
shipCameraSettings.followVelocityThreshold = followVelocityThreshold;
shipCameraSettings.isZoomEnabled = isZoomEnabled;
shipCameraSettings.lockCameraPosition = lockCameraPosition;
shipCameraSettings.lockToTargetPosition = lockToTargetPosition;
shipCameraSettings.lockToTargetRotation = lockToTargetRotation;
shipCameraSettings.maxShakeDuration = maxShakeDuration;
shipCameraSettings.maxShakeStrength = maxShakeStrength;
shipCameraSettings.minClipMoveSpeed = minClipMoveSpeed;
shipCameraSettings.moveSpeed = moveSpeed;
shipCameraSettings.orientUpwards = orientUpwards;
shipCameraSettings.targetOffset = targetOffset;
shipCameraSettings.targetOffsetCoordinates = targetOffsetCoordinates;
shipCameraSettings.targetOffsetDamping = targetOffsetDamping;
shipCameraSettings.turnSpeed = turnSpeed;
shipCameraSettings.unzoomDelay = unzoomDelay;
shipCameraSettings.unzoomedFoV = unzoomedFoV;
shipCameraSettings.updateType = updateType;
shipCameraSettings.zoomDamping = zoomDamping;
shipCameraSettings.zoomDuration = zoomDuration;
shipCameraSettings.zoomedInFoV = zoomedInFoV;
shipCameraSettings.zoomedOutFoV = zoomedOutFoV;
}
}
///
/// Get the current target ship for the camera. If the camera is currently not assigned to a ship, it will return null.
///
///
public ShipControlModule GetTarget()
{
return target;
}
///
/// The amount of zoom currently used.
/// 0 is no zoom.
/// 1 is fully zoomed in.
/// -1 is fully zoomed out.
///
///
public float GetZoomAmount()
{
return zoomFactor;
}
///
/// Start the camera rendering. If the camera is disabled, it will remain so.
/// Call EnableCamera() to allow it to also move.
/// If there was an Audio Listener component attached and enabled when this
/// module was initialised, the listener will be re-enabled.
///
public void StartCamera()
{
if (camera1 != null && !camera1.enabled)
{
camera1.enabled = true;
if (isAudioListenerConfigured && audioListener != null) { audioListener.enabled = true; }
}
}
///
/// Stops the camera from rendering. Also disables the camera from moving.
/// If there was an Audio Listener component attached and enabled when this
/// module was initialised, the listener will be disabled.
///
public void StopCamera()
{
StopCameraShake();
cameraIsEnabled = false;
if (camera1 != null && camera1.enabled) { camera1.enabled = false; }
if (isAudioListenerConfigured && audioListener != null) { audioListener.enabled = false; }
}
///
/// Set's a new target for the camera and calls ReinitialiseTargetVariables().
///
///
public void SetTarget(ShipControlModule shipControlModule)
{
// reset the previous target's camera shake callback method
if (target != null && target.shipInstance != null) { target.shipInstance.callbackOnCameraShake = null; }
target = shipControlModule;
ReinitialiseTargetVariables();
}
///
/// Moves the camera. Should be called during an update function (Update, LateUpdate or FixedUpdate).
/// Typically, you should not call this yourself, as it is called automatically each frame. However,
/// it can prove useful in the case where you need to force a camera movement update for this frame.
///
public void MoveCamera()
{
#region Initialisation
targetPosition = targetTrfm.position;
targetRotation = targetTrfm.rotation;
// Get current position
currentPosition = transform.position;
// Get target transform up and forwards directions
targetTrfmUp = targetRotation * Vector3.up;
targetTrfmFwd = targetRotation * Vector3.forward;
// By default this always off. Is only available below
// when CameraRotationMode is AimAtTarget
bool isLockCameraPosition = false;
// Is camera still shaking?
if (isShaking)
{
shakeTimer -= Time.deltaTime;
if (shakeTimer <= 0f) { StopCameraShake(); }
}
#endregion
#region Find Optimal Camera Rotation
//bool followingTargetRotation = false;
// In late update mode, use the interpolated world velocity of the rigidbody instead of the normal world velocity
Vector3 shipWorldVelocity = Vector3.zero;
if (cameraRotationMode == CameraRotationMode.FollowVelocity || cameraRotationMode == CameraRotationMode.TopDownFollowVelocity)
{
if (IsCameraInFixedUpdate)
{
shipWorldVelocity = target.shipInstance.WorldVelocity;
}
else
{
shipWorldVelocity = target.shipInstance.GetInterpolatedWorldVelocity();
}
}
// Find the optimal camera up direction
if (cameraRotationMode == CameraRotationMode.TopDownFollowVelocity)
{
// When above a given velocity, this is the ship's flattened velocity vector
if (shipWorldVelocity.sqrMagnitude > followVelocityThreshold * followVelocityThreshold)
{
optimalCameraUp = shipWorldVelocity;
optimalCameraUp.y = 0f;
optimalCameraUp.Normalize();
}
// When below that velocity, this is the ship's flattened forward vector
else
{
optimalCameraUp = targetTrfmFwd;
optimalCameraUp.y = 0f;
optimalCameraUp.Normalize();
}
}
else if (cameraRotationMode == CameraRotationMode.TopDownFollowTargetRotation)
{
// If we are using the top down follow target rotation mode, this is the ship's flattened forward vector
optimalCameraUp = targetTrfmFwd;
optimalCameraUp.y = 0f;
optimalCameraUp.Normalize();
}
else if (cameraRotationMode == CameraRotationMode.Fixed)
{
// If we are using the fixed mode, this is the camera rotation's up vector
optimalCameraUp = Quaternion.Euler(cameraFixedRotation) * Vector3.up;
}
else
{
// If we are orienting upwards, this is the world up direction
if (orientUpwards) { optimalCameraUp = Vector3.up; }
// Otherwise this is the target up direction
else { optimalCameraUp = targetTrfmUp; }
}
// Find the optimal camera forwards direction
if (cameraRotationMode == CameraRotationMode.AimAtTarget)
{
// Aim at target mode: Forwards is towards the target
optimalCameraForward = (targetPosition - currentPosition).normalized;
isLockCameraPosition = lockCameraPosition;
//followingTargetRotation = false;
}
else if (cameraRotationMode == CameraRotationMode.FollowVelocity)
{
// Follow velocity mode: When above a given velocity, forwards is in direction of target velocity
if (shipWorldVelocity.sqrMagnitude > followVelocityThreshold * followVelocityThreshold)
{
optimalCameraForward = shipWorldVelocity.normalized;
//followingTargetRotation = false;
}
// When below that velocity, forwards is in direction of target forwards direction
else
{
optimalCameraForward = targetTrfmFwd;
//followingTargetRotation = true;
}
}
else if (cameraRotationMode == CameraRotationMode.FollowTargetRotation)
{
// Follow target rotation mode: Forwards is in direction of target forwards direction
optimalCameraForward = targetTrfmFwd;
}
else if (cameraRotationMode == CameraRotationMode.Fixed)
{
// Fixed mode: Forwards is camera rotation's forwards vector
optimalCameraForward = Quaternion.Euler(cameraFixedRotation) * Vector3.forward;
}
else
{
// Top down modes: Forwards is world down direction
optimalCameraForward = Vector3.down;
}
// Set optimal camera rotation from optimal camera forwards and up directions
optimalCameraRotation = Quaternion.LookRotation(optimalCameraForward, optimalCameraUp);
#endregion
#region Find Optimal Camera Position
if (!isLockCameraPosition)
{
if (targetOffsetDamping) { TargetOffsetPositionDamping(); }
else { targetOffsetDamped = targetOffset; }
//targetOffsetDamped = targetOffset;
// Optimal camera position is always target position plus some offset
if (targetOffsetCoordinates == TargetOffsetCoordinates.TargetRotation)
{
// Offset is applied with respect to target rotation
optimalCameraPosition = targetPosition + (targetRotation * targetOffsetDamped);
}
else if (targetOffsetCoordinates == TargetOffsetCoordinates.TargetRotationFlat)
{
// Offset is applied with respect to target rotation, but only rotation on the world y-axis
Vector3 targetTrfmFwdFlat = targetTrfmFwd;
targetTrfmFwdFlat.y = 0f;
optimalCameraPosition = targetPosition + (Quaternion.LookRotation(targetTrfmFwdFlat, Vector3.up) * targetOffsetDamped);
}
else if (targetOffsetCoordinates == TargetOffsetCoordinates.CameraRotation)
{
// Offset is applied with respect to optimal camera rotation forwards and up direction vectors
optimalCameraPosition = targetPosition + (targetOffsetDamped.z * optimalCameraForward) + (targetOffsetDamped.y * optimalCameraUp);
if (targetOffsetDamped.x != 0f)
{
optimalCameraPosition += targetOffsetDamped.x * Vector3.Cross(optimalCameraUp, optimalCameraForward);
}
}
else
{
// Offset is applied with respect to the world coordinate system
optimalCameraPosition = targetPosition + targetOffsetDamped;
}
}
#endregion
#region Object Clipping
bool isClippingThisFrame = false;
if (clipObjects) { isClippingThisFrame = ObjectClipping(targetPosition, targetRotation, ref optimalCameraPosition, ref optimalCameraRotation); }
#endregion
#region Compute Movement Towards Optimal Camera Position / Rotation
if (isLockCameraPosition && isShaking)
{
// Might be a way of doing this in one step
//transform.position = previousOptimalCameraPosition;
//transform.Translate(Random.insideUnitCircle * shakeStrength);
}
if (!isLockCameraPosition)
{
// Move immediately to optimal camera position
if (lockToTargetPosition)
{
if (isShaking)
{
// Might be a way of doing this in one step
transform.position = optimalCameraPosition;
transform.Translate(Random.insideUnitCircle * shakeStrength);
}
else { transform.position = optimalCameraPosition; }
}
// Move gradually towards optimal camera position
else
{
// Smooth follow code (substitute for Lerp)
// [IMPORTANT] If you get a NaN error message, it means you didn't call DisableCamera() before setting Time.timeScale = 0.
// When clipObjects is enabled, and something is blocking camera, move quickly to target position
transform.position = SmoothFollowPosition(currentPosition, previousOptimalCameraPosition, optimalCameraPosition, Time.deltaTime, isClippingThisFrame && moveSpeed < minClipMoveSpeed ? minClipMoveSpeed : moveSpeed);
if (isShaking)
{
transform.Translate(Random.insideUnitCircle * shakeStrength);
}
previousOptimalCameraPosition = optimalCameraPosition;
}
}
// Move immediately to optimal camera rotation
if (lockToTargetRotation) { transform.rotation = optimalCameraRotation; }
// Move gradually towards optimal camera rotation
else
{
// Smooth damp code (substitute for Lerp) - modified to work with rotation
// First, convert quaternions into Euler angles vectors
currentCameraRotationEulerAngles = transform.rotation.eulerAngles;
optimalCameraRotationEulerAngles = optimalCameraRotation.eulerAngles;
// Then, use SmoothDampAngle on each of the three axes separately
currentCameraRotationEulerAngles.x = Mathf.SmoothDampAngle(currentCameraRotationEulerAngles.x, optimalCameraRotationEulerAngles.x,
ref currentTurnDampVelocity.x, 1f / turnSpeed, Mathf.Infinity, Time.deltaTime);
currentCameraRotationEulerAngles.y = Mathf.SmoothDampAngle(currentCameraRotationEulerAngles.y, optimalCameraRotationEulerAngles.y,
ref currentTurnDampVelocity.y, 1f / turnSpeed, Mathf.Infinity, Time.deltaTime);
currentCameraRotationEulerAngles.z = Mathf.SmoothDampAngle(currentCameraRotationEulerAngles.z, optimalCameraRotationEulerAngles.z,
ref currentTurnDampVelocity.z, 1f / turnSpeed, Mathf.Infinity, Time.deltaTime);
// Finally, convert Euler angles vector back to quaternion
transform.rotation = Quaternion.Euler(currentCameraRotationEulerAngles);
}
#endregion
}
///
/// Tele-port the camera to a new position.
/// Rotation is xyz euler angles in degrees.
/// See also TelePort(..)
///
///
///
public void MoveTo(Vector3 position, Vector3 rotation)
{
// Should be the same as setting the transform directly but included
// we'll update the variables too.
previousOptimalCameraPosition = position;
optimalCameraRotation = Quaternion.Euler(rotation);
transform.SetPositionAndRotation(position, optimalCameraRotation);
}
///
/// Call this whenever changing camera properties
///
public void ReinitialiseCameraSettings()
{
if (camera1 == null) { camera1 = GetComponent(); }
if (camera1 != null)
{
// Calculate the half width, height and depth of the camera view. This can be used with
// Physics.Boxcast to "see" objects in front of the camera.
viewHalfExtents = Vector3.zero;
// fieldOfView is in radians. Convert to degrees.
viewHalfExtents.y = camera1.nearClipPlane * Mathf.Tan(0.5f * Mathf.Deg2Rad * camera1.fieldOfView);
viewHalfExtents.x = viewHalfExtents.y * camera1.aspect;
// Reset zoom
SetZoomAmount(0f);
}
// The start value is set to Nothing (0) intentially. We then set it to something sensible
if (clipObjectMask == 0) { ResetClipObjectSettings(); }
}
///
/// Reset the clip object settings to their default values
///
public void ResetClipObjectSettings()
{
// Everything, except TransparentFX (1), IgnoreRaycast (2), UI (5)
//clipObjectMask = ~((1 << 1) | (1 << 2) | (1 << 5));
clipObjectMask = DefaultClipObjectMask;
}
///
/// Re-initialises variables related to the target.
/// Call after modifying target or shake public variables.
/// This will stop camera from shaking.
///
public void ReinitialiseTargetVariables()
{
if (target != null)
{
targetTrfm = target.transform;
targetRBody = target.GetComponent();
if (target.shipInstance != null)
{
if (maxShakeStrength > 0f && maxShakeDuration > 0f) { target.shipInstance.callbackOnCameraShake = ShakeCameraInternal; }
else { target.shipInstance.callbackOnCameraShake = null; }
}
}
else
{
targetTrfm = null;
targetRBody = null;
}
dampingOffsetAdjPitch = 0f;
dampingOffsetAdjYaw = 0f;
StopCameraShake();
}
///
/// Shake the camera for maxShakeDuration seconds which maxShakeStrength or force.
/// If the camera is not enabled or the duration and/or strength are 0 or less,
/// StopCameraShake() will be automatically called and the inputs ignored.
///
public void ShakeCamera()
{
ShakeCamera(maxShakeDuration, maxShakeStrength);
}
///
/// Shake the camera with strength and duration relative to
/// the current maxShakeDuration and maxShakeStrength.
/// Range should be between 0.0 and 1.0.
///
/// Normalised value between 0.0 and 1.0
public void ShakeCamera (float relativeStrength)
{
if (relativeStrength < 0.0001f) { StopCameraShake(); }
else if (relativeStrength >= 1f) { ShakeCamera(maxShakeDuration, maxShakeStrength); }
else { ShakeCameraInternal(relativeStrength); }
}
///
/// Shake the camera for specified seconds which the given relative strength or force.
/// If the camera is not enabled or the duration and/or strength are 0 or less,
/// StopCameraShake() will be automatically called and the inputs ignored.
///
///
///
public void ShakeCamera (float duration, float strength)
{
if (cameraIsEnabled && duration > 0f && strength > 0f)
{
isShaking = true;
shakeStrength = strength;
shakeTimer = duration;
}
else
{
StopCameraShake();
}
}
///
/// Shake the camera after initial delay in seconds, with the
/// current maxShakeDuration and maxShakeStrength.
///
///
public void ShakeCameraDelayed (float delayTime)
{
if (delayTime > 0f)
{
Invoke("ShakeCamera", delayTime);
}
else { ShakeCamera(maxShakeDuration, maxShakeStrength); }
}
///
/// Send zoom input to the camera. It is ignored if Zoom is not enabled.
/// Values should be between -1.0 and 1.0.
/// Zoom in for +ve values. Zoom out for -ve values.
/// The value is automatically reset to 0 after use.
///
///
public void SendZoomInput (float zoomInput)
{
if (isZoomEnabled)
{
currentZoomInput = zoomInput < -1f ? -1f : zoomInput > 1f ? 1f : zoomInput;
currentZoomInput = SSCMath.DampValue(previousZoomInput, currentZoomInput, Time.deltaTime, zoomDamping);
previousZoomInput = currentZoomInput;
if (isZoomInputProcessed) { isZoomInputProcessed = false; }
}
}
///
/// Set the unzoom delay, in seconds, for the camera.
///
///
public void SetUnzoomDelay(float newValue)
{
if (newValue < 0f) { newValue = 0f; }
unzoomDelay = newValue;
if (newValue < unzoomTimer) { unzoomTimer = newValue; }
}
///
/// Set the camera to use a particular display or screen. Displays or monitors are numbered from 1 to 8.
///
/// 1 to 8
public void SetCameraTargetDisplay(int displayNumber)
{
Camera camera = GetComponent();
if (camera != null && SSCUtils.VerifyTargetDisplay(displayNumber, true)) { camera.targetDisplay = displayNumber - 1; }
}
///
/// Attempt to set a new maxShakeDuration
///
///
public void SetMaxShakeDuration (float newDuration)
{
if (newDuration > 0f)
{
maxShakeDuration = newDuration;
}
}
///
/// Attempt to set a new maxShakeStrenth
///
///
public void SetMaxShakeStrength (float newStrength)
{
if (newStrength > 0f)
{
maxShakeStrength = newStrength;
}
}
///
/// The amount of zoom to apply.
/// 0 is no zoom.
/// 1 is fully zoomed in.
/// -1 is fully zoomed out.
///
///
public void SetZoomAmount(float zoomAmount)
{
if (zoomAmount > 1f) { zoomFactor = 1f; }
else if (zoomAmount < -1f) { zoomFactor = 1f; }
else { zoomFactor = zoomAmount; }
}
///
/// Stop the camera from shaking
///
public void StopCameraShake()
{
isShaking = false;
shakeTimer = 0f;
}
///
/// Teleport the camera to a new location by moving by an amount
/// in the x, y and z directions. This could be useful if changing
/// the origin or centre of your world to compensate for float-point
/// error.
///
///
public void TelePort (Vector3 delta)
{
previousOptimalCameraPosition += delta;
transform.position += delta;
}
///
/// Teleport the camera to a new location with given rotation in world space
///
///
///
public void TelePort (Vector3 newPosition, Quaternion newRotation)
{
Vector3 delta = newPosition - transform.position;
previousOptimalCameraPosition += delta;
transform.position = newPosition;
transform.rotation = newRotation;
}
///
/// TODO - Zoom WIP
/// Apply the current zoom amount to the camera.
/// This gets automatically applied in LateUpdate()
/// when zoom is enabled.
///
public void ZoomCamera()
{
if (camera1 != null)
{
if (zoomFactor == 0f)
{
camera1.fieldOfView = unzoomedFoV;
}
else if (zoomFactor > 0f)
{
camera1.fieldOfView = unzoomedFoV - (zoomFactor * (unzoomedFoV - zoomedInFoV));
}
else
{
camera1.fieldOfView = unzoomedFoV - (zoomFactor * (zoomedOutFoV - unzoomedFoV));
}
}
}
#endregion
}
}