Souljack/Assets/_Project/Scripts/PlayerController.cs
2026-09-19 03:43:12 +03:00

490 lines
17 KiB
C#
Raw 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 UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(CapsuleCollider))]
public class PlayerController : MonoBehaviour
{
[Header("Камера и Мышь")]
[SerializeField] private Transform cameraPosition;
[SerializeField] private float mouseSensitivity = 0.1f;
[SerializeField] private float maxLookAngle = 80f;
[Header("Скорости передвижения (м/с)")]
[SerializeField] private float moveSpeed = 6.5f; // Обычный бег (~250 u/s)
[SerializeField] private float sprintSpeed = 9.5f; // Спринт (~360 u/s)
[SerializeField] private float crouchSpeed = 3f;
[Header("Параметры Подката (Slide)")]
[Tooltip("Начальная взрывная скорость подката")]
[SerializeField] private float slideInitialSpeed = 15f; // (~580 u/s)
[Tooltip("Длительность скольжения в секундах")]
[SerializeField] private float slideDuration = 0.85f;
[Tooltip("Трение коленей при подкате")]
[SerializeField] private float slideFriction = 2.8f;
[Tooltip("Наклон камеры набок при подкате (градусы)")]
[SerializeField] private float slideCameraTiltAngle = 4f;
[SerializeField] private float slideCooldown = 0.35f;
[Header("Физика CS / Source (Bunnyhop & Strafe)")]
[Tooltip("Трение на земле (гасит скорость, если не успели прыгнуть вовремя)")]
[SerializeField] private float groundFriction = 5f;
[Tooltip("Ускорение на земле")]
[SerializeField] private float groundAcceleration = 10f;
[Tooltip("Сила набора скорости в воздухе при стрейфах (sv_airaccelerate)")]
[SerializeField] private float airAcceleration = 80f;
[Tooltip("Лимит добавления скорости в воздухе")]
[SerializeField] private float airCap = 2.0f;
[Tooltip("Показывать спидометр на экране")]
[SerializeField] private bool showSpeedometer = true;
[Header("Гравитация и Прыжок")]
[SerializeField] private float jumpHeight = 1.35f;
[SerializeField] private float gravity = -25f;
[SerializeField] private float fallMultiplier = 1.8f;
[SerializeField] private LayerMask groundLayer;
[SerializeField] private float groundCheckDistance = 0.12f;
[Tooltip("Буфер прыжка (время поблажки для колесика мыши)")]
[SerializeField] private float jumpBufferTime = 0.12f;
[Header("Параметры приседания")]
[SerializeField] private float standingHeight = 2f;
[SerializeField] private float crouchingHeight = 1.05f;
[SerializeField] private float standingCameraY = 1.6f;
[SerializeField] private float crouchingCameraY = 0.8f;
[SerializeField] private float crouchTransitionSpeed = 15f;
[Header("Тряска Камеры (Headbob)")]
public bool enableHeadBob = true;
[SerializeField] private float walkBobSpeed = 12f;
[SerializeField] private float walkBobAmount = 0.04f;
[SerializeField] private float sprintBobSpeed = 15f;
[SerializeField] private float sprintBobAmount = 0.07f;
[SerializeField] private float impactRecoverySpeed = 6f;
[Header("Звуки (Audio)")]
[SerializeField] private AudioSource audioSource;
[SerializeField] private AudioClip[] footstepSounds;
[SerializeField] private AudioClip jumpSound;
[SerializeField] private AudioClip landSound;
[SerializeField] private AudioClip slideSound;
// Внутренние ссылки и ввод
private PlayerControls controls;
private Rigidbody rb;
private CapsuleCollider col;
private Vector2 moveInput;
private Vector2 lookInput;
private Vector3 horizontalVelocity = Vector3.zero;
private float verticalVelocity = 0f;
// Камера
private float cameraPitch = 0f;
private float yRotation = 0f;
private float currentCameraTilt = 0f;
// Состояния
private bool isGrounded;
private bool wasGrounded;
private bool isSprinting = false;
private bool isCrouching = false;
private float jumpBufferTimer = 0f;
// Подкат
private bool isSliding = false;
private float slideTimer = 0f;
private float slideCurrentSpeed = 0f;
private Vector3 slideDirection = Vector3.zero;
private float slideCooldownTimer = 0f;
// Headbob & Impact
private float baseCameraY;
private float bobTimer = 0f;
private Vector3 bobOffset = Vector3.zero;
private float currentImpactOffset = 0f;
private bool stepTaken = false;
private void Awake()
{
rb = GetComponent<Rigidbody>();
col = GetComponent<CapsuleCollider>();
mouseSensitivity = PlayerPrefs.GetFloat("MouseSensitivity", mouseSensitivity);
rb.useGravity = false;
rb.freezeRotation = true;
rb.interpolation = RigidbodyInterpolation.None;
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
if (cameraPosition == null && Camera.main != null)
cameraPosition = Camera.main.transform;
baseCameraY = standingCameraY;
yRotation = transform.eulerAngles.y;
if (audioSource == null)
{
audioSource = GetComponent<AudioSource>();
if (audioSource == null) audioSource = gameObject.AddComponent<AudioSource>();
}
controls = new PlayerControls();
// Спринт
controls.Player.Sprint.started += ctx => isSprinting = true;
controls.Player.Sprint.canceled += ctx => isSprinting = false;
// Присед и Подкат
controls.Player.Crouch.started += ctx => OnCrouchPressed();
controls.Player.Crouch.canceled += ctx => OnCrouchReleased();
// Прыжок на Пробел (через буфер)
controls.Player.Jump.performed += ctx => QueueJump();
}
private void OnEnable() => controls.Player.Enable();
private void OnDisable() => controls.Player.Disable();
private void Update()
{
moveInput = controls.Player.Move.ReadValue<Vector2>();
lookInput = controls.Player.Look.ReadValue<Vector2>();
// Таймер буфера прыжка
if (jumpBufferTimer > 0f)
jumpBufferTimer -= Time.deltaTime;
if (slideCooldownTimer > 0f)
slideCooldownTimer -= Time.deltaTime;
// ПРЫЖОК НА КОЛЁСИКО МЫШИ (вниз и вверх)
if (Mouse.current != null)
{
float scrollY = Mouse.current.scroll.y.ReadValue();
if (Mathf.Abs(scrollY) > 0.1f) // Крутанули вниз или вверх
{
QueueJump();
}
}
HandleMouseLook();
HandleCameraAndBobbing();
}
private void FixedUpdate()
{
CheckGrounded();
CalculateCustomGravity();
// Проверяем прыжок/бхоп (если зажат пробел или крутанули колесо)
if (jumpBufferTimer > 0f && isGrounded)
{
ExecuteJump();
}
// Движение (земля, воздух, подкат)
ApplyMovement();
// Применяем скорость в Rigidbody
rb.linearVelocity = new Vector3(horizontalVelocity.x, verticalVelocity, horizontalVelocity.z);
wasGrounded = isGrounded;
}
private void QueueJump()
{
jumpBufferTimer = jumpBufferTime;
}
#region Логика Подката (Slide)
private void OnCrouchPressed()
{
// Подкат: если бежим вперед, на земле и кулдаун прошел
if (isSprinting && isGrounded && moveInput.y > 0.2f && slideCooldownTimer <= 0f)
{
StartSlide();
}
else
{
isCrouching = true;
}
}
private void OnCrouchReleased()
{
isCrouching = false;
if (isSliding)
{
StopSlide();
}
}
private void StartSlide()
{
isSliding = true;
slideTimer = slideDuration;
slideCurrentSpeed = slideInitialSpeed;
// Направление подката берется от взгляда и нажатых клавиш
slideDirection = (transform.forward * moveInput.y + transform.right * moveInput.x).normalized;
if (slideDirection.sqrMagnitude < 0.01f) slideDirection = transform.forward;
if (slideSound != null)
audioSource.PlayOneShot(slideSound, 0.7f);
}
private void StopSlide()
{
isSliding = false;
slideCooldownTimer = slideCooldown;
}
#endregion
#region Физика Движения и CS Баннихоп
private void ApplyMovement()
{
// 1. ПОДКАT
if (isSliding)
{
slideCurrentSpeed = Mathf.Lerp(slideCurrentSpeed, crouchSpeed, Time.fixedDeltaTime * slideFriction);
slideTimer -= Time.fixedDeltaTime;
// Небольшое подруливание в подкате
Vector3 steerDir = (transform.forward * moveInput.y + transform.right * moveInput.x).normalized;
if (steerDir.sqrMagnitude > 0.01f)
slideDirection = Vector3.Slerp(slideDirection, steerDir, Time.fixedDeltaTime * 2.5f);
horizontalVelocity = slideDirection * slideCurrentSpeed;
if (slideTimer <= 0f || slideCurrentSpeed <= crouchSpeed + 0.5f)
{
StopSlide();
isCrouching = controls.Player.Crouch.IsPressed();
}
return;
}
// Вектор желаемого направления (WishDir)
Vector3 wishDir = (transform.forward * moveInput.y + transform.right * moveInput.x);
wishDir.y = 0f;
wishDir.Normalize();
float targetSpeed = isCrouching ? crouchSpeed : (isSprinting ? sprintSpeed : moveSpeed);
// 2. НА ЗЕМЛЕ
if (isGrounded)
{
// Если игрок делает баннихоп прямо сейчас — НЕ применяем трение пола!
if (jumpBufferTimer <= 0f)
{
ApplyGroundFriction();
}
Accelerate(wishDir, targetSpeed, groundAcceleration);
}
// 3. В ВОЗДУХЕ (Формула CS AirAccelerate)
else
{
AirAccelerate(wishDir, targetSpeed, airAcceleration, airCap);
}
}
private void ApplyGroundFriction()
{
float speed = horizontalVelocity.magnitude;
if (speed < 0.0001f)
{
horizontalVelocity = Vector3.zero;
return;
}
float drop = speed * groundFriction * Time.fixedDeltaTime;
float newSpeed = Mathf.Max(speed - drop, 0f);
horizontalVelocity *= (newSpeed / speed);
}
private void Accelerate(Vector3 wishDir, float wishSpeed, float accel)
{
float currentSpeed = Vector3.Dot(horizontalVelocity, wishDir);
float addSpeed = wishSpeed - currentSpeed;
if (addSpeed <= 0f) return;
float accelSpeed = accel * wishSpeed * Time.fixedDeltaTime;
accelSpeed = Mathf.Min(accelSpeed, addSpeed);
horizontalVelocity += wishDir * accelSpeed;
}
private void AirAccelerate(Vector3 wishDir, float wishSpeed, float accel, float cap)
{
float wishSpeedClamped = Mathf.Min(wishSpeed, cap);
float currentSpeedInWishDir = Vector3.Dot(horizontalVelocity, wishDir);
float addSpeed = wishSpeedClamped - currentSpeedInWishDir;
if (addSpeed <= 0f) return;
float accelSpeed = accel * wishSpeed * Time.fixedDeltaTime;
accelSpeed = Mathf.Min(accelSpeed, addSpeed);
// Добавляем поперечный вектор скорости для стрейфа
horizontalVelocity += wishDir * accelSpeed;
}
private void ExecuteJump()
{
// SLIDE-JUMP: Если прыгаем из подката — сохраняем скорость в воздух!
if (isSliding)
{
horizontalVelocity = slideDirection * slideCurrentSpeed;
StopSlide();
}
verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);
jumpBufferTimer = 0f; // Сбрасываем запрос на прыжок
if (enableHeadBob)
currentImpactOffset = 0.1f;
if (jumpSound != null)
audioSource.PlayOneShot(jumpSound, 0.7f);
}
#endregion
#region Камера, Земля и Аудио
private void CheckGrounded()
{
float checkRadius = col.radius * 0.85f;
float bottomOffset = (col.height * 0.5f) - checkRadius;
Vector3 rayOrigin = transform.position + col.center - (Vector3.up * bottomOffset) + (Vector3.up * 0.05f);
isGrounded = Physics.SphereCast(
rayOrigin, checkRadius, Vector3.down, out _, groundCheckDistance + 0.05f, groundLayer, QueryTriggerInteraction.Ignore
);
if (!wasGrounded && isGrounded)
{
if (landSound != null) audioSource.PlayOneShot(landSound, 0.5f);
}
if (isGrounded && jumpBufferTimer <= 0f)
{
if (rb.linearVelocity.magnitude > 0.01f && horizontalVelocity == Vector3.zero)
horizontalVelocity = new Vector3(rb.linearVelocity.x, 0f, rb.linearVelocity.z);
}
}
private void CalculateCustomGravity()
{
if (isGrounded && verticalVelocity < 0f)
{
verticalVelocity = -2f;
}
else
{
float currentGravity = (verticalVelocity < 0f) ? (gravity * fallMultiplier) : gravity;
verticalVelocity += currentGravity * Time.fixedDeltaTime;
}
}
private void HandleMouseLook()
{
// Вверх / Вниз
cameraPitch -= lookInput.y * mouseSensitivity;
cameraPitch = Mathf.Clamp(cameraPitch, -maxLookAngle, maxLookAngle);
// Наклон камеры в подкате (Camera Roll)
float targetTilt = isSliding ? -slideCameraTiltAngle : 0f;
currentCameraTilt = Mathf.Lerp(currentCameraTilt, targetTilt, Time.deltaTime * 10f);
cameraPosition.localRotation = Quaternion.Euler(cameraPitch, 0f, currentCameraTilt);
// Влево / Вправо
yRotation += lookInput.x * mouseSensitivity;
transform.rotation = Quaternion.Euler(0f, yRotation, 0f);
}
private void HandleCameraAndBobbing()
{
bool isLowered = isCrouching || isSliding;
float targetHeight = isLowered ? crouchingHeight : standingHeight;
float targetCamY = isLowered ? crouchingCameraY : standingCameraY;
col.height = Mathf.Lerp(col.height, targetHeight, Time.deltaTime * crouchTransitionSpeed);
col.center = new Vector3(col.center.x, col.height * 0.5f, col.center.z);
baseCameraY = Mathf.Lerp(baseCameraY, targetCamY, Time.deltaTime * crouchTransitionSpeed);
// В подкате и в воздухе шаги не трясут камеру
bool isMoving = moveInput.sqrMagnitude > 0.01f && isGrounded && !isSliding;
if (isMoving && enableHeadBob)
{
float speed = isSprinting ? sprintBobSpeed : walkBobSpeed;
float amount = isSprinting ? sprintBobAmount : walkBobAmount;
bobTimer += Time.deltaTime * speed;
bobOffset.x = Mathf.Cos(bobTimer * 0.5f) * amount;
bobOffset.y = Mathf.Sin(bobTimer) * amount;
if (Mathf.Sin(bobTimer) <= -0.9f && !stepTaken)
{
PlayFootstep();
stepTaken = true;
}
else if (Mathf.Sin(bobTimer) > -0.2f)
{
stepTaken = false;
}
}
else
{
bobTimer = 0f;
stepTaken = false;
bobOffset = Vector3.Lerp(bobOffset, Vector3.zero, Time.deltaTime * 10f);
}
currentImpactOffset = Mathf.Lerp(currentImpactOffset, 0f, Time.deltaTime * impactRecoverySpeed);
cameraPosition.localPosition = new Vector3(
bobOffset.x,
baseCameraY + bobOffset.y - currentImpactOffset,
0f
);
}
public void SetSensitivity(float newSens)
{
mouseSensitivity = newSens;
}
private void PlayFootstep()
{
if (footstepSounds == null || footstepSounds.Length == 0) return;
int randomIndex = Random.Range(0, footstepSounds.Length);
audioSource.pitch = Random.Range(0.9f, 1.1f);
audioSource.PlayOneShot(footstepSounds[randomIndex], isSprinting ? 0.6f : 0.4f);
}
#endregion
private void OnGUI()
{
if (!showSpeedometer) return;
float speed = horizontalVelocity.magnitude;
float csUnits = speed * 38.5f;
GUIStyle style = new GUIStyle();
style.fontSize = 24;
style.normal.textColor = isSliding ? Color.yellow : (isGrounded ? Color.white : Color.green);
style.fontStyle = FontStyle.Bold;
string state = isSliding ? " [SLIDE]" : (!isGrounded ? " [AIR]" : "");
GUI.Label(new Rect(30, 30, 500, 50), $"Speed: {speed:F1} m/s ({csUnits:F0} u/s){state}", style);
}
}