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(); col = GetComponent(); 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(); if (audioSource == null) audioSource = gameObject.AddComponent(); } 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(); lookInput = controls.Player.Look.ReadValue(); // Таймер буфера прыжка 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); } }