Actually Decent Enemy AI

Somewhat comprehensible 450 lines of text Made at 1 am midnight because funny most importantly, it works
mail@pastecode.io avatar
unknown
csharp
2 years ago
13 kB
11
Indexable
using UnityEngine;
using System.Collections;
using UnityEngine.AI;

public class OperatorAI : MonoBehaviour
{
    public Transform player;
    [SerializeField] Animator animator;
    public LayerMask groundMask, playerMask;

    [SerializeField] public NavMeshAgent navMeshAgent;
    [SerializeField] private enum AIState { Idle, Chase, Attack, Disorient, Alert }
    [SerializeField] private AIState currentState;
    [SerializeField] float chaseRange;
    [SerializeField] float attackRange;
    [SerializeField] float walkPointRange = 20;

    private float chaseRangeSquared;
    private float attackRangeSquared;
    [SerializeField] private bool chasingPlayer = false;

    [Header("State Vars")]
    [SerializeField] bool pathAvailable;
    NavMeshPath navMeshPath;

    [Header("Idle State Variables")]
    public Vector3 walkPoint;
    public Vector2 idleDelay = new Vector2(3.15f, 5);
    [SerializeField] bool walkPointSet, hasStopped;

    public bool hasSetWalkPoints = false;
    [SerializeField] Transform[] fixedWalkPoints;
    [SerializeField] int idleAnims = 1;

    public delegate void PlayerDetected();
    public event PlayerDetected OnPlayerDetected;
    private float disorientDuration;
    private float disorientTimer;
    private Vector3 lastKnownPlayerLocation;

    private State currentStateInstance;

//-------------------------------------------------------------------------------------
    #region  Main Stuff

    private void Awake()
    {
        navMeshPath = new NavMeshPath();
    }
    private void Start()
    {
        currentState = AIState.Idle;
        chaseRangeSquared = Mathf.Pow(chaseRange, 2f);
        attackRangeSquared = Mathf.Pow(attackRange, 2f);

        // Create instances of the states
        State idleState = new IdleState(this);
        State chaseState = new ChaseState(this);
        State attackState = new AttackState(this);
        State disorientState = new DisorientState(this);
        State alertState = new AlertState(this);

        // Set the initial state
        currentStateInstance = idleState;
        SetStateIdle();  //maybe setting it twice would work
    }
    private void Update()
    {
        currentStateInstance.Update();
    }
    private void LateUpdate()
    {   
        animator.SetBool("isWalking", navMeshAgent.velocity.magnitude >= 0.75f);
    }

    private void ChangeState(State newState)
    {
        if (currentStateInstance != null)
        {
            currentStateInstance.Exit();
        }

        currentStateInstance = newState;
        currentStateInstance.Enter();
    }

    private bool IsPlayerInRange(float rangeSquared)
    {
        // Calculate the squared distance between the enemy and the player
        Vector3 playerPosition = player.position;
        Vector3 enemyPosition = transform.position;
        float distanceSquared = (playerPosition - enemyPosition).sqrMagnitude;

        // Check if the squared distance is within the desired range
        return distanceSquared <= rangeSquared;
    }
    private bool IsPlayerInAttackRange()
    {
        // Calculate the distance between the enemy and the player
        float distance = Vector3.Distance(transform.position, player.position);

        // Check if the player is within the attack range
        return distance <= attackRange;
    }

    bool CalculateNewPath()
    {
        navMeshAgent.CalculatePath(walkPoint, navMeshPath);
        if (navMeshPath.status != NavMeshPathStatus.PathComplete) return false;  else return true;
    }

    public void StartDisorient(float duration)
    {
        if (currentState != AIState.Disorient)
        {
            currentState = AIState.Disorient;
            disorientDuration = duration;
            disorientTimer = Time.time + duration;
            StartCoroutine(DisorientCoroutine());
        }
    }

    private IEnumerator DisorientCoroutine()
    {
        while (Time.time < disorientTimer)
        {
            yield return null;
        }
        SetStateAlert();
    }

    void SearchWalkPoint()
    {
        if (hasSetWalkPoints)
        {
            int randomPoint = Random.Range(0, fixedWalkPoints.Length);
            walkPoint = fixedWalkPoints[randomPoint].position;
        }
        else
        {
            float randomZ = Random.Range(-walkPointRange, walkPointRange);
            float randomX = Random.Range(-walkPointRange, walkPointRange);
            walkPoint = new Vector3(transform.position.x + randomX, transform.position.y, transform.position.z + randomZ);
        }

        if (Physics.Raycast(walkPoint, -transform.up, 1.7f, groundMask))
        {
            walkPointSet = true;
        }

        if (CalculateNewPath() == true) pathAvailable = true; else pathAvailable = false;
    }

    public void SetStateIdle()
    {
        ChangeState(new IdleState(this));
    }

    public void SetStateChase()
    {
        ChangeState(new ChaseState(this));
    }

    public void SetStateAttack()
    {
        ChangeState(new AttackState(this));
    }

    public void SetStateDisorient()
    {
        ChangeState(new DisorientState(this));
    }

    public void SetStateAlert()
    {
        ChangeState(new AlertState(this));
    }

    public bool IsChasingPlayer()
    {
        return chasingPlayer;
    }

    public void StartChasingPlayer()
    {
        chasingPlayer = true;
    }

    public void StopChasingPlayer()
    {
        chasingPlayer = false;
    }


    #endregion
//=--=-=-=--------------------------------------=========================================----------
    #region State Classes

    private abstract class State
    {
        protected OperatorAI owner;

        public State(OperatorAI owner)
        {
            this.owner = owner;
        }

        public abstract void Enter();           // When state gets entered into
        public abstract void Update();          // While state runs
        public abstract void Exit();            // When state gets exited from
    }

//-----------------------------------------------------------------------------------------
// Idle
    private class IdleState : State
    {
        public IdleState(OperatorAI owner) : base(owner) { }

        public override void Enter()
        {
            Debug.Log("Idling");
        }

        public override void Update()
        {
            if (!owner.walkPointSet || !owner.pathAvailable) owner.SearchWalkPoint();

            Vector3 distToWalkPoint = owner.transform.position - owner.walkPoint;
            if (distToWalkPoint.magnitude <= 1f)
            {
                owner.walkPointSet = false;
            }

            // Transition to the chase state if the player is within a certain range
            if (owner.IsPlayerInRange(owner.chaseRangeSquared) && !owner.IsChasingPlayer())
            {
                // Trigger the player detection event
                owner.OnPlayerDetected?.Invoke();
                owner.SetStateChase();
            }

            if (owner.walkPointSet && owner.pathAvailable && !owner.hasStopped)
            {
                owner.hasStopped = true;
                owner.StartCoroutine(IdleToDestination(owner.walkPoint, owner.idleDelay.x, owner.idleDelay.y));
                return;
            }
        }

        IEnumerator IdleToDestination(Vector3 position, float delayTimeStart = 0f, float delayTimeEnd = 0f)
        {
            float _delay = Random.Range(delayTimeStart, delayTimeEnd);
            int _idle = Random.Range(0, owner.idleAnims);

            owner.animator.SetFloat("IdleAnim", _idle);
            yield return new WaitForSeconds(_delay);

            owner.navMeshAgent.SetDestination(position);
            owner.hasStopped = false;
            }

        public override void Exit()
        {
            // Perform exit behavior
        }
    }
//-----------------------------------------------------------------------------------------
// Chase
    private class ChaseState : State
    {
        public ChaseState(OperatorAI owner) : base(owner) { }

        public override void Enter()
        {
            // Perform enter behavior
            Debug.Log("Chasing");
            owner.StartChasingPlayer(); // Set the chasing flag
        }

        public override void Update()
        {
            owner.navMeshAgent.SetDestination(owner.player.position);

            // Transition to the attack state if the player is within attack range
            if (owner.IsPlayerInAttackRange())
            {
                owner.SetStateAttack();
            }
            // Transition back to the idle state if the player moves out of range
            else if (!owner.IsPlayerInRange(owner.chaseRangeSquared))
            {
                owner.StopChasingPlayer(); // Clear the chasing flag
                owner.SetStateIdle();
            }
        }

        public override void Exit()
        {
            owner.StopChasingPlayer();
        }
    }
//-----------------------------------------------------------------------------------------
// Attack
    private class AttackState : State
    {
        public AttackState(OperatorAI owner) : base(owner) { }

        public override void Enter()
        {
            Debug.Log("Attacking");
            owner.animator.SetBool("isAttacking",true);
            owner.navMeshAgent.SetDestination(owner.transform.position);
        }

        public override void Update()
        {
            // Transition back to the chase state if the player moves out of attack range
            if (!owner.IsPlayerInAttackRange())
            {
                owner.SetStateChase();
            }
        }

        public override void Exit()
        {
            owner.animator.SetBool("isAttacking",false);
        }
    }

//-----------------------------------------------------------------------------------------
// Disorient
    private class DisorientState : State
    {
        private bool disorientFinished = false;

        public DisorientState(OperatorAI owner) : base(owner) { }

        public override void Enter()
        {
            // Perform enter behavior
            Debug.Log("Disoriented");
            owner.animator.SetBool("hasBeenFlashed",true);
            owner.SetStateAlert();
            owner.lastKnownPlayerLocation = owner.player.position; // Store the last known player location
        }

        public override void Update()
        {
            if (Time.time >= owner.disorientTimer)
            {
                disorientFinished = true;
            }
        }

        public override void Exit()
        {
            owner.animator.SetBool("hasBeenFlashed",false);
        }

        public bool IsDisorientFinished()
        {
            return disorientFinished;
        }
    }

//-----------------------------------------------------------------------------------------
// Alert
    private class AlertState : State
    {
        public AlertState(OperatorAI owner) : base(owner) { }
        private NavMeshAgent navMeshAgent;

        public override void Enter()
        {
            // Perform enter behavior
            Debug.Log("Alert");
            owner.animator.SetBool("isAlerted",true);
            // Example: Use a NavMeshAgent to navigate towards the location
            navMeshAgent = owner.navMeshAgent;
            navMeshAgent.SetDestination(owner.lastKnownPlayerLocation);
            Debug.Log("Moving to last known player location: " + owner.lastKnownPlayerLocation);
        }

        public override void Update()
        {
            if (owner.currentState != AIState.Disorient)
            {
                owner.SetStateIdle();
                return;
            }

            if (owner.currentState == AIState.Disorient)
            {
                DisorientState disorientState = owner.currentStateInstance as DisorientState;
                if (disorientState != null && disorientState.IsDisorientFinished())
                {
                    owner.SetStateAlert();
                    navMeshAgent.SetDestination(owner.lastKnownPlayerLocation);
                }
            }
            if (owner.IsPlayerInRange(owner.chaseRange))
            {
                owner.SetStateChase();
            }
        }

        public override void Exit()
        {
            Debug.Log("Never knew we uhh we uhh");
        }
    }


// For easy debugging ofc
    private void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(transform.position, attackRange);
        Gizmos.color = Color.green;
        Gizmos.DrawWireSphere(transform.position, chaseRange);

        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, walkPointRange);
        try
        {
            Gizmos.DrawLine(gameObject.transform.position, player.position);
        }
        catch { };
    }
    #endregion
}