using UnityEngine; using UnityEngine.InputSystem; namespace _Project.Scripts.PlayerUtils { [RequireComponent(typeof(Rigidbody), typeof(CapsuleCollider))] public class PlayerMovement : MonoBehaviour { [Header("Скорости передвижения (м/с)")] [SerializeField] private float moveSpeed = 6.5f; [SerializeField] private float sprintSpeed = 9.5f; [SerializeField] private float crouchSpeed = 3f; [Header("Параметры Подката (Slide)")] [SerializeField] private bool canSlide = true; [SerializeField] private float slideInitialSpeed = 15f; [SerializeField] private float slideDuration = 0.85f; [SerializeField] private float slideFriction = 2.8f; [SerializeField] private float slideCooldown = 0.35f; [Header("Физика CS / Source (Bunnyhop & Strafe)")] [SerializeField] private float groundFriction = 5f; [SerializeField] private float groundAcceleration = 10f; [SerializeField] private float airAcceleration = 80f; [SerializeField] private float airCap = 2.0f; [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; [SerializeField] private float jumpBufferTime = 0.12f; [Header("Коллайдер (Приседание)")] [SerializeField] private float standingHeight = 2f; [SerializeField] private float crouchingHeight = 1.05f; [SerializeField] private float crouchTransitionSpeed = 15f; private Rigidbody rb; private CapsuleCollider col; private PlayerControls controls; public bool IsGrounded { get; private set; } public bool IsSprinting { get; private set; } public bool IsCrouching { get; private set; } public bool IsSliding { get; private set; } public Vector2 MoveInput { get; private set; } public float CurrentSpeed => horizontalVelocity.magnitude; private bool wasGrounded; private float jumpBufferTimer = 0f; private Vector3 horizontalVelocity = Vector3.zero; private float verticalVelocity = 0f; private float slideTimer = 0f; private float slideCurrentSpeed = 0f; private Vector3 slideDirection = Vector3.zero; private float slideCooldownTimer = 0f; private void Awake() { rb = GetComponent(); col = GetComponent(); rb.useGravity = false; rb.freezeRotation = true; } private void Start() { controls = Player.Instance.Controls; controls.Player.Sprint.started += _ => IsSprinting = true; controls.Player.Sprint.canceled += _ => IsSprinting = false; controls.Player.Crouch.started += _ => OnCrouchPressed(); controls.Player.Crouch.canceled += _ => OnCrouchReleased(); controls.Player.Jump.performed += _ => QueueJump(); } public void ApplyProfile(BodyProfile profile) { moveSpeed = profile.moveSpeed; sprintSpeed = profile.sprintSpeed; crouchSpeed = profile.crouchSpeed; canSlide = profile.canSlide; slideInitialSpeed = profile.slideInitialSpeed; slideDuration = profile.slideDuration; airAcceleration = profile.airAcceleration; jumpHeight = profile.jumpHeight; gravity = profile.gravity; standingHeight = profile.standingHeight; if (!canSlide && IsSliding) StopSlide(); } public void AddVerticalImpulse(float velocityY) { verticalVelocity = velocityY; IsGrounded = false; } private void Update() { MoveInput = controls.Player.Move.ReadValue(); float dt = Time.unscaledDeltaTime; if (jumpBufferTimer > 0f) jumpBufferTimer -= dt; if (slideCooldownTimer > 0f) slideCooldownTimer -= dt; if (Mouse.current != null && Mathf.Abs(Mouse.current.scroll.y.ReadValue()) > 0.1f) QueueJump(); HandleColliderHeight(); } private void FixedUpdate() { CheckGrounded(); CalculateCustomGravity(); if (jumpBufferTimer > 0f && IsGrounded) ExecuteJump(); ApplyMovement(); float timeMult = Player.Instance.Possession != null ? Player.Instance.Possession.PlayerTimeMultiplier : 1f; rb.linearVelocity = new Vector3( horizontalVelocity.x * timeMult, verticalVelocity, horizontalVelocity.z * timeMult); wasGrounded = IsGrounded; } private void HandleColliderHeight() { bool isLowered = IsCrouching || IsSliding; float targetHeight = isLowered ? crouchingHeight : standingHeight; col.height = Mathf.Lerp(col.height, targetHeight, Time.unscaledDeltaTime * crouchTransitionSpeed); col.center = new Vector3(col.center.x, col.height * 0.5f, col.center.z); } private void QueueJump() => jumpBufferTimer = jumpBufferTime; private void ExecuteJump() { if (IsSliding) { horizontalVelocity = slideDirection * slideCurrentSpeed; StopSlide(); } verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity); jumpBufferTimer = 0f; Player.Instance.CameraController.TriggerJumpImpact(); Player.Instance.Sound.PlayJump(); } private void ApplyMovement() { 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; } Vector3 wishDir = (transform.forward * MoveInput.y + transform.right * MoveInput.x); wishDir.y = 0f; wishDir.Normalize(); float targetSpeed = IsCrouching ? crouchSpeed : (IsSprinting ? sprintSpeed : moveSpeed); if (IsGrounded) { if (jumpBufferTimer <= 0f) ApplyGroundFriction(); Accelerate(wishDir, targetSpeed, groundAcceleration); } 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; horizontalVelocity *= (Mathf.Max(speed - drop, 0f) / speed); } private void Accelerate(Vector3 wishDir, float wishSpeed, float accel) { float currentSpeed = Vector3.Dot(horizontalVelocity, wishDir); float addSpeed = Mathf.Max(wishSpeed - currentSpeed, 0f); horizontalVelocity += wishDir * Mathf.Min(accel * wishSpeed * Time.fixedDeltaTime, addSpeed); } private void AirAccelerate(Vector3 wishDir, float wishSpeed, float accel, float cap) { float currentSpeed = Vector3.Dot(horizontalVelocity, wishDir); float addSpeed = Mathf.Max(Mathf.Min(wishSpeed, cap) - currentSpeed, 0f); horizontalVelocity += wishDir * Mathf.Min(accel * wishSpeed * Time.fixedDeltaTime, addSpeed); } 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) { Player.Instance.Sound.PlayLand(); } if (IsGrounded && jumpBufferTimer <= 0f && 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 verticalVelocity += ((verticalVelocity < 0f) ? (gravity * fallMultiplier) : gravity) * Time.fixedDeltaTime; } private void OnCrouchPressed() { if (canSlide && 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; Player.Instance.Sound.PlaySlide(); } private void StopSlide() { IsSliding = false; slideCooldownTimer = slideCooldown; } } }