Kursach_StealthAction/Assets/Scripts/EchoTrigger.cs
2026-10-23 15:44:09 +03:00

519 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>
/// Полностью глушит эхолокацию: шум шагов, выстрелы и броски не создают волн
/// </summary>
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<Material> allEchoMaterials = new List<Material>();
void Awake()
{
if (Instance == null) Instance = this;
}
/// <summary>
/// Активна ли прямо сейчас хоть одна световая волна эхолокации (отрисовывается ли шейдер)
/// </summary>
public bool HasActiveWave
{
get
{
for (int i = 0; i < MAX_WAVES; i++)
{
if (waveActive[i] && waveRadii[i] > 0.05f) return true;
}
return false;
}
}
/// <summary>
/// Издает ли игрок шум или движется ли он прямо сейчас (шаги, клавиши, активные волны)
/// </summary>
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<CharacterController>();
if (playerAnimator == null)
playerAnimator = GetComponentInChildren<Animator>();
if (playerNoise == null)
playerNoise = GetComponent<PlayerNoise>();
if (audioSource == null)
audioSource = GetComponent<AudioSource>();
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<Renderer>(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<Transform>();
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<BlindMutantAI>();
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));
}
}