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 } }