369 lines
14 KiB
C#
369 lines
14 KiB
C#
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<CharacterController>();
|
||
|
||
if (playerAnimator == null)
|
||
playerAnimator = GetComponentInChildren<Animator>();
|
||
|
||
if (playerNoise == null)
|
||
playerNoise = GetComponent<PlayerNoise>();
|
||
|
||
if (audioSource == null)
|
||
audioSource = GetComponent<AudioSource>();
|
||
|
||
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<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();
|
||
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<BlindMutantAI>();
|
||
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);
|
||
}
|
||
} |