346 lines
14 KiB
C#
346 lines
14 KiB
C#
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<SurfaceLayer> 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<CharacterController>();
|
||
|
||
if (footstepSource == null)
|
||
footstepSource = GetComponent<AudioSource>();
|
||
|
||
// 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<CharacterController>();
|
||
_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<Terrain>();
|
||
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<CreakSoundTrigger>(out var creakTrigger))
|
||
{
|
||
_activeCreakTrigger = creakTrigger;
|
||
}
|
||
}
|
||
|
||
private void OnTriggerExit(Collider other)
|
||
{
|
||
if (other.TryGetComponent<CreakSoundTrigger>(out var creakTrigger) && _activeCreakTrigger == creakTrigger)
|
||
{
|
||
_activeCreakTrigger = null;
|
||
}
|
||
}
|
||
|
||
public void PlayOneShotSound(AudioClip clip)
|
||
{
|
||
if (clip != null) footstepSource.PlayOneShot(clip);
|
||
}
|
||
}
|
||
} |