using System.Collections; using System.Collections.Generic; using UnityEngine; // Sci-Fi Ship Controller. Copyright (c) 2018-2023 SCSM Pty Ltd. All rights reserved. namespace SciFiShipController { /// /// Component to control the docking and undocking of ships on larger ships (motherships) /// or stationary objects like hangars. Works with ShipDockingStation. /// [AddComponentMenu("Sci-Fi Ship Controller/Docking Components/Ship Docking")] [HelpURL("http://scsmmedia.com/ssc-documentation")] [RequireComponent(typeof(ShipControlModule))] public class ShipDocking : MonoBehaviour { #region Enumerations /// /// The possible states of a ship which supports docking. /// public enum DockingState { /// /// Ship is not docked and can fly around unhindered /// NotDocked = 0, /// /// Ship is currently attempting to dock at a docking point on a Ship Docking Station. /// It may be assigned to a docking point Entry Path. /// Docking = 1, /// /// Ship is currently attempting to depart from a docking point on a Ship Docking station. /// It may be assigned to a docking point Exit Path. /// Undocking = 2, /// /// Ship is docked at a docking point on a Ship Docking Station. Typically it wll be set /// to kinematic. /// Docked = 3 } /// /// [INTERNAL USE ONLY] /// This is a subset of DockingState /// public enum InitialDockingState { NotDocked = 0, Docked = 3 } #endregion #region Public Static Properties // variables to avoid enumeration lookups public static readonly int notDockedInt = (int)DockingState.NotDocked; public static readonly int dockingInt = (int)DockingState.Docking; public static readonly int undockingInt = (int)DockingState.Undocking; public static readonly int dockedInt = (int)DockingState.Docked; #endregion #region Public Variables - General /// /// If enabled, the Initialise() will be called as soon as Awake() runs. This should be disabled if you are /// instantiating the ShipDocking through code. /// public bool initialiseOnAwake = false; /// /// Ships can start in a state of Docked or Undocked. /// public InitialDockingState initialDockingState = InitialDockingState.NotDocked; /// /// How close the ship has to be (in metres) to the docking position before it can become docked. /// public float landingDistancePrecision = 0.01f; [Range(0.1f, 10f)] /// /// How close the ship has to be (in degrees) to the docking rotation before it can become docked. /// public float landingAnglePrecision = 2f; /// /// How close the ship has to be (in metres) to the hovering position before it is deemed to have reached the hover position. /// public float hoverDistancePrecision = 1f; /// /// How close the ship has to be (in degrees) to the hovering rotation before it is deemed to have reached the hover position. /// [Range(0.1f, 30f)] public float hoverAnglePrecision = 10f; /// /// Target time to lift off from the landing position and move to the hover position. /// Has no effect if the docking point hover height is 0. /// [Range(0f, 60f)] public float liftOffDuration = 2f; /// /// Target time to move from the hover position to the landing position. /// Has no effect if the docking point hover height is 0. /// [Range(0f, 60f)] public float landingDuration = 2f; /// /// Should physics collisions been detected when the state is Docked? /// public bool detectCollisionsWhenDocked = false; /// /// When used with ShipDockingStation.UndockShip(..), the number of seconds /// that the undocking manoeuvre is delayed. This allows you to create cinematic /// effects or perform other actions, before the Undocking process begins. /// [Range(0f, 60f)] public float undockingDelay = 0f; /// /// When value is greater than 0, the number of seconds the ship waits while docked, /// before automatically attempting to start the undocking procedure. /// [Range(0f, 300f)] public float autoUndockTime = 0f; /// /// This is additional velocity in an upwards direction relative to the mothership. /// public float undockVertVelocity = 2f; /// /// This is additional velocity in a forward direction relative to the mothership. /// public float undockFwdVelocity = 2f; /// /// The amount of force applied by the catapult when undocking in KiloNewtons. /// public float catapultThrust = 0f; /// /// The number of seconds that the force is applied from the catapult to the ship /// [Range(0f, 30f)] public float catapultDuration = 2f; /// /// A list of ship docking adapters. These are points on the ship where /// it can dock with a ShipDockingPoint on a ShipDockingStation. /// Typically you should not be updating this list yourself. /// public List adapterList; /// /// [INTERNAL ONLY] /// [HideInInspector] public bool allowRepaint = false; /// /// [INTERNAL ONLY] /// Is the adapter list expanded in the ShipDockingEditor? /// public bool isAdapterListExpanded = false; #endregion #region Public Variables - Events /// /// Methods that get called immediately after the ship has finished docking. /// public SSCDockingEvt1 onPostDocked = null; /// /// The time, in seconds, to delay the actioning of any onPostDocked methods. /// [Range(0f, 30f)] public float onPostDockedDelay = 0f; /// /// Methods that get called immediately after the Hover point is reached when docking. /// Typically used to perform a non-docking API action like lowering landing gear, /// disabling radar, disarming weapons etc. /// WARNING: Be careful not to call other docking APIs that might create a circular loop. /// public SSCDockingEvt1 onPostDockingHover = null; /// /// The time, in seconds, to delay the actioning of any onPostDockingHover methods. /// [Range(0f, 30f)] public float onPostDockingHoverDelay = 0f; /// /// Methods that get called immediately after docking has started. /// Typically used to perform a non-docking API action like chatter with ground staff, /// disabling radar, preparing for landing etc. /// WARNING: Be careful not to call other docking APIs that might create a circular loop. /// public SSCDockingEvt1 onPostDockingStart = null; /// /// The time, in seconds, to delay the actioning of any onPostDockingStart methods. /// [Range(0f, 30f)] public float onPostDockingStartDelay = 0f; /// /// Methods that get called immediately after the ship has finished undocking. /// public SSCDockingEvt1 onPostUndocked = null; /// /// The time, in seconds, to delay the actioning of any onPostUndocked methods. /// [Range(0f, 30f)] public float onPostUndockedDelay = 0f; /// /// Methods that get called immediately after the Hover point is reached when undocking. /// Typically used to perform a non-docking API action like raising landing gear, enabling /// radar, arming weapons etc. /// WARNING: Be careful not to call other docking APIs that might create a circular loop. /// public SSCDockingEvt1 onPostUndockingHover = null; /// /// The time, in seconds, to delay the actioning of any onPostUndockingHover methods. /// [Range(0f, 30f)] public float onPostUndockingHoverDelay = 0f; /// /// Methods that get called immediately after undocking has started. /// Typically used to perform a non-docking API action like dust or steam particle effects /// or opening hanger doors. /// WARNING: Be careful not to call other docking APIs that might create a circular loop. /// public SSCDockingEvt1 onPostUndockingStart = null; /// /// The time, in seconds, to delay the actioning of any onPostUndockingStart methods. /// [Range(0f, 30f)] public float onPostUndockingStartDelay = 0f; #endregion #region Public Properties /// /// [READ ONLY] The ID or the docking point on the shipDockingStation. /// To set, call shipDockingStation.AssignShipToDockingPoint(..). /// Internally uses the index in the list of docking points - however, /// this is subject to change. /// public int DockingPointId { get; internal set; } /// /// Typically used for debugging, is the Hover Point the target? /// public bool IsHoverPointTarget { get { return isHoverTarget; } } /// /// Is the docking component initialised? /// public bool IsInitialised { get; private set; } /// /// [READ ONLY] The docking station the ship may be docked with. /// To set, call shipDockingStation.AssignShipToDockingPoint(..) /// public ShipDockingStation shipDockingStation { get; internal set; } #endregion #region Public Delegates public delegate void CallbackOnStateChange(ShipDocking shipDocking, ShipControlModule shipControlModule, ShipAIInputModule shipAIInputModule, DockingState previousDockingState); /// /// The name of the custom method that is called immediately after the state is changed. /// Your method must take 4 parameters: shipDocking (never null), shipControlModule (never null), /// shipAIInputModule (could be null) and previousDockingState. /// This should be a lightweight method to avoid performance issues. /// Your method will NOT be called if ShipDocking.IsInitialised is false. /// public CallbackOnStateChange callbackOnStateChange = null; #endregion #region Private variables /// /// [INTERNAL ONLY] /// Remember which tabs etc were shown in the editor /// [SerializeField] private int selectedTabInt = 0; private ShipControlModule shipControlModule; private ShipAIInputModule shipAIInputModule; private bool isShipInitialised = false; internal ShipControlModule dockWithShip; private SSCManager sscManager; /// /// The current state of the ship /// private DockingState dockingState = DockingState.NotDocked; private int dockingStateInt = 0; // TODO: CHECK USAGE private Vector3 dockedRelativePosition; private Quaternion dockedRelativeRotation; internal RigidbodyInterpolation originalRBInterpolation = RigidbodyInterpolation.None; internal bool isOriginalRBInterpolationSet = false; private Vector3 targetDockingPosition = Vector3.zero; private Quaternion targetDockingRotation = Quaternion.identity; private bool isInAIDockingState = false; private bool isHoverTarget = false; private float dockingActionCompletedDistance = 1f; private float dockingActionCompletedAngle = 1f; private float autoUndockTimer = 0f; private ShipAIInputModule.CallbackCompletedStateAction originalCompletedStateActionCallback = null; #endregion #region Initialisation Methods void Awake() { if (initialiseOnAwake) { Initialise(); } } public void Initialise() { if (!IsInitialised) { shipControlModule = GetComponent(); if (shipControlModule != null) { shipAIInputModule = GetComponent(); // cache to avoid having to check for null etc in FixedUpdate isShipInitialised = shipControlModule.IsInitialised; } // Add capacity for 1 docking adapter as this is the current default. if (adapterList == null) { adapterList = new List(1); } if (adapterList != null && adapterList.Count == 0) { adapterList.Add(new ShipDockingAdapter()); } // Keep compiler happy if (selectedTabInt < 0) { } IsInitialised = true; } } #endregion #region Event Methods /// /// Automatically called by Unity immediately before the object is destroyed /// private void OnDestroy() { RemoveListeners(); CancelInvoke(); StopAllCoroutines(); } #endregion #region Update Methods private void Update () { if (isShipInitialised) { // Is docked? if (dockingStateInt == dockedInt) { // IsKinematic should be enabled because ship is disabled if (shipControlModule.ShipRigidbody.isKinematic) { if (dockWithShip != null && dockWithShip.ShipIsEnabled()) { // Translate the relative local-space offset of the docked ship // Update the rotation of the docked ship by adding the local rotation of the docked ship relative to the mother ship. transform.SetPositionAndRotation( dockWithShip.transform.position + dockWithShip.transform.TransformDirection(dockedRelativePosition), dockWithShip.transform.rotation * dockedRelativeRotation); } } } // Is this a moving ship docking station? if (isInAIDockingState && shipDockingStation != null && shipDockingStation.IsMotherShip) { UpdateDockingWSPositionAndRotation(shipDockingStation.GetDockingPoint(DockingPointId), shipAIInputModule, isHoverTarget, dockingActionCompletedDistance, dockingActionCompletedAngle); } // Auto undocking - do last // Check if auto undock is active (gets activated at end of SetState(..) if (autoUndockTime > 0f && dockingStateInt == dockedInt && autoUndockTimer > 0f) { autoUndockTimer += Time.deltaTime; if (autoUndockTimer > autoUndockTime) { // Reset time (auto undock is not active) autoUndockTimer = 0f; shipDockingStation.UnDockShip(shipControlModule); } } } } #endregion #region Private Methods private bool IsShipDocked() { if (dockingStateInt == dockedInt) { // May be other conditions... return true; } else { return false; } } /// /// Assign a new path to an AI Ship. /// If there is no matching path, then this will return false. /// /// private bool AssignNewPath(int pathGUIDHash) { bool isAssigned = false; if (sscManager == null) { sscManager = SSCManager.GetOrCreateManager(); } if (sscManager != null) { PathData pathData = sscManager.GetPath(pathGUIDHash); if (pathData != null) { // Set the ship's state to the "move to" state shipAIInputModule.SetState(AIState.moveToStateID); // Set the target path to the new path shipAIInputModule.AssignTargetPath(pathData); isAssigned = true; } } return isAssigned; } /// /// Invoke any methods configured in the editor (persistent) or with AddListener (non-persistent) /// after the delayTime in seconds. /// /// /// /// /// /// private IEnumerator OnPostDockedDelayed (int shipDockingStationID, int shipId, int dockingPointId, float delayTime) { yield return new WaitForSeconds(delayTime); // These will ALWAYS fire although events are checked before calling this method. if (onPostDocked != null) { onPostDocked.Invoke(shipDockingStationID, shipId, dockingPointId, Vector3.zero); } } /// /// Invoke any methods configured in the editor (persistent) or with AddListener (non-persistent) /// after the delayTime in seconds. /// /// /// /// /// /// private IEnumerator OnPostDockingHoverDelayed (int shipDockingStationID, int shipId, int dockingPointId, float delayTime) { yield return new WaitForSeconds(delayTime); // These will ALWAYS fire although events are checked before calling this method. if (onPostDockingHover != null) { onPostDockingHover.Invoke(shipDockingStationID, shipId, dockingPointId, Vector3.zero); } } /// /// Invoke any methods configured in the editor (persistent) or with AddListener (non-persistent) /// after the delayTime in seconds. /// /// /// /// /// /// private IEnumerator OnPostDockingStartDelayed (int shipDockingStationID, int shipId, int dockingPointId, float delayTime) { yield return new WaitForSeconds(delayTime); // These will ALWAYS fire although events are checked before calling this method. if (onPostDockingStart != null) { onPostDockingStart.Invoke(shipDockingStationID, shipId, dockingPointId, Vector3.zero); } } /// /// Invoke any methods configured in the editor (persistent) or with AddListener (non-persistent) /// after the delayTime in seconds. /// /// /// /// /// /// private IEnumerator OnPostUndockedDelayed (int shipDockingStationID, int shipId, int dockingPointId, float delayTime) { yield return new WaitForSeconds(delayTime); // These will ALWAYS fire although events are checked before calling this method. if (onPostUndocked != null) { onPostUndocked.Invoke(shipDockingStationID, shipId, dockingPointId, Vector3.zero); } } /// /// Invoke any methods configured in the editor (persistent) or with AddListener (non-persistent) /// after the delayTime in seconds. /// /// /// /// /// /// private IEnumerator OnPostUndockingHoverDelayed (int shipDockingStationID, int shipId, int dockingPointId, float delayTime) { yield return new WaitForSeconds(delayTime); // These will ALWAYS fire although events are checked before calling this method. if (onPostUndockingHover != null) { onPostUndockingHover.Invoke(shipDockingStationID, shipId, dockingPointId, Vector3.zero); } } /// /// Invoke any methods configured in the editor (persistent) or with AddListener (non-persistent) /// after the delayTime in seconds. /// /// /// /// /// /// private IEnumerator OnPostUndockingStartDelayed (int shipDockingStationID, int shipId, int dockingPointId, float delayTime) { yield return new WaitForSeconds(delayTime); // These will ALWAYS fire although events are checked before calling this method. if (onPostUndockingStart != null) { onPostUndockingStart.Invoke(shipDockingStationID, shipId, dockingPointId, Vector3.zero); } } /// /// Calculates the world space docking position and rotation for an AI ship at a given docking point. /// If hoverPosition is true, it will calculate the offset hover position. /// Then it updates the ship AI Input module with this information. /// Important: It does not set the docking state. /// /// /// /// /// private void UpdateDockingWSPositionAndRotation (ShipDockingPoint shipDockingPoint, ShipAIInputModule shipAIInputModule, bool hoverPosition, float actionCompletedDistance, float actionCompletedAngularDistance) { if (shipDockingPoint != null) { Vector3 targetDockingPos = Vector3.zero; Quaternion targetDockingRot = Quaternion.identity; // Calculate the world space docking position and rotation CalculateDockingWSPositionAndRotation(shipDockingPoint, hoverPosition, ref targetDockingPos, ref targetDockingRot); // Assign target information to AI ship AssignDockingWSPositionAndRotation(shipAIInputModule, targetDockingPos, targetDockingRot, shipDockingPoint, actionCompletedDistance, actionCompletedAngularDistance); } } /// /// Calculates the world space docking position and rotation for an AI ship at a given docking point. /// If hoverPosition is true, it will calculate the offset hover position. /// /// /// /// /// private void CalculateDockingWSPositionAndRotation (ShipDockingPoint shipDockingPoint, bool hoverPosition, ref Vector3 targetDockingPos, ref Quaternion targetDockingRot) { // Target docking position calculation: // Start with the world space position of the docking point targetDockingPos = shipDockingStation.GetDockingPointPositionWS(shipDockingPoint); // If required, offset the position by a hover distance if (hoverPosition) { targetDockingPos += shipDockingStation.GetDockingPointRotation(shipDockingPoint) * Vector3.up * shipDockingPoint.hoverHeight; } // Calculate the target rotation of the ship relative to the docking point Vector3 adapterDirection = adapterList[0].relativeDirection; float XYPlaneLength = Mathf.Sqrt((adapterDirection.x * adapterDirection.x) + (adapterDirection.y * adapterDirection.y)); // Idea is that we first rotate the ship around the z-axis to get the XY direction pointing downwards, then // we rotate around the x-axis to get the YZ direction pointing downwards Quaternion shipRelativeRotation = Quaternion.Euler(Mathf.Atan2(adapterDirection.z, XYPlaneLength) * Mathf.Rad2Deg, 0f, -Mathf.Atan2(adapterDirection.x, -adapterDirection.y) * Mathf.Rad2Deg); // For special case of directly forwards direction, flip upside down to give more natural direction if ((adapterDirection.x > 0f ? adapterDirection.x : -adapterDirection.x) < 0.001f && (adapterDirection.y > 0f ? adapterDirection.y : -adapterDirection.y) < 0.001f && adapterDirection.z > 0.001f) { shipRelativeRotation *= Quaternion.Euler(0f, 0f, 180f); } // Target docking rotation calculation: // Start with the world space rotation of the docking point targetDockingRot = shipDockingStation.GetDockingPointRotation(shipDockingPoint); // Then add the rotation of the ship relative to the docking point targetDockingRot *= shipRelativeRotation; // Subtract the adapter relative position (in world space), so that the adapter point will line up with the docking point targetDockingPos -= targetDockingRot * adapterList[0].relativePosition; } /// /// Assigns a target position, rotation and radius to an AI ship in the docking AI state. /// /// /// /// /// /// /// private void AssignDockingWSPositionAndRotation (ShipAIInputModule shipAIInputModule, Vector3 targetDockingPos, Quaternion targetDockingRot, ShipDockingPoint shipDockingPoint, float actionCompletedDistance, float actionCompletedAngularDistance) { // Assign target position, rotation and radius data to AI ship shipAIInputModule.AssignTargetPosition(targetDockingPos); shipAIInputModule.AssignTargetRotation(targetDockingRot); // Target radius must be at least 1 metre shipAIInputModule.AssignTargetRadius(shipDockingPoint.hoverHeight > 1f ? shipDockingPoint.hoverHeight : 1f); shipAIInputModule.AssignTargetDistance(actionCompletedDistance); shipAIInputModule.AssignTargetAngularDistance(actionCompletedAngularDistance); shipAIInputModule.AssignTargetVelocity(shipDockingStation.GetStationVelocity()); } /// /// Try to join an entry or exit path at the closest point to the ship /// /// /// private bool TryJointPathAtClosestPoint(int guidHashPath) { bool isJoinedPath = false; // Attempt ot join exit path (if there is one) if (AssignNewPath(guidHashPath)) { // Find the closest point on the exit path PathData pathData = sscManager.GetPath(guidHashPath); if (pathData != null) { Vector3 closestPointOnPath = Vector3.zero; float closestPointOnPathTValue = 0f; int prevPathLocationIdx = 0; if (SSCMath.FindClosestPointOnPath(pathData, shipControlModule.shipInstance.TransformPosition, ref closestPointOnPath, ref closestPointOnPathTValue, ref prevPathLocationIdx)) { shipAIInputModule.SetPreviousTargetPathLocationIndex(prevPathLocationIdx); int nextExitPathLocationIndex = SSCManager.GetNextPathLocationIndex(pathData, prevPathLocationIdx, pathData.isClosedCircuit); if (nextExitPathLocationIndex >= 0) { shipAIInputModule.SetCurrentTargetPathLocationIndex(nextExitPathLocationIndex, closestPointOnPathTValue); shipAIInputModule.SetPreviousTargetPathLocationIndex(prevPathLocationIdx); isJoinedPath = true; } } } } return isJoinedPath; } #endregion #region Internal Methods /// /// If it has not already been saved (set), record /// the original rigidbody interpolation setting /// of the ship this component is attached to. /// internal void SaveInterpolation() { if (!isOriginalRBInterpolationSet) { // If the ship is initialised use the cached rigidbody, else fetch it Rigidbody rbShip = isShipInitialised ? shipControlModule.ShipRigidbody : shipControlModule.GetComponent(); // Remember the interpolation setting on the ship if (rbShip != null) { originalRBInterpolation = rbShip.interpolation; isOriginalRBInterpolationSet = true; } } } /// /// This can be used when another feature triggers undocking. It ensures /// that if the countdown has begun it doesn't continue. /// internal void ResetAutoUndock() { autoUndockTimer = 0f; } #endregion #region Internal Callback Methods /// /// Callback for when the ship AI has completed a state action. /// /// internal void AICompletedStateActionCallback(ShipAIInputModule shipAIInputModule) { if (shipAIInputModule != null && shipAIInputModule.IsInitialised) { // By default, not in AI docking state isInAIDockingState = false; dockingStateInt = (int)dockingState; int shipAIStateID = shipAIInputModule.GetState(); #region If Docking if (dockingStateInt == dockingInt) { ShipDockingPoint shipDockingPoint = shipDockingStation.GetDockingPoint(DockingPointId); if (shipAIStateID == AIState.dockingStateID) { // Calculate the world space docking position and rotation for the landing position Vector3 targetDockingPos = Vector3.zero; Quaternion targetDockingRot = Quaternion.identity; CalculateDockingWSPositionAndRotation(shipDockingPoint, false, ref targetDockingPos, ref targetDockingRot); // Check if we have reached the landing position if (Vector3.SqrMagnitude(shipAIInputModule.GetTargetPosition() - targetDockingPos) < landingDistancePrecision * landingDistancePrecision) { // We have now finished the landing manoeuvre, set the state to docked SetState(DockingState.Docked); // Clean up - set the callback for CompletedStateAction back to its original value shipAIInputModule.callbackCompletedStateAction = originalCompletedStateActionCallback; shipAIInputModule.SetState(AIState.idleStateID); // If there are Post Docked event peristent AND/OR non-persistent listeners. if (SSCUtils.HasListeners(onPostDocked)) { if (onPostDockedDelay > 0f) { StartCoroutine(OnPostDockedDelayed(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, onPostDockingHoverDelay)); } else { onPostDocked.Invoke(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, Vector3.zero); } } } else { // We have now finished the hover manoeuvre // Now we need to start the landing manoeuvre // Reset the state action completed flag shipAIInputModule.SetHasCompletedStateAction(false); // Set the ship to target the landing position (we have already calculated it) // Action will be completed when we get with x metres of the target position dockingActionCompletedDistance = landingDistancePrecision; dockingActionCompletedAngle = landingAnglePrecision; AssignDockingWSPositionAndRotation(shipAIInputModule, targetDockingPos, targetDockingRot, shipDockingPoint, dockingActionCompletedDistance, dockingActionCompletedAngle); shipAIInputModule.AssignTargetTime(landingDuration); isInAIDockingState = true; isHoverTarget = false; // If there are Post Docking Hover event peristent AND/OR non-persistent listeners. if (SSCUtils.HasListeners(onPostDockingHover)) { if (onPostDockingHoverDelay > 0f) { StartCoroutine(OnPostDockingHoverDelayed(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, onPostDockingHoverDelay)); } else { onPostDockingHover.Invoke(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, Vector3.zero); } } } } else { // If we were docking, we have now reached the end of the entry path // Now we need to start the landing manoeuvre // Set the ship's state to the "docking" state shipAIInputModule.SetState(AIState.dockingStateID); // Set the ship to target the hover position // Action will be completed when we get with hover distance & angle of the target (hover) position dockingActionCompletedDistance = hoverDistancePrecision; dockingActionCompletedAngle = hoverAnglePrecision; UpdateDockingWSPositionAndRotation(shipDockingPoint, shipAIInputModule, true, dockingActionCompletedDistance, dockingActionCompletedAngle); shipAIInputModule.AssignTargetTime(landingDuration); isInAIDockingState = true; isHoverTarget = true; } } #endregion #region Else Undocking else if (dockingStateInt == undockingInt) { if (shipAIStateID == AIState.dockingStateID) { // If we were undocking, we have now finished the liftoff manoeuvre // We need to transition to following the exit path ShipDockingPoint shipDockingPoint = shipDockingStation.GetDockingPoint(DockingPointId); if (shipDockingPoint != null) { isHoverTarget = false; if (!AssignNewPath(shipDockingPoint.guidHashExitPath)) { // If no valid exit path, immediately finish undocking manoeuvre SetState(DockingState.NotDocked); // Clean up - set the callback for CompletedStateAction back to its original value shipAIInputModule.callbackCompletedStateAction = originalCompletedStateActionCallback; shipAIInputModule.SetState(AIState.idleStateID); // Call the callback to let the script know that it needs to take action if (originalCompletedStateActionCallback != null) { originalCompletedStateActionCallback(shipAIInputModule); } } // Catapult launch for AI or AI assisted with an Exit path once hover height has been reached else if (catapultThrust > 0f) { shipControlModule.shipInstance.AddBoost(Vector3.forward, catapultThrust, catapultDuration); } // If there is a Post Undocking Hover event peristent AND/OR non-persistent listeners. if (SSCUtils.HasListeners(onPostUndockingHover)) { if (onPostUndockingHoverDelay > 0f) { StartCoroutine(OnPostUndockingHoverDelayed(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, onPostUndockingHoverDelay)); } else { onPostUndockingHover.Invoke(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, Vector3.zero); } } } } else { // If we were undocking, we have now reached the end of the exit path // Set the state to not docked SetState(DockingState.NotDocked); // Clean up - set the callback for CompletedStateAction back to its original value shipAIInputModule.callbackCompletedStateAction = originalCompletedStateActionCallback; // v1.2.3+ Undocking AI ships become idle at end of exit path shipAIInputModule.SetState(AIState.idleStateID); // Call the callback to let the script know that it needs to take action if (originalCompletedStateActionCallback != null) { originalCompletedStateActionCallback(shipAIInputModule); } // If there any Post Undocked event peristent AND/OR non-persistent listeners. if (SSCUtils.HasListeners(onPostUndocked)) { if (onPostUndockedDelay > 0f) { StartCoroutine(OnPostUndockedDelayed(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, onPostDockingHoverDelay)); } else { onPostUndocked.Invoke(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, Vector3.zero); } } } } #endregion } } #endregion #region Public API Methods /// /// Return the current docking state /// /// public DockingState GetState() { return dockingState; } /// /// Return the current docking state as an integer. /// /// public int GetStateInt() { return dockingStateInt; } /// /// Call this when you wish to remove any custom (non-persistent) event listeners, /// like after creating them in code and then destroying the object. /// This is automatically called by OnDestroy. /// public void RemoveListeners() { if (IsInitialised) { if (onPostDocked != null) { onPostDocked.RemoveAllListeners(); } if (onPostDockingHover != null) { onPostDockingHover.RemoveAllListeners(); } if (onPostDockingStart != null) { onPostDockingStart.RemoveAllListeners(); } if (onPostUndocked != null) { onPostUndocked.RemoveAllListeners(); } if (onPostUndockingHover != null) { onPostUndockingHover.RemoveAllListeners(); } if (onPostUndockingStart != null) { onPostUndockingStart.RemoveAllListeners(); } } } /// /// Set the docking state of the ship. /// When undocking, the velocity of ShipDockingStation (mothership) is considered. /// If configured, a custom method is called after the state has been changed. /// /// public void SetState (DockingState dockingState) { if (!IsInitialised) { #if UNITY_EDITOR Debug.LogWarning("ERROR: ShipDocking.SetState was called before it was initialised on " + gameObject.name + ". Either set initialiseOnAwake in the Editor or call Initialise() at runtime."); #endif return; } int previousDockingStateInt = dockingStateInt; this.dockingState = dockingState; dockingStateInt = (int)dockingState; // By default, not in AI docking state isInAIDockingState = false; if (shipControlModule != null) { // Used for AI docking without a path bool immediatelyCompleteState = false; // Ensure autoundocking is reset if the state is changed by another feature or API call. autoUndockTimer = 0f; #region Docked if (dockingStateInt == dockedInt) { if (isShipInitialised && !shipControlModule.shipInstance.isThrusterFXStationary) { shipControlModule.StopThrusterEffects(); } // NOTE: This will pause (but not stop) the thruster FX. shipControlModule.DisableShipMovement(); // Check if we should override default behaviour after ship physics have been disabled if (detectCollisionsWhenDocked) { shipControlModule.ShipRigidbody.detectCollisions = true; } isHoverTarget = false; if (shipDockingStation != null) { ShipDockingPoint shipDockingPoint = shipDockingStation.GetDockingPoint(DockingPointId); if (shipDockingPoint != null) { // NOTE: The scaled relative position hasn't been tested with moving motherships. dockedRelativePosition = shipDockingStation.GetScaledRelativePosition(shipDockingPoint); dockedRelativeRotation = Quaternion.Euler(shipDockingPoint.relativeRotation); // Set the initial position and rotation of the ship - snap to the docking point CalculateDockingWSPositionAndRotation(shipDockingPoint, false, ref targetDockingPosition, ref targetDockingRotation); transform.SetPositionAndRotation(targetDockingPosition, targetDockingRotation); } } // If the ship is initialised use the cached rigidbody, else fetch it Rigidbody rbShip = isShipInitialised ? shipControlModule.ShipRigidbody : shipControlModule.GetComponent(); // Remember the interpolation setting on the docking ship if (rbShip != null && !isOriginalRBInterpolationSet) { originalRBInterpolation = rbShip.interpolation; isOriginalRBInterpolationSet = true; } // Does this Docking Station have a mothership? if (dockWithShip != null) { // Where possible use the cached rigidbodies if (isShipInitialised && dockWithShip.IsInitialised) { // Match the interpolation setting on the ship being docked to, so that we avoid jerky behaviour shipControlModule.ShipRigidbody.interpolation = dockWithShip.ShipRigidbody.interpolation; } else if (rbShip != null) { // Match the interpolation setting on the ship being docked to, so that we avoid jerky behaviour Rigidbody rbShipToDockWith = dockWithShip.GetComponent(); if (rbShipToDockWith != null) { rbShip.interpolation = rbShipToDockWith.interpolation; } } } } #endregion #region NotDocked else if (dockingStateInt == notDockedInt) { // If previous state was docked, reset the velocity, else don't. shipControlModule.EnableShipMovement(previousDockingStateInt == dockedInt); isHoverTarget = false; // If previous state was docked, docking or undocking restore the original // rigidbody interpolation. if (previousDockingStateInt != notDockedInt) { // If the ship is initialised use the cached rigidbody, else fetch it Rigidbody rbShip = shipControlModule.IsInitialised ? shipControlModule.ShipRigidbody : shipControlModule.GetComponent(); // Restore the original rigidbody interpolation if (rbShip != null && isOriginalRBInterpolationSet) { rbShip.interpolation = originalRBInterpolation; isOriginalRBInterpolationSet = false; } // Check for a mothership. i.e. a Docking Station on a Ship if (shipControlModule.IsInitialised && dockWithShip != null && dockWithShip.IsInitialised) { // Undock with the same velocity as the mothership shipControlModule.ShipRigidbody.velocity = dockWithShip.ShipRigidbody.velocity + (dockWithShip.shipInstance.RigidbodyUp * undockVertVelocity) + (dockWithShip.shipInstance.RigidbodyForward * undockFwdVelocity) + Vector3.Cross(dockWithShip.ShipRigidbody.angularVelocity, shipControlModule.ShipRigidbody.position - dockWithShip.transform.TransformPoint(dockWithShip.shipInstance.centreOfMass)); shipControlModule.ShipRigidbody.angularVelocity = dockWithShip.ShipRigidbody.angularVelocity; } // Check for catapult with non-AI assisted undocking OR AI-assist with no Exit Path if (previousDockingStateInt == dockedInt && catapultThrust > 0f) { shipControlModule.shipInstance.AddBoost(Vector3.forward, catapultThrust, catapultDuration); } } } #endregion #region Docking or UnDocking else if (dockingStateInt == dockingInt || dockingStateInt == undockingInt) { if (previousDockingStateInt == dockedInt) { // If the ship was previously docked, we'll probably need to also reenable it. if (!shipControlModule.ShipMovementIsEnabled() || !shipControlModule.ShipIsEnabled()) { shipControlModule.EnableShip(false, true); } } if (shipDockingStation != null && shipDockingStation.IsDockingPointPathsInitialised) { ShipDockingPoint shipDockingPoint = shipDockingStation.GetDockingPoint(DockingPointId); if (shipDockingPoint != null) { // If this is an AI Ship (or AI-assisted player ship), assign it a path to follow (if it is setup in the docking point) if (shipAIInputModule != null && shipAIInputModule.IsInitialised) { // Is switching directly to docking while currently undocking if (previousDockingStateInt == undockingInt && dockingStateInt == dockingInt) { // Restore user callback method (if any) - this gets updated again below shipAIInputModule.callbackCompletedStateAction = originalCompletedStateActionCallback; // Where are we in the current undocking maneouvre? int shipAIStateID = shipAIInputModule.GetState(); // While undocking, was ship heading towards the hover position? if (isHoverTarget) { // Move directly towards the docking point isHoverTarget = false; isInAIDockingState = true; } // On Exit path, attempt to join Entry path else if (shipAIStateID == AIState.moveToStateID) { //Debug.Log("[DEBUG] switching directly between undocking and docking while on Exit path. AIState: " + shipAIStateID + " T:" + Time.time); // Attempt ot join entry path (if there is one) if (!TryJointPathAtClosestPoint(shipDockingPoint.guidHashEntryPath)) { // No valid path, one was not specified, or close to end of path, so assume entry path has been completed. immediatelyCompleteState = true; } } } // Is switching directly to undocking while currently docking else if (previousDockingStateInt == dockingInt && dockingStateInt == undockingInt) { // Restore user callback method (if any) - this gets updated again below shipAIInputModule.callbackCompletedStateAction = originalCompletedStateActionCallback; // Where are we in the current docking maneouvre? int shipAIStateID = shipAIInputModule.GetState(); // While docking, was ship heading towards the hover position? if (isHoverTarget) { // Stop flying towards hover position, and attempt to fly along exit path (if there is one). immediatelyCompleteState = true; } // Descending from hover positon to docking point else if (shipAIStateID == AIState.dockingStateID) { // Commence undocking shipAIInputModule.SetState(AIState.dockingStateID); // Action will be completed when we get close to the target position dockingActionCompletedDistance = hoverDistancePrecision; dockingActionCompletedAngle = hoverAnglePrecision; UpdateDockingWSPositionAndRotation(shipDockingPoint, shipAIInputModule, true, dockingActionCompletedDistance, dockingActionCompletedAngle); shipAIInputModule.AssignTargetTime(landingDuration); isInAIDockingState = true; isHoverTarget = true; } // On entry path else if (shipAIStateID == AIState.moveToStateID) { // Try to join the exit path at the closest point to the ship if (!TryJointPathAtClosestPoint(shipDockingPoint.guidHashExitPath)) { immediatelyCompleteState = true; } } } // Assign the path when docking else if (dockingStateInt == dockingInt) { if (!AssignNewPath(shipDockingPoint.guidHashEntryPath)) { // No path or one was not specified, so assume entry path has been completed. immediatelyCompleteState = true; } } else { // When exiting (undocking), set the ship's AI state to the "docking" // state to carry out the liftoff manoeuvre, before following the // exit path (if there is one). // NOTE: The ShipAI state of "docking" covers both docking and undocking. shipAIInputModule.SetState(AIState.dockingStateID); // Action will be completed when we get close to the target position dockingActionCompletedDistance = hoverDistancePrecision; dockingActionCompletedAngle = hoverAnglePrecision; UpdateDockingWSPositionAndRotation(shipDockingPoint, shipAIInputModule, true, dockingActionCompletedDistance, dockingActionCompletedAngle); shipAIInputModule.AssignTargetTime(liftOffDuration); isInAIDockingState = true; isHoverTarget = true; } // Remember any callback the user had for CompletedStateAction originalCompletedStateActionCallback = shipAIInputModule.callbackCompletedStateAction; // Replace it with our own callback shipAIInputModule.callbackCompletedStateAction = AICompletedStateActionCallback; } } } #if UNITY_EDITOR else { Debug.Log("ERROR: attempting Docking or Undocking without a ShipDockingStation or without correctly initialising it"); } #endif } #endregion #region Callbacks, Notifications and AutoUndocking if (callbackOnStateChange != null) { callbackOnStateChange(this, shipControlModule, shipAIInputModule, (DockingState)previousDockingStateInt); } // AI docking without an entry path (added in 1.2.1) if (immediatelyCompleteState) { AICompletedStateActionCallback(shipAIInputModule); } if (dockingStateInt == dockedInt && shipDockingStation != null) { // Do we need to notify the ship docking station that the ship has finished docking? // This will call any event methods configured on the ship docking station. if (SSCUtils.HasListeners(shipDockingStation.onPostDocked)) { shipDockingStation.onPostDocked.Invoke(shipDockingStation.ShipDockingStationId, shipControlModule.GetShipId, DockingPointId + 1, Vector3.zero); } // If auto undocking is enabled, restart (activate) the timer. if (autoUndockTime > 0f) { autoUndockTimer = 0.0001f; } } // Has started undocking from the Docked state if (dockingStateInt == undockingInt && previousDockingStateInt == dockedInt) { // If there is a Post Undocking Start event peristent AND/OR non-persistent listeners. if (SSCUtils.HasListeners(onPostUndockingStart)) { if (onPostUndockingStartDelay > 0f) { StartCoroutine(OnPostUndockingStartDelayed(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, onPostUndockingStartDelay)); } else { onPostUndockingStart.Invoke(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, Vector3.zero); } } } // Has started docking from the NotDocked state else if (dockingStateInt == dockingInt && previousDockingStateInt == notDockedInt) { // If there is a Post Docking Start event peristent AND/OR non-persistent listeners. if (SSCUtils.HasListeners(onPostDockingStart)) { if (onPostDockingStartDelay > 0f) { StartCoroutine(OnPostDockingStartDelayed(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, onPostDockingStartDelay)); } else { onPostDockingStart.Invoke(shipDockingStation.ShipDockingStationId, shipControlModule.shipInstance.shipId, DockingPointId + 1, Vector3.zero); } } } #endregion } } /// /// Prevent a delayed Post Docked event from running once the delay has been triggered by the ship has docked. /// public void StopOnPostDocked() { StopCoroutine("OnPostDockedDelayed"); } /// /// Prevent a delayed Post Docking Hover event from running once the delay has been triggered /// by the ship reaching the hover point while docking. /// public void StopOnPostDockingHover() { StopCoroutine("OnPostDockingHoverDelayed"); } /// /// Prevent a delayed Post Docking Start event from running once the delay has been triggered /// by the ship starting to dock. /// public void StopOnPostDockingStart() { StopCoroutine("OnPostDockingStartDelayed"); } /// /// Prevent a delayed Post Undocked event from running once the delay has been triggered by the /// ship finishing the docking manoeuvre. /// public void StopOnPostUndocked() { StopCoroutine("OnPostUndockedDelayed"); } /// /// Prevent a delayed Post Undocking Hover event from running once the delay has been triggered /// by the ship reaching the hover point while undocking. /// public void StopOnPostUndockingHover() { StopCoroutine("OnPostUndockingHoverDelayed"); } /// /// Prevent a delayed Post Undocking Start event from running once the delay has been triggered /// by the ship starting to undock. /// public void StopOnPostUndockingStart() { StopCoroutine("OnPostUndockingStartDelayed"); } #endregion } }