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 { /// /// Class containing data for an AI Behaviour Input. /// public class AIBehaviourInput { #region Enumerations /// /// The type of behaviour. Multiple behaviours can /// be combined together. /// public enum AIBehaviourType { /// /// Comes to a complete stop. /// Required inputs: Weighting. /// Idle = 0, /// /// Moves directly towards target position. /// Required inputs: Target position, weighting. /// Optional inputs: Use targeting accuracy. /// Seek = 1, /// /// Moves directly away from target position. /// Required inputs: Target position, weighting. /// Flee = 2, /// /// Moves towards the future position of an object currently at target position moving with a /// velocity of target velocity. /// Required inputs: Target position, target velocity, weighting. /// Optional inputs: Use targeting accuracy. /// Pursuit = 3, /// /// Moves away from the future position of an object currently at target position moving with a /// velocity of target velocity. /// Required inputs: Target position, target velocity, weighting. /// Evasion = 4, /// /// Moves directly towards target position, slowing down when nearing target position to come to a /// complete stop upon reaching it. /// Required inputs: Target position, weighting. /// Optional inputs: Use targeting accuracy. /// SeekArrival = 5, /// /// Moves directly towards target position, changing speed when nearing the target position to /// match target velocity upon reaching it. /// Required inputs: Target position, target velocity, weighting. /// Optional inputs: Use targeting accuracy. /// SeekMovingArrival = 6, /// /// Moves towards the future position of an object currently at target position moving with a /// velocity of target velocity, changing speed when nearing the target position to match /// target velocity upon reaching it. /// Required inputs: Target position, target velocity, weighting. /// Optional inputs: Use targeting accuracy. /// PursuitArrival = 7, //Avoid = 11, //Follow = 12, //BlockCylinder = 16, //BlockCone = 17, /// /// Moves out of an imaginary cylinder. The cylinder starts at the target position, stretches out /// infinitely in the direction of target forwards and has a radius of target radius. If the ship /// is not in the cylinder, returns a zero output. /// Required inputs: Target position, target forwards, target radius, weighting. /// UnblockCylinder = 19, /// /// Moves out of an imaginary cone. The cone starts at the target position, stretches out /// infinitely in the direction of target forwards, and the angle between its central axis and its /// edges is target FOV angle. If the ship is not in the cone, returns a zero output. /// Required inputs: Target position, target forwards, target FOV angle, weighting. /// UnblockCone = 20, /// /// Takes preventative action to avoid obstacles. If the ship does need to take preventative /// action, returns a zero output. /// Required inputs: Weighting. /// ObstacleAvoidance = 22, //Wander = 25, /// /// Moves onto and then follows the target path. /// Required inputs: Target path, weighting. /// Optional inputs: Use targeting accuracy. /// FollowPath = 28, /// /// Moves directly towards target position and (when it gets within target radius) attempts to match orientation /// of target forwards and target up. Target velocity indicates the velocity of the target position (set it to /// Vector3.zero if it is not moving). Target time indicates the time it will attempt to take to move from the target /// radius to the target position. /// Required inputs: Target position, target forwards, target up, target radius, target velocity, target time, weighting. /// Dock = 31, CustomIdle = 200, CustomSeek = 201, CustomFlee = 202, CustomPursuit = 203, CustomEvasion = 204, CustomSeekArrival = 205, CustomSeekMovingArrival = 206, CustomPursuitArrival = 207, //CustomFollow = 211, //CustomAvoid = 212, //CustomBlockCylinder = 216, //CustomBlockCone = 217, CustomUnblockCylinder = 219, CustomUnblockCone = 220, CustomObstacleAvoidance = 222, //CustomWander = 225 CustomFollowPath = 228, CustomDock = 231 } #endregion #region Public Variables // IMPORTANT - when changing this section also update SetClassDefault() // Also update ClassName(ClassName className) Clone Constructor (if there is one) /// /// The type of behaviour to set this behaviour input with. /// public AIBehaviourType behaviourType; /// /// The Ship instance for this AI ship. /// public Ship shipInstance; /// /// The Ship Control Module instance for this AI ship. /// public ShipControlModule shipControlModuleInstance; /// /// The Ship AI Input Module instance for this AI ship. /// public ShipAIInputModule shipAIInputModuleInstance; /// /// The relative weighting of this behaviour input. The specific use of this is determined by the behaviour combiner /// used by the current state. /// If the behaviour combiner is Priority Only, behaviour inputs with a nonzero weighting will be used while behaviour /// inputs with a zero weighting will be skipped. /// If the behaviour combiner is Prioritised Dithering, the weighting specifies the probability that the behaviour input /// will be used (instead of being skipped). For example, if the weighting is 0.7, there is a 70% chance of the behaviour /// input being used and a 30% chance of it being skipped. /// If the behaviour combiner is Weighted Average, the weighting specifies how much weighting will be given to this /// behaviour input when all of the behaviour inputs are summed together to obtain the combined behaviour input. /// public float weighting; /// /// The target position provided to this behaviour input. /// public Vector3 targetPosition; /// /// The target path provided to this behaviour input. /// public PathData targetPath; /// /// The target velocity provided to this behaviour input. /// public Vector3 targetVelocity; /// /// The target forwards direction provided to this behaviour input. NOTE: This must be a normalised vector. /// public Vector3 targetForwards; /// /// The target up direction provided to this behaviour input. NOTE: This must be a normalised vector. /// public Vector3 targetUp; /// /// The target radius (in metres) provided to this behaviour input. /// public float targetRadius; /// /// The target Field Of View (FOV) angle (in degrees) provided to this behaviour input. NOTE: This is the angle for /// "one side" of the field of view, i.e. the angle is half of the entire field of view. /// public float targetFOVAngle; /// /// The target time (in seconds) provided to this behaviour input. /// public float targetTime; /// /// Whether targeting accuracy should be taken into account by this behaviour input. /// public bool useTargetingAccuracy; #endregion #region Private Static Variables // Steering behaviour variables private static Vector3 headingVector; private static float headingVectorMagnitude; private static float headingVectorSqrMagnitude; private static Vector3 headingVectorNormalised; private static float desiredSpeed; private static Vector3 currentWanderDirection; private static Ray OARay = new Ray(Vector3.zero, Vector3.forward); private static RaycastHit OARaycastHit; private static Rigidbody OARaycastHitRigidbody; #endregion #region Class Constructors // Class constructor #1 public AIBehaviourInput() { SetClassDefaults(); } // Class constructor #2 public AIBehaviourInput(ShipControlModule ourShipControlModule, ShipAIInputModule ourShipAI) { SetClassDefaults(); this.shipInstance = ourShipControlModule.shipInstance; this.shipControlModuleInstance = ourShipControlModule; this.shipAIInputModuleInstance = ourShipAI; } // Copy constructor public AIBehaviourInput (AIBehaviourInput behaviourInput) { if (behaviourInput == null) { SetClassDefaults(); } else { this.behaviourType = behaviourInput.behaviourType; this.shipInstance = behaviourInput.shipInstance; this.shipControlModuleInstance = behaviourInput.shipControlModuleInstance; this.shipAIInputModuleInstance = behaviourInput.shipAIInputModuleInstance; this.weighting = behaviourInput.weighting; this.targetPosition = behaviourInput.targetPosition; this.targetPath = behaviourInput.targetPath; this.targetVelocity = behaviourInput.targetVelocity; this.targetForwards = behaviourInput.targetForwards; this.targetUp = behaviourInput.targetUp; this.targetRadius = behaviourInput.targetRadius; this.targetFOVAngle = behaviourInput.targetFOVAngle; this.targetTime = behaviourInput.targetTime; this.useTargetingAccuracy = behaviourInput.useTargetingAccuracy; } } #endregion #region Public Member Methods public void SetClassDefaults() { behaviourType = AIBehaviourType.Idle; shipInstance = null; shipControlModuleInstance = null; shipAIInputModuleInstance = null; weighting = 0f; targetPosition = Vector3.zero; targetPath = null; targetVelocity = Vector3.zero; targetForwards = Vector3.forward; targetUp = Vector3.up; targetRadius = 0f; targetFOVAngle = 0f; targetTime = 0f; useTargetingAccuracy = false; } /// /// Clears the behaviour-dependent settings of a behaviour input. /// public void ClearBehaviourInput () { // Set the weighting to zero weighting = 0f; // Set the behaviour input parameters to "zero" values // Set the target position to Vector3.zero targetPosition.x = 0f; targetPosition.y = 0f; targetPosition.z = 0f; // Set the target path to null targetPath = null; // Set the target velocity to zero targetVelocity.x = 0f; targetVelocity.y = 0f; targetVelocity.z = 0f; // Set the target forwards to zero targetForwards.x = 0f; targetForwards.y = 0f; targetForwards.z = 0f; // Set the target up to zero targetUp.x = 0f; targetUp.y = 0f; targetUp.z = 0f; // Set the target radius to zero targetRadius = 0f; // Set the target FOV angle to zero targetFOVAngle = 0f; // Set the target time to zero targetTime = 0f; } #endregion #region Public Static Methods #region Helper Functions /// /// Calculates an approximate time for the ship to "catch up to" another ship. Used in look ahead times. /// /// /// /// private static float ApproxInterceptionTime(Vector3 ourPosition, Vector3 ourVelocity, Vector3 targetPosition) { float ourSpeed = ourVelocity.magnitude; return (targetPosition - ourPosition).magnitude / (ourSpeed < 0.1f ? 0.1f : ourSpeed); // Returns Infinity when magnitude is 0. //return (targetPosition - ourPosition).magnitude / ourVelocity.magnitude; // Implementation #1 //return (targetPosition - shipControlModule.shipInstance.TransformPosition).magnitude / Mathf.Max((targetVelocity - shipControlModule.shipInstance.WorldVelocity).magnitude, 0.1f); // Implementation #2 //Vector3 relativePos = targetPosition - shipControlModule.shipInstance.TransformPosition; //if (Vector3.Dot(relativePos, shipControlModule.shipInstance.WorldVelocity) > 0f) //{ // Vector3 relativeVelo = shipControlModule.shipInstance.WorldVelocity - targetVelocity; // float dotProduct = Vector3.Dot(relativePos, relativeVelo / relativeVelo.sqrMagnitude); // return dotProduct > 0f ? dotProduct : 0f; //} //else { return 0f; } } /// /// Returns whether two objects (approximated as spheres) within given position, velocity and radius will collide /// within a given look ahead time. /// /// /// /// /// /// /// /// /// public static bool OnCollisionCourse(Vector3 object1Position, Vector3 object1Velocity, float object1Radius, Vector3 object2Position, Vector3 object2Velocity, float object2Radius, float lookAheadTime) { // Calculate relative position and velocity Vector3 relativePosition = object2Position - object1Position; Vector3 relativeVelocity = object2Velocity - object1Velocity; // Calculate maximum distance between objects required for a collision float collisionDistance = object1Radius + object2Radius; // Check if the objects are currently colliding if (relativePosition.magnitude < collisionDistance) { return true; } // Check if the objects will collide at the look ahead time if ((relativePosition + relativeVelocity * lookAheadTime).magnitude < collisionDistance) { return true; } // Check if the objects will collide between now and the look ahead time float closestApproachTime = -Vector3.Dot(relativeVelocity, relativePosition) / Vector3.Dot(relativeVelocity, relativeVelocity); if (closestApproachTime > 0f && closestApproachTime < lookAheadTime) { if ((relativePosition + relativeVelocity * closestApproachTime).magnitude < collisionDistance) { return true; } else { return false; } } else { return false; } } /// /// Performs a sweep test of sweepType along sweepRay to a maximum distance of sweepMaxDistance. /// sweepRay.direction must be normalised. /// Sweep types are: 0: No sweep test. 1: Raycast. 2: Spherecast. 3: Rigidbody sweep test. /// /// /// /// /// /// /// private static bool PerformSweep (AIBehaviourInput behaviourInput, int sweepType, Ray sweepRay, ref RaycastHit sweepRaycastHit, float sweepMaxDistance) { bool sweepDidHit = false; switch (sweepType) { case 0: // No sweep test sweepDidHit = false; break; case 1: // Raycast sweepDidHit = Physics.Raycast(sweepRay, out sweepRaycastHit, sweepMaxDistance, behaviourInput.shipAIInputModuleInstance.obstacleLayerMask); break; case 2: // Spherecast sweepRay.origin -= (sweepRay.direction * behaviourInput.shipAIInputModuleInstance.shipRadius); sweepDidHit = Physics.SphereCast(sweepRay, behaviourInput.shipAIInputModuleInstance.shipRadius, out sweepRaycastHit, sweepMaxDistance + behaviourInput.shipAIInputModuleInstance.shipRadius, behaviourInput.shipAIInputModuleInstance.obstacleLayerMask); break; case 3: // Rigidbody sweep test sweepDidHit = behaviourInput.shipControlModuleInstance.ShipRigidbody.SweepTest(sweepRay.direction, out sweepRaycastHit, sweepMaxDistance); break; } return sweepDidHit; } /// /// Calculates the maximum speed an AI ship can fly at in order to reach a target position and velocity from its /// current position and velocity in world space. /// /// /// /// /// public static float CalculateMaxTurnSpeed (Vector3 turnTargetPosition, Vector3 turnTargetVelocity, AIBehaviourInput behaviourInput) { // TODO need to take into account following cases: // 1. Target velocity = 0 - then just use constant radius curve from start radius // 2. Weird curves: // a) Dot product of start and target velocity is negative // b) Dot product is positive float maxTurnSpeed = 0f; // Calculate the vector from the start position to the target position Vector3 startToTarget = turnTargetPosition - behaviourInput.shipInstance.TransformPosition; // Calculate the square distance between the start and target points float startToTargetSqrDistance = startToTarget.sqrMagnitude; // Calculate the angle from the startToTarget vector to the turnStartVelocity vector float startAngle = Vector3.Angle(startToTarget, behaviourInput.shipInstance.WorldVelocity) * Mathf.Deg2Rad; // Calculate the turn start radius float turnStartRadius = Mathf.Sqrt(startToTargetSqrDistance / (2f * (1f - Mathf.Cos(2f * startAngle)))); if (turnTargetVelocity.sqrMagnitude > 0.01f) { // Case 1: Target velocity is nonzero // Calculate the angle from the startToTarget vector to the turnTargetVelocity vector float targetAngle = Vector3.Angle(startToTarget, turnTargetVelocity) * Mathf.Deg2Rad; // Calculate the turn end (target) radius float turnEndRadius = Mathf.Sqrt(startToTargetSqrDistance / (2f * (1f - Mathf.Cos(2f * targetAngle)))); // Calculate the maximum speed along the curve to reach the target maxTurnSpeed = behaviourInput.shipAIInputModuleInstance.MaxSpeedAlongCurve(turnStartRadius, turnEndRadius, Mathf.Sqrt(startToTargetSqrDistance), behaviourInput.shipInstance.IsGrounded); } else { // Case 2: Target velocity is zero // Calculate the maximum speed along the curve to reach the target maxTurnSpeed = behaviourInput.shipAIInputModuleInstance.MaxSpeedAlongCurve(turnStartRadius, turnStartRadius, Mathf.Sqrt(startToTargetSqrDistance), behaviourInput.shipInstance.IsGrounded); } return maxTurnSpeed; } #endregion #region Set Behaviour Methods /// /// Sets a behaviour output to an "idle" behaviour output. /// Required inputs: Weighting. /// /// /// public static void SetIdleBehaviourOutput(AIBehaviourInput behaviourInput, AIBehaviourOutput behaviourOutput) { // Desired heading is current forwards direction behaviourOutput.heading = behaviourInput.shipInstance.TransformForward; // Desired velocity is zero behaviourOutput.velocity = Vector3.zero; // Target is not set behaviourOutput.target = Vector3.zero; behaviourOutput.setTarget = true; // No desired upwards orientation (?) behaviourOutput.up = Vector3.zero; // Never use targeting accuracy behaviourOutput.useTargetingAccuracy = false; } /// /// Sets a behaviour output to a "seek" behaviour output. /// Required inputs: Target position, weighting. /// /// /// public static void SetSeekBehaviourOutput(AIBehaviourInput behaviourInput, AIBehaviourOutput behaviourOutput) { // Desired heading is towards the target position headingVectorNormalised = (behaviourInput.targetPosition - behaviourInput.shipInstance.TransformPosition).normalized; behaviourOutput.heading = headingVectorNormalised; // Desired velocity is max speed in direction of desired heading behaviourOutput.velocity = headingVectorNormalised * behaviourInput.shipAIInputModuleInstance.maxSpeed; // Target is the target position behaviourOutput.target = behaviourInput.targetPosition; behaviourOutput.setTarget = true; // No desired upwards orientation behaviourOutput.up = Vector3.zero; // Whether we use targeting accuracy depends on behaviour input settings behaviourOutput.useTargetingAccuracy = behaviourInput.useTargetingAccuracy; } /// /// Sets a behaviour output to a "flee" behaviour output. /// Required inputs: Target position, weighting. /// /// /// public static void SetFleeBehaviourOutput(AIBehaviourInput behaviourInput, AIBehaviourOutput behaviourOutput) { // Desired heading is away from the target position headingVectorNormalised = (behaviourInput.shipInstance.TransformPosition - behaviourInput.targetPosition).normalized; behaviourOutput.heading = headingVectorNormalised; // Desired velocity is max speed in direction of desired heading behaviourOutput.velocity = headingVectorNormalised * behaviourInput.shipAIInputModuleInstance.maxSpeed; // Target is not set behaviourOutput.target = Vector3.zero; behaviourOutput.setTarget = true; // No desired upwards orientation behaviourOutput.up = Vector3.zero; // Never use targeting accuracy behaviourOutput.useTargetingAccuracy = false; } /// /// Sets a behaviour output to a "pursuit" behaviour output. /// Required inputs: Target position, target velocity, weighting. /// /// /// public static void SetPursuitBehaviourOutput(AIBehaviourInput behaviourInput, AIBehaviourOutput behaviourOutput) { // Desired heading is towards the predicted target position //headingVectorNormalised = (behaviourInput.targetPosition + (behaviourInput.targetVelocity * PredictionIntervalTime(behaviourInput.targetPosition, behaviourInput.targetVelocity)) // - behaviourInput.shipInstance.TransformPosition).normalized; headingVectorNormalised = (behaviourInput.targetPosition + (behaviourInput.targetVelocity * ApproxInterceptionTime(behaviourInput.shipInstance.TransformPosition, behaviourInput.shipInstance.WorldVelocity, behaviourInput.targetPosition)) - behaviourInput.shipInstance.TransformPosition).normalized; behaviourOutput.heading = headingVectorNormalised; // Desired velocity is max speed in direction of desired heading behaviourOutput.velocity = headingVectorNormalised * behaviourInput.shipAIInputModuleInstance.maxSpeed; // Target is the target position behaviourOutput.target = behaviourInput.targetPosition; behaviourOutput.setTarget = true; // No desired upwards orientation behaviourOutput.up = Vector3.zero; // Whether we use targeting accuracy depends on behaviour input settings behaviourOutput.useTargetingAccuracy = behaviourInput.useTargetingAccuracy; } /// /// Sets a behaviour output to an "evasion" behaviour output. /// Required inputs: Target position, target velocity, weighting. /// /// /// public static void SetEvasionBehaviourOutput(AIBehaviourInput behaviourInput, AIBehaviourOutput behaviourOutput) { // Desired heading is away from the predicted target position headingVectorNormalised = (behaviourInput.shipInstance.TransformPosition - behaviourInput.targetPosition - (behaviourInput.targetVelocity * ApproxInterceptionTime(behaviourInput.shipInstance.TransformPosition, behaviourInput.shipInstance.WorldVelocity, behaviourInput.targetPosition))).normalized; behaviourOutput.heading = headingVectorNormalised; // Desired velocity is max speed in direction of desired heading behaviourOutput.velocity = headingVectorNormalised * behaviourInput.shipAIInputModuleInstance.maxSpeed; // Target is not set behaviourOutput.target = Vector3.zero; behaviourOutput.setTarget = true; // No desired upwards orientation behaviourOutput.up = Vector3.zero; // Never use targeting accuracy behaviourOutput.useTargetingAccuracy = false; } ///// ///// Sets a behaviour input to an "avoid" behaviour input for a fixed target position ///// ///// //public static void SetAvoidInputBehaviour(AIBehaviourInput behaviourInput) //{ // behaviourInput.velocityOutput = Vector3.zero; // behaviourInput.weighting = 0f; //} ///// ///// Sets a behaviour input to an "follow" behaviour input for a moving target ///// Follow should probably store some kind of vector3 offset or distance at which to ///// follow the target. ///// ///// ///// ///// ///// //public static void SetFollowInputBehaviour(AIBehaviourInput behaviourInput) //{ // behaviourInput.velocityOutput = Vector3.zero; // behaviourInput.weighting = 0f; //} /// /// Sets a behaviour output to a "seek arrival" behaviour output for a fixed target position. /// Required inputs: Target position, weighting. /// /// /// public static void SetSeekArrivalBehaviourOutput(AIBehaviourInput behaviourInput, AIBehaviourOutput behaviourOutput) { // Desired heading is towards the target position headingVector = behaviourInput.targetPosition - behaviourInput.shipInstance.TransformPosition; headingVectorMagnitude = headingVector.magnitude; headingVectorNormalised = headingVector / headingVector.magnitude; behaviourOutput.heading = headingVectorNormalised; // Desired velocity is generally max speed in direction of desired heading, but decreases when nearing the target //behaviourInput.velocityOutput = headingVectorNormalised * (float)System.Math.Sqrt(2f * behaviourInput.shipAIInputModuleInstance.decelerationRate * headingVectorMagnitude); behaviourOutput.velocity = headingVectorNormalised * behaviourInput.shipAIInputModuleInstance.MaxSpeedFromBrakingDistance(0f, headingVectorMagnitude, behaviourInput.shipInstance.LocalVelocity.normalized); // Re-clamp velocity magnitude to max speed if needed if (behaviourOutput.velocity.sqrMagnitude > behaviourInput.shipAIInputModuleInstance.maxSpeed * behaviourInput.shipAIInputModuleInstance.maxSpeed) { behaviourOutput.velocity = headingVectorNormalised * behaviourInput.shipAIInputModuleInstance.maxSpeed; } // Target is the target position behaviourOutput.target = behaviourInput.targetPosition; behaviourOutput.setTarget = true; // No desired upwards orientation behaviourOutput.up = Vector3.zero; // Whether we use targeting accuracy depends on behaviour input settings behaviourOutput.useTargetingAccuracy = behaviourInput.useTargetingAccuracy; } /// /// Sets a behaviour output to a "seek arrival" behaviour output for a moving target position. /// Required inputs: Target position, target velocity, weighting. /// /// /// public static void SetSeekMovingArrivalBehaviourOutput(AIBehaviourInput behaviourInput, AIBehaviourOutput behaviourOutput) { // Desired heading is towards the target position headingVector = behaviourInput.targetPosition - behaviourInput.shipInstance.TransformPosition; headingVectorMagnitude = headingVector.magnitude; headingVectorNormalised = headingVector / headingVector.magnitude; behaviourOutput.heading = headingVectorNormalised; // Desired velocity is generally max speed in direction of desired heading, but decreases when nearing the target //behaviourInput.velocityOutput = behaviourInput.targetVelocity + (headingVectorNormalised * (float)System.Math.Sqrt(2f * behaviourInput.shipAIInputModuleInstance.decelerationRate * headingVectorMagnitude)); // Braking distance code // TODO probably convert > to >= (but only when algorithm is fixed) // Check whether target velocity and heading are in the same direction float targetDotHeading = Vector3.Dot(behaviourInput.targetVelocity, headingVectorNormalised); if (targetDotHeading > 0f) { // Target velocity and heading are in the same direction // TODO optimise code // Split target velocity into two components - that in the direction of the heading and whatever is left over Vector3 targetVelocityHeadingComponent = Vector3.Project(behaviourInput.targetVelocity, headingVectorNormalised); Vector3 targetVelocityOtherComponent = behaviourInput.targetVelocity - targetVelocityHeadingComponent; // Use braking distance code to determine required velocity in direction of heading, then add the // the other component of the target velocity to it // TODO what if heading vector is in opposite direction to target velocity? behaviourOutput.velocity = targetVelocityOtherComponent + (headingVectorNormalised * behaviourInput.shipAIInputModuleInstance.MaxSpeedFromBrakingDistance(targetVelocityHeadingComponent.magnitude, headingVectorMagnitude, behaviourInput.shipInstance.LocalVelocity.normalized)); } else { // Target velocity and heading are in opposite directions // TODO optimise code // Split target velocity into two components - that in the direction of the heading and whatever is left over Vector3 targetVelocityHeadingComponent = Vector3.Project(behaviourInput.targetVelocity, headingVectorNormalised); Vector3 targetVelocityOtherComponent = behaviourInput.targetVelocity - targetVelocityHeadingComponent; // Use braking distance code to determine required velocity in direction of heading, then add the // the other component of the target velocity to it //behaviourOutput.velocity = targetVelocityOtherComponent; behaviourOutput.velocity = targetVelocityOtherComponent + (headingVectorNormalised * behaviourInput.shipAIInputModuleInstance.MaxSpeedFromBrakingDistance(targetVelocityHeadingComponent.magnitude, headingVectorMagnitude, behaviourInput.shipInstance.LocalVelocity.normalized)); } // Calculate max speed the ship can travel at while still making the turn float maxAllowedSpeed = CalculateMaxTurnSpeed(behaviourInput.targetPosition, behaviourInput.targetVelocity, behaviourInput); // Clamp to ship AI max speed if (maxAllowedSpeed > behaviourInput.shipAIInputModuleInstance.maxSpeed) { maxAllowedSpeed = behaviourInput.shipAIInputModuleInstance.maxSpeed; } // Clamp velocity magnitude to max allowed speed if (behaviourOutput.velocity.sqrMagnitude > maxAllowedSpeed * maxAllowedSpeed) { behaviourOutput.velocity = headingVectorNormalised * maxAllowedSpeed; } // Target is the target position behaviourOutput.target = behaviourInput.targetPosition; behaviourOutput.setTarget = true; // No desired upwards orientation behaviourOutput.up = Vector3.zero; // Whether we use targeting accuracy depends on behaviour input settings behaviourOutput.useTargetingAccuracy = behaviourInput.useTargetingAccuracy; } /// /// Sets a behaviour output to a "pursuit arrival" behaviour output. /// Required inputs: Target position, target velocity, weighting. /// /// /// public static void SetPursuitArrivalBehaviourOutput(AIBehaviourInput behaviourInput, AIBehaviourOutput behaviourOutput) { // Target is the predicted target position behaviourOutput.target = behaviourInput.targetPosition + (behaviourInput.targetVelocity * ApproxInterceptionTime(behaviourInput.shipInstance.TransformPosition, behaviourInput.shipInstance.WorldVelocity, behaviourInput.targetPosition)); behaviourOutput.setTarget = true; // Desired heading is towards the predicted target position headingVector = behaviourOutput.target - behaviourInput.shipInstance.TransformPosition; headingVectorMagnitude = headingVector.magnitude; headingVectorNormalised = headingVector / headingVector.magnitude; behaviourOutput.heading = headingVectorNormalised; // Desired velocity is generally max speed in direction of desired heading, but decreases when nearing the target // For "pursue arrival" behaviour, "zero" velocity is the target's velocity //behaviourInput.velocityOutput = behaviourInput.targetVelocity + (headingVectorNormalised * (float)System.Math.Sqrt(2f * behaviourInput.shipAIInputModuleInstance.decelerationRate * headingVectorMagnitude)); // Braking distance code // Check whether target velocity and heading are in the same direction float targetDotHeading = Vector3.Dot(behaviourInput.targetVelocity, headingVectorNormalised); if (targetDotHeading > 0f) { // Target velocity and heading are in the same direction // TODO optimise code // Split target velocity into two components - that in the direction of the heading and whatever is left over Vector3 targetVelocityHeadingComponent = Vector3.Project(behaviourInput.targetVelocity, headingVectorNormalised); Vector3 targetVelocityOtherComponent = behaviourInput.targetVelocity - targetVelocityHeadingComponent; // Use braking distance code to determine required velocity in direction of heading, then add the // the other component of the target velocity to it // TODO what if heading vector is in opposite direction to target velocity? // MaxSpeedFromBraking(..) can return zero which results in velocityOutput being Vector3.Zero behaviourOutput.velocity = targetVelocityOtherComponent + (headingVectorNormalised * behaviourInput.shipAIInputModuleInstance.MaxSpeedFromBrakingDistance(targetVelocityHeadingComponent.magnitude, headingVectorMagnitude, behaviourInput.shipInstance.LocalVelocity.normalized)); } else { // Target velocity and heading are in opposite directions // TODO optimise code // Split target velocity into two components - that in the direction of the heading and whatever is left over Vector3 targetVelocityHeadingComponent = Vector3.Project(behaviourInput.targetVelocity, headingVectorNormalised); Vector3 targetVelocityOtherComponent = behaviourInput.targetVelocity - targetVelocityHeadingComponent; // Use braking distance code to determine required velocity in direction of heading, then add the // the other component of the target velocity to it // TODO what if heading vector is in opposite direction to target velocity? // MaxSpeedFromBraking(..) can return zero which results in velocityOutput being Vector3.Zero //behaviourOutput.velocity = targetVelocityOtherComponent; behaviourOutput.velocity = targetVelocityOtherComponent + (headingVectorNormalised * behaviourInput.shipAIInputModuleInstance.MaxSpeedFromBrakingDistance(targetVelocityHeadingComponent.magnitude, headingVectorMagnitude, behaviourInput.shipInstance.LocalVelocity.normalized)); } // Calculate max speed the ship can travel at while still making the turn float maxAllowedSpeed = CalculateMaxTurnSpeed(behaviourInput.targetPosition, behaviourInput.targetVelocity, behaviourInput); // Clamp to ship AI max speed if (maxAllowedSpeed > behaviourInput.shipAIInputModuleInstance.maxSpeed) { maxAllowedSpeed = behaviourInput.shipAIInputModuleInstance.maxSpeed; } // Clamp velocity magnitude to max allowed speed if (behaviourOutput.velocity.sqrMagnitude > maxAllowedSpeed * maxAllowedSpeed) { behaviourOutput.velocity = headingVectorNormalised * maxAllowedSpeed; } // No desired upwards orientation behaviourOutput.up = Vector3.zero; // Whether we use targeting accuracy depends on behaviour input settings behaviourOutput.useTargetingAccuracy = behaviourInput.useTargetingAccuracy; } /// /// Sets a behaviour output to a "unblock cylinder" behaviour output. /// Required inputs: Target position, target forwards, target radius, weighting. /// /// /// public static void SetUnblockCylinderBehaviourOutput(AIBehaviourInput behaviourInput, AIBehaviourOutput behaviourOutput) { // First check whether we are within the "blocking" region // This region is defined as being within a cylinder of targetRadius along the targetForwards direction from targetPosition float fwdProjectionAmount = Vector3.Dot(behaviourInput.shipInstance.TransformPosition - behaviourInput.targetPosition, behaviourInput.targetForwards); if (fwdProjectionAmount > 0f) { Vector3 fwdProjection = fwdProjectionAmount * behaviourInput.targetForwards; // Heading vector is from closest point on centre of cylinder to current position headingVector = behaviourInput.shipInstance.TransformPosition - (behaviourInput.targetPosition + fwdProjection); headingVectorSqrMagnitude = headingVector.sqrMagnitude; if (headingVectorSqrMagnitude < behaviourInput.targetRadius * behaviourInput.targetRadius) { // If we are within the cylinder, set an output to vacate the region behaviourOutput.heading = headingVector / (float)System.Math.Sqrt(headingVectorSqrMagnitude); behaviourOutput.velocity = behaviourOutput.heading * behaviourInput.shipAIInputModuleInstance.maxSpeed; float exitAngle = 30f * Mathf.Deg2Rad; headingVectorNormalised = (behaviourOutput.heading * Mathf.Cos(exitAngle)) + (behaviourInput.targetForwards * Mathf.Sin(exitAngle)); behaviourOutput.heading = headingVectorNormalised; } else { // If we are not within the cylinder, no output is needed for this behaviour behaviourOutput.heading = Vector3.zero; behaviourOutput.velocity = Vector3.zero; } } else { // If we are not within the cone, no output is needed for this behaviour behaviourOutput.heading = Vector3.zero; behaviourOutput.velocity = Vector3.zero; } // Target is not set behaviourOutput.target = Vector3.zero; behaviourOutput.setTarget = true; // No desired upwards orientation behaviourOutput.up = Vector3.zero; // Never use targeting accuracy behaviourOutput.useTargetingAccuracy = false; } /// /// Sets a behaviour output to a "unblock cone" behaviour output. /// Required inputs: Target position, target forwards, target FOV angle, weighting. /// /// /// public static void SetUnblockConeBehaviourOutput(AIBehaviourInput behaviourInput, AIBehaviourOutput behaviourOutput) { // First check whether we are within the "blocking" region // This region is defined as being within a cone of targetFOVAngle along the targetForwards direction from targetPosition float fwdProjectionAmount = Vector3.Dot(behaviourInput.shipInstance.TransformPosition - behaviourInput.targetPosition, behaviourInput.targetForwards); if (fwdProjectionAmount > 0f) { Vector3 fwdProjection = fwdProjectionAmount * behaviourInput.targetForwards; // Heading vector is from closest point on centre of cylinder to current position headingVector = behaviourInput.shipInstance.TransformPosition - (behaviourInput.targetPosition + fwdProjection); headingVectorMagnitude = headingVector.magnitude; if ((float)System.Math.Atan(headingVectorMagnitude / fwdProjection.magnitude) * Mathf.Rad2Deg < behaviourInput.targetFOVAngle) { // If we are within the cone, set an output to vacate the region behaviourOutput.heading = headingVector / headingVectorMagnitude; //behaviourInput.velocityOutput = behaviourInput.headingOutput * behaviourInput.shipAIInputModuleInstance.maxSpeed; behaviourOutput.velocity = behaviourInput.shipInstance.WorldVelocity.normalized * behaviourInput.shipAIInputModuleInstance.maxSpeed; float exitAngle = (behaviourInput.targetFOVAngle + 30f) * Mathf.Deg2Rad; headingVectorNormalised = (behaviourOutput.heading * Mathf.Cos(exitAngle)) + (behaviourInput.targetForwards * Mathf.Sin(exitAngle)); behaviourOutput.heading = headingVectorNormalised; } else { // If we are not within the cone, no output is needed for this behaviour behaviourOutput.heading = Vector3.zero; behaviourOutput.velocity = Vector3.zero; } } else { // If we are not within the cone, no output is needed for this behaviour behaviourOutput.heading = Vector3.zero; behaviourOutput.velocity = Vector3.zero; } // Target is not set behaviourOutput.target = Vector3.zero; behaviourOutput.setTarget = true; // No desired upwards orientation behaviourOutput.up = Vector3.zero; // Never use targeting accuracy behaviourOutput.useTargetingAccuracy = false; } /// /// Sets a behaviour output to a "obstacle avoidance" behaviour output. /// Required inputs: Weighting. /// /// /// public static void SetObstacleAvoidanceBehaviourOutput(AIBehaviourInput behaviourInput, AIBehaviourOutput behaviourOutput) { #region Step 1: Preliminary Checks // Sweep variables - these are used since we will be changing the sweep type based on obstacle avoidance quality // Whether the sweep hit anything bool sweepDidHit = false; // The type of sweep: // 0 - No sweep, 1 - raycast, 2 - spherecast, 3 - rigidbody sweep test // NOTE: Rigidbody sweep test is not used if the obstacle layers are not everything // or the default raycast layers (everything but the ignore raycast layer) int sweepType = 0; // Choose which type of sweep test to perform based on quality switch (behaviourInput.shipAIInputModuleInstance.obstacleAvoidanceQuality) { case ShipAIInputModule.AIObstacleAvoidanceQuality.Off: // No obstacle avoidance - don't perform a sweep sweepType = 0; break; case ShipAIInputModule.AIObstacleAvoidanceQuality.Low: // Low quality - use a spherecast sweepType = 2; break; case ShipAIInputModule.AIObstacleAvoidanceQuality.Medium: // Medium quality - use a spherecast sweepType = 2; break; case ShipAIInputModule.AIObstacleAvoidanceQuality.High: // High quality - use a rigidbody sweep test (unless we are using a custom layer selection) if (behaviourInput.shipAIInputModuleInstance.obstacleLayerMask == Physics.AllLayers || behaviourInput.shipAIInputModuleInstance.obstacleLayerMask == Physics.DefaultRaycastLayers) { sweepType = 3; } // If we are using a custom layer selection, revert to using a spherecast else { sweepType = 2; } break; default: // Default to a spherecast sweepType = 2; break; } // Calculate the position of the centre of the ship, shifted forward by the specified amount // This is to avoid issues where colliders near the front of the ship can interfere with obstacle avoidance Vector3 shiftedShipCentrePosition = behaviourInput.shipInstance.RigidbodyPosition + (behaviourInput.shipInstance.RigidbodyForward * behaviourInput.shipAIInputModuleInstance.raycastStartOffsetZ); // Find the last target position Vector3 worldSpaceTargetPosition = behaviourInput.shipAIInputModuleInstance.GetLastBehaviourInputTarget(); // If the last target position is actually set (i.e. it isn't Vector3.zero), check if there is an obstacle // between us and the target position if (worldSpaceTargetPosition.sqrMagnitude > 0.01 || sweepType == 0) { // Set sweep test origin and direction OARay.direction = (worldSpaceTargetPosition - behaviourInput.shipInstance.RigidbodyPosition); OARay.origin = behaviourInput.shipInstance.RigidbodyPosition + (OARay.direction * behaviourInput.shipAIInputModuleInstance.raycastStartOffsetZ); float distanceToCurrentTarget = (worldSpaceTargetPosition - OARay.origin).magnitude; OARay.direction /= distanceToCurrentTarget; sweepDidHit = PerformSweep(behaviourInput, sweepType, OARay, ref OARaycastHit, distanceToCurrentTarget); // DEBUGGING //if (!sweepDidHit) { Debug.DrawRay(OARay.origin, OARay.direction * distanceToCurrentTarget, Color.green); } //else { Debug.DrawRay(OARay.origin, OARay.direction * distanceToCurrentTarget, Color.red); } // If the sweep from the ship to the target position did not hit, assume that we do not // need to take corrective action (i.e. that no obstacle avoidance is required) if (!sweepDidHit) { // Set the behaviour output to a "null" output behaviourOutput.heading = Vector3.zero; behaviourOutput.up = Vector3.zero; behaviourOutput.velocity = Vector3.zero; behaviourOutput.target = Vector3.zero; behaviourOutput.setTarget = false; return; } } #endregion #region Step 2: Precalculate Obstacle Avoidance Information // TODO this should be determined by ship maneuverability float OALookAheadTime = 4f; //float OALookAheadTime = behaviourInput.shipAIInputModuleInstance.ObstacleAvoidanceLookAheadTime(); // Get the current speed of the ship in the forwards direction float currentSpeed = behaviourInput.shipInstance.LocalVelocity.z > 0f ? behaviourInput.shipInstance.LocalVelocity.z : 0f; // Calculate how far we should look ahead // This is the maximum of OALookAheadTime seconds ahead and ship radius * 5 float OALookAheadDistance = currentSpeed * OALookAheadTime; if (OALookAheadDistance < behaviourInput.shipAIInputModuleInstance.shipRadius * 5f) { OALookAheadDistance = behaviourInput.shipAIInputModuleInstance.shipRadius * 5f; } float distToObstacle = 0f; #endregion #region Step 3: Determine If Action Is Required // Determine if we need to take action to avoid an obstacle // There are three instances in which this could occur: // a) There is an obstacle blocking the "forwards" direction of the ship // b) There is an obstacle blocking the future "forwards" direction of the ship // c) [NOT USED CURRENTLY] There is an obstacle blocking the "velocity" direction of the ship bool forwardsDirectionBlocked = false; bool futureForwardsDirectionBlocked = false; // First check the "forwards" direction // Set sweep test origin and direction OARay.origin = shiftedShipCentrePosition; OARay.direction = behaviourInput.shipInstance.RigidbodyForward; sweepDidHit = PerformSweep(behaviourInput, sweepType, OARay, ref OARaycastHit, OALookAheadDistance); if (sweepDidHit) { OARaycastHitRigidbody = OARaycastHit.rigidbody; if (OARaycastHitRigidbody != null) { // If the object hit was a rigidbody, we need to check if we are on a collision course with it forwardsDirectionBlocked = OnCollisionCourse(shiftedShipCentrePosition, behaviourInput.shipInstance.WorldVelocity, behaviourInput.shipAIInputModuleInstance.shipRadius, OARaycastHitRigidbody.position, OARaycastHitRigidbody.velocity, behaviourInput.shipAIInputModuleInstance.shipRadius, OALookAheadTime); } else { forwardsDirectionBlocked = true; distToObstacle = OARaycastHit.distance; } } // DEBUGGING (TODO REMOVE) //if (forwardsDirectionBlocked) { Debug.DrawRay(OARay.origin, OARay.direction * OALookAheadDistance, Color.red); } //else { Debug.DrawRay(OARay.origin, OARay.direction * OALookAheadDistance, Color.green); } // If the "forwards" direction is not blocked, check the future "forwards" direction if (!forwardsDirectionBlocked) { // Set up a ray to check the future "forwards" direction of the ship // i.e. the "forwards" direction that will be reached in the future assuming // we maintain a constant angular velocity // Set sweep test origin and direction float angularVeloLookAheadTime = OALookAheadTime * 0.25f * Mathf.Rad2Deg; OARay.origin = shiftedShipCentrePosition; OARay.direction = Quaternion.Euler(behaviourInput.shipInstance.WorldAngularVelocity * angularVeloLookAheadTime) * behaviourInput.shipInstance.RigidbodyForward; sweepDidHit = PerformSweep(behaviourInput, sweepType, OARay, ref OARaycastHit, OALookAheadDistance); if (sweepDidHit) { OARaycastHitRigidbody = OARaycastHit.rigidbody; if (OARaycastHitRigidbody != null) { // If the object hit was a rigidbody, we need to check if we are on a collision course with it futureForwardsDirectionBlocked = OnCollisionCourse(shiftedShipCentrePosition, behaviourInput.shipInstance.WorldVelocity, behaviourInput.shipAIInputModuleInstance.shipRadius, OARaycastHitRigidbody.position, OARaycastHitRigidbody.velocity, behaviourInput.shipAIInputModuleInstance.shipRadius, OALookAheadTime); } else { futureForwardsDirectionBlocked = true; distToObstacle = OARaycastHit.distance; } } // DEBUGGING //if (futureForwardsDirectionBlocked) { Debug.DrawRay(OARay.origin, OARay.direction * OALookAheadDistance, Color.red); } //else { Debug.DrawRay(OARay.origin, OARay.direction * OALookAheadDistance, Color.green); } } #endregion #region Step 4: Find Valid Path // If step 3 determined that we need to take action to avoid an obstacle, // examine other possible directions we could move in to avoid the obstacle. Then // determine what ship input we need to move in the chosen direction. Otherwise if // step 3 did not determine that we need to take action, set the input of this behaviour // to a "null" input. // TODO IMPROVEMENTS: // - "Whisker" method - move away from obstacles on each side // - Use improved braking/cornering algoriths // Check if either the forwards direction or the future forwards direction are blocked if (forwardsDirectionBlocked || futureForwardsDirectionBlocked) { // Search for an alternative route bool foundViableRoute = false; float raycastAngle = 10f; OARay.origin = shiftedShipCentrePosition; // Check if the forwards direction of the ship is not blocked // (if it isn't blocked we can use that) if (!forwardsDirectionBlocked) { behaviourOutput.heading = behaviourInput.shipInstance.RigidbodyForward; behaviourOutput.velocity = behaviourInput.shipInstance.RigidbodyForward * behaviourInput.shipAIInputModuleInstance.maxSpeed; foundViableRoute = true; } // TODO: Should use pitch/yaw/roll acceleration instead of 100f // TODO: Should probably use newly derived curve speed calculations to calculate speeds float averageAngularVelocity = 100f * Mathf.Deg2Rad * distToObstacle / currentSpeed * 0.5f; // Calculate an order for raycasting based on the passed in target position // Directions closer to the target position are evaluated first int numberOfAvailableDirections = 4; Vector3[] XYPlaneRayDirections = new Vector3[4]; Vector3 localSpaceTargetPosition = behaviourInput.shipInstance.RigidbodyInverseRotation * worldSpaceTargetPosition; // Is the target position more towards the right than the left? bool localTargetXPositive = localSpaceTargetPosition.x >= 0f; // Is the target position more towards up than down? bool localTargetYPositive = localSpaceTargetPosition.y >= 0f; // If the ship is grounded, we can only (reliably) use left and right directions to avoid obstacles if (behaviourInput.shipInstance.IsGrounded) { // Simply order the X-directions if we are grounded XYPlaneRayDirections[0] = behaviourInput.shipInstance.RigidbodyRight * (localTargetXPositive ? 1f : -1f); XYPlaneRayDirections[1] = behaviourInput.shipInstance.RigidbodyRight * (localTargetXPositive ? -1f : 1f); // Two directions available for obstacle avoidance numberOfAvailableDirections = 2; } // Otherwise if the ship is not grounded, we can use all four directions // Is the target position more along the x-axis than the y-axis... else if ((localTargetXPositive ? localSpaceTargetPosition.x : -localSpaceTargetPosition.x) > (localTargetYPositive ? localSpaceTargetPosition.y : -localSpaceTargetPosition.y)) { // Local space target position is more along x-axis than y-axis // X-directions are first and last XYPlaneRayDirections[0] = behaviourInput.shipInstance.RigidbodyRight * (localTargetXPositive ? 1f : -1f); XYPlaneRayDirections[3] = behaviourInput.shipInstance.RigidbodyRight * (localTargetXPositive ? -1f : 1f); // Y-directions are second and third XYPlaneRayDirections[1] = behaviourInput.shipInstance.RigidbodyUp * (localTargetYPositive ? 1f : -1f); XYPlaneRayDirections[2] = behaviourInput.shipInstance.RigidbodyUp * (localTargetYPositive ? -1f : 1f); // Four directions available for obstacle avoidance numberOfAvailableDirections = 4; } // ... or is the target position more along the y-axis than the x-axis? else { // Local space target position is more along y-axis than x-axis // Y-directions are first and last XYPlaneRayDirections[0] = behaviourInput.shipInstance.RigidbodyUp * (localTargetYPositive ? 1f : -1f); XYPlaneRayDirections[3] = behaviourInput.shipInstance.RigidbodyUp * (localTargetYPositive ? -1f : 1f); // X-directions are second and third XYPlaneRayDirections[1] = behaviourInput.shipInstance.RigidbodyRight * (localTargetXPositive ? 1f : -1f); XYPlaneRayDirections[2] = behaviourInput.shipInstance.RigidbodyRight * (localTargetXPositive ? -1f : 1f); // Four directions available for obstacle avoidance numberOfAvailableDirections = 4; } float sineRaycastAngle, cosineRaycastAngle, maxTurnVelocity, neededHorizontalDistance; // Loop through increasing raycast angles in order to find a viable route while (!foundViableRoute) { // Take the sine and cosine of the raycast angle to use for calculating the direction vector of the sweep sineRaycastAngle = Mathf.Sin(raycastAngle * Mathf.Deg2Rad); cosineRaycastAngle = Mathf.Cos(raycastAngle * Mathf.Deg2Rad); if (raycastAngle < 89f) { // TODO: Should also check the flight acceleration etc. neededHorizontalDistance = distToObstacle * (sineRaycastAngle / cosineRaycastAngle); maxTurnVelocity = averageAngularVelocity * (neededHorizontalDistance * neededHorizontalDistance + distToObstacle * distToObstacle) / (2f * neededHorizontalDistance); } else { // TODO maybe have some sort of min speed? maxTurnVelocity = 10f; } if (maxTurnVelocity > behaviourInput.shipAIInputModuleInstance.maxSpeed || float.IsNaN(maxTurnVelocity) || float.IsInfinity(maxTurnVelocity)) { maxTurnVelocity = behaviourInput.shipAIInputModuleInstance.maxSpeed; } // Choose which type of sweep test to perform based on quality switch (behaviourInput.shipAIInputModuleInstance.obstacleAvoidanceQuality) { case ShipAIInputModule.AIObstacleAvoidanceQuality.Off: // No obstacle avoidance - don't perform a sweep sweepType = 0; break; case ShipAIInputModule.AIObstacleAvoidanceQuality.Low: // Low quality - use a raycast sweepType = 1; break; case ShipAIInputModule.AIObstacleAvoidanceQuality.Medium: // Medium quality - use a raycast sweepType = 1; break; case ShipAIInputModule.AIObstacleAvoidanceQuality.High: // High quality - use a spherecast sweepType = 2; break; default: // Default to a raycast sweepType = 1; break; } // Loop through the raycast directions in order of directions closest to furthest from the target position for (int i = 0; i < numberOfAvailableDirections; i++) { // Set raycast direction OARay.direction = (behaviourInput.shipInstance.RigidbodyForward * cosineRaycastAngle) + (XYPlaneRayDirections[i] * sineRaycastAngle); // Set sweep test origin and direction OARay.origin = shiftedShipCentrePosition; OARay.direction = (behaviourInput.shipInstance.RigidbodyForward * cosineRaycastAngle) + (XYPlaneRayDirections[i] * sineRaycastAngle); sweepDidHit = PerformSweep(behaviourInput, sweepType, OARay, ref OARaycastHit, OALookAheadDistance); if (!sweepDidHit) { // The sweep did not return a hit, hence we should go in this direction //Debug.DrawRay(OARay.origin, OARay.direction * OALookAheadDistance, Color.green); // Heading is in direction of the ray we cast, // velocity is calculated turn velocity in direction of heading behaviourOutput.heading = OARay.direction; behaviourOutput.velocity = OARay.direction * maxTurnVelocity; //if (givenHorizontalDistance > neededHorizontalDistance) //{ // behaviourInput.velocity = OARay.direction * behaviourInput.shipAIInputModuleInstance.maxSpeed; //} //else //{ // //behaviourInput.velocity = XYPlaneRayDirections[i] * behaviourInput.shipAIInputModuleInstance.maxSpeed; //} // We have found a viable route, so break out of the loop foundViableRoute = true; break; } //else { Debug.DrawRay(OARay.origin, OARay.direction * OALookAheadDistance, Color.red); } } // Increment the raycast angle raycastAngle += 20f; if (raycastAngle > 90.1f) { behaviourOutput.heading = behaviourInput.shipInstance.RigidbodyForward * -1f; behaviourOutput.velocity = behaviourInput.shipInstance.RigidbodyForward * -behaviourInput.shipAIInputModuleInstance.maxSpeed; break; } } // Up and target are not set behaviourOutput.up = Vector3.zero; behaviourOutput.target = Vector3.zero; behaviourOutput.setTarget = false; } else { // If it was determined that no action is required, set the behaviour output to a "null" output behaviourOutput.heading = Vector3.zero; behaviourOutput.up = Vector3.zero; behaviourOutput.velocity = Vector3.zero; behaviourOutput.target = Vector3.zero; behaviourOutput.setTarget = false; } // Never use targeting accuracy behaviourOutput.useTargetingAccuracy = false; #endregion } /// /// Sets a behaviour output to a "follow path" behaviour output. /// Required inputs: Target path, weighting. /// /// /// public static void SetFollowPathBehaviourOutput(AIBehaviourInput behaviourInput, AIBehaviourOutput behaviourOutput) { // Check that location data list is valid if (behaviourInput.targetPath != null && behaviourInput.targetPath.pathLocationDataList != null) { // TODO do something about non-closed circuits // TODO check that path functions are successful i.e. return true. If they do not return true, // do something... // Get the current target path index int currentTargetPathIndex = behaviourInput.shipAIInputModuleInstance.GetCurrentTargetPathLocationIndex(); // TODO: if the current index is -1, try and find the closest path point if (currentTargetPathIndex < 0) { currentTargetPathIndex = SSCManager.GetNextPathLocationIndex(behaviourInput.targetPath, -1, true); } // Try and find the current path point int targetPathLocationCount = behaviourInput.targetPath.pathLocationDataList.Count; if (currentTargetPathIndex < 0 || targetPathLocationCount < 2) { // Default to facing forward and stopping if no valid Path Locations behaviourOutput.heading = Vector3.forward; behaviourOutput.up = Vector3.zero; behaviourOutput.velocity = Vector3.zero; behaviourOutput.target = Vector3.zero; behaviourOutput.setTarget = true; } else { // Get the indices of the first and last path locations int firstTargetPathLocationIdx = SSCManager.GetFirstAssignedLocationIdx(behaviourInput.targetPath); int lastTargetPathLocationIdx = SSCManager.GetLastAssignedLocationIdx(behaviourInput.targetPath); // Validate Path Data if (firstTargetPathLocationIdx == lastTargetPathLocationIdx || firstTargetPathLocationIdx < 0 || lastTargetPathLocationIdx < 0) { // Default to facing forward and stopping if no valid Path Locations behaviourOutput.heading = Vector3.forward; behaviourOutput.up = Vector3.zero; behaviourOutput.velocity = Vector3.zero; behaviourOutput.target = Vector3.zero; behaviourOutput.setTarget = true; } else { #region Set Up Quality Settings // The (exact) number of iterations used in the future speed look ahead loop int speedLookAheadIterations = 5; // The (approximate) number of iterations used in the GetFurtherPointOnPathData function int furtherPathPointCalculationIterations = 5; switch (behaviourInput.shipAIInputModuleInstance.pathFollowingQuality) { // Low quality case ShipAIInputModule.AIPathFollowingQuality.VeryLow: speedLookAheadIterations = 1; furtherPathPointCalculationIterations = 1; break; case ShipAIInputModule.AIPathFollowingQuality.Low: speedLookAheadIterations = 3; furtherPathPointCalculationIterations = 3; break; // Medium quality case ShipAIInputModule.AIPathFollowingQuality.Medium: speedLookAheadIterations = 5; furtherPathPointCalculationIterations = 4; break; // High quality case ShipAIInputModule.AIPathFollowingQuality.High: speedLookAheadIterations = 10; furtherPathPointCalculationIterations = 5; break; } #endregion #region Update Current Waypoint // If this path is not a closed circuit, the current target path location index is not allowed // to be the first assigned location index (only the previous index can refer to the first point) if (!behaviourInput.targetPath.isClosedCircuit && currentTargetPathIndex == firstTargetPathLocationIdx) { // If it is the first index, set it to the next one currentTargetPathIndex = SSCManager.GetNextPathLocationIndex(behaviourInput.targetPath, currentTargetPathIndex, true); } // Check if we have gone "past" the current path point i.e. crossed the plane of its tangent Vector3 pathTangent = Vector3.zero; SSCMath.GetPathTangent(behaviourInput.targetPath, currentTargetPathIndex, 0f, ref pathTangent); if (Vector3.Dot(behaviourInput.shipInstance.TransformPosition - behaviourInput.targetPath.pathLocationDataList[currentTargetPathIndex].locationData.position, pathTangent) > 0f) { // Don't try and go to the next point if this is not a closed circuit and this point is the last point if (!behaviourInput.targetPath.isClosedCircuit && currentTargetPathIndex == lastTargetPathLocationIdx) { // Instead set the state action as completed behaviourInput.shipAIInputModuleInstance.SetHasCompletedStateAction(true); } else { // Increment the target path index (Get next valid Path Location) int nextTargetPathIndex = SSCManager.GetNextPathLocationIndex(behaviourInput.targetPath, currentTargetPathIndex, behaviourInput.targetPath.isClosedCircuit); // If we didn't find another Location, keep same target. Not sure what else to do... if (nextTargetPathIndex >= 0) { currentTargetPathIndex = nextTargetPathIndex; } } } // Set the new target path index behaviourInput.shipAIInputModuleInstance.SetCurrentTargetPathLocationIndex(currentTargetPathIndex); #endregion // Get the previous path index int lastTargetPathIndex = SSCManager.GetPreviousPathLocationIndex(behaviourInput.targetPath, currentTargetPathIndex, true); // TODO eventually will need to get from path data float pathRadius = 5f; #region Find Closest Point On Path Vector3 closestPointOnPath = Vector3.zero; float closestPointOnPathTValue = 0f; Vector3 closestPointOnPathTangent = Vector3.zero; Vector3 closestPointOnPathNormal = Vector3.zero; Vector3 closestPointOnPathBinormal = Vector3.zero; float closestPointOnPathCurvature = 0f; // Find the position and t-value of the closest point on the path SSCMath.FindClosestPointOnPath(behaviourInput.targetPath, lastTargetPathIndex, behaviourInput.shipInstance.TransformPosition, ref closestPointOnPath, ref closestPointOnPathTValue); // Get the tangent, normal and binormal information SSCMath.GetPathFrenetData(behaviourInput.targetPath, lastTargetPathIndex, closestPointOnPathTValue, ref closestPointOnPathTangent, ref closestPointOnPathNormal, ref closestPointOnPathBinormal); // Get the curvature of this point // If the ship is sticking to a ground surface, calculate the curvature projected into the plane // of the ground surface. This way speed calculation will only care about changes in the path // perpendicular to the ground surface if (behaviourInput.shipInstance.IsGrounded) { SSCMath.GetPathCurvatureInPlane(behaviourInput.targetPath, lastTargetPathIndex, closestPointOnPathTValue, behaviourInput.shipInstance.WorldTargetPlaneNormal, ref closestPointOnPathCurvature); } // Otherwise just calculate curvature normally else { SSCMath.GetPathCurvature(behaviourInput.targetPath, lastTargetPathIndex, closestPointOnPathTValue, ref closestPointOnPathCurvature); } // Calculate the distance from the ship to the path // Subtract the projection onto the tangent (to get vector rejection) // This gives the position of the ship projected onto a vector normal to the path // TODO optimise Vector3 projectedShipPosition = behaviourInput.shipInstance.TransformPosition - Vector3.Project(behaviourInput.shipInstance.TransformPosition - closestPointOnPath, closestPointOnPathTangent); // If the ship is sticking to a ground surface, project the projected ship position // into the plane of that ground surface // This way we will only care about aspects of the path in the plane of the ground surface if (behaviourInput.shipInstance.IsGrounded) { // TODO optimise projectedShipPosition = Vector3.ProjectOnPlane(projectedShipPosition - closestPointOnPath, behaviourInput.shipInstance.WorldTargetPlaneNormal) + closestPointOnPath; } // Measure the distance (perpendicular to the path tangent) from the projected ship position to the path float distanceToPath = Vector3.Distance(projectedShipPosition, closestPointOnPath); // Compare the distance to the path radius to compute whether we are currenly within the bounds of the path bool withinPathRadius = distanceToPath < pathRadius; // Set the time value between the last point and the next point on the path. behaviourInput.shipAIInputModuleInstance.SetCurrentTargetPathTValue(closestPointOnPathTValue); #endregion #region Steering // TODO DEBUG LINES //Debug.DrawLine(behaviourInput.shipInstance.TransformPosition, closestPointOnPath, Color.green); //Debug.DrawRay(closestPointOnPath, (projectedShipPosition - closestPointOnPath).normalized * pathRadius, Color.red); //Debug.DrawRay(closestPointOnPath + (closestPointOnPathTangent * 0.1f), (projectedShipPosition - closestPointOnPath).normalized * distanceToPath, Color.yellow); // Look ahead a given distance along the path and use that point on the path to inform our heading // Get the current speed of the ship in the forwards direction float currentSpeed = behaviourInput.shipInstance.LocalVelocity.z > 0f ? behaviourInput.shipInstance.LocalVelocity.z : 0f; // Calculate the distance to look ahead for steering // TODO improve algorithm, don't use hardcoded values (account for maneuverability), optimise float steerLookAheadTime = Mathf.Lerp(0.2f, 0.4f, 1f - (closestPointOnPathCurvature * 500f)); float steerLookAheadDistance = steerLookAheadTime * currentSpeed; // If we are on the path, minimum steer look ahead distance is proportional to distance from the path // TODO NOTE ONLY: first is * 20f, second is * 5f if (withinPathRadius && steerLookAheadDistance < distanceToPath * 15f) { steerLookAheadDistance = distanceToPath * 15f; } // If we are not on the path, minimum steer look ahead distance is proportional to path radius else if (!withinPathRadius && steerLookAheadDistance < pathRadius * 15f) { steerLookAheadDistance = pathRadius * 15f; } // Look ahead distance must be at least the ship's assumed diameter if (steerLookAheadDistance < behaviourInput.shipAIInputModuleInstance.shipRadius * 2f) { steerLookAheadDistance = behaviourInput.shipAIInputModuleInstance.shipRadius * 2f; } // Find the point that distance along the path Vector3 steerTargetPathPoint = Vector3.zero; float steerTargetPathPointTValue = 0f; Vector3 steerTargetPathTangent = Vector3.zero; float steerTargetPathPointCurvature = 0f; int steerTargetLastTargetPathIndex = 0; // TODO change "5" based on quality SSCMath.GetFurtherPointOnPathData(behaviourInput.targetPath, lastTargetPathIndex, closestPointOnPathTValue, steerLookAheadDistance, 5, ref steerTargetPathPoint, ref steerTargetPathPointCurvature, ref steerTargetLastTargetPathIndex, ref steerTargetPathPointTValue); // Get the tangent of the steer path point SSCMath.GetPathTangent(behaviourInput.targetPath, steerTargetLastTargetPathIndex, steerTargetPathPointTValue, ref steerTargetPathTangent); // Find the point to use for the target output point // This will be some point at or further than the steer target path point // How far ahead of the steer target path point it is is determined by ship speed and path curvature // TODO optimise float bhTargetLookAheadDistance = Mathf.Lerp(0.25f, 0.5f, 1f - (closestPointOnPathCurvature * 500f)) * currentSpeed; // Get the target point Vector3 bhTargetPathPoint = Vector3.zero; float bhTargetPathPointTValue = 0f; float bhTargetPathPointCurvature = 0f; int bhLastTargetPathIndex = 0; // TODO change "5" based on quality SSCMath.GetFurtherPointOnPathData(behaviourInput.targetPath, steerTargetLastTargetPathIndex, steerTargetPathPointTValue, bhTargetLookAheadDistance, 5, ref bhTargetPathPoint, ref bhTargetPathPointCurvature, ref bhLastTargetPathIndex, ref bhTargetPathPointTValue); // If the ship is sticking to a ground surface, project the steer target point and target point // into the plane of that ground surface // This way we will only care about aspects of the path in the plane of the ground surface if (behaviourInput.shipInstance.IsGrounded) { // TODO optimise steerTargetPathPoint = Vector3.ProjectOnPlane(steerTargetPathPoint - behaviourInput.shipInstance.TransformPosition, behaviourInput.shipInstance.WorldTargetPlaneNormal) + behaviourInput.shipInstance.TransformPosition; bhTargetPathPoint = Vector3.ProjectOnPlane(bhTargetPathPoint - behaviourInput.shipInstance.TransformPosition, behaviourInput.shipInstance.WorldTargetPlaneNormal) + behaviourInput.shipInstance.TransformPosition; } // Calculate heading from the point on the path we found float distToSteerTargetPoint = (steerTargetPathPoint - behaviourInput.shipInstance.TransformPosition).magnitude; behaviourOutput.heading = (steerTargetPathPoint - behaviourInput.shipInstance.TransformPosition) / distToSteerTargetPoint; // Assign the calculated target point behaviourOutput.target = bhTargetPathPoint; behaviourOutput.setTarget = true; // No desired upwards orientation behaviourOutput.up = Vector3.zero; // TODO DEBUG LINE //Debug.DrawLine(behaviourInput.shipInstance.TransformPosition, steerTargetPathPoint, Color.magenta); //Debug.DrawRay(behaviourInput.shipInstance.TransformPosition, behaviourInput.shipInstance.WorldTargetPlaneNormal * 10f, Color.gray); #endregion #region Current Speed // Calculate the radius of the curve that we want to be turning through at the steer target point // We set this to the radius of the curve at the closest point on the path so that we match our // speed to the curve correctly where we are currently float curveEndingRadius = 1f / closestPointOnPathCurvature; // Calculate the effective radius of the curve that we are currently turning through // This is measured in the same plane of the curve at the closest point on the curve float effectiveAngularVelocity = 1f; if (behaviourInput.shipInstance.IsGrounded) { // TODO optimise effectiveAngularVelocity = Vector3.Dot(behaviourInput.shipInstance.WorldAngularVelocity, behaviourInput.shipInstance.WorldTargetPlaneNormal); // TODO need to work out correct sign if (effectiveAngularVelocity < 0f) { effectiveAngularVelocity = -effectiveAngularVelocity; } } else { // TODO optimise effectiveAngularVelocity = Vector3.Dot(behaviourInput.shipInstance.WorldAngularVelocity, closestPointOnPathBinormal); // TODO need to work out correct sign if (effectiveAngularVelocity < 0f) { effectiveAngularVelocity = -effectiveAngularVelocity; } } float curveStartingRadius = effectiveAngularVelocity > 0f ? (currentSpeed / effectiveAngularVelocity) : 10000000f; // Prevents case of zero curve starting radius if (curveStartingRadius < 0.1f) { curveStartingRadius = 10000000f; } // Calculate a maximum speed based on the current path curvature // This assumes the ideal case: That we are following the path exactly // TODO: Maybe dist to steer target point should be * 0.5? float currentTargetSpeed = behaviourInput.shipAIInputModuleInstance.MaxSpeedAlongCurve(curveStartingRadius, curveEndingRadius, distToSteerTargetPoint, behaviourInput.shipInstance.IsGrounded); // Calculate the speed required based on our actual position relative to the path if (!withinPathRadius) { // Since we are outside the bounds of the path, we need to rejoin the path // Radius of curvature requires an angle and a distance it is over // Distance is the distance to the steer target point // Calculate the angle for the "in" curve and the "out" curve float turnAngle1 = (float)System.Math.Acos(Vector3.Dot(behaviourInput.shipInstance.WorldVelocity.normalized, steerTargetPathPoint - behaviourInput.shipInstance.TransformPosition) / distToSteerTargetPoint); float turnAngle2 = (float)System.Math.Acos(Vector3.Dot(steerTargetPathTangent, steerTargetPathPoint - behaviourInput.shipInstance.TransformPosition) / distToSteerTargetPoint); // Calculate the radius for the "in" curve and the "out" curve float turnRadius1 = distToSteerTargetPoint / (2f * (float)System.Math.Sin(turnAngle1)); float turnRadius2 = distToSteerTargetPoint / (2f * (float)System.Math.Sin(turnAngle2)); // Calculate the maximum turn speed for the "in" curve and the out "curve" float turnSpeed1 = behaviourInput.shipAIInputModuleInstance.MaxSpeedAlongConstantRadiusCurve(turnRadius1, behaviourInput.shipInstance.IsGrounded); float turnSpeed2 = behaviourInput.shipAIInputModuleInstance.MaxSpeedAlongConstantRadiusCurve(turnRadius2, behaviourInput.shipInstance.IsGrounded); // Update the current target speed accordingly if (turnSpeed1 < currentTargetSpeed) { currentTargetSpeed = turnSpeed1; } if (turnSpeed2 < currentTargetSpeed) { currentTargetSpeed = turnSpeed2; } } #endregion #region Future Speed // Declare variables for use Vector3 pointOnPath = Vector3.zero; float tValue = 0f; float newTValue = 0f; int newLastTargetPathIndex = 0; float pathCurvature = 0f; // Calculate total look-ahead distance based on stopping distance // POSSIBLE BUG: Probably LocalVelocity should be normalised as a parameter for BrakingDistance(...) float lookAheadDistance = behaviourInput.shipAIInputModuleInstance.BrakingDistance(currentSpeed, 0.1f, behaviourInput.shipInstance.LocalVelocity); // Choose the distance increment size based on the user-specified path following quality float distanceIncrement = lookAheadDistance / speedLookAheadIterations; if (distanceIncrement < 0.001f) { distanceIncrement = 0.001f; } float totalObservedDistance = distanceIncrement; // Remember the curvature of the closest point on the path as the last path point curvature float lastPathCurvature = closestPointOnPathCurvature; // Start at the closest point on the path tValue = closestPointOnPathTValue; // Iterate over the path at regular distance intervals to find what speed we should be doing right now int penultimateTargetPathLocationIdx = SSCManager.GetPreviousPathLocationIndex(behaviourInput.targetPath, lastTargetPathLocationIdx, false); while (totalObservedDistance < lookAheadDistance + 0.001f) { // If this is not a closed ciruit, don't go past the last path point if (!behaviourInput.targetPath.isClosedCircuit && (lastTargetPathIndex == lastTargetPathLocationIdx || (lastTargetPathIndex == penultimateTargetPathLocationIdx && newTValue > 0.999f))) { totalObservedDistance = lookAheadDistance + 1f; } else { // Get the path data at the new point on the path SSCMath.GetFurtherPointOnPathData(behaviourInput.targetPath, lastTargetPathIndex, tValue, distanceIncrement, furtherPathPointCalculationIterations, ref pointOnPath, ref pathCurvature, ref newLastTargetPathIndex, ref newTValue); if (behaviourInput.shipInstance.IsGrounded) { // Project the curvature into the ground plane if we are on the ground // This isn't technically accurate (since the ground plane could have changed by the time that // we reach that point on the track) but does improve performance significantly SSCMath.GetPathCurvatureInPlane(behaviourInput.targetPath, newLastTargetPathIndex, newTValue, behaviourInput.shipInstance.WorldTargetPlaneNormal, ref pathCurvature); } // Calculate the required speed from the curvature (and the rate of change of curvature) float maxCurveSpeed = behaviourInput.shipAIInputModuleInstance.MaxSpeedAlongChangingRadiusCurve( 1f / lastPathCurvature, 1f / pathCurvature, distanceIncrement); // ORIGINAL CODE //float maxCurrentSpeed = behaviourInput.shipAIInputModuleInstance.MaxSpeedFromBrakingDistance(maxCurveSpeed, totalObservedDistance - distanceIncrement); // 31/03/2020 CODE - Takes into account distance to path for braking float maxCurrentSpeed = behaviourInput.shipAIInputModuleInstance.MaxSpeedFromBrakingDistance(maxCurveSpeed, totalObservedDistance - distanceIncrement + distanceToPath, behaviourInput.shipInstance.LocalVelocity.normalized); // If the required current speed is lower than our current target speed, update our current target speed if (maxCurrentSpeed < currentTargetSpeed) { currentTargetSpeed = maxCurrentSpeed; } // Set the path data for the next point on the path lastTargetPathIndex = newLastTargetPathIndex; tValue = newTValue; // Increment the observed distance totalObservedDistance += distanceIncrement; // Remember the last value of the path curvature lastPathCurvature = pathCurvature; } } // Current target speed is not allowed to exceed the set max speed if (currentTargetSpeed > behaviourInput.shipAIInputModuleInstance.maxSpeed) { currentTargetSpeed = behaviourInput.shipAIInputModuleInstance.maxSpeed; } #endregion // Set velocity input from the heading and the target speed we calculated behaviourOutput.velocity = behaviourOutput.heading * currentTargetSpeed; // Add the velocity of the path Vector3 pathVelocity = Vector3.zero; SSCMath.GetPathVelocity(behaviourInput.targetPath, behaviourInput.shipControlModuleInstance.shipInstance.TransformPosition, ref pathVelocity); behaviourOutput.velocity += pathVelocity; } } } else { // Default to facing forward and stopping if the path is invalid behaviourOutput.heading = Vector3.forward; behaviourOutput.up = Vector3.zero; behaviourOutput.velocity = Vector3.zero; behaviourOutput.target = Vector3.zero; behaviourOutput.setTarget = true; } // Whether we use targeting accuracy depends on behaviour input settings behaviourOutput.useTargetingAccuracy = behaviourInput.useTargetingAccuracy; } /// /// Sets a behaviour output to an "dock" behaviour output. /// Required inputs: Target position, target forwards, target up, target radius, target velocity, weighting. /// /// /// public static void SetDockBehaviourOutput(AIBehaviourInput behaviourInput, AIBehaviourOutput behaviourOutput) { // Do precalculation for heading vector headingVector = behaviourInput.targetPosition - behaviourInput.shipControlModuleInstance.shipInstance.TransformPosition; headingVectorSqrMagnitude = headingVector.sqrMagnitude; headingVectorMagnitude = Mathf.Sqrt(headingVectorSqrMagnitude); headingVectorNormalised = headingVector / headingVectorMagnitude; if (headingVectorMagnitude > behaviourInput.targetRadius) { // When outside the target radius... // Desired velocity is generally max speed in direction of desired heading, but decreases when nearing the target behaviourOutput.velocity = headingVectorNormalised * behaviourInput.shipAIInputModuleInstance.MaxSpeedFromBrakingDistance(0f, headingVectorMagnitude * 0.5f, behaviourInput.shipInstance.LocalVelocity.normalized); // Re-clamp velocity magnitude to max speed if needed if (behaviourOutput.velocity.sqrMagnitude > behaviourInput.shipAIInputModuleInstance.maxSpeed * behaviourInput.shipAIInputModuleInstance.maxSpeed) { behaviourOutput.velocity = headingVectorNormalised * behaviourInput.shipAIInputModuleInstance.maxSpeed; } // Add target velocity to output velocity to adjust for moving target behaviourOutput.velocity += behaviourInput.targetVelocity; // Target is the target position, but shifted towards the ship by the target radius // This is done to prevent obstacle avoidance being triggered behaviourOutput.target = behaviourInput.targetPosition - (headingVectorNormalised * behaviourInput.targetRadius); behaviourOutput.setTarget = true; //Debug.DrawRay(behaviourInput.shipControlModuleInstance.shipInstance.TransformPosition, behaviourOutput.velocity, Color.red); } else { // When inside the target radius... // Desired velocity is towards the target position // Minimum speed is so that it would take five times the target time to reach the target position from the target radius float minSpeed = 1f; if (behaviourInput.targetTime > 0f) { minSpeed = behaviourInput.targetRadius / (behaviourInput.targetTime * 5f); } if (minSpeed < 1f) { minSpeed = 1f; } // Maximum speed depends on the braking distance // NOTE: Currently requires a Reverse Thruster. float maxSpeed = behaviourInput.shipAIInputModuleInstance.MaxSpeedFromBrakingDistance(0f, headingVectorMagnitude * 0.5f, behaviourInput.shipInstance.LocalVelocity.normalized); // Target speed is proportional to the square distance to the target float speedMultiplier = 1f; if (behaviourInput.targetTime > 0f) { // Adjust speed multiplier so that the time to move from target radius to position at which min speed is reached // will be approximately the target time speedMultiplier = 1f / (behaviourInput.targetRadius * ((1f / (behaviourInput.targetTime * minSpeed)) - 1f)); } // NOTE: Previously the below was linear instead of quadratic // v = m*x^2 float targetSpeed = speedMultiplier * headingVectorMagnitude * headingVectorMagnitude; // ATTEMPTED IMPROVEMENT #1: Slow down if the angle to the target rotation is too great //float angleDelta = Quaternion.Angle(behaviourInput.shipControlModuleInstance.shipInstance.TransformRotation, Quaternion.LookRotation(behaviourInput.targetForwards, behaviourInput.targetUp)); //float maxAngleDelta = Mathf.InverseLerp(1f, 10f, headingVectorMagnitude / behaviourInput.targetRadius); //float targetAngleDelta = Mathf.InverseLerp(0f, 5f, headingVectorMagnitude / behaviourInput.targetRadius); //targetSpeed *= Mathf.InverseLerp(targetAngleDelta, maxAngleDelta, angleDelta); // ATTEMPTED IMPROVEMENT #2: Redirect velocity if we are going off-course float zFactor = 1f; float veloAngleDelta = Vector3.Dot(headingVectorNormalised, behaviourInput.shipControlModuleInstance.shipInstance.WorldVelocity) / behaviourInput.shipControlModuleInstance.shipInstance.WorldVelocity.magnitude * Mathf.Rad2Deg; float maxVeloAngleDelta = Mathf.InverseLerp(5f, 30f, headingVectorMagnitude / behaviourInput.targetRadius); float targetVeloAngleDelta = Mathf.InverseLerp(0f, 10f, headingVectorMagnitude / behaviourInput.targetRadius); zFactor = Mathf.InverseLerp(targetVeloAngleDelta, maxVeloAngleDelta, veloAngleDelta); behaviourOutput.velocity = headingVectorNormalised; Vector3 rVector = behaviourInput.shipControlModuleInstance.shipInstance.WorldVelocity - Vector3.Project(behaviourInput.shipControlModuleInstance.shipInstance.WorldVelocity, headingVectorNormalised); behaviourOutput.velocity += rVector.normalized * zFactor; // Normalise the vector - but if the vector is zero, just set it to the heading vector if (behaviourOutput.velocity.sqrMagnitude > Mathf.Epsilon) { behaviourOutput.velocity.Normalize(); } else { behaviourOutput.velocity = headingVectorNormalised; } // Clamp target speed between min and max speeds if (targetSpeed < minSpeed) { targetSpeed = minSpeed; } else if (targetSpeed > maxSpeed) { targetSpeed = maxSpeed; } behaviourOutput.velocity *= targetSpeed; // Add target velocity to output velocity to adjust for moving target behaviourOutput.velocity += behaviourInput.targetVelocity; // Target is the target position behaviourOutput.target = behaviourInput.targetPosition; behaviourOutput.setTarget = true; //Debug.DrawRay(behaviourInput.shipControlModuleInstance.shipInstance.TransformPosition, behaviourOutput.velocity, Color.green); } // Calculate interpolation float: 0 is inside target radius, 1 is outside 2 * target radius float interpolationValue = (headingVectorMagnitude / behaviourInput.targetRadius) - 1f; if (interpolationValue < 0f) { interpolationValue = 0f; } else if (interpolationValue > 1f) { interpolationValue = 1f; } // Desired heading and up directions are interpolated around the target radius // Inside the target radius: // - Desired heading is target forwards direction // - Desired up is target upwards direction // Outside the target radius: // - Desired heading is towards the target position // - Desired up direction is vector rejection of target up direction onto heading vector behaviourOutput.heading = Vector3.Slerp(behaviourInput.targetForwards, (headingVector + behaviourInput.targetVelocity).normalized, interpolationValue); behaviourOutput.up = behaviourInput.targetUp - (Vector3.Project(behaviourOutput.up, headingVector) * interpolationValue); //Debug.DrawLine(behaviourInput.shipControlModuleInstance.shipInstance.TransformPosition, behaviourOutput.target, Color.blue); //Debug.DrawLine(behaviourOutput.target, behaviourInput.targetPosition, Color.cyan); //Debug.DrawLine(behaviourInput.shipControlModuleInstance.shipInstance.TransformPosition, behaviourInput.targetPosition, Color.grey); // Never use targeting accuracy behaviourOutput.useTargetingAccuracy = false; } #endregion #endregion } }