using System.Collections.Generic; using UnityEngine; using UnityEngine.InputSystem; public class EchoTrigger : MonoBehaviour { [Header("Связь с шейдером")] public Material echoMaterial; [Header("Синхронизация с шагами персонажа")] [Tooltip("Включить синхронизацию волн эхолокации с анимацией шагов")] public bool syncWithAnimation = true; public Animator playerAnimator; public CharacterController characterController; [Tooltip("Позиционировать волну строго на кости наступающей ноги (L/R)")] public bool spawnAtFootBone = true; public Transform leftFootBone; public Transform rightFootBone; [Header("Фазы касания земли в анимации (0.0 - 1.0)")] [Tooltip("Фаза шага левой ногой при обычной ходьбе")] [Range(0f, 1f)] public float leftFootPhaseWalk = 0.08f; [Tooltip("Фаза шага правой ногой при обычной ходьбе")] [Range(0f, 1f)] public float rightFootPhaseWalk = 0.55f; [Tooltip("Фаза шага левой ногой при спринте")] [Range(0f, 1f)] public float leftFootPhaseSprint = 0.12f; [Tooltip("Фаза шага правой ногой при спринте")] [Range(0f, 1f)] public float rightFootPhaseSprint = 0.62f; [Header("Резервный режим (по дистанции)")] [Tooltip("Используется только если аниматор не найден или выключена синхронизация")] public float stepDistance = 1.5f; public float waveSpeed = 15f; [Header("Радиусы звука (Шум)")] public float sneakRadius = 5f; public float walkRadius = 15f; public float runRadius = 30f; [Header("Яркость волны (Интенсивность)")] public float sneakIntensity = 0.3f; // Тусклый свет для стелса public float walkIntensity = 1.0f; // Обычный свет для шага public float runIntensity = 3.0f; // Очень яркая вспышка для бега [Header("Звуки шагов (Опционально)")] public AudioSource audioSource; public AudioClip[] footstepSounds; [Range(0f, 1f)] public float footstepVolume = 0.6f; [Header("Реакция мутантов")] public bool alertMutants = true; [Header("Акустическое ослепление (Босс)")] private float blindnessTimer = 0f; public bool IsBlinded => blindnessTimer > 0f; /// /// Полностью глушит эхолокацию: шум шагов, выстрелы и броски не создают волн /// public void ApplyBlindness(float duration) { blindnessTimer = Mathf.Max(blindnessTimer, duration); for (int i = 0; i < MAX_WAVES; i++) { waveActive[i] = false; SetFloatOnAllEchoMaterials("_EchoRadius" + (i + 1), 0f); SetFloatOnAllEchoMaterials("_EchoOpacity" + (i + 1), 0f); } } // Пул из 3 одновременных волн private const int MAX_WAVES = 3; private float[] waveRadii = new float[MAX_WAVES]; private float[] waveMaxRadii = new float[MAX_WAVES]; private float[] waveIntensities = new float[MAX_WAVES]; private bool[] waveActive = new bool[MAX_WAVES]; public static EchoTrigger Instance; private Vector3 lastPosition; private float distanceMoved = 0f; private bool wasMoving = false; private int lastLeftStepCycle = int.MinValue; private int lastRightStepCycle = int.MinValue; private PlayerNoise playerNoise; private readonly List allEchoMaterials = new List(); void Awake() { if (Instance == null) Instance = this; } /// /// Активна ли прямо сейчас хоть одна световая волна эхолокации (отрисовывается ли шейдер) /// public bool HasActiveWave { get { for (int i = 0; i < MAX_WAVES; i++) { if (waveActive[i] && waveRadii[i] > 0.05f) return true; } return false; } } /// /// Издает ли игрок шум или движется ли он прямо сейчас (шаги, клавиши, активные волны) /// public bool IsPlayerMakingNoise() { // 1. Отрисовывается ли прямо сейчас волна эхолокации в шейдере if (HasActiveWave) return true; // 2. Движется ли контроллер персонажа if (characterController != null && characterController.velocity.sqrMagnitude > 0.05f) return true; // 3. Активно ли перемещение в аниматоре if (playerAnimator != null && playerAnimator.GetBool("IsMoving")) return true; // 4. Зажаты ли клавиши перемещения var keyboard = Keyboard.current; if (keyboard != null && (keyboard.wKey.isPressed || keyboard.aKey.isPressed || keyboard.sKey.isPressed || keyboard.dKey.isPressed)) return true; return false; } void Start() { CollectAllEchoMaterials(); for (int i = 0; i < MAX_WAVES; i++) { SetFloatOnAllEchoMaterials("_EchoRadius" + (i + 1), 0f); SetFloatOnAllEchoMaterials("_EchoOpacity" + (i + 1), 0f); SetVectorOnAllEchoMaterials("_EchoPosition" + (i + 1), Vector4.zero); } lastPosition = transform.position; if (characterController == null) characterController = GetComponent(); if (playerAnimator == null) playerAnimator = GetComponentInChildren(); if (playerNoise == null) playerNoise = GetComponent(); if (audioSource == null) audioSource = GetComponent(); FindFootBones(); // Начальная ознакомительная волна при старте сцены, чтобы игрок сразу увидел комнату Vector3 startWavePos = transform.position; if (characterController != null) { float floorY = transform.position.y + characterController.center.y - (characterController.height * 0.5f); startWavePos.y = floorY + 0.05f; } TriggerExternalWave(startWavePos, walkRadius, 1.0f); } public void CollectAllEchoMaterials() { allEchoMaterials.Clear(); if (echoMaterial != null && !allEchoMaterials.Contains(echoMaterial)) allEchoMaterials.Add(echoMaterial); Renderer[] allRenderers = Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); foreach (var r in allRenderers) { if (r == null || r.sharedMaterials == null) continue; foreach (var m in r.sharedMaterials) { if (m != null && m.shader != null && (m.shader.name == "Custom/EchoCloudShader" || m.shader.name == "Custom/EchoCloudShader_Glass")) { if (!allEchoMaterials.Contains(m)) allEchoMaterials.Add(m); } } } } public void SetFloatOnAllEchoMaterials(string propName, float value) { Shader.SetGlobalFloat(propName, value); for (int i = 0; i < allEchoMaterials.Count; i++) { if (allEchoMaterials[i] != null) allEchoMaterials[i].SetFloat(propName, value); } } public void SetVectorOnAllEchoMaterials(string propName, Vector4 value) { Shader.SetGlobalVector(propName, value); for (int i = 0; i < allEchoMaterials.Count; i++) { if (allEchoMaterials[i] != null) allEchoMaterials[i].SetVector(propName, value); } } public void FindFootBones() { if (playerAnimator == null) return; if (playerAnimator.isHuman) { if (leftFootBone == null) leftFootBone = playerAnimator.GetBoneTransform(HumanBodyBones.LeftFoot); if (rightFootBone == null) rightFootBone = playerAnimator.GetBoneTransform(HumanBodyBones.RightFoot); } // Резервный поиск по имени, если аватар Generic или кости не замаплены if (leftFootBone == null || rightFootBone == null) { Transform[] allChildren = playerAnimator.GetComponentsInChildren(); foreach (var t in allChildren) { string lower = t.name.ToLower(); if (leftFootBone == null && (lower.Contains("l_foot") || lower.Contains("foot_l") || lower.Contains("leftfoot"))) leftFootBone = t; if (rightFootBone == null && (lower.Contains("r_foot") || lower.Contains("foot_r") || lower.Contains("rightfoot"))) rightFootBone = t; } } } void Update() { if (blindnessTimer > 0f) { blindnessTimer -= Time.deltaTime; for (int i = 0; i < MAX_WAVES; i++) { SetFloatOnAllEchoMaterials("_EchoOpacity" + (i + 1), 0f); } } HandleStepDetection(); HandleManualEchoPulse(); UpdateWaveShaderProgress(); } void HandleManualEchoPulse() { if (IsBlinded) return; // Возможность выпустить эхолокационный импульс (Space, Q или ПКМ) var keyboard = Keyboard.current; var mouse = Mouse.current; bool pulseKey = (keyboard != null && (keyboard.spaceKey.wasPressedThisFrame || keyboard.qKey.wasPressedThisFrame)) || (mouse != null && mouse.rightButton.wasPressedThisFrame); if (pulseKey) { Vector3 origin = transform.position; if (characterController != null) { origin.y = transform.position.y + characterController.center.y - (characterController.height * 0.5f) + 0.05f; } TriggerExternalWave(origin, walkRadius * 1.2f, 1.2f); } } void HandleStepDetection() { // Всегда отслеживаем физическое перемещение Vector3 currentPos = new Vector3(transform.position.x, 0, transform.position.z); Vector3 lastPos = new Vector3(lastPosition.x, 0, lastPosition.z); float delta = Vector3.Distance(currentPos, lastPos); distanceMoved += delta; lastPosition = transform.position; bool stepTriggeredByAnimation = false; if (syncWithAnimation && playerAnimator != null) { stepTriggeredByAnimation = CheckAnimationSteps(); } // Если шаг был по анимации — сбрасываем счетчик дистанции if (stepTriggeredByAnimation) { distanceMoved = 0f; } // Резерв: если по какой-то причине аниматор не сработал за stepDistance метров движения else if (distanceMoved >= stepDistance) { distanceMoved = 0f; TriggerFootstep(true); } } bool CheckAnimationSteps() { AnimatorStateInfo stateInfo = playerAnimator.GetCurrentAnimatorStateInfo(0); bool isMoving = playerAnimator.GetBool("IsMoving"); float animSpeed = playerAnimator.GetFloat("Speed"); bool inLocomotion = stateInfo.IsName("Locomotion") || playerAnimator.IsInTransition(0); if (isMoving && animSpeed > 0.05f && inLocomotion) { float speedRatio = Mathf.InverseLerp(1.5f, 6.0f, animSpeed); float targetLeftPhase = Mathf.Lerp(leftFootPhaseWalk, leftFootPhaseSprint, speedRatio); float targetRightPhase = Mathf.Lerp(rightFootPhaseWalk, rightFootPhaseSprint, speedRatio); float currTime = stateInfo.normalizedTime; if (!wasMoving) { lastLeftStepCycle = Mathf.FloorToInt(currTime - targetLeftPhase); lastRightStepCycle = Mathf.FloorToInt(currTime - targetRightPhase); wasMoving = true; return false; } bool stepped = false; int leftCycle = Mathf.FloorToInt(currTime - targetLeftPhase); if (leftCycle > lastLeftStepCycle) { lastLeftStepCycle = leftCycle; TriggerFootstep(true); stepped = true; } int rightCycle = Mathf.FloorToInt(currTime - targetRightPhase); if (rightCycle > lastRightStepCycle) { lastRightStepCycle = rightCycle; TriggerFootstep(false); stepped = true; } return stepped; } else { wasMoving = false; return false; } } public void TriggerStepWave() { TriggerFootstep(true); } void TriggerFootstep(bool isLeftFoot) { Vector3 spawnPos = transform.position; if (spawnAtFootBone) { Transform foot = isLeftFoot ? leftFootBone : rightFootBone; if (foot != null) { spawnPos = foot.position; } } // Привязываем высоту волны точно к уровню пола под персонажем if (characterController != null) { float floorY = transform.position.y + characterController.center.y - (characterController.height * 0.5f); spawnPos.y = floorY + 0.05f; } // Определяем режим передвижения (Sneak / Walk / Run) через InputSystem var keyboard = Keyboard.current; bool isRunning = keyboard != null && keyboard.leftShiftKey.isPressed; bool isSneaking = keyboard != null && (keyboard.leftCtrlKey.isPressed || keyboard.cKey.isPressed); if (playerAnimator != null) { float speedVal = playerAnimator.GetFloat("Speed"); if (speedVal > 4.5f) isRunning = true; else if (speedVal > 0f && speedVal < 2.0f && !isRunning) isSneaking = true; } float targetMax = walkRadius; float targetIntensity = walkIntensity; if (isRunning) { targetMax = runRadius; targetIntensity = runIntensity; } else if (isSneaking) { targetMax = sneakRadius; targetIntensity = sneakIntensity; } // Запуск световой волны эхолокации в шейдере TriggerExternalWave(spawnPos, targetMax, targetIntensity); // Воспроизведение звука шага (если задан) if (audioSource != null && footstepSounds != null && footstepSounds.Length > 0) { AudioClip clip = footstepSounds[Random.Range(0, footstepSounds.Length)]; if (clip != null) { float vol = isSneaking ? footstepVolume * 0.4f : (isRunning ? footstepVolume * 1.3f : footstepVolume); audioSource.PlayOneShot(clip, vol); } } // Синхронизация с PlayerNoise или прямое оповещение мутантов if (playerNoise != null) { playerNoise.EmitFootstep(spawnPos, targetMax); } else if (alertMutants) { Collider[] colliders = Physics.OverlapSphere(spawnPos, targetMax); foreach (Collider hit in colliders) { BlindMutantAI mutant = hit.GetComponent(); if (mutant != null) mutant.HearSound(spawnPos); } } } void UpdateWaveShaderProgress() { if (allEchoMaterials.Count == 0) { CollectAllEchoMaterials(); if (allEchoMaterials.Count == 0 && echoMaterial == null) return; } for (int i = 0; i < MAX_WAVES; i++) { if (waveActive[i]) { waveRadii[i] += waveSpeed * Time.deltaTime; float currentRadius = waveRadii[i]; float maxRadius = waveMaxRadii[i]; float progress = Mathf.Clamp01(currentRadius / maxRadius); // Быстрое появление: уже к 0.4м (за 0.025с) волна на 100% яркости у ног float fadeIn = Mathf.Clamp01(currentRadius / 0.4f); // Плавное рассеивание к концу дистанции float fadeOut = Mathf.Pow(1f - progress, 1.2f); float opacity = (fadeIn * fadeOut) * waveIntensities[i]; opacity = Mathf.Max(0f, opacity); SetFloatOnAllEchoMaterials("_EchoRadius" + (i + 1), currentRadius); SetFloatOnAllEchoMaterials("_EchoOpacity" + (i + 1), opacity); if (currentRadius >= maxRadius) { waveActive[i] = false; SetFloatOnAllEchoMaterials("_EchoRadius" + (i + 1), 0f); SetFloatOnAllEchoMaterials("_EchoOpacity" + (i + 1), 0f); } } } } public void TriggerExternalWave(Vector3 position, float targetMax, float targetIntensity) { if (IsBlinded) return; if (allEchoMaterials.Count == 0) { CollectAllEchoMaterials(); if (allEchoMaterials.Count == 0 && echoMaterial == null) return; } // Ищем свободный слот int waveIndex = -1; for (int i = 0; i < MAX_WAVES; i++) { if (!waveActive[i]) { waveIndex = i; break; } } // Если все слоты заняты, берем самую старую / дальнюю волну if (waveIndex == -1) { float maxRad = -1f; for (int i = 0; i < MAX_WAVES; i++) { if (waveRadii[i] > maxRad) { maxRad = waveRadii[i]; waveIndex = i; } } } // Назначаем волне её радиус и яркость waveMaxRadii[waveIndex] = targetMax; waveIntensities[waveIndex] = targetIntensity; waveRadii[waveIndex] = 0.05f; // Начинаем чуть выше 0, чтобы сразу вступала в силу waveActive[waveIndex] = true; // Начальная установка координат и стартовой прозрачности SetFloatOnAllEchoMaterials("_EchoRadius" + (waveIndex + 1), 0.05f); SetFloatOnAllEchoMaterials("_EchoOpacity" + (waveIndex + 1), 0.2f * targetIntensity); SetVectorOnAllEchoMaterials("_EchoPosition" + (waveIndex + 1), new Vector4(position.x, position.y, position.z, 1.0f)); } }