297 lines
7.8 KiB
C#
297 lines
7.8 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.AI;
|
|
|
|
[RequireComponent(typeof(CapsuleCollider))]
|
|
public class EnemyStateMachine : MonoBehaviour
|
|
{
|
|
public enum State
|
|
{
|
|
Patrol,
|
|
Suspicion,
|
|
Alert
|
|
}
|
|
|
|
[Header("References")]
|
|
public Transform[] patrolPoints;
|
|
public NavMeshAgent agent;
|
|
public EnemyVision vision;
|
|
|
|
[Header("Movement")]
|
|
public float patrolSpeed = 2.2f;
|
|
public float suspicionSpeed = 2.8f;
|
|
public float alertSpeed = 3.8f;
|
|
public float manualRotationSpeed = 8f;
|
|
public float waypointTolerance = 0.7f;
|
|
|
|
[Header("Awareness")]
|
|
public float hearingRange = 12f;
|
|
public float suspicionWaitDuration = 2.5f;
|
|
public float loseSightDelay = 1.3f;
|
|
public float captureDistance = 1.6f;
|
|
public int captureDamage = 10;
|
|
public float captureCooldown = 1f;
|
|
|
|
public State CurrentState { get; private set; } = State.Patrol;
|
|
public static IReadOnlyList<EnemyStateMachine> ActiveEnemies => activeEnemies;
|
|
|
|
private static readonly List<EnemyStateMachine> activeEnemies = new List<EnemyStateMachine>();
|
|
|
|
private Transform player;
|
|
private PlayerHealth playerHealth;
|
|
private int patrolIndex;
|
|
private Vector3 investigationTarget;
|
|
private Vector3 lastKnownPlayerPosition;
|
|
private float suspicionTimer;
|
|
private float lastSeenTime = -999f;
|
|
private float lastCaptureTime = -999f;
|
|
private float lastHandledNoiseTimestamp = -999f;
|
|
|
|
private void OnEnable()
|
|
{
|
|
if (!activeEnemies.Contains(this))
|
|
{
|
|
activeEnemies.Add(this);
|
|
}
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
activeEnemies.Remove(this);
|
|
}
|
|
|
|
private void Awake()
|
|
{
|
|
agent = agent != null ? agent : GetComponent<NavMeshAgent>();
|
|
vision = vision != null ? vision : GetComponent<EnemyVision>();
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
AcquirePlayer();
|
|
ApplySpeedForState(CurrentState);
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
AcquirePlayer();
|
|
ListenForNoise();
|
|
|
|
switch (CurrentState)
|
|
{
|
|
case State.Patrol:
|
|
UpdatePatrol();
|
|
break;
|
|
case State.Suspicion:
|
|
UpdateSuspicion();
|
|
break;
|
|
case State.Alert:
|
|
UpdateAlert();
|
|
break;
|
|
}
|
|
|
|
if (CurrentState == State.Alert && Time.time - lastSeenTime > loseSightDelay)
|
|
{
|
|
investigationTarget = lastKnownPlayerPosition;
|
|
EnterState(State.Suspicion);
|
|
}
|
|
}
|
|
|
|
public void OnPlayerDetected(Vector3 playerPos)
|
|
{
|
|
lastKnownPlayerPosition = playerPos;
|
|
lastSeenTime = Time.time;
|
|
|
|
if (CurrentState != State.Alert)
|
|
{
|
|
EnterState(State.Alert);
|
|
}
|
|
}
|
|
|
|
public void OnPlayerLost()
|
|
{
|
|
if (CurrentState == State.Alert)
|
|
{
|
|
investigationTarget = lastKnownPlayerPosition;
|
|
}
|
|
}
|
|
|
|
public void OnNoiseHeard(Vector3 noisePosition)
|
|
{
|
|
if (CurrentState == State.Alert)
|
|
{
|
|
return;
|
|
}
|
|
|
|
investigationTarget = noisePosition;
|
|
suspicionTimer = 0f;
|
|
EnterState(State.Suspicion);
|
|
}
|
|
|
|
private void UpdatePatrol()
|
|
{
|
|
if (patrolPoints == null || patrolPoints.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Transform targetPoint = patrolPoints[patrolIndex];
|
|
MoveTowards(targetPoint.position, patrolSpeed);
|
|
|
|
if (HasReached(targetPoint.position))
|
|
{
|
|
patrolIndex = (patrolIndex + 1) % patrolPoints.Length;
|
|
}
|
|
}
|
|
|
|
private void UpdateSuspicion()
|
|
{
|
|
MoveTowards(investigationTarget, suspicionSpeed);
|
|
|
|
if (HasReached(investigationTarget))
|
|
{
|
|
suspicionTimer += Time.deltaTime;
|
|
if (suspicionTimer >= suspicionWaitDuration)
|
|
{
|
|
suspicionTimer = 0f;
|
|
EnterState(State.Patrol);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
suspicionTimer = 0f;
|
|
}
|
|
}
|
|
|
|
private void UpdateAlert()
|
|
{
|
|
if (player == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
lastKnownPlayerPosition = player.position;
|
|
MoveTowards(lastKnownPlayerPosition, alertSpeed);
|
|
|
|
if (Vector3.Distance(transform.position, player.position) <= captureDistance && Time.time >= lastCaptureTime + captureCooldown)
|
|
{
|
|
lastCaptureTime = Time.time;
|
|
if (playerHealth != null)
|
|
{
|
|
playerHealth.TakeDamage(captureDamage);
|
|
}
|
|
Debug.Log("Игрок обнаружен!");
|
|
}
|
|
}
|
|
|
|
private void ListenForNoise()
|
|
{
|
|
NoiseManager.NoiseEvent noise = NoiseManager.GetClosestNoise(transform.position, hearingRange);
|
|
if (noise == null || noise.timestamp <= lastHandledNoiseTimestamp)
|
|
{
|
|
return;
|
|
}
|
|
|
|
lastHandledNoiseTimestamp = noise.timestamp;
|
|
OnNoiseHeard(noise.position);
|
|
}
|
|
|
|
private void EnterState(State newState)
|
|
{
|
|
CurrentState = newState;
|
|
ApplySpeedForState(newState);
|
|
|
|
switch (newState)
|
|
{
|
|
case State.Patrol:
|
|
suspicionTimer = 0f;
|
|
break;
|
|
case State.Suspicion:
|
|
suspicionTimer = 0f;
|
|
break;
|
|
case State.Alert:
|
|
lastSeenTime = Time.time;
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void ApplySpeedForState(State state)
|
|
{
|
|
if (agent == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch (state)
|
|
{
|
|
case State.Patrol:
|
|
agent.speed = patrolSpeed;
|
|
break;
|
|
case State.Suspicion:
|
|
agent.speed = suspicionSpeed;
|
|
break;
|
|
case State.Alert:
|
|
agent.speed = alertSpeed;
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void MoveTowards(Vector3 destination, float manualSpeed)
|
|
{
|
|
if (agent != null && agent.isActiveAndEnabled && agent.isOnNavMesh)
|
|
{
|
|
agent.SetDestination(destination);
|
|
return;
|
|
}
|
|
|
|
// Fallback нужен на случай, если NavMesh ещё не запечён или агент не смог встать на него.
|
|
Vector3 flatDestination = new Vector3(destination.x, transform.position.y, destination.z);
|
|
Vector3 direction = flatDestination - transform.position;
|
|
direction.y = 0f;
|
|
|
|
if (direction.sqrMagnitude <= 0.001f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Vector3 step = direction.normalized * manualSpeed * Time.deltaTime;
|
|
transform.position += step.sqrMagnitude > direction.sqrMagnitude ? direction : step;
|
|
|
|
Quaternion targetRotation = Quaternion.LookRotation(direction.normalized, Vector3.up);
|
|
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * manualRotationSpeed);
|
|
}
|
|
|
|
private bool HasReached(Vector3 destination)
|
|
{
|
|
if (agent != null && agent.isActiveAndEnabled && agent.isOnNavMesh)
|
|
{
|
|
if (agent.pathPending)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return agent.remainingDistance <= Mathf.Max(agent.stoppingDistance, waypointTolerance);
|
|
}
|
|
|
|
Vector3 flatDestination = new Vector3(destination.x, transform.position.y, destination.z);
|
|
return Vector3.Distance(transform.position, flatDestination) <= waypointTolerance;
|
|
}
|
|
|
|
private void AcquirePlayer()
|
|
{
|
|
if (player != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
GameObject playerObject = GameObject.FindGameObjectWithTag("Player");
|
|
if (playerObject == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
player = playerObject.transform;
|
|
playerHealth = playerObject.GetComponent<PlayerHealth>();
|
|
}
|
|
}
|