using System.Collections; using System.Collections.Generic; using UnityEngine; using EmeraldAI.Utility; namespace EmeraldAI { /// /// This script handles all of Emerald AI's behaviors and states. Most functions can be overridden to create custom behaviors or functionality. /// [System.Serializable] [HelpURL("https://black-horizon-studios.gitbook.io/emerald-ai-wiki/emerald-components-required/behaviors-component")] public class EmeraldBehaviors : MonoBehaviour { #region Behavior Variables protected EmeraldSystem EmeraldComponent; public enum BehaviorTypes { Passive = 0, Coward = 1, Aggressive = 2}; public BehaviorTypes CurrentBehaviorType = BehaviorTypes.Aggressive; public Transform TargetToFollow; public int CautiousSeconds = 0; public bool InfititeChase; public int ChaseSeconds = 5; public int FleeSeconds = 5; public bool RequireObstruction; public int PercentToFlee = 20; public float UpdateFleePositionSeconds = 1.5f; public int MaxDistanceFromStartingArea = 30; public float FollowingStoppingDistance = 2f; public bool IsAiming; public delegate void StartFleeHandler(); public event StartFleeHandler OnFlee; public YesOrNo FleeOnLowHealth = YesOrNo.No; public YesOrNo StayNearStartingArea = YesOrNo.No; /// /// A timer used for tracking how long an AI is in the cautious state. /// protected float CautiousTimer; /// /// A timer used for controlling how often flee positions are updated. /// protected float UpdateFleePositionTimer; /// /// A timer used for tracking how long a target is outside of an AI's detection radius. /// protected float GiveUpTimer; /// /// A timer used for tracking the cooldown length of an AI's attacks. /// protected float AttackTimer; /// /// A string used for tracking an AI's current behavior state. This is a string so it can be customized as needed, given that a behvaior has multiple stages or states. /// public string BehaviorState = "Non Combat"; #endregion #region Editor Variables [HideInInspector] public bool HideSettingsFoldout; [HideInInspector] public bool BehaviorSettingsFoldout; [HideInInspector] public bool CustomSettingsFoldout; #endregion public virtual void Start() { InitailizeBehaviors(); } /// /// Initialize the Behavior Component. /// public virtual void InitailizeBehaviors () { EmeraldComponent = GetComponent(); ResetState(); if (TargetToFollow != null) { if (!TargetToFollow.gameObject.activeSelf) { Debug.LogError("The '" + gameObject.name + "' AI's Follower Target '" + TargetToFollow.name + "' is disabled so it has been removed as the AI's follower. You can enable said gameobject or use the SetFollowerTarget(Transform) API to assign a follower through code if needed."); TargetToFollow = null; } else { StartCoroutine(SetFollowerTargetInternal()); //Use a slight delay to ensure all other components have been initialized before assigning the AI's Target to Follow. } } EmeraldComponent.DetectionComponent.OnEnemyTargetDetected += OnDetectTarget; EmeraldComponent.DetectionComponent.OnNullTarget += ResetState; EmeraldComponent.CombatComponent.OnKilledTarget += OnKilledTarget; EmeraldComponent.HealthComponent.OnTakeDamage += OnTakeDamage; if (CurrentBehaviorType == BehaviorTypes.Passive) { if (!gameObject.CompareTag("Untagged")) { gameObject.tag = "Untagged"; } if (gameObject.layer != 0) { gameObject.layer = 0; } } } /// /// Use a slight delay to ensure all other components have been initialized before assigning the AI's Target to Follow. /// IEnumerator SetFollowerTargetInternal () { yield return new WaitForSeconds(0.1f); EmeraldComponent.DetectionComponent.SetTargetToFollow(TargetToFollow); } /// /// Continiously updates the BehaviorObject. This acts like an Update function that can run within this behavior using the information from the passed EmeraldComponent and its /// public virtual void BehaviorUpdate() { if (EmeraldComponent.AnimationComponent.IsDead) return; switch (BehaviorState) { case "Non Combat": WanderBehavior(); break; case "Cautious": CautiousBehavior(); break; case "Aggressive": AggressiveBehavior(); break; case "Flee": CowardBehavior(); break; } //Update the DetectTargetTracker virtual method (which tracks when targets are within the detection radius and clears them when needed) //This can be overridden if these mechanics need to be customized. DetectTargetTracker(); } /// /// Play a warning animation and look at the current target. If the CautiousSeconds are met, change the state to be aggressive. /// public virtual void CautiousBehavior() { if (EmeraldComponent.CombatTarget && EmeraldComponent.CombatComponent.CombatState && EmeraldComponent.MovementComponent.AIAgentActive && CurrentBehaviorType != BehaviorTypes.Passive) { //Bypass the cautious timer if the AI has a follower target. if (CurrentBehaviorType == BehaviorTypes.Aggressive && EmeraldComponent.TargetToFollow) BehaviorState = "Aggressive"; CautiousTimer += Time.deltaTime; if (CautiousTimer >= CautiousSeconds) { if (CurrentBehaviorType == BehaviorTypes.Aggressive) { BehaviorState = "Aggressive"; } else if (CurrentBehaviorType == BehaviorTypes.Coward) { OnFlee?.Invoke(); //Invoke the OnFlee delegate. BehaviorState = "Flee"; } CautiousTimer = 0; } if (CautiousTimer > 2) { EmeraldComponent.AnimationComponent.PlayWarningAnimation(); } } } /// /// Actively chase and attack the current target. /// public virtual void AggressiveBehavior() { if (EmeraldComponent.CombatComponent.CombatState && EmeraldComponent.MovementComponent.AIAgentActive && EmeraldComponent.CombatTarget) { //Only attempt to chase and attack the current target if a path exists to them. bool CanReachTarget = EmeraldComponent.MovementComponent.CanReachTarget; //Use Emerald AI's built-in Combat Movement (which simply sets the AI's destination equal to the current target's position). A custom function can be used for added functionality, if desired. //This will also backup the AI if they get too close to their target. if (!EmeraldComponent.MovementComponent.DefaultMovementPaused) { EmeraldComponent.MovementComponent.CombatMovement(); } else if (!EmeraldComponent.MovementComponent.DefaultMovementPaused && CanReachTarget) { EmeraldComponent.m_NavMeshAgent.SetDestination(transform.position + transform.forward * 2); } //Updates an AI's list of Combat Actions, but only while in the Aggressive state (Combat State). //Don't allow Combat Actions to update for 2 seconds after being enabled or spawned to avoid them triggering before the AI is fully initialized. if (Time.time - EmeraldComponent.TimeSinceEnabled > 2f) EmeraldComponent.CombatComponent.UpdateActions(); Attack(); //Continuously check to see if the conditions are right to trigger an attack, given the AI is in the Aggressive State. //If FleeOnLowHealth is enabled, and the AI's health reaches the threshold, set the AI's BehaviorState to Flee. if (FleeOnLowHealth == YesOrNo.Yes && ((float)EmeraldComponent.HealthComponent.CurrentHealth / (float)EmeraldComponent.HealthComponent.StartingHealth) < (PercentToFlee * 0.01f)) { EmeraldComponent.AnimationComponent.ResetTriggers(0); OnFlee?.Invoke(); //Invoke the OnFlee delegate. BehaviorState = "Flee"; } } } /// /// Use Emerald AI's built-in Flee Movement (which simply generates a destination opposite to the current target's position). This is updated based /// on the UpdateFleePositionSeconds or if the AI gets close the generated waypoint. A custom function can be used for added functionality, if desired. /// public virtual void CowardBehavior() { if (!EmeraldComponent.MovementComponent.DefaultMovementPaused) { UpdateFleePositionTimer += Time.deltaTime; if (UpdateFleePositionTimer > UpdateFleePositionSeconds || EmeraldComponent.m_NavMeshAgent.remainingDistance <= EmeraldComponent.MovementComponent.StoppingDistance) { EmeraldComponent.MovementComponent.FleeMovement(); UpdateFleePositionTimer = 0; } } } /// /// Use Emerald AI's built-in Wandering when not in combat (based off of the user set WanderType within the Emerald AI Movement Editor) /// public virtual void WanderBehavior() { if (EmeraldComponent.MovementComponent.AIAgentActive && !EmeraldComponent.CombatComponent.CombatState && !EmeraldComponent.CombatComponent.DeathDelayActive) { if (!EmeraldComponent.TargetToFollow) { EmeraldComponent.MovementComponent.Wander(); } else { EmeraldComponent.MovementComponent.FollowCompanionTarget(FollowingStoppingDistance); } } } /// /// Used for tracking when a target is outside of an AI's detection radius. /// public virtual void DetectTargetTracker() { if (!EmeraldComponent.CombatComponent.CombatState || EmeraldComponent.CombatComponent.DeathDelayActive || InfititeChase || EmeraldComponent.TargetToFollow) return; //Track how long a target is outside of the AI's detection radius. If the time is exceeded, give up on the target and set the AI to its defaul state. if (EmeraldComponent.CombatComponent.DistanceFromTarget > EmeraldComponent.DetectionComponent.DetectionRadius && !RequireObstruction || RequireObstruction && EmeraldComponent.DetectionComponent.TargetObstructed) { GiveUpTimer += Time.deltaTime; if (GiveUpTimer >= ChaseSeconds && CurrentBehaviorType == BehaviorTypes.Aggressive || GiveUpTimer >= FleeSeconds && CurrentBehaviorType == BehaviorTypes.Coward || BehaviorState == "Cautious") { CancelCombat(); //Stops the AI from fighting and chasing, or fleeing from, its current target. } } else { GiveUpTimer = 0; } //Used for tracking when an AI's distance from its starting position is exceeded. if (CurrentBehaviorType == BehaviorTypes.Aggressive && StayNearStartingArea == YesOrNo.Yes && Vector3.Distance(EmeraldComponent.MovementComponent.StartingDestination, transform.position) > MaxDistanceFromStartingArea) { EmeraldComponent.MovementComponent.EnableReturnToStart(); //Returns the AI to its starting area CancelCombat(); //Stops the AI from fighting and chasing its current target } } /// /// Continuously check to see if the conditions are right to trigger an attack, given the AI is in the Aggressive State. This is a priority state. /// public virtual void Attack() { var EnterConditions = EmeraldComponent.AnimationComponent.IsIdling || EmeraldComponent.AnimationComponent.IsMoving; var CooldownConditions = EmeraldComponent.AnimationComponent.IsIdling || EmeraldComponent.AnimationComponent.IsMoving || EmeraldComponent.AnimationComponent.IsBackingUp || EmeraldComponent.AnimationComponent.IsTurningLeft || EmeraldComponent.AnimationComponent.IsTurningRight || EmeraldComponent.AnimationComponent.IsGettingHit; if (CooldownConditions) AttackTimer += Time.deltaTime; if (EmeraldCombatManager.AllowedToAttack(EmeraldComponent) && EnterConditions && !IsAiming && AttackTimer >= EmeraldComponent.CombatComponent.CurrentAttackCooldown) { EmeraldCombatManager.CheckAttackHeight(EmeraldComponent); if (!EmeraldComponent.CombatComponent.TargetOutOfHeightRange) { EmeraldComponent.AnimationComponent.IsMoving = false; EmeraldComponent.CombatComponent.AdjustCooldowns(); EmeraldComponent.CombatComponent.AttackPosition = EmeraldComponent.CombatTarget.position - transform.position; EmeraldComponent.CombatComponent.AttackPosition.y = 0; EmeraldComponent.AnimationComponent.PlayAttackAnimation(); AttackTimer = 0; } else { AttackTimer = 0; } } //Cancel the attack if it's triggered and the target is out of range if (AttackTimer >= EmeraldComponent.CombatComponent.CurrentAttackCooldown) { if (EmeraldComponent.m_NavMeshAgent.remainingDistance > EmeraldComponent.m_NavMeshAgent.stoppingDistance && EmeraldComponent.AIAnimator.GetBool("Attack")) { EmeraldComponent.AIAnimator.ResetTrigger("Attack"); AttackTimer = 0; } } } /// /// Stops the AI from fighting and chasing, or fleeing from, its current target. /// public virtual void CancelCombat() { EmeraldComponent.CombatComponent.ClearTarget(); EmeraldComponent.CombatComponent.DeathDelayActive = true; EmeraldComponent.m_NavMeshAgent.SetDestination(transform.position + EmeraldComponent.transform.forward * (EmeraldComponent.MovementComponent.StoppingDistance * 1.5f)); EmeraldComponent.AnimationComponent.ResetTriggers(0); ResetState(); } /// /// Reset the settings back to their default values. /// public virtual void ResetState () { BehaviorState = "Non Combat"; CautiousTimer = 0; UpdateFleePositionTimer = 0; GiveUpTimer = 0; AttackTimer = 0; } /// /// When killing a target, return the BehaviorState back to Non Combat. If another target is found, it will be updated. /// public virtual void OnKilledTarget() { BehaviorState = "Non Combat"; GiveUpTimer = 0; EmeraldComponent.AnimationComponent.WarningAnimationTriggered = false; } /// /// When detecting a target, update the AI's current destination to be equal to their current position until the combat movement code can take over. /// public virtual void OnDetectTarget() { BehaviorState = "Cautious"; if (isActiveAndEnabled) EmeraldComponent.m_NavMeshAgent.SetDestination(transform.position); } /// /// If an AI takes damage before its Cautious State has finished, change the BehaviorState so it can handle its attack according to its behavior. /// public virtual void OnTakeDamage() { if (BehaviorState == "Cautious" && CurrentBehaviorType == BehaviorTypes.Aggressive) BehaviorState = "Aggressive"; else if (BehaviorState == "Cautious" && CurrentBehaviorType == BehaviorTypes.Coward) BehaviorState = "Flee"; } } }