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 used for AI states. /// public class AIState { #region Public Enumerations public enum BehaviourCombiner { /// /// Chooses the first non-zero behaviour in the list. /// PriorityOnly = 10, /// /// Loops through the non-zero behaviours in the list (in order) and has a probability specified by weighting /// of choosing each one. /// PrioritisedDithering = 20, /// /// Uses a weighted combination of all the non-zero behaviours in the list. /// WeightedAverage = 30 } #endregion #region Public Non-Static Variables /// /// The name of the state. /// public string name; /// /// The ID number of the state. /// public int id; /// /// The method called by the state when it is the current state for a ship. /// public ShipAIInputModule.CallbackStateMethod callbackStateMethod; /// /// The method used by the state for combining behaviours. /// public BehaviourCombiner behaviourCombiner; #endregion #region Private Static Variables private static bool isInitialising = false; private static bool isInitialised = false; private static List aiStatesList; #endregion #region Public Static Readonly Variables /// /// The state ID number for the Idle state. The Idle state has no required inputs. /// While in the Idle state, the AI ship will remain stationary. /// public static readonly int idleStateID = 0; /// /// The state ID number for the Move To state. The Move To state takes the following required inputs: /// TargetPath / TargetLocation / TargetPosition. It also takes the following optional inputs: ShipsToEvade. /// While in the Move To state, the AI ship will either follow TargetPath, or if that is null, move towards /// TargetLocation, or if that is null, move towards TargetPosition. It will also evade the targeting regions /// of up to 5 ships in the ShipsToEvade list. The state action is set as completed when the ship is within /// the ship radius of TargetPosition / TargetLocation or it if reaches the end of TargetPath. /// public static readonly int moveToStateID = 1; /// /// The state ID number for the Dogfight state. The Dogfight state takes the following required inputs: TargetShip. /// While in the Dogfight state, the AI ship will attack TargetShip, whilst also trying to evade TargetShip if /// TargetShip ends up behind it. The state action is set as completed when TargetShip is destroyed. /// public static readonly int dogfightStateID = 2; /// /// The state ID number for the Docking state. The Docking state takes the following required inputs: /// TargetPosition, TargetRotation, TargetRadius, TargetDistance, TargetAngularDistance, TargetVelocity. /// While in the Docking state, the AI ship will move directly towards TargetPosition (a position moving with velocity /// TargetVelocity) and (when it gets within TargetRadius) attempt to match TargetRotation. The state action is set as /// completed once the ship is within TargetDistance of TargetPosition and TargetAngularDistance of TargetRotation. /// public static readonly int dockingStateID = 3; /// /// The state ID number for the Strafing Run state. The Strafing Run state takes the following required inputs: /// TargetLocation / TargetPosition, TargetRadius. It also takes the following optional inputs: SurfaceTurretsToEvade. /// While in the Strafing Run state, the AI ship will move directly towards TargetLocation / TargetPosition until it gets /// within TargetRadius. Then it will move past and away from TargetLocation / TargetPosition until it escapes the /// TargetRadius, at which point it will set the state action as completed. /// public static readonly int strafingRunStateID = 4; #endregion #region Constructors // Class constructor public AIState (string stateName, int stateID, ShipAIInputModule.CallbackStateMethod stateMethod, BehaviourCombiner stateBehaviourCombiner) { this.name = stateName; this.id = stateID; this.callbackStateMethod = stateMethod; this.behaviourCombiner = stateBehaviourCombiner; } #endregion #region Private Static Methods /// /// The state method for the Idle state. /// /// private static void IdleState(AIStateMethodParameters stateMethodParameters) { stateMethodParameters.aiBehaviourInputsList[0].behaviourType = AIBehaviourInput.AIBehaviourType.CustomIdle; stateMethodParameters.aiBehaviourInputsList[0].weighting = 1f; } /// /// The state method for the Move To state. /// /// private static void MoveToState (AIStateMethodParameters stateMethodParameters) { // Priority #1: Obstacle avoidance stateMethodParameters.aiBehaviourInputsList[0].behaviourType = AIBehaviourInput.AIBehaviourType.CustomObstacleAvoidance; stateMethodParameters.aiBehaviourInputsList[0].weighting = 1f; // Priority #2: Evade the targeting regions of a list of ships int shipsToEvadeCount = 0; if (stateMethodParameters.shipsToEvade != null) { shipsToEvadeCount = stateMethodParameters.shipsToEvade.Count; // Limit ships to evade to a maximum of 5 if (shipsToEvadeCount > 5) { shipsToEvadeCount = 5; } // Loop over all the ships to evade Ship shipToEvade; for (int i = 0; i < shipsToEvadeCount; i++) { shipToEvade = stateMethodParameters.shipsToEvade[i]; if (shipToEvade != null && !shipToEvade.Destroyed()) { stateMethodParameters.aiBehaviourInputsList[1 + i].behaviourType = AIBehaviourInput.AIBehaviourType.CustomUnblockCone; stateMethodParameters.aiBehaviourInputsList[1 + i].targetPosition = stateMethodParameters.shipsToEvade[i].TransformPosition; stateMethodParameters.aiBehaviourInputsList[1 + i].targetForwards = stateMethodParameters.shipsToEvade[i].TransformForward; stateMethodParameters.aiBehaviourInputsList[1 + i].targetFOVAngle = 5f; //stateMethodParameters.aiBehaviourInputsList[1 + i].weighting = 0.2f; stateMethodParameters.aiBehaviourInputsList[1 + i].weighting = 1f / shipsToEvadeCount; //stateMethodParameters.aiBehaviourInputsList[1 + i].behaviourType = AIBehaviourInput.AIBehaviourType.CustomUnblockCylinder; //stateMethodParameters.aiBehaviourInputsList[1 + i].targetPosition = shipToEvade.TransformPosition; //stateMethodParameters.aiBehaviourInputsList[1 + i].targetForwards = shipToEvade.TransformForward; //stateMethodParameters.aiBehaviourInputsList[1 + i].targetRadius = stateMethodParameters.shipAIInputModule.shipRadius; //stateMethodParameters.aiBehaviourInputsList[1 + i].weighting = 0.2f; } } } // Priority #3: Follow a path / move to a location / move to a position if (stateMethodParameters.targetPath != null) { stateMethodParameters.aiBehaviourInputsList[shipsToEvadeCount + 1].behaviourType = AIBehaviourInput.AIBehaviourType.CustomFollowPath; stateMethodParameters.aiBehaviourInputsList[shipsToEvadeCount + 1].targetPath = stateMethodParameters.targetPath; stateMethodParameters.aiBehaviourInputsList[shipsToEvadeCount + 1].weighting = 1f; } else if (stateMethodParameters.targetLocation != null) { stateMethodParameters.aiBehaviourInputsList[shipsToEvadeCount + 1].behaviourType = AIBehaviourInput.AIBehaviourType.CustomSeekArrival; stateMethodParameters.aiBehaviourInputsList[shipsToEvadeCount + 1].targetPosition = stateMethodParameters.targetLocation.position; stateMethodParameters.aiBehaviourInputsList[shipsToEvadeCount + 1].weighting = 1f; // If distance to target location is less than ship radius, set the state action as completed if ((stateMethodParameters.shipControlModule.shipInstance.TransformPosition - stateMethodParameters.targetLocation.position).sqrMagnitude < stateMethodParameters.shipAIInputModule.shipRadius) { stateMethodParameters.shipAIInputModule.SetHasCompletedStateAction(true); } } else { stateMethodParameters.aiBehaviourInputsList[shipsToEvadeCount + 1].behaviourType = AIBehaviourInput.AIBehaviourType.CustomSeekArrival; stateMethodParameters.aiBehaviourInputsList[shipsToEvadeCount + 1].targetPosition = stateMethodParameters.targetPosition; stateMethodParameters.aiBehaviourInputsList[shipsToEvadeCount + 1].weighting = 1f; // If distance to target position is less than ship radius, set the state action as completed if ((stateMethodParameters.shipControlModule.shipInstance.TransformPosition - stateMethodParameters.targetPosition).sqrMagnitude < stateMethodParameters.shipAIInputModule.shipRadius) { stateMethodParameters.shipAIInputModule.SetHasCompletedStateAction(true); } } } /// /// The state method for the Dogfight state. /// /// private static void DogfightState (AIStateMethodParameters stateMethodParameters) { if (stateMethodParameters.targetShip != null) { // Pre-calculation Vector3 fromTargetShipVector = stateMethodParameters.shipControlModule.shipInstance.TransformPosition - stateMethodParameters.targetShip.TransformPosition; float distToTargetShip = fromTargetShipVector.magnitude; float approxPursueInterceptionTime = stateMethodParameters.shipControlModule.shipInstance.WorldVelocity.sqrMagnitude > 0.01f ? distToTargetShip / stateMethodParameters.shipControlModule.shipInstance.WorldVelocity.magnitude : 1000f; float approxEvadeInterceptionTime = stateMethodParameters.targetShip.WorldVelocity.sqrMagnitude > 0.01f ? distToTargetShip / stateMethodParameters.targetShip.WorldVelocity.magnitude : 1000f; // Priority #1: Obstacle avoidance stateMethodParameters.aiBehaviourInputsList[0].behaviourType = AIBehaviourInput.AIBehaviourType.CustomObstacleAvoidance; stateMethodParameters.aiBehaviourInputsList[0].weighting = 1f; // Priority #2: Evade target ship's targeting region stateMethodParameters.aiBehaviourInputsList[1].behaviourType = AIBehaviourInput.AIBehaviourType.CustomUnblockCone; stateMethodParameters.aiBehaviourInputsList[1].targetPosition = stateMethodParameters.targetShip.TransformPosition; stateMethodParameters.aiBehaviourInputsList[1].targetForwards = stateMethodParameters.targetShip.TransformForward; stateMethodParameters.aiBehaviourInputsList[1].targetFOVAngle = 5f; stateMethodParameters.aiBehaviourInputsList[1].weighting = 1f; // Priority #3: Evade target ship (if we are "in front" of the target ship and within 3 seconds evade interception time) if (Vector3.Dot(fromTargetShipVector, stateMethodParameters.targetShip.TransformForward) > 0f && Vector3.Dot(fromTargetShipVector, stateMethodParameters.shipControlModule.shipInstance.TransformForward) > 0f && approxEvadeInterceptionTime < 3f) { stateMethodParameters.aiBehaviourInputsList[2].behaviourType = AIBehaviourInput.AIBehaviourType.CustomFlee; stateMethodParameters.aiBehaviourInputsList[2].targetPosition = stateMethodParameters.targetShip.TransformPosition; stateMethodParameters.aiBehaviourInputsList[2].targetVelocity = stateMethodParameters.targetShip.WorldVelocity; stateMethodParameters.aiBehaviourInputsList[2].weighting = 1f; } // Priority #4: Pursue/seek target ship if (approxPursueInterceptionTime > 3f && approxPursueInterceptionTime < 10f) { stateMethodParameters.aiBehaviourInputsList[3].behaviourType = AIBehaviourInput.AIBehaviourType.CustomPursuitArrival; } else { stateMethodParameters.aiBehaviourInputsList[3].behaviourType = AIBehaviourInput.AIBehaviourType.CustomSeekMovingArrival; } stateMethodParameters.aiBehaviourInputsList[3].targetPosition = stateMethodParameters.targetShip.TransformPosition; stateMethodParameters.aiBehaviourInputsList[3].targetVelocity = stateMethodParameters.targetShip.WorldVelocity; stateMethodParameters.aiBehaviourInputsList[3].useTargetingAccuracy = true; stateMethodParameters.aiBehaviourInputsList[3].weighting = 1f; // Set the state action as completed once the target ship is destroyed // TODO: should possibly choose different action if the target ship is destroyed? if (stateMethodParameters.targetShip.Destroyed()) { stateMethodParameters.shipAIInputModule.SetHasCompletedStateAction(true); } } else { // Fallback: If the target ship is null (which it shouldn't be) simply seek the target position with obstacle avoidance // Priority #1: Obstacle avoidance stateMethodParameters.aiBehaviourInputsList[0].behaviourType = AIBehaviourInput.AIBehaviourType.CustomObstacleAvoidance; stateMethodParameters.aiBehaviourInputsList[0].weighting = 1f; // Priority #2: Seek target position stateMethodParameters.aiBehaviourInputsList[1].behaviourType = AIBehaviourInput.AIBehaviourType.CustomSeek; stateMethodParameters.aiBehaviourInputsList[1].targetPosition = stateMethodParameters.targetPosition; stateMethodParameters.aiBehaviourInputsList[1].weighting = 1f; } } /// /// The state method for the Docking state. /// /// private static void DockingState(AIStateMethodParameters stateMethodParameters) { float sqrPosDelta = (stateMethodParameters.shipControlModule.shipInstance.TransformPosition - stateMethodParameters.targetPosition).sqrMagnitude; float sqrRadius = stateMethodParameters.targetRadius * stateMethodParameters.targetRadius; // Priority #1: Obstacle avoidance // Only activates when outside of the target radius if (sqrPosDelta > sqrRadius) { stateMethodParameters.aiBehaviourInputsList[0].behaviourType = AIBehaviourInput.AIBehaviourType.CustomObstacleAvoidance; //stateMethodParameters.aiBehaviourInputsList[0].weighting = 1f; } // Priority #2: Docking stateMethodParameters.aiBehaviourInputsList[1].behaviourType = AIBehaviourInput.AIBehaviourType.CustomDock; stateMethodParameters.aiBehaviourInputsList[1].targetPosition = stateMethodParameters.targetPosition; stateMethodParameters.aiBehaviourInputsList[1].targetForwards = stateMethodParameters.targetRotation * Vector3.forward; stateMethodParameters.aiBehaviourInputsList[1].targetUp = stateMethodParameters.targetRotation * Vector3.up; stateMethodParameters.aiBehaviourInputsList[1].targetRadius = stateMethodParameters.targetRadius; stateMethodParameters.aiBehaviourInputsList[1].targetVelocity = stateMethodParameters.targetVelocity; stateMethodParameters.aiBehaviourInputsList[1].targetTime = stateMethodParameters.targetTime; stateMethodParameters.aiBehaviourInputsList[1].weighting = 1f; // If we reach the following conditions, set the state action as completed: // - Within target distance metres of target position // - Within target angular distance degrees of the target rotation float angleDelta = Quaternion.Angle(stateMethodParameters.shipControlModule.shipInstance.TransformRotation, stateMethodParameters.targetRotation); if (sqrPosDelta <= stateMethodParameters.targetDistance * stateMethodParameters.targetDistance && angleDelta <= stateMethodParameters.targetAngularDistance) { stateMethodParameters.shipAIInputModule.SetHasCompletedStateAction(true); } } /// /// The state method for the Strafing Run state. /// /// private static void StrafingRunState(AIStateMethodParameters stateMethodParameters) { // Priority #1: Obstacle avoidance stateMethodParameters.aiBehaviourInputsList[0].behaviourType = AIBehaviourInput.AIBehaviourType.CustomObstacleAvoidance; stateMethodParameters.aiBehaviourInputsList[0].weighting = 1f; // Priority #2: Evade the targeting regions of a list of ships int surfaceTurretsToEvadeCount = 0; if (stateMethodParameters.surfaceTurretsToEvade != null) { surfaceTurretsToEvadeCount = stateMethodParameters.surfaceTurretsToEvade.Count; // Limit surface turrets to evade to a maximum of 5 if (surfaceTurretsToEvadeCount > 5) { surfaceTurretsToEvadeCount = 5; } // Loop over all the ships to evade SurfaceTurretModule surfaceTurretToEvade; for (int i = 0; i < surfaceTurretsToEvadeCount; i++) { surfaceTurretToEvade = stateMethodParameters.surfaceTurretsToEvade[i]; if (surfaceTurretToEvade != null && (surfaceTurretToEvade.weapon.Health > 0f || !surfaceTurretToEvade.isDestroyOnNoHealth)) { // CURRENT VERSION: TODO FIX TRANSFORM FORWARD //stateMethodParameters.aiBehaviourInputsList[1 + i].behaviourType = AIBehaviourInput.AIBehaviourType.CustomUnblockCone; //stateMethodParameters.aiBehaviourInputsList[1 + i].targetPosition = stateMethodParameters.surfaceTurretsToEvade[i].TransformPosition; //stateMethodParameters.aiBehaviourInputsList[1 + i].targetForwards = stateMethodParameters.surfaceTurretsToEvade[i].TransformForward; //stateMethodParameters.aiBehaviourInputsList[1 + i].targetFOVAngle = 5f; //stateMethodParameters.aiBehaviourInputsList[1 + i].weighting = 0.2f; // What is this? //stateMethodParameters.aiBehaviourInputsList[1 + i].behaviourType = AIBehaviourInput.AIBehaviourType.CustomUnblockCylinder; //stateMethodParameters.aiBehaviourInputsList[1 + i].targetPosition = shipToEvade.TransformPosition; //stateMethodParameters.aiBehaviourInputsList[1 + i].targetForwards = shipToEvade.TransformForward; //stateMethodParameters.aiBehaviourInputsList[1 + i].targetRadius = stateMethodParameters.shipAIInputModule.shipRadius; //stateMethodParameters.aiBehaviourInputsList[1 + i].weighting = 0.2f; } } } // Get the direction and distance to the target position Vector3 fromTargetPositionVector = Vector3.zero; if (stateMethodParameters.targetLocation != null) { fromTargetPositionVector = stateMethodParameters.shipControlModule.shipInstance.TransformPosition - stateMethodParameters.targetLocation.position; } else { fromTargetPositionVector = stateMethodParameters.shipControlModule.shipInstance.TransformPosition - stateMethodParameters.targetPosition; } float distToTargetPosition = fromTargetPositionVector.magnitude; // Get the current state stage index int currentStateStageIndex = stateMethodParameters.shipAIInputModule.GetCurrentStateStageIndex(); if (currentStateStageIndex == 0) { // Stage 1: Going towards the target position // Priority #3: Move to a location / move to a position if (stateMethodParameters.targetLocation != null) { stateMethodParameters.aiBehaviourInputsList[surfaceTurretsToEvadeCount + 1].behaviourType = AIBehaviourInput.AIBehaviourType.CustomSeek; // Target position is moved towards our ship by the target radius, to prevent obstacle // avoidance from picking up the target object stateMethodParameters.aiBehaviourInputsList[surfaceTurretsToEvadeCount + 1].targetPosition = stateMethodParameters.targetLocation.position + (fromTargetPositionVector / distToTargetPosition * stateMethodParameters.targetRadius); stateMethodParameters.aiBehaviourInputsList[surfaceTurretsToEvadeCount + 1].useTargetingAccuracy = true; stateMethodParameters.aiBehaviourInputsList[surfaceTurretsToEvadeCount + 1].weighting = 1f; } else { stateMethodParameters.aiBehaviourInputsList[surfaceTurretsToEvadeCount + 1].behaviourType = AIBehaviourInput.AIBehaviourType.CustomSeek; // Target position is moved towards our ship by the target radius, to prevent obstacle // avoidance from picking up the target object stateMethodParameters.aiBehaviourInputsList[surfaceTurretsToEvadeCount + 1].targetPosition = stateMethodParameters.targetPosition + (fromTargetPositionVector / distToTargetPosition * stateMethodParameters.targetRadius); stateMethodParameters.aiBehaviourInputsList[surfaceTurretsToEvadeCount + 1].useTargetingAccuracy = true; stateMethodParameters.aiBehaviourInputsList[surfaceTurretsToEvadeCount + 1].weighting = 1f; } // If we get within the target radius, go to stage 2 if (distToTargetPosition < stateMethodParameters.targetRadius) { stateMethodParameters.shipAIInputModule.SetCurrentStateStageIndex(1); } } else if (currentStateStageIndex == 1) { // Stage 2: Going away from the target position // Priority #3: Move away from a location / move away from a position if (stateMethodParameters.targetLocation != null) { stateMethodParameters.aiBehaviourInputsList[surfaceTurretsToEvadeCount + 1].behaviourType = AIBehaviourInput.AIBehaviourType.CustomSeek; stateMethodParameters.aiBehaviourInputsList[surfaceTurretsToEvadeCount + 1].targetPosition = stateMethodParameters.targetLocation.position + Vector3.Reflect(-fromTargetPositionVector / distToTargetPosition * stateMethodParameters.targetRadius * 2f, Vector3.up); stateMethodParameters.aiBehaviourInputsList[surfaceTurretsToEvadeCount + 1].weighting = 1f; } else { stateMethodParameters.aiBehaviourInputsList[surfaceTurretsToEvadeCount + 1].behaviourType = AIBehaviourInput.AIBehaviourType.CustomFlee; stateMethodParameters.aiBehaviourInputsList[surfaceTurretsToEvadeCount + 1].targetPosition = stateMethodParameters.targetPosition + Vector3.Reflect(-fromTargetPositionVector / distToTargetPosition * stateMethodParameters.targetRadius * 2f, Vector3.up); stateMethodParameters.aiBehaviourInputsList[surfaceTurretsToEvadeCount + 1].weighting = 1f; } // If we get outside the target radius, set the state action as completed if (distToTargetPosition > stateMethodParameters.targetRadius) { stateMethodParameters.shipAIInputModule.SetHasCompletedStateAction(true); } } } #endregion #region Public Static API Methods public static void Initialise () { if (isInitialised || isInitialising) { return; } else { isInitialising = true; // Create the AI States List with the number of elements equal to the number of predefined states aiStatesList = new List(5); // Add the predefined states AddState("Idle", IdleState, BehaviourCombiner.PriorityOnly); AddState("Move To", MoveToState, BehaviourCombiner.PrioritisedDithering); AddState("Dogfight", DogfightState, BehaviourCombiner.PriorityOnly); AddState("Docking", DockingState, BehaviourCombiner.PriorityOnly); AddState("Strafing Run", StrafingRunState, BehaviourCombiner.PrioritisedDithering); isInitialising = false; isInitialised = true; } } /// /// Adds a new AI state with a given name and state method. Returns the ID of the new state. /// /// /// /// public static int AddState (string newStateName, ShipAIInputModule.CallbackStateMethod newStateMethod, BehaviourCombiner newStateBehaviourCombiner = BehaviourCombiner.PriorityOnly) { // Allow AddState to be called during Initialise() if (isInitialised || isInitialising) { // Create a new state with the given name and method // The ID is set to what its index in the list is going to be AIState newAIState = new AIState(newStateName, aiStatesList.Count, newStateMethod, newStateBehaviourCombiner); // Add the state to the list aiStatesList.Add(newAIState); // Return the ID of the newly created state return newAIState.id; } else { #if UNITY_EDITOR Debug.LogWarning("AIState.AddState - AIState is not initialised. Please call AIState.Initialise() first."); #endif return -1; } } /// /// Returns the AI state with the corresponding state ID. /// /// /// public static AIState GetState (int stateID) { // First check that the supplied ID is valid if (stateID >= 0 && stateID < aiStatesList.Count) { return aiStatesList[stateID]; } else { return null; } } /// /// Attempts to return the state name. Use with caution as this /// will generate GC. When comparing in code, use: /// if (stateID == AIState.moveToState) etc instead to avoid GC. /// /// /// public static string GetStateName (int stateID) { AIState aiState = GetState(stateID); if (aiState != null) { return string.IsNullOrEmpty(aiState.name) ? "Unnamed State" : aiState.name; } else { return "State is null"; } } #endregion #region Public Member API Methods public override string ToString() { return name + " id: " + id.ToString(); } #endregion } }