using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Linq; using static UnityEngine.GraphicsBuffer; namespace EmeraldAI { [HelpURL("https://black-horizon-studios.gitbook.io/emerald-ai-wiki/emerald-components-required/detection-component")] public class EmeraldDetection : MonoBehaviour, IFaction { #region Detection Variables public List IgnoredColliders = new List(); public static LayerMask LBDLayers; public Transform CurrentObstruction; public Transform HeadTransform; public float DetectionFrequency = 1; public LayerMask DetectionLayerMask = 3; public LayerMask ObstructionDetectionLayerMask = 4; LayerMask InternalObstructionLayerMask = 4; public string PlayerTag = "Player"; public float ObstructionDetectionFrequency = 0.1f; public float ObstructionDetectionUpdateTimer; public float ObstructionSeconds = 1.5f; public int StartingDetectionRadius; public int DetectionRadius = 18; public int StartingChaseDistance; public int FieldOfViewAngle = 270; public int StartingFieldOfViewAngle; public enum DetectionStates { Alert = 0, Unaware = 1 }; public DetectionStates CurrentDetectionState = DetectionStates.Unaware; public PickTargetTypes PickTargetType = PickTargetTypes.Closest; public bool TargetObstructed = false; public enum ObstructedTypes { AI, Other, None }; public ObstructedTypes ObstructionType = ObstructedTypes.None; public List LineOfSightTargets = new List(); public List CurrentFollowers = new List(); public delegate void OnDetectTargetHandler(); public event OnDetectTargetHandler OnDetectionUpdate; public delegate void OnEnemyTargetDetectedHandler(); public event OnEnemyTargetDetectedHandler OnEnemyTargetDetected; public delegate void OnNullTargetHandler(); public event OnNullTargetHandler OnNullTarget; public delegate void OnPlayerDetectedHandler(); public event OnPlayerDetectedHandler OnPlayerDetected; public static List IgnoredTargetsList = new List(); #endregion #region Faction Variables [SerializeField] public int CurrentFaction; public static EmeraldFactionData FactionData; [SerializeField] public static List StringFactionList = new List(); public List FactionRelations = new List(); [SerializeField] public List FactionRelationsList = new List(); [SerializeField] public List AIFactionsList = new List(); #endregion #region Editor Specific Variables public bool HideSettingsFoldout; public bool DetectionFoldout; public bool TagFoldout; public bool FactionFoldout; #endregion #region Private Variables float DetectionTimer; Vector3 TargetDirection; float ObstructionTimer; EmeraldSystem EmeraldComponent; #endregion void Start() { InitializeDetection(); Invoke(nameof(InitializeLayers), 0.1f); } /// /// Copy the layers from the ObstructionDetectionLayerMask. This also adds the AI's internal /// collider layer to its layers so its own colliders don't cause a false obstruction. /// void InitializeLayers () { InternalObstructionLayerMask = ObstructionDetectionLayerMask; for (int i = 0; i < 32; i++) { if (LBDLayers == (LBDLayers | (1 << i))) { InternalObstructionLayerMask |= (1 << i); } } } /// /// Initialize all detection related settings. /// void InitializeDetection () { EmeraldComponent = GetComponent(); EmeraldComponent.CombatComponent.OnExitCombat += ReturnToDefaultState; //Subscribe the ReturnToDefaultState function to the OnExitCombat delegate EmeraldComponent.HealthComponent.OnDeath += ClearTargetToFollow; //Subscribe the RemoveTargetToFollow function to the OnDeath delegate OnNullTarget += NullNonCombatTarget; //Subscribe the NullNonCombatTarget function to the OnNullTarget delegate if (FactionData == null) FactionData = Resources.Load("Faction Data") as EmeraldFactionData; if (EmeraldComponent.LBDComponent == null) Utility.EmeraldCombatManager.DisableRagdoll(EmeraldComponent); StartingDetectionRadius = DetectionRadius; TargetObstructed = true; StartingFieldOfViewAngle = FieldOfViewAngle; StartingDetectionRadius = DetectionRadius; //If the user forgot to add a head transform, create a temporary one to avoid an error and still allow the AI to function. if (HeadTransform == null) { Transform TempHeadTransform = new GameObject("AI Head Transform").transform; TempHeadTransform.SetParent(transform); TempHeadTransform.localPosition = new Vector3(0, 1, 0); HeadTransform = TempHeadTransform; } SetupFactions(); Invoke(nameof(CheckFactionRelations), 0.1f); } /// /// Used during initialization to check and notify the user if an AI has its own faction as an Enemy Relation. /// void CheckFactionRelations() { if (AIFactionsList.Contains(CurrentFaction) && FactionRelations[AIFactionsList.IndexOf(CurrentFaction)] == 0) { Debug.LogError("The AI '" + gameObject.name + "' contains an Enemy Faction Relation of its own Faction '" + GetTargetFactionName(transform) + "'. Please remove the faction from the AI Faction Relation List (within the AI's Detection Component) or change it to Friendly to avoid incorrect target detection."); } } /// /// Sets up the Factions to be used during runtime. /// void SetupFactions() { for (int i = 0; i < FactionRelationsList.Count; i++) { AIFactionsList.Add(FactionRelationsList[i].FactionIndex); FactionRelations.Add((int)FactionRelationsList[i].RelationType); } } void FixedUpdate() { if (EmeraldComponent.BehaviorsComponent.CurrentBehaviorType == EmeraldBehaviors.BehaviorTypes.Passive) return; //Don't allow passive AI to use line of sight if (!EmeraldComponent.CombatComponent.CombatState || EmeraldComponent.CombatComponent.DeathDelayActive) { LineOfSightDetection(); } } /// /// A custom update function for the EmeraldDetection script called through the EmeraldAISystem script. /// public void DetectionUpdate() { if (EmeraldComponent.CombatComponent.CombatState) CheckForObstructions(EmeraldComponent.CombatTarget); //When in combat, check for obstructions by casting a ray from the AI's Head Transform to its target. else if (!EmeraldComponent.CombatComponent.CombatState) CheckForObstructions(EmeraldComponent.LookAtTarget); //When in not in combat, check for obstructions by casting a ray from the AI's Head Transform to its look at target. //Update the AI's OverlapShere function based on the DetectionFrequency if (EmeraldComponent.CombatComponent.TargetDetectionActive && !EmeraldComponent.MovementComponent.ReturningToStartInProgress) { DetectionTimer += Time.deltaTime; if (DetectionTimer >= DetectionFrequency) { UpdateAIDetection(); //Casts a Physics.OverlapSphere and only searches for layers based on the user set DetectionLayerMask. LookAtTargetDistanceCheck(); //Check that the LookAtTarget is within the AI's DetectionRadius. OnDetectionUpdate?.Invoke(); //Invoke the OnDetectionUpdate event. DetectionTimer = 0; } } CheckForNullTarget(); //Monitors the AI's TargetSource to see if it becomes null. If it does, invoke the OnNullTarget callback. CheckLookAtTarget(); //Monitors the AI's LookAtTarget to see if its health reaches 0. If it does, clear the LookAtTarget information. ObstructionAction(); //Controls what happens depending on if the AI is obstructed by another AI or by something else, while in combat. } /// /// Monitors the AI's TargetSource to see if it becomes null. If it does, invoke the OnNullTarget callback. /// void CheckForNullTarget() { if (EmeraldComponent.CurrentTargetInfo.TargetSource == null && EmeraldComponent.CurrentTargetInfo.CurrentICombat != null) { OnNullTarget?.Invoke(); } } /// /// Called through the OnNullTarget callback when a target becomes null. If this happens, clear all non-combat targets. /// void NullNonCombatTarget () { if (!EmeraldComponent.CombatComponent.CombatState) { EmeraldComponent.TargetToFollow = null; EmeraldComponent.LookAtTarget = null; EmeraldComponent.CurrentTargetInfo.TargetSource = null; EmeraldComponent.CurrentTargetInfo.CurrentIDamageable = null; EmeraldComponent.CurrentTargetInfo.CurrentICombat = null; } } /// /// Monitors the AI's LookAtTarget to see if its health reaches 0. If it does, clear the LookAtTarget information. /// void CheckLookAtTarget () { if (EmeraldComponent.LookAtTarget && !EmeraldComponent.CombatComponent.CombatState && EmeraldComponent.CurrentTargetInfo.CurrentIDamageable.Health <= 0) { EmeraldComponent.LookAtTarget = null; EmeraldComponent.CurrentTargetInfo.CurrentIDamageable = null; EmeraldComponent.CurrentTargetInfo.CurrentICombat = null; } } /// /// Controls what happens depending on if the AI is obstructed by another AI or by something else, while in combat. /// void ObstructionAction () { if (TargetObstructed && EmeraldComponent.CombatComponent.CombatState && EmeraldComponent.MovementComponent.CanReachTarget) { ObstructionTimer += Time.deltaTime; if (ObstructionTimer >= ObstructionSeconds) { if (ObstructionType == ObstructedTypes.AI) { SearchForTarget(PickTargetTypes.Random); } else if (ObstructionType == ObstructedTypes.Other) { EmeraldComponent.m_NavMeshAgent.stoppingDistance = 3; EmeraldAPI.Internal.GenerateRandomPositionWithinRadius(EmeraldComponent); } ObstructionTimer = 0; } } else if (!TargetObstructed && EmeraldComponent.CombatComponent.CombatState && !EmeraldComponent.AnimationComponent.IsBackingUp) { EmeraldComponent.m_NavMeshAgent.stoppingDistance = EmeraldComponent.CombatComponent.AttackDistance; } } /// /// Casts a Physics.OverlapSphere and only searches for layers based on the user set DetectionLayerMask. /// public void UpdateAIDetection() { if (LineOfSightTargets.Count > 0) LineOfSightTargetsDistanceCheck(); Collider[] CurrentlyDetectedTargets = Physics.OverlapSphere(transform.position, DetectionRadius, DetectionLayerMask); foreach (Collider C in CurrentlyDetectedTargets) { if (C.gameObject != this.gameObject && IsValidTarget(C.transform)) { DetectTarget(C.transform); } } } /// /// Assigns a target based on the passed parameter and an AI's settings. Store the target within LineOfSightTargets to be used elsewhere. /// void DetectTarget(Transform Target) { if (IgnoredTargetsList.Contains(Target)) return; if (Target != EmeraldComponent.TargetToFollow && !CurrentFollowers.Contains(Target) && IsEnemyTarget(Target) && EmeraldComponent.BehaviorsComponent.CurrentBehaviorType != EmeraldBehaviors.BehaviorTypes.Passive) { CurrentDetectionState = DetectionStates.Alert; if (!LineOfSightTargets.Contains(Target.GetComponent())) LineOfSightTargets.Add(Target.GetComponent()); } if (EmeraldComponent.LookAtTarget == null && EmeraldComponent.CombatTarget == null) { if (IsLookAtTarget(Target)) { EmeraldComponent.LookAtTarget = Target; GetTargetInfo(EmeraldComponent.LookAtTarget); OnPlayerDetected?.Invoke(); } } } /// /// Calculates the AI's line of sight mechanics. For each target that is within the AI's LineOfSightTargets, cast a raycast. If a target is unobstructed, and within the AI's line of sight angle, call the SearchForTarget function. /// void LineOfSightDetection () { if (CurrentDetectionState == DetectionStates.Alert && EmeraldComponent.CombatTarget == null && !EmeraldComponent.AnimationComponent.IsDead) { for (int i = LineOfSightTargets.Count - 1; i >= 0; --i) { if (LineOfSightTargets[i] == null) { LineOfSightTargets.RemoveAt(i); } else { Vector3 direction = LineOfSightTargets[i].bounds.center - HeadTransform.position; float angle = Vector3.Angle(new Vector3(direction.x, 0, direction.z), transform.forward); //Only check targets that are within the AI's line of sight. if (angle < FieldOfViewAngle * 0.5f) { if (!EmeraldComponent.CombatComponent.CombatState) { RaycastHit hit; //Use a special layer mask that also includes the layers of internal colliders (from the LDB component) as these can block the AI's line of sight. if (Physics.Raycast(HeadTransform.position, direction, out hit, DetectionRadius, ~InternalObstructionLayerMask)) { if (hit.collider != null && LineOfSightTargets.Contains(hit.collider)) { SearchForTarget(PickTargetType); break; } } } else if (EmeraldComponent.CombatComponent.CombatState) { SearchForTarget(PickTargetType); } } } } } } /// /// Return all currently visible targets within the AI's detection radius. /// public List GetVisibleTargets() { List VisibleTargets = new List(); foreach (Collider C in LineOfSightTargets.ToArray()) { RaycastHit hit; Vector3 direction = C.bounds.center - HeadTransform.position; if (Physics.Raycast(HeadTransform.position, direction, out hit, DetectionRadius, ~InternalObstructionLayerMask)) { if (hit.collider != null && LineOfSightTargets.Contains(hit.collider)) { if (!VisibleTargets.Contains(hit.collider.transform) && EmeraldComponent.CombatTarget != hit.collider.transform || hit.collider.CompareTag("Player")) { VisibleTargets.Add(hit.collider.transform); } } } } return VisibleTargets; } /// /// Searches for a currently visible target within the LineOfSightTargets list using passed PickTargetType. This can be assigned using EmeraldDetection.PickTargetTypes. /// public void SearchForTarget (PickTargetTypes pickTargetType) { List VisibleTargets = GetVisibleTargets(); if (EmeraldComponent.CombatTarget != null) VisibleTargets.Remove(EmeraldComponent.CombatTarget); //Remove the current target so it isn't picked again if (VisibleTargets.Count > 0) { if (pickTargetType == PickTargetTypes.Closest) { VisibleTargets = VisibleTargets.OrderBy(Target => (Target.position - transform.position).sqrMagnitude).ToList(); SetDetectedTarget(VisibleTargets[0]); } else if (pickTargetType == PickTargetTypes.Random) { SetDetectedTarget(VisibleTargets[Random.Range(0, VisibleTargets.Count)]); } else if (pickTargetType == PickTargetTypes.FirstDetected) { SetDetectedTarget(VisibleTargets[0]); } } } /// /// Check for obstructions by casting a ray from the AI's Head Transform to its target. /// void CheckForObstructions (Transform TargetSource) { ObstructionDetectionUpdateTimer += Time.deltaTime; if (ObstructionDetectionUpdateTimer >= ObstructionDetectionFrequency && TargetSource != null && EmeraldComponent.CurrentTargetInfo.CurrentICombat != null) { TargetDirection = EmeraldComponent.CurrentTargetInfo.CurrentICombat.DamagePosition() - HeadTransform.position; RaycastHit hit; //Check for obstructions and incrementally lower our AI's stopping distance until one is found. If none are found when the distance has reached 5 or below, search for a new target to see if there is a better option if (Physics.Raycast(HeadTransform.position, (TargetDirection), out hit, EmeraldComponent.CombatComponent.DistanceFromTarget, ~InternalObstructionLayerMask)) { if (!hit.collider.transform.IsChildOf(TargetSource) && !hit.collider.transform.IsChildOf(this.transform) && hit.collider.transform != TargetSource && !IgnoredColliders.Contains(hit.collider)) { //Set the ObstructionType so different actions can be taken when an AI's line of sight becomes obstructed. if ((LBDLayers & (1 << hit.collider.gameObject.layer)) != 0 || (DetectionLayerMask & (1 << hit.collider.gameObject.layer)) != 0) { ObstructionType = ObstructedTypes.AI; } else { ObstructionType = ObstructedTypes.Other; } EmeraldComponent.AIAnimator.ResetTrigger("Attack"); TargetObstructed = true; CurrentObstruction = hit.collider.transform; } else { TargetObstructed = false; CurrentObstruction = null; ObstructionType = ObstructedTypes.None; } } else { TargetObstructed = false; CurrentObstruction = null; ObstructionType = ObstructedTypes.None; } ObstructionDetectionUpdateTimer = 0; } } /// /// Detects the passed target's Target Type and assigns it as the AI's current target. /// public void SetDetectedTarget (Transform DetectedTarget) { //Don't assign the newly detected target if it's the same as the current target. if (EmeraldComponent.CombatTarget == DetectedTarget) return; EmeraldAI.Utility.EmeraldCombatManager.ActivateCombatState(EmeraldComponent); //Active the Combat State ResetDetectionValues(); //Once a target has been found, reset some of its settings back to their defaults. GetTargetInfo(DetectedTarget); EmeraldComponent.CombatTarget = DetectedTarget; OnEnemyTargetDetected?.Invoke(); //Invoke the OnEnemyTargetDetected when an enemy target has been found. } /// /// Once a target has been found, reset some of its settings back to their defaults. /// void ResetDetectionValues () { DetectionRadius = StartingDetectionRadius; FieldOfViewAngle = StartingFieldOfViewAngle; EmeraldComponent.m_NavMeshAgent.stoppingDistance = EmeraldComponent.CombatComponent.AttackDistance; EmeraldComponent.AnimationComponent.IsTurning = false; EmeraldComponent.CombatComponent.DeathDelayActive = false; EmeraldComponent.CombatComponent.DeathDelayTimer = 0; } /// /// Get the target's info (from the passed Target parameter). /// public void GetTargetInfo (Transform Target, bool? OverrideFactionRequirement = false) { if (Target != null) { EmeraldComponent.CurrentTargetInfo.TargetSource = Target; EmeraldComponent.CurrentTargetInfo.CurrentIDamageable = Target.GetComponent(); EmeraldComponent.CurrentTargetInfo.CurrentICombat = Target.GetComponent(); } } /// /// Check that each LineOfSightTarget is within the AI's DetectionRadius. If not, remove it from the list. /// void LineOfSightTargetsDistanceCheck() { for (int i = 0; i < LineOfSightTargets.Count; i++) { //Remove any targets that become null during the distance check. if (LineOfSightTargets[i] == null) { LineOfSightTargets.RemoveAt(i); } else { float distance = Vector3.Distance(LineOfSightTargets[i].transform.position, transform.position); //If the distance of the detected target is greater than the DetectionRadius, remove it from the LineOfSightTargets list. if (distance > DetectionRadius) LineOfSightTargets.Remove(LineOfSightTargets[i]); } } } /// /// Check that the LookAtTarget is within the AI's DetectionRadius. If not, remove it as the current LookAtTarget. /// void LookAtTargetDistanceCheck () { if (EmeraldComponent.LookAtTarget != null) { float distance = Vector3.Distance(EmeraldComponent.LookAtTarget.transform.position, transform.position); //If the distance of the detected LookAtTarget is greater than the DetectionRadius, remove it as the current LookAtTarget. if (distance > DetectionRadius) NullNonCombatTarget(); } } /// /// Return to the default state and assign the Look At Target info, if it's not null. This is called through the OnExitCombat callback. /// void ReturnToDefaultState () { CurrentDetectionState = DetectionStates.Unaware; if (EmeraldComponent.LookAtTarget != null) GetTargetInfo(EmeraldComponent.LookAtTarget); } /// /// Gets the Faction Relation name of the passed target and this AI in the form of a string (Enemy, Neutral, or Friendly). If a faction cannot be found, or if it is not a valid target, you will receive a value of Invalid Target. /// public string GetTargetFactionRelation (Transform Target) { return EmeraldAPI.Faction.GetTargetFactionRelation(EmeraldComponent, Target); } /// /// Gets the faction name of the passed AI target. /// public string GetTargetFactionName(Transform Target) { return EmeraldAPI.Faction.GetTargetFactionName(Target); } /// /// Assigns a new follow target for an AI to follow. /// public void SetTargetToFollow(Transform Target, bool CopyFactionData = true) { EmeraldSystem TargetEmeraldComponent = Target.GetComponent(); //Attempt to get the Target's EmeraldComponent if (TargetEmeraldComponent != null) { if (TargetEmeraldComponent.CombatTarget == transform) TargetEmeraldComponent.CombatComponent.ClearTarget(); //If the Target is another AI, clear its targets TargetEmeraldComponent.DetectionComponent.CurrentFollowers.Add(transform); //Add this AI as a follower of the leader AI if (TargetEmeraldComponent.CombatComponent.CombatState) TargetEmeraldComponent.CombatComponent.DeathDelayActive = true; //Copies the Target to Follow's Faction Data so it will react the same way the follower does to detected targets. if (CopyFactionData) { CurrentFaction = TargetEmeraldComponent.DetectionComponent.CurrentFaction; //Make the Current Faction the same as the AI's new Target to Follow AIFactionsList = TargetEmeraldComponent.DetectionComponent.AIFactionsList; //Make the Faction List the same as the AI's new Target to Follow FactionRelations = TargetEmeraldComponent.DetectionComponent.FactionRelations; //Make the Faction Relations the same as the AI's new Target to Follow FactionRelationsList = TargetEmeraldComponent.DetectionComponent.FactionRelationsList; //Make the Faction Relations List the same as the AI's new Target to Follow } } if (Target == EmeraldComponent.CombatTarget) EmeraldComponent.CombatComponent.ClearTarget(); if (EmeraldComponent.CombatComponent.CombatState) EmeraldComponent.CombatComponent.DeathDelayActive = true; //EmeraldComponent.m_NavMeshAgent.stoppingDistance = EmeraldComponent.MovementComponent.FollowingStoppingDistance; EmeraldComponent.TargetToFollow = Target; EmeraldComponent.BehaviorsComponent.TargetToFollow = Target; EmeraldComponent.BehaviorsComponent.ResetState(); EmeraldComponent.MovementComponent.CurrentMovementState = EmeraldMovement.MovementStates.Run; EmeraldComponent.MovementComponent.WanderType = EmeraldMovement.WanderTypes.Stationary; } /// /// Clears the AI's Target to Follow transform so it will be no longer following it. This will also stop the AI from being a Companion AI. /// public void ClearTargetToFollow() { //This is also called through the OnDeath callback and removes this AI as a follower of its Target to Follow. if (EmeraldComponent.TargetToFollow) { EmeraldSystem TargetEmeraldComponent = EmeraldComponent.TargetToFollow.GetComponent(); //Attempt to get this AI's Target to Follow if (TargetEmeraldComponent) { TargetEmeraldComponent.DetectionComponent.CurrentFollowers.Remove(transform); //Remove this AI as a follower of the leader AI } } EmeraldComponent.BehaviorsComponent.TargetToFollow = null; EmeraldComponent.TargetToFollow = null; if (!EmeraldComponent.AnimationComponent.IsDead) { EmeraldComponent.BehaviorsComponent.ResetState(); } } /// /// Returns true if the currently passed transform is an enemy target. /// bool IsEnemyTarget (Transform Target) { int ReceivedFaction = Target.GetComponent().GetFaction(); return AIFactionsList.Contains(ReceivedFaction) && FactionRelations[AIFactionsList.IndexOf(ReceivedFaction)] == 0; } /// /// Returns true if the currently passed transform is a valid look at target. /// bool IsLookAtTarget(Transform Target) { return Target.gameObject.CompareTag(PlayerTag) && GetTargetFactionRelation(Target) != "Enemy"; } /// /// Checks to see if the passed target is a valid player, AI, or non-AI target. /// bool IsValidTarget (Transform Target) { if (Target.GetComponent() != null) { return true; } else { Debug.Log("The " + Target.name + " object is set as a valid target (both Tag and Layer), but does not have a Faction Extension component on it. Please add one in order for this target to be properly detected."); return false; } } public int GetFaction() { return CurrentFaction; } } }