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; // Пул из 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; void Awake() { if (Instance == null) Instance = this; } void Start() { if (echoMaterial != null) { for (int i = 0; i < MAX_WAVES; i++) { echoMaterial.SetFloat("_EchoRadius" + (i + 1), 0f); echoMaterial.SetFloat("_EchoOpacity" + (i + 1), 0f); } } lastPosition = transform.position; if (characterController == null) characterController = GetComponent(); if (playerAnimator == null) playerAnimator = GetComponentInChildren(); if (playerNoise == null) playerNoise = GetComponent(); if (audioSource == null) audioSource = GetComponent(); FindFootBones(); } 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() { HandleStepDetection(); UpdateWaveShaderProgress(); } void HandleStepDetection() { if (syncWithAnimation && playerAnimator != null) { HandleAnimationSteps(); } else { HandleDistanceSteps(); } } void HandleAnimationSteps() { AnimatorStateInfo stateInfo = playerAnimator.GetCurrentAnimatorStateInfo(0); bool isMoving = playerAnimator.GetBool("IsMoving"); float animSpeed = playerAnimator.GetFloat("Speed"); bool isGrounded = characterController == null || characterController.isGrounded; // Синхронизируем шаги только когда персонаж движется по земле в режиме перемещения if (isMoving && animSpeed > 0.1f && isGrounded && stateInfo.IsName("Locomotion")) { 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; } else { int leftCycle = Mathf.FloorToInt(currTime - targetLeftPhase); if (leftCycle > lastLeftStepCycle) { lastLeftStepCycle = leftCycle; TriggerFootstep(true); } int rightCycle = Mathf.FloorToInt(currTime - targetRightPhase); if (rightCycle > lastRightStepCycle) { lastRightStepCycle = rightCycle; TriggerFootstep(false); } } } else { wasMoving = false; } } void HandleDistanceSteps() { Vector3 currentPos = new Vector3(transform.position.x, 0, transform.position.z); Vector3 lastPos = new Vector3(lastPosition.x, 0, lastPosition.z); distanceMoved += Vector3.Distance(currentPos, lastPos); lastPosition = transform.position; if (distanceMoved >= stepDistance) { distanceMoved = 0f; TriggerFootstep(true); } } 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) 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 (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); // Плавное появление (Fade-in): в первые 18% пути волна мягко разгорается (без резкой вспышки) float fadeIn = Mathf.SmoothStep(0f, 1f, progress / 0.18f); // Плавное рассеивание (Fade-out): к концу облако плавно растворяется в темноте float fadeOut = Mathf.Pow(1f - progress, 1.4f); float opacity = (fadeIn * fadeOut) * waveIntensities[i]; opacity = Mathf.Max(0f, opacity); echoMaterial.SetFloat("_EchoRadius" + (i + 1), currentRadius); echoMaterial.SetFloat("_EchoOpacity" + (i + 1), opacity); Shader.SetGlobalFloat("_EchoRadius" + (i + 1), currentRadius); Shader.SetGlobalFloat("_EchoOpacity" + (i + 1), opacity); if (currentRadius >= maxRadius) { waveActive[i] = false; echoMaterial.SetFloat("_EchoRadius" + (i + 1), 0f); echoMaterial.SetFloat("_EchoOpacity" + (i + 1), 0f); Shader.SetGlobalFloat("_EchoRadius" + (i + 1), 0f); Shader.SetGlobalFloat("_EchoOpacity" + (i + 1), 0f); } } } } public void TriggerExternalWave(Vector3 position, float targetMax, float targetIntensity) { if (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] = 0f; waveActive[waveIndex] = true; // Начинаем плавно с 0, чтобы разгореться через fadeIn echoMaterial.SetFloat("_EchoOpacity" + (waveIndex + 1), 0f); echoMaterial.SetVector("_EchoPosition" + (waveIndex + 1), position); Shader.SetGlobalFloat("_EchoOpacity" + (waveIndex + 1), 0f); Shader.SetGlobalVector("_EchoPosition" + (waveIndex + 1), position); } }