267 lines
8.5 KiB
C#
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
|
|
} |