using System.Collections.Generic; using UnityEngine; using UnityEngine.Audio; namespace Player { [RequireComponent(typeof(CharacterController))] public class PlayerController : MonoBehaviour { [Header("Movement Settings")] [SerializeField] private float walkSpeed = 5f; [SerializeField] private float crouchSpeed = 2.5f; [SerializeField] private float gravity = -9.81f; [Header("Look Settings")] public float mouseSensitivity = 2f; [SerializeField] private float lookXLimit = 80f; [Header("Crouch Settings")] [SerializeField] private float crouchHeight = 1f; [SerializeField] private float standHeight = 1.8f; [SerializeField] private float crouchTransitionSpeed = 10f; [SerializeField] Transform cameraRoot; [Header("Lean Settings (Q/E)")] [SerializeField] private float leanAngle = 15f; [SerializeField] private float leanOffset = 0.5f; [SerializeField] private float leanSpeed = 5f; [HideInInspector] public bool inputLocked = false; [Header("Audio Settings")] [SerializeField] private AudioSource footstepSource; [Tooltip("Расстояние между шагами при ходьбе (в метрах)")] [SerializeField] private float walkStepDistance = 2.0f; [Tooltip("Расстояние между шагами при присяде (в метрах)")] [SerializeField] private float crouchStepDistance = 1.5f; [SerializeField] private AudioMixerGroup footstepMixerGroup; // NEW: Reference to the mixer group [SerializeField] private float walkStepInterval = 0.5f; [SerializeField] private float crouchStepInterval = 0.8f; [System.Serializable] public struct SurfaceLayer { public string surfaceName; public AudioClip footstepSound; } [SerializeField] private List surfaceLayers; [SerializeField] private AudioClip defaultSound; private float _distanceCovered; private CharacterController _characterController; private Vector3 _velocity; private Vector2 _rotation = Vector2.zero; private float _currentHeight; private Vector3 _lastPosition; // Стейты private bool _isCrouching; // Ссылка на активный триггер скрипа private CreakSoundTrigger _activeCreakTrigger; private float _currentLean; private void Awake() { _characterController = GetComponent(); if (footstepSource == null) footstepSource = GetComponent(); // NEW: Assign the mixer group to the AudioSource if (footstepSource != null && footstepMixerGroup != null) { footstepSource.outputAudioMixerGroup = footstepMixerGroup; } else { Debug.LogError("Footstep AudioSource or MixerGroup is not assigned!", this); } } void Start() { _characterController = GetComponent(); _currentHeight = standHeight; _lastPosition = transform.position; // Скрываем курсор Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; } void Update() { if (inputLocked) { HandleLean(true); return; } HandleMovement(); HandleMouseLook(); HandleCrouch(); HandleLean(false); HandleFootsteps(); } private void HandleMovement() { // Определяем, на земле ли мы bool isGrounded = _characterController.isGrounded; if (isGrounded && _velocity.y < 0) { _velocity.y = -2f; } // Ввод WASD float moveX = Input.GetAxis("Horizontal"); float moveZ = Input.GetAxis("Vertical"); // Выбор скорости (идем или крадемся) float speed = _isCrouching ? crouchSpeed : walkSpeed; // Вектор движения относительно направления взгляда Vector3 move = transform.right * moveX + transform.forward * moveZ; // Применяем движение _characterController.Move(move * (speed * Time.deltaTime)); // Применяем гравитацию _velocity.y += gravity * Time.deltaTime; _characterController.Move(_velocity * Time.deltaTime); } private void HandleMouseLook() { _rotation.y += Input.GetAxis("Mouse X") * mouseSensitivity; _rotation.x += -Input.GetAxis("Mouse Y") * mouseSensitivity; // Ограничение взгляда вверх/вниз _rotation.x = Mathf.Clamp(_rotation.x, -lookXLimit, lookXLimit); // Вращаем тело персонажа по Y (влево-вправо) transform.localRotation = Quaternion.Euler(0, _rotation.y, 0); // Вращаем камеру по X (вверх-вниз). // ВАЖНО: Вращаем CameraRoot, а не саму камеру, чтобы не ломать наклоны cameraRoot.localRotation = Quaternion.Euler(_rotation.x, 0, 0); } private void HandleCrouch() { // Левый Ctrl if (Input.GetKeyDown(KeyCode.LeftControl)) _isCrouching = true; if (Input.GetKeyUp(KeyCode.LeftControl)) _isCrouching = false; // Вычисляем целевую высоту float targetHeight = _isCrouching ? crouchHeight : standHeight; // Плавное изменение высоты контроллера (чтобы не проваливаться сквозь пол, меняем center) // Но проще менять высоту контроллера и позицию камеры _currentHeight = Mathf.Lerp(_currentHeight, targetHeight, Time.deltaTime * crouchTransitionSpeed); _characterController.height = _currentHeight; // Корректируем центр контроллера, чтобы ноги оставались на земле _characterController.center = new Vector3(0, _currentHeight / 2, 0); // Корректируем высоту камеры (глаз) // Когда стоим - камера высоко, когда сидим - ниже. // Предполагаем, что глаза находятся чуть ниже макушки float targetCamY = _currentHeight - 0.2f; Vector3 camPos = cameraRoot.localPosition; camPos.y = targetCamY; // Тут интерполяцию можно не делать, так как height интерполируется выше, но для подстраховки: cameraRoot.localPosition = camPos; } private void HandleLean(bool forceReset) { float targetLean = 0f; if (!forceReset) { if (Input.GetKey(KeyCode.Q)) targetLean = 1f; else if (Input.GetKey(KeyCode.E)) targetLean = -1f; } // Плавный переход к цели _currentLean = Mathf.MoveTowards(_currentLean, targetLean, leanSpeed * Time.deltaTime); Quaternion lookRotation = Quaternion.Euler(_rotation.x, 0, 0); Quaternion leanRotation = Quaternion.Euler(0, 0, _currentLean * leanAngle); cameraRoot.localRotation = lookRotation * leanRotation; // Рассчитываем наклон и смещение // Вращение по Z (Roll) // Нам нужно применить наклон поверх взгляда вверх-вниз. // Мы знаем, что в HandleMouseLook мы задаем (rotation.x, 0, 0). // Добавим к этому Z поворот. cameraRoot.localRotation = Quaternion.Euler(_rotation.x, 0, _currentLean * leanAngle); // Смещение позиции (чтобы голова реально сдвигалась, а не просто крутилась) // Сдвигаем дочернюю камеру, а не рут, или сам рут локально // Лучше сдвигать CameraRoot локально по X Vector3 currentPos = cameraRoot.localPosition; float targetX = -_currentLean * leanOffset; currentPos.x = targetX; cameraRoot.localPosition = currentPos; } private void HandleFootsteps() { if (!_characterController.isGrounded) return; // перемещение по горизонтали Vector3 currentPosFlat = new Vector3(transform.position.x, 0, transform.position.z); Vector3 lastPosFlat = new Vector3(_lastPosition.x, 0, _lastPosition.z); float distanceMoved = Vector3.Distance(currentPosFlat, lastPosFlat); _lastPosition = transform.position; // Если мы вообще двигались, добавляем пройденное расстояние к счетчику if (distanceMoved > 0) { _distanceCovered += distanceMoved; } float currentInterval = _isCrouching ? crouchStepInterval : walkStepInterval; var _stepTimer = Time.deltaTime; // Определяем, какое расстояние нужно пройти для следующего шага float stepDistance = _isCrouching ? crouchStepDistance : walkStepDistance; // Если накопленное расстояние превышает дистанцию шага if (_distanceCovered >= stepDistance) { PlayFootstepSound(); _distanceCovered -= stepDistance; // Вычитаем дистанцию шага, а не сбрасываем в 0 } } private void PlayFootstepSound() { if (_activeCreakTrigger != null && _activeCreakTrigger.CreakSound != null) { PlaySound(_activeCreakTrigger.CreakSound); return; // Выходим, чтобы не проигрывать обычный звук шага } RaycastHit hit; if (Physics.Raycast(transform.position + Vector3.up * 0.5f, Vector3.down, out hit, 2.0f)) { string keyName = ""; Terrain terrain = hit.collider.GetComponent(); if (terrain != null) { keyName = GetDominantTextureName(transform.position, terrain); } else { keyName = hit.collider.tag; } // Ищем звук SurfaceLayer foundLayer = surfaceLayers.Find(layer => layer.surfaceName == keyName); if (foundLayer.footstepSound != null) { PlaySound(foundLayer.footstepSound); } else { if (defaultSound != null) PlaySound(defaultSound); } } } private string GetDominantTextureName(Vector3 playerPos, Terrain terrain) { TerrainData terrainData = terrain.terrainData; Vector3 terrainPos = terrain.transform.position; // 1. Переводим координаты игрока в координаты карты текстур (Alphamap) int mapX = (int)(((playerPos.x - terrainPos.x) / terrainData.size.x) * terrainData.alphamapWidth); int mapZ = (int)(((playerPos.z - terrainPos.z) / terrainData.size.z) * terrainData.alphamapHeight); // 2. Получаем смешивание текстур в этой точке (3-мерный массив [z, x, layer]) float[,,] splatmapData = terrainData.GetAlphamaps(mapX, mapZ, 1, 1); // 3. Ищем индекс самой "сильной" текстуры float maxMix = 0; int maxIndex = 0; // Проходимся по всем слоям текстур for (int i = 0; i < terrainData.alphamapLayers; i++) { if (splatmapData[0, 0, i] > maxMix) { maxMix = splatmapData[0, 0, i]; maxIndex = i; } } // 4. Возвращаем имя слоя (Terrain Layer Name) return terrainData.terrainLayers[maxIndex].name; } private void PlaySound(AudioClip clip) { if (clip == null) return; footstepSource.pitch = Random.Range(0.95f, 1.05f); footstepSource.volume = Random.Range(0.9f, 1.0f); footstepSource.PlayOneShot(clip); } private void OnTriggerEnter(Collider other) { if (other.TryGetComponent(out var creakTrigger)) { _activeCreakTrigger = creakTrigger; } } private void OnTriggerExit(Collider other) { if (other.TryGetComponent(out var creakTrigger) && _activeCreakTrigger == creakTrigger) { _activeCreakTrigger = null; } } public void PlayOneShotSound(AudioClip clip) { if (clip != null) footstepSource.PlayOneShot(clip); } } }