using UnityEngine; using UnityEngine.AI; // Add a StateMachine component automatically when this script is added. [RequireComponent(typeof(StateMachine))] public class EnemyControllerState : MonoBehaviour { #region Variables // References & Components private PlayerController player; public Rigidbody theRB; public Animator anim; private StateMachine brain; // Stats & Config public float moveSpeed = 3.5f; public float chaseRange = 15f; public float attackRange = 4f; // Renamed from the original's "stopCloseRange" public float currentHealth = 25f; // Patrol State Variables public Transform pointsHolder; public Transform[] PatrolPoints; private int currentPatrolPoint; public float pointWaitTimer = 3f; private float waitCounter; // Chase State Variables private float strafeAmount; // Attack State Variables private float attackCooldown = 1f; private float attackTimer = 0f; // Dead State Variables private float waitToDisappear = 4f; private bool isDead = false; // Booleans to drive state transitions, calculated in Update() private bool playerInChaseRange; private bool playerInAttackRange; #endregion void Start() { // --- Component Initialization --- brain = GetComponent(); player = FindFirstObjectByType(); // Assuming the Animator is on a child object, like the character model. anim = GetComponentInChildren(); theRB = GetComponent(); // --- State Variable Setup --- // Detach patrol points from the enemy so they don't move with it. if (pointsHolder != null) { pointsHolder.SetParent(null); } waitCounter = Random.Range(0.75f, 1.25f) * pointWaitTimer; strafeAmount = Random.Range(-0.75f, 0.75f); // --- Initial State --- // The enemy starts in the Patrol state. brain.PushState(Patrol, OnPatrolEnter, null); } void Update() { // Continuously check the distance to the player to see if we need to change states. // This check is performed here, but the decision to transition is made within each state's logic. if (isDead || player == null || PlayerController.instance.isDead) { playerInChaseRange = false; playerInAttackRange = false; } else { float distanceToPlayer = Vector3.Distance(player.transform.position, transform.position); playerInChaseRange = distanceToPlayer < chaseRange; playerInAttackRange = distanceToPlayer < attackRange; } // The StateMachine's own Update method (from StateMachine.cs) handles executing the // logic for whichever state is currently active. } #region State Methods // --- PATROL STATE --- // The enemy moves between waypoints or stands idle. void OnPatrolEnter() { anim.SetFloat("speed", 0.5f); // Set animation to walking speed. } void Patrol() { // TRANSITION: If player is in range, switch to Chase state. if (playerInChaseRange) { brain.PushState(Chase, OnChaseEnter, OnChaseExit); return; } // ACTION: Execute patrol logic. if (PatrolPoints.Length > 0) { // If at a patrol point, wait for a bit. if (Vector3.Distance(transform.position, PatrolPoints[currentPatrolPoint].position) < 1f) { waitCounter -= Time.deltaTime; theRB.linearVelocity = Vector3.zero; anim.SetFloat("speed", 0f); // Idle animation. if (waitCounter <= 0) { currentPatrolPoint = (currentPatrolPoint + 1) % PatrolPoints.Length; waitCounter = Random.Range(0.75f, 1.25f) * pointWaitTimer; anim.SetFloat("speed", 0.5f); // Resume walking animation. } } else // If not at a point, move towards the next one. { transform.LookAt(new Vector3(PatrolPoints[currentPatrolPoint].position.x, transform.position.y, PatrolPoints[currentPatrolPoint].position.z)); theRB.linearVelocity = transform.forward * (moveSpeed * 0.75f); // Move at a slightly slower patrol speed. } } } // --- CHASE STATE --- // The enemy has spotted the player and is moving to intercept. void OnChaseEnter() { anim.SetFloat("speed", 1f); // Set animation to running speed. } void Chase() { // TRANSITION 1: If close enough to attack, switch to Attack state. if (playerInAttackRange) { brain.PushState(Attack, OnAttackEnter, OnAttackExit); return; } // TRANSITION 2: If the player escapes, go back to the previous state (Patrol). if (!playerInChaseRange) { brain.PopState(); return; } // ACTION: Move towards the player with some sideways strafing. transform.LookAt(new Vector3(player.transform.position.x, transform.position.y, player.transform.position.z)); theRB.linearVelocity = (transform.forward + transform.right * strafeAmount) * moveSpeed; } void OnChaseExit() { theRB.linearVelocity = Vector3.zero; // Stop moving when exiting the chase. anim.SetFloat("speed", 0f); } // --- ATTACK STATE --- // The enemy is close enough to the player and is attacking. void OnAttackEnter() { theRB.linearVelocity = Vector3.zero; // Stand still to attack. anim.SetFloat("speed", 0f); attackTimer = 0f; // Attack immediately upon entering the state. } void Attack() { // TRANSITION: If player moves out of attack range, go back to chasing. if (!playerInAttackRange) { brain.PopState(); return; } // ACTION: Look at the player and attack on a cooldown. transform.LookAt(new Vector3(player.transform.position.x, transform.position.y, player.transform.position.z)); attackTimer -= Time.deltaTime; if (attackTimer <= 0f) { anim.SetTrigger("attack"); attackTimer = attackCooldown; } } void OnAttackExit() { // No specific action is needed when we stop attacking, // as the next state (Chase) will set the correct animation. } // --- DEAD STATE --- // The enemy's health has reached zero. void OnDeadEnter() { isDead = true; anim.SetTrigger("dead"); // Disable physics and collision. theRB.linearVelocity = Vector3.zero; theRB.isKinematic = true; GetComponent().enabled = false; // Clean up the patrol points holder. if (pointsHolder != null) { Destroy(pointsHolder.gameObject); } } void Dead() { // ACTION: Wait for a moment, then shrink and disappear. waitToDisappear -= Time.deltaTime; if (waitToDisappear <= 0) { transform.localScale = Vector3.MoveTowards(transform.localScale, Vector3.zero, Time.deltaTime * 2f); if (transform.localScale.magnitude < 0.1f) { Destroy(gameObject); } } } #endregion #region Public Methods // This method is called by an Animation Event in the attack animation clip. public void DealDamage() { if (isDead || player == null) return; // Check if player is still in range when the damage frame of the animation is reached. if (Vector3.Distance(player.transform.position, transform.position) < attackRange + 0.5f) { PlayerHealthController.instance.TakeDamage(5f); } } // This method is called by other scripts (e.g., player's weapon) to damage the enemy. public void TakeDamage(float damageToTake) { if (isDead) return; // Can't take damage if already dead. currentHealth -= damageToTake; if (currentHealth <= 0) { // Force a transition to the Dead state. // First, clear any other states from the stack (like Attack or Chase). while (brain.States.Count > 0) { brain.PopState(); } // Then, push the Dead state, which it will never exit. brain.PushState(Dead, OnDeadEnter, null); } } #endregion }