rabidus-test/Assets/SCSM/SciFiShipController/Scripts/AI/AIBehaviourInput.cs

1817 lines
106 KiB
C#
Raw Permalink Normal View History

2023-07-24 16:38:13 +03:00
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
{
/// <summary>
/// Class containing data for an AI Behaviour Input.
/// </summary>
public class AIBehaviourInput
{
#region Enumerations
/// <summary>
/// The type of behaviour. Multiple behaviours can
/// be combined together.
/// </summary>
public enum AIBehaviourType
{
/// <summary>
/// Comes to a complete stop.
/// Required inputs: Weighting.
/// </summary>
Idle = 0,
/// <summary>
/// Moves directly towards target position.
/// Required inputs: Target position, weighting.
/// Optional inputs: Use targeting accuracy.
/// </summary>
Seek = 1,
/// <summary>
/// Moves directly away from target position.
/// Required inputs: Target position, weighting.
/// </summary>
Flee = 2,
/// <summary>
/// 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.
/// </summary>
Pursuit = 3,
/// <summary>
/// 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.
/// </summary>
Evasion = 4,
/// <summary>
/// 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.
/// </summary>
SeekArrival = 5,
/// <summary>
/// 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.
/// </summary>
SeekMovingArrival = 6,
/// <summary>
/// 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.
/// </summary>
PursuitArrival = 7,
//Avoid = 11,
//Follow = 12,
//BlockCylinder = 16,
//BlockCone = 17,
/// <summary>
/// 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.
/// </summary>
UnblockCylinder = 19,
/// <summary>
/// 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.
/// </summary>
UnblockCone = 20,
/// <summary>
/// Takes preventative action to avoid obstacles. If the ship does need to take preventative
/// action, returns a zero output.
/// Required inputs: Weighting.
/// </summary>
ObstacleAvoidance = 22,
//Wander = 25,
/// <summary>
/// Moves onto and then follows the target path.
/// Required inputs: Target path, weighting.
/// Optional inputs: Use targeting accuracy.
/// </summary>
FollowPath = 28,
/// <summary>
/// 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.
/// </summary>
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)
/// <summary>
/// The type of behaviour to set this behaviour input with.
/// </summary>
public AIBehaviourType behaviourType;
/// <summary>
/// The Ship instance for this AI ship.
/// </summary>
public Ship shipInstance;
/// <summary>
/// The Ship Control Module instance for this AI ship.
/// </summary>
public ShipControlModule shipControlModuleInstance;
/// <summary>
/// The Ship AI Input Module instance for this AI ship.
/// </summary>
public ShipAIInputModule shipAIInputModuleInstance;
/// <summary>
/// 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.
/// </summary>
public float weighting;
/// <summary>
/// The target position provided to this behaviour input.
/// </summary>
public Vector3 targetPosition;
/// <summary>
/// The target path provided to this behaviour input.
/// </summary>
public PathData targetPath;
/// <summary>
/// The target velocity provided to this behaviour input.
/// </summary>
public Vector3 targetVelocity;
/// <summary>
/// The target forwards direction provided to this behaviour input. NOTE: This must be a normalised vector.
/// </summary>
public Vector3 targetForwards;
/// <summary>
/// The target up direction provided to this behaviour input. NOTE: This must be a normalised vector.
/// </summary>
public Vector3 targetUp;
/// <summary>
/// The target radius (in metres) provided to this behaviour input.
/// </summary>
public float targetRadius;
/// <summary>
/// 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.
/// </summary>
public float targetFOVAngle;
/// <summary>
/// The target time (in seconds) provided to this behaviour input.
/// </summary>
public float targetTime;
/// <summary>
/// Whether targeting accuracy should be taken into account by this behaviour input.
/// </summary>
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;
}
/// <summary>
/// Clears the behaviour-dependent settings of a behaviour input.
/// </summary>
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
/// <summary>
/// Calculates an approximate time for the ship to "catch up to" another ship. Used in look ahead times.
/// </summary>
/// <param name="targetPosition"></param>
/// <param name="targetVelocity"></param>
/// <returns></returns>
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; }
}
/// <summary>
/// Returns whether two objects (approximated as spheres) within given position, velocity and radius will collide
/// within a given look ahead time.
/// </summary>
/// <param name="object1Position"></param>
/// <param name="object1Velocity"></param>
/// <param name="object2Position"></param>
/// <param name="object2Velocity"></param>
/// <param name="object1Radius"></param>
/// <param name="object2Radius"></param>
/// <param name="lookAheadTime"></param>
/// <returns></returns>
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; }
}
/// <summary>
/// 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.
/// </summary>
/// <param name="behaviourInput"></param>
/// <param name="sweepType"></param>
/// <param name="sweepRay"></param>
/// <param name="sweepRaycastHit"></param>
/// <param name="sweepMaxDistance"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="turnEndPosition"></param>
/// <param name="turnEndVelocity"></param>
/// <param name="behaviourInput"></param>
/// <returns></returns>
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
/// <summary>
/// Sets a behaviour output to an "idle" behaviour output.
/// Required inputs: Weighting.
/// </summary>
/// <param name="behaviourInput"></param>
/// <param name="behaviourOutput"></param>
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;
}
/// <summary>
/// Sets a behaviour output to a "seek" behaviour output.
/// Required inputs: Target position, weighting.
/// </summary>
/// <param name="behaviourInput"></param>
/// <param name="behaviourOutput"></param>
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;
}
/// <summary>
/// Sets a behaviour output to a "flee" behaviour output.
/// Required inputs: Target position, weighting.
/// </summary>
/// <param name="behaviourInput"></param>
/// <param name="behaviourOutput"></param>
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;
}
/// <summary>
/// Sets a behaviour output to a "pursuit" behaviour output.
/// Required inputs: Target position, target velocity, weighting.
/// </summary>
/// <param name="behaviourInput"></param>
/// <param name="behaviourOutput"></param>
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;
}
/// <summary>
/// Sets a behaviour output to an "evasion" behaviour output.
/// Required inputs: Target position, target velocity, weighting.
/// </summary>
/// <param name="behaviourInput"></param>
/// <param name="behaviourOutput"></param>
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;
}
///// <summary>
///// Sets a behaviour input to an "avoid" behaviour input for a fixed target position
///// </summary>
///// <param name="behaviourInput"></param>
//public static void SetAvoidInputBehaviour(AIBehaviourInput behaviourInput)
//{
// behaviourInput.velocityOutput = Vector3.zero;
// behaviourInput.weighting = 0f;
//}
///// <summary>
///// 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.
///// </summary>
///// <param name="behaviourInput"></param>
///// <param name="targetPosition"></param>
///// <param name="targetVelocity"></param>
///// <param name="behaviourWeighting"></param>
//public static void SetFollowInputBehaviour(AIBehaviourInput behaviourInput)
//{
// behaviourInput.velocityOutput = Vector3.zero;
// behaviourInput.weighting = 0f;
//}
/// <summary>
/// Sets a behaviour output to a "seek arrival" behaviour output for a fixed target position.
/// Required inputs: Target position, weighting.
/// </summary>
/// <param name="behaviourInput"></param>
/// <param name="behaviourOutput"></param>
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;
}
/// <summary>
/// Sets a behaviour output to a "seek arrival" behaviour output for a moving target position.
/// Required inputs: Target position, target velocity, weighting.
/// </summary>
/// <param name="behaviourInput"></param>
/// <param name="behaviourOutput"></param>
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;
}
/// <summary>
/// Sets a behaviour output to a "pursuit arrival" behaviour output.
/// Required inputs: Target position, target velocity, weighting.
/// </summary>
/// <param name="behaviourInput"></param>
/// <param name="behaviourOutput"></param>
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;
}
/// <summary>
/// Sets a behaviour output to a "unblock cylinder" behaviour output.
/// Required inputs: Target position, target forwards, target radius, weighting.
/// </summary>
/// <param name="behaviourInput"></param>
/// <param name="behaviourOutput"></param>
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;
}
/// <summary>
/// Sets a behaviour output to a "unblock cone" behaviour output.
/// Required inputs: Target position, target forwards, target FOV angle, weighting.
/// </summary>
/// <param name="behaviourInput"></param>
/// <param name="behaviourOutput"></param>
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;
}
/// <summary>
/// Sets a behaviour output to a "obstacle avoidance" behaviour output.
/// Required inputs: Weighting.
/// </summary>
/// <param name="behaviourInput"></param>
/// <param name="behaviourOutput"></param>
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
}
/// <summary>
/// Sets a behaviour output to a "follow path" behaviour output.
/// Required inputs: Target path, weighting.
/// </summary>
/// <param name="behaviourInput"></param>
/// <param name="behaviourOutput"></param>
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;
}
/// <summary>
/// Sets a behaviour output to an "dock" behaviour output.
/// Required inputs: Target position, target forwards, target up, target radius, target velocity, weighting.
/// </summary>
/// <param name="behaviourInput"></param>
/// <param name="behaviourOutput"></param>
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
}
}