Untitled
unknown
plain_text
a month ago
17 kB
3
Indexable
// This namespace gives access to Unity's basic classes like MonoBehaviour, Transform, Vector3, Time, and Gizmos.
using UnityEngine;
// This namespace gives access to Unity's AI Navigation system, including NavMeshAgent.
using UnityEngine.AI;
// This makes sure the GameObject has a NavMeshAgent.
// If the GameObject does not have one, Unity will automatically add it.
[RequireComponent(typeof(NavMeshAgent))]
// This makes sure the GameObject has an Animator component.
// The Animator is needed for Idle, Walk, and Run animations.
[RequireComponent(typeof(Animator))]
public class EnemyNavMeshAI : MonoBehaviour
{
// This enum stores the possible behavior states of the enemy.
// Idle means the enemy is not moving.
// Patrol means the enemy walks between patrol points.
// Chase means the enemy runs after the player.
private enum EnemyState
{
Idle,
Patrol,
Chase
}
// -----------------------------
// REFERENCES
// -----------------------------
[Header("References")]
// This is the player that the enemy will chase.
// You can assign this manually in the Inspector.
[SerializeField] private Transform player;
// These are the patrol points where the enemy will walk.
// Create empty GameObjects in the scene and assign them here.
[SerializeField] private Transform[] patrolPoints;
// -----------------------------
// DETECTION SETTINGS
// -----------------------------
[Header("Detection")]
// If the player is inside this range, the enemy starts chasing.
[SerializeField] private float chaseRange = 10f;
// If the player becomes farther than this range, the enemy stops chasing.
// This should be bigger than chaseRange to prevent rapid switching.
[SerializeField] private float loseRange = 16f;
// -----------------------------
// MOVEMENT SETTINGS
// -----------------------------
[Header("Movement")]
// The speed of the enemy while patrolling.
[SerializeField] private float walkSpeed = 2f;
// The speed of the enemy while chasing.
[SerializeField] private float runSpeed = 5f;
// How close the enemy can get to a patrol point before stopping.
[SerializeField] private float patrolStoppingDistance = 0.2f;
// How close the enemy can get to the player before stopping.
// Useful if you later add attack behavior.
[SerializeField] private float chaseStoppingDistance = 1.8f;
// Extra distance check for deciding whether the patrol point has been reached.
[SerializeField] private float waypointReachDistance = 0.4f;
// How long the enemy waits at each patrol point.
[SerializeField] private float waitTimeAtWaypoint = 1.5f;
// If true, the enemy chooses random patrol points.
// If false, the enemy follows the patrol points in order.
[SerializeField] private bool randomPatrol = false;
// -----------------------------
// ANIMATION SETTINGS
// -----------------------------
[Header("Animation")]
// This must match the float parameter name inside the Animator Controller.
// In this lesson, the Animator parameter is named "Speed".
[SerializeField] private string speedParameter = "Speed";
// This smooths the animation value so the transition does not look too sudden.
[SerializeField] private float animationDampTime = 0.1f;
// -----------------------------
// ROTATION SETTINGS
// -----------------------------
[Header("Rotation")]
// How quickly the enemy rotates to face the player when close.
[SerializeField] private float facePlayerSpeed = 8f;
// -----------------------------
// PRIVATE VARIABLES
// -----------------------------
// Reference to the NavMeshAgent component.
private NavMeshAgent agent;
// Reference to the Animator component.
private Animator animator;
// Stores the enemy's current behavior state.
private EnemyState currentState;
// Used to allow the first state to initialize correctly.
private bool stateInitialized;
// Stores which patrol point the enemy is currently moving toward.
private int patrolIndex;
// Counts how long the enemy has waited at a patrol point.
private float waitTimer;
// This property checks if the patrolPoints array exists and has at least one patrol point.
private bool HasPatrolPoints
{
get
{
return patrolPoints != null && patrolPoints.Length > 0;
}
}
// Awake runs before Start.
// We use Awake to get component references.
private void Awake()
{
// Get the NavMeshAgent attached to this enemy.
agent = GetComponent<NavMeshAgent>();
// Get the Animator attached to this enemy.
animator = GetComponent<Animator>();
// If the player was not assigned in the Inspector,
// try to find a GameObject with the tag "Player".
if (player == null)
{
GameObject playerObject = GameObject.FindGameObjectWithTag("Player");
// If a tagged Player object was found, store its Transform.
if (playerObject != null)
{
player = playerObject.transform;
}
}
}
// Start runs once before the first frame update.
private void Start()
{
// If patrol points exist, start the enemy in Patrol state.
if (HasPatrolPoints)
{
ChangeState(EnemyState.Patrol);
}
// If no patrol points exist, the enemy will stay idle.
else
{
ChangeState(EnemyState.Idle);
}
}
// Update runs once every frame.
private void Update()
{
// First, check if the player is close enough to chase
// or far enough to stop chasing.
CheckForPlayer();
// Run behavior depending on the current state.
switch (currentState)
{
case EnemyState.Idle:
UpdateIdle();
break;
case EnemyState.Patrol:
UpdatePatrol();
break;
case EnemyState.Chase:
UpdateChase();
break;
}
// Update the animation after movement behavior is processed.
UpdateAnimations();
}
// This method checks the player's distance from the enemy.
private void CheckForPlayer()
{
// If there is no player reference, do nothing.
if (player == null)
{
return;
}
// Measure the distance between the enemy and the player.
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
// If the enemy is not already chasing and the player is near,
// switch to Chase state.
if (currentState != EnemyState.Chase && distanceToPlayer <= chaseRange)
{
ChangeState(EnemyState.Chase);
}
// If the enemy is chasing but the player gets too far,
// return to Patrol or Idle.
else if (currentState == EnemyState.Chase && distanceToPlayer >= loseRange)
{
// If patrol points exist, go back to patrolling.
if (HasPatrolPoints)
{
ChangeState(EnemyState.Patrol);
}
// If no patrol points exist, go back to idle.
else
{
ChangeState(EnemyState.Idle);
}
}
}
// This method changes the enemy's current state.
private void ChangeState(EnemyState newState)
{
// If the state was already initialized and the enemy is already in this state,
// do not restart the same state again.
if (stateInitialized && currentState == newState)
{
return;
}
// Mark that at least one state has now been initialized.
stateInitialized = true;
// Store the new state.
currentState = newState;
// Run the correct "Enter" method for the new state.
switch (currentState)
{
case EnemyState.Idle:
EnterIdle();
break;
case EnemyState.Patrol:
EnterPatrol();
break;
case EnemyState.Chase:
EnterChase();
break;
}
}
// -----------------------------
// IDLE STATE
// -----------------------------
// This method runs once when the enemy enters Idle state.
private void EnterIdle()
{
// Stop the NavMeshAgent from moving.
agent.isStopped = true;
// Clear the current path.
agent.ResetPath();
// Reset the waiting timer.
waitTimer = 0f;
}
// This method runs every frame while the enemy is in Idle state.
private void UpdateIdle()
{
// The enemy stays still here.
// The CheckForPlayer method will switch it to Chase if the player gets close.
}
// -----------------------------
// PATROL STATE
// -----------------------------
// This method runs once when the enemy enters Patrol state.
private void EnterPatrol()
{
// If there are no patrol points, switch to Idle instead.
if (!HasPatrolPoints)
{
ChangeState(EnemyState.Idle);
return;
}
// Allow the NavMeshAgent to move.
agent.isStopped = false;
// Set the agent speed to walking speed.
agent.speed = walkSpeed;
// Set how close the agent should get to the patrol point.
agent.stoppingDistance = patrolStoppingDistance;
// Reset the wait timer.
waitTimer = 0f;
// Move toward the current patrol point.
SetCurrentPatrolDestination();
}
// This method runs every frame while the enemy is patrolling.
private void UpdatePatrol()
{
// If patrol points were removed, switch to Idle.
if (!HasPatrolPoints)
{
ChangeState(EnemyState.Idle);
return;
}
// If the enemy has not reached the destination yet, keep moving.
if (!ReachedDestination())
{
return;
}
// If the enemy reached the patrol point, stop moving.
agent.isStopped = true;
// Count how long the enemy has waited.
waitTimer += Time.deltaTime;
// If the enemy has waited long enough, choose the next patrol point.
if (waitTimer >= waitTimeAtWaypoint)
{
// Reset the wait timer.
waitTimer = 0f;
// Choose the next patrol point.
ChooseNextPatrolPoint();
// Allow the enemy to move again.
agent.isStopped = false;
// Send the enemy to the new patrol point.
SetCurrentPatrolDestination();
}
}
// -----------------------------
// CHASE STATE
// -----------------------------
// This method runs once when the enemy enters Chase state.
private void EnterChase()
{
// Allow the NavMeshAgent to move.
agent.isStopped = false;
// Set the agent speed to running speed.
agent.speed = runSpeed;
// Set how close the enemy should get to the player.
agent.stoppingDistance = chaseStoppingDistance;
// Reset the wait timer because chasing does not use patrol waiting.
waitTimer = 0f;
}
// This method runs every frame while the enemy is chasing.
private void UpdateChase()
{
// If the player no longer exists, return to Patrol or Idle.
if (player == null)
{
if (HasPatrolPoints)
{
ChangeState(EnemyState.Patrol);
}
else
{
ChangeState(EnemyState.Idle);
}
return;
}
// Measure the distance between the enemy and the player.
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
// If the enemy is close enough to the player, stop moving.
if (distanceToPlayer <= chaseStoppingDistance)
{
// Stop the agent.
agent.isStopped = true;
// Clear the current path.
agent.ResetPath();
// Face the player.
FaceTarget(player.position);
}
// If the player is still far enough, keep chasing.
else
{
// Make sure the agent is allowed to move.
agent.isStopped = false;
// Set the player's position as the destination.
agent.SetDestination(player.position);
}
}
// -----------------------------
// DESTINATION CHECKING
// -----------------------------
// This method checks if the NavMeshAgent has reached its current destination.
private bool ReachedDestination()
{
// If the path is still being calculated, the destination is not reached yet.
if (agent.pathPending)
{
return false;
}
// Sometimes remainingDistance can be infinity while the path is unknown.
// If that happens, do not count it as reached.
if (agent.remainingDistance == Mathf.Infinity)
{
return false;
}
// Use whichever value is bigger:
// the agent's stopping distance or our custom waypoint reach distance.
float reachDistance = Mathf.Max(agent.stoppingDistance, waypointReachDistance);
// If the remaining distance is small enough, the destination is reached.
return agent.remainingDistance <= reachDistance;
}
// This method sends the enemy to the current patrol point.
private void SetCurrentPatrolDestination()
{
// If there are no patrol points, do nothing.
if (!HasPatrolPoints)
{
return;
}
// Get the patrol point at the current index.
Transform point = patrolPoints[patrolIndex];
// If this patrol point is missing, choose another one.
if (point == null)
{
ChooseNextPatrolPoint();
point = patrolPoints[patrolIndex];
}
// If the patrol point is valid, set it as the agent's destination.
if (point != null)
{
agent.SetDestination(point.position);
}
}
// This method chooses the next patrol point.
private void ChooseNextPatrolPoint()
{
// If there are no patrol points, do nothing.
if (!HasPatrolPoints)
{
return;
}
// If random patrol is enabled and there is more than one patrol point,
// choose a random patrol point.
if (randomPatrol && patrolPoints.Length > 1)
{
// Store the current index first.
int nextIndex = patrolIndex;
// Keep choosing until the next point is different from the current point.
while (nextIndex == patrolIndex)
{
nextIndex = Random.Range(0, patrolPoints.Length);
}
// Set the new random patrol index.
patrolIndex = nextIndex;
}
// If random patrol is disabled, go to the next patrol point in order.
else
{
// Move to the next patrol point.
patrolIndex++;
// If the patrol index goes past the array length,
// reset it back to the first patrol point.
if (patrolIndex >= patrolPoints.Length)
{
patrolIndex = 0;
}
}
}
// -----------------------------
// GIZMOS
// -----------------------------
// This draws visual detection ranges in the Scene view when the enemy is selected.
private void OnDrawGizmosSelected()
{
// Draw the chase range using yellow.
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, chaseRange);
// Draw the lose range using red.
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, loseRange);
}
}Editor is loading...
Leave a Comment