Afterparty/Assets/Scripts/Player/PlayerController.cs
2026-01-11 17:04:23 +03:00

346 lines
14 KiB
C#
Raw Permalink 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.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);
}
}
}