488 lines
18 KiB
C#
488 lines
18 KiB
C#
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;
|
||
|
||
// Пул из 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()
|
||
{
|
||
HandleStepDetection();
|
||
HandleManualEchoPulse();
|
||
UpdateWaveShaderProgress();
|
||
}
|
||
|
||
void HandleManualEchoPulse()
|
||
{
|
||
// Возможность выпустить эхолокационный импульс (Space, E, Q или ПКМ)
|
||
var keyboard = Keyboard.current;
|
||
var mouse = Mouse.current;
|
||
bool pulseKey = (keyboard != null && (keyboard.spaceKey.wasPressedThisFrame || keyboard.eKey.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 (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));
|
||
}
|
||
} |