404 lines
14 KiB
C#
404 lines
14 KiB
C#
using UnityEngine;
|
||
using UnityEngine.AI;
|
||
|
||
public class BlindMutantAI : MonoBehaviour
|
||
{
|
||
private NavMeshAgent agent;
|
||
private Animator animator;
|
||
|
||
[Header("Настройки скорости")]
|
||
public float walkSpeed = 2f;
|
||
public float runSpeed = 5f;
|
||
|
||
[Header("Патрулирование")]
|
||
public Transform[] patrolPoints;
|
||
[Tooltip("Время паузы на точке патрулирования для прислушивания")]
|
||
public float waitTimeAtPoint = 1.5f;
|
||
private float waitTimer = 0f;
|
||
private int currentPatrolIndex;
|
||
private bool isInvestigating = false;
|
||
private float investigationTimer = 0f;
|
||
|
||
[Header("Режим охраны комнаты")]
|
||
[Tooltip("Если включено, мутант приписан к комнате и патрулирует строго ее, но реагирует на звуки снаружи")]
|
||
public bool isRoomConfined = false;
|
||
|
||
[Header("Обнаружение игрока")]
|
||
public float detectionRadius = 3.5f; // Радиус обостренных чувств (слух/запах в упор)
|
||
public float losePlayerDistance = 14f; // Дистанция потери цели
|
||
private Transform targetPlayer;
|
||
private PlayerHealth targetPlayerHealth;
|
||
private EchoTrigger echoTrigger;
|
||
private float silentPlayerTimer = 0f;
|
||
|
||
[Header("Настройки атаки")]
|
||
public float attackRange = 1.8f; // Дистанция ближнего боя
|
||
public float attackDamage = 8f; // Урон за один удар (снижен с 20 для баланса)
|
||
public float attackCooldown = 1.35f; // Кулдаун между ударами
|
||
private float nextAttackTime = 0f;
|
||
|
||
void Start()
|
||
{
|
||
agent = GetComponent<NavMeshAgent>();
|
||
if (agent != null)
|
||
{
|
||
agent.speed = walkSpeed;
|
||
// Слегка варьируем приоритет избегания, чтобы мутанты уступали друг другу дорогу
|
||
agent.avoidancePriority = Random.Range(30, 70);
|
||
|
||
// Если агент чуть выше пола или не на сетке, привязываем его к ближайшей точке NavMesh
|
||
if (!agent.isOnNavMesh)
|
||
{
|
||
if (NavMesh.SamplePosition(transform.position, out NavMeshHit hit, 5.0f, NavMesh.AllAreas))
|
||
{
|
||
agent.Warp(hit.position);
|
||
}
|
||
}
|
||
}
|
||
|
||
animator = GetComponentInChildren<Animator>();
|
||
|
||
// Автоматически находим игрока на сцене
|
||
var playerObj = FindAnyObjectByType<PlayerHealth>();
|
||
if (playerObj != null)
|
||
{
|
||
targetPlayerHealth = playerObj;
|
||
}
|
||
|
||
if (echoTrigger == null)
|
||
{
|
||
echoTrigger = FindAnyObjectByType<EchoTrigger>();
|
||
}
|
||
|
||
// Если точки патрулирования не заданы или массив состоит из null, ищем PatrolWaypoints на сцене
|
||
if (IsPatrolArrayEmptyOrNull())
|
||
{
|
||
AutoAssignPatrolWaypoints();
|
||
}
|
||
|
||
// Рассинхронизируем начальные точки и таймеры, чтобы толпа мутантов не двигалась синхронно
|
||
if (patrolPoints != null && patrolPoints.Length > 0)
|
||
{
|
||
currentPatrolIndex = Random.Range(0, patrolPoints.Length);
|
||
}
|
||
waitTimer = Random.Range(0.2f, waitTimeAtPoint);
|
||
|
||
GoToNextPatrolPoint();
|
||
}
|
||
|
||
private bool IsPatrolArrayEmptyOrNull()
|
||
{
|
||
if (patrolPoints == null || patrolPoints.Length == 0) return true;
|
||
for (int i = 0; i < patrolPoints.Length; i++)
|
||
{
|
||
if (patrolPoints[i] != null) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
private void AutoAssignPatrolWaypoints()
|
||
{
|
||
var patrolRoot = GameObject.Find("PatrolWaypoints");
|
||
if (patrolRoot != null && patrolRoot.transform.childCount > 0)
|
||
{
|
||
patrolPoints = new Transform[patrolRoot.transform.childCount];
|
||
for (int i = 0; i < patrolRoot.transform.childCount; i++)
|
||
{
|
||
patrolPoints[i] = patrolRoot.transform.GetChild(i);
|
||
}
|
||
Debug.Log($"<color=cyan>[BlindMutantAI] Автоматически обнаружено {patrolPoints.Length} точек патрулирования в объекте 'PatrolWaypoints'.</color>");
|
||
}
|
||
}
|
||
|
||
void Update()
|
||
{
|
||
UpdateAnimator();
|
||
|
||
// 1. Поведение при активном преследовании игрока
|
||
if (targetPlayer != null)
|
||
{
|
||
HandlePlayerChaseAndAttack();
|
||
return;
|
||
}
|
||
|
||
// 2. Проверка близости игрока (если подошел слишком близко — чует даже слепой)
|
||
CheckProximityDetection();
|
||
|
||
// 3. Патрулирование и расследование звуков
|
||
HandlePatrolAndInvestigation();
|
||
}
|
||
|
||
private void UpdateAnimator()
|
||
{
|
||
if (animator == null) return;
|
||
float currentSpeed = agent != null && agent.enabled && agent.isOnNavMesh ? agent.velocity.magnitude : 0f;
|
||
animator.SetFloat("Speed", currentSpeed);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Проверяет, стоит ли игрок неподвижно и без звуков (шейдер волн эхолокации не активен)
|
||
/// </summary>
|
||
public bool IsPlayerSilentAndStill()
|
||
{
|
||
if (echoTrigger == null)
|
||
{
|
||
echoTrigger = EchoTrigger.Instance != null ? EchoTrigger.Instance : FindAnyObjectByType<EchoTrigger>();
|
||
}
|
||
|
||
if (echoTrigger != null)
|
||
{
|
||
return !echoTrigger.IsPlayerMakingNoise();
|
||
}
|
||
|
||
// Резервная проверка, если EchoTrigger не найден
|
||
if (targetPlayerHealth != null)
|
||
{
|
||
var cc = targetPlayerHealth.GetComponent<CharacterController>();
|
||
if (cc != null && cc.velocity.sqrMagnitude > 0.05f) return false;
|
||
var anim = targetPlayerHealth.GetComponentInChildren<Animator>();
|
||
if (anim != null && anim.GetBool("IsMoving")) return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private void HandlePlayerChaseAndAttack()
|
||
{
|
||
if (agent == null || !agent.isOnNavMesh) return;
|
||
|
||
if (targetPlayerHealth != null && targetPlayerHealth.IsDead)
|
||
{
|
||
LosePlayerTarget();
|
||
return;
|
||
}
|
||
|
||
// Если игрок замер на месте и не издает звуков — слепой мутант не может его заметить и атаковать!
|
||
if (IsPlayerSilentAndStill())
|
||
{
|
||
agent.isStopped = true;
|
||
silentPlayerTimer += Time.deltaTime;
|
||
|
||
// Если игрок сохраняет тишину и неподвижность 0.8 секунды — мутант теряет цель и возвращается к патрулю
|
||
if (silentPlayerTimer >= 0.8f)
|
||
{
|
||
LosePlayerTarget();
|
||
}
|
||
return;
|
||
}
|
||
else
|
||
{
|
||
silentPlayerTimer = 0f;
|
||
}
|
||
|
||
float distanceToPlayer = Vector3.Distance(transform.position, targetPlayer.position);
|
||
|
||
// Игрок убежал слишком далеко
|
||
if (distanceToPlayer > losePlayerDistance)
|
||
{
|
||
LosePlayerTarget();
|
||
return;
|
||
}
|
||
|
||
// Игрок в зоне удара
|
||
if (distanceToPlayer <= attackRange)
|
||
{
|
||
agent.isStopped = true;
|
||
|
||
// Разворачиваемся к игроку
|
||
Vector3 lookDirection = targetPlayer.position - transform.position;
|
||
lookDirection.y = 0f;
|
||
if (lookDirection.sqrMagnitude > 0.001f)
|
||
{
|
||
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(lookDirection), Time.deltaTime * 10f);
|
||
}
|
||
|
||
// Наносим урон по кулдауну
|
||
if (Time.time >= nextAttackTime)
|
||
{
|
||
nextAttackTime = Time.time + attackCooldown;
|
||
if (animator != null)
|
||
{
|
||
animator.SetTrigger("Attack");
|
||
}
|
||
if (targetPlayerHealth != null)
|
||
{
|
||
targetPlayerHealth.TakeDamage(attackDamage);
|
||
Debug.Log($"Мутант нанес удар игроку! Урон: {attackDamage}");
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Бежим к игроку
|
||
agent.isStopped = false;
|
||
agent.speed = runSpeed;
|
||
agent.SetDestination(targetPlayer.position);
|
||
}
|
||
}
|
||
|
||
private void CheckProximityDetection()
|
||
{
|
||
if (targetPlayerHealth == null || targetPlayerHealth.IsDead) return;
|
||
|
||
// Если игрок стоит на месте и не издает звуков (нет волн эхолокации в шейдере) — слепой мутант его не замечает!
|
||
if (IsPlayerSilentAndStill()) return;
|
||
|
||
float distance = Vector3.Distance(transform.position, targetPlayerHealth.transform.position);
|
||
if (distance <= detectionRadius)
|
||
{
|
||
EngagePlayer(targetPlayerHealth.transform);
|
||
}
|
||
}
|
||
|
||
private void HandlePatrolAndInvestigation()
|
||
{
|
||
if (agent == null || !agent.isOnNavMesh) return;
|
||
|
||
if (isInvestigating)
|
||
{
|
||
investigationTimer += Time.deltaTime;
|
||
// Защита от застревания: если не дошел до точки звука за 10 сек, возвращаемся к патрулю
|
||
if (investigationTimer >= 10f)
|
||
{
|
||
isInvestigating = false;
|
||
investigationTimer = 0f;
|
||
agent.speed = walkSpeed;
|
||
waitTimer = waitTimeAtPoint;
|
||
}
|
||
}
|
||
|
||
if (!agent.pathPending && (agent.remainingDistance < 0.8f || !agent.hasPath))
|
||
{
|
||
if (isInvestigating)
|
||
{
|
||
isInvestigating = false;
|
||
investigationTimer = 0f;
|
||
agent.speed = walkSpeed;
|
||
waitTimer = waitTimeAtPoint;
|
||
}
|
||
|
||
if (waitTimer > 0f)
|
||
{
|
||
waitTimer -= Time.deltaTime;
|
||
agent.isStopped = true;
|
||
return;
|
||
}
|
||
|
||
agent.isStopped = false;
|
||
waitTimer = waitTimeAtPoint;
|
||
GoToNextPatrolPoint();
|
||
}
|
||
}
|
||
|
||
private void EngagePlayer(Transform playerTransform)
|
||
{
|
||
if (agent == null || !agent.isOnNavMesh) return;
|
||
|
||
targetPlayer = playerTransform;
|
||
if (targetPlayerHealth == null)
|
||
{
|
||
targetPlayerHealth = playerTransform.GetComponent<PlayerHealth>();
|
||
}
|
||
|
||
isInvestigating = false;
|
||
waitTimer = 0f;
|
||
agent.isStopped = false;
|
||
agent.speed = runSpeed;
|
||
agent.SetDestination(targetPlayer.position);
|
||
Debug.Log("Мутант обнаружил игрока и начинает атаку!");
|
||
}
|
||
|
||
private void LosePlayerTarget()
|
||
{
|
||
targetPlayer = null;
|
||
silentPlayerTimer = 0f;
|
||
if (agent != null && agent.isOnNavMesh)
|
||
{
|
||
agent.isStopped = false;
|
||
agent.speed = walkSpeed;
|
||
}
|
||
isInvestigating = false;
|
||
waitTimer = 0f;
|
||
|
||
GoToNextPatrolPoint();
|
||
Debug.Log("Мутант потерял игрока (игрок замер в тишине) и возвращается к патрулированию.");
|
||
}
|
||
|
||
private void GoToNextPatrolPoint()
|
||
{
|
||
if (agent == null || !agent.isOnNavMesh) return;
|
||
|
||
if (patrolPoints != null && patrolPoints.Length > 0)
|
||
{
|
||
for (int attempts = 0; attempts < patrolPoints.Length; attempts++)
|
||
{
|
||
var wp = patrolPoints[currentPatrolIndex];
|
||
currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length;
|
||
|
||
if (wp != null)
|
||
{
|
||
agent.isStopped = false;
|
||
agent.speed = walkSpeed;
|
||
agent.SetDestination(wp.position);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Если точек нет вообще — свободное блуждание по комплексу
|
||
WanderRandomly();
|
||
}
|
||
|
||
private void WanderRandomly()
|
||
{
|
||
if (agent == null || !agent.isOnNavMesh) return;
|
||
|
||
Vector3 randomDir = Random.insideUnitSphere * 15f;
|
||
randomDir += transform.position;
|
||
if (NavMesh.SamplePosition(randomDir, out NavMeshHit hit, 15f, NavMesh.AllAreas))
|
||
{
|
||
agent.isStopped = false;
|
||
agent.speed = walkSpeed;
|
||
agent.SetDestination(hit.position);
|
||
}
|
||
}
|
||
|
||
public void HearSound(Vector3 soundPosition)
|
||
{
|
||
// Если уже бьем игрока в упор, не отвлекаемся на случайный шум вдалеке
|
||
if (targetPlayer != null && Vector3.Distance(transform.position, targetPlayer.position) <= attackRange)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (agent == null || !agent.isOnNavMesh) return;
|
||
|
||
isInvestigating = true;
|
||
investigationTimer = 0f;
|
||
waitTimer = 0f;
|
||
agent.isStopped = false;
|
||
agent.speed = runSpeed;
|
||
agent.SetDestination(soundPosition);
|
||
}
|
||
|
||
public void OnDamagedByPlayer(Transform player)
|
||
{
|
||
if (animator != null)
|
||
{
|
||
animator.SetTrigger("GetHit");
|
||
}
|
||
silentPlayerTimer = 0f;
|
||
EngagePlayer(player);
|
||
}
|
||
|
||
private void OnDrawGizmosSelected()
|
||
{
|
||
// Радиус ближнего обнаружения
|
||
Gizmos.color = Color.yellow;
|
||
Gizmos.DrawWireSphere(transform.position, detectionRadius);
|
||
|
||
// Радиус атаки
|
||
Gizmos.color = Color.red;
|
||
Gizmos.DrawWireSphere(transform.position, attackRange);
|
||
}
|
||
} |