Files
CartoonFPS/Assets/Scripts/EnemyControllerState.cs
2025-08-06 23:18:38 +01:00

267 lines
8.5 KiB
C#

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<StateMachine>();
player = FindFirstObjectByType<PlayerController>();
// Assuming the Animator is on a child object, like the character model.
anim = GetComponentInChildren<Animator>();
theRB = GetComponent<Rigidbody>();
// --- 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<Collider>().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
}