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("Оружие и Стрельба (Hitscan)")] [SerializeField] private float damage = 25f; [Tooltip("Задержка между выстрелами в секундах (0.1 = 600 выстр/мин)")] [SerializeField] private float fireRate = 0.12f; [SerializeField] private float weaponRange = 100f; [SerializeField] private LayerMask hitLayers = ~0; // По кому попадаем (по умолчанию всё) [SerializeField] private bool isAutomatic = true; [Tooltip("Сила вертикального пинка камеры при выстреле")] [SerializeField] private float recoilKick = 1.2f; [SerializeField] private ParticleSystem muzzleFlash; [SerializeField] private GameObject impactPrefab; // Префаб искр / дырки от пули [SerializeField] private AudioClip shootSound; [Header("Скорости передвижения (м/с)")] [SerializeField] private float moveSpeed = 6.5f; [SerializeField] private float sprintSpeed = 9.5f; [SerializeField] private float crouchSpeed = 3f; [Header("Параметры Подката (Slide)")] [SerializeField] private float slideInitialSpeed = 15f; [SerializeField] private float slideDuration = 0.85f; [SerializeField] private float slideFriction = 2.8f; [SerializeField] private float slideCameraTiltAngle = 4f; [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; [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; [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 shootTimer = 0f; private bool isShootingInput = false; // Камера 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(); // Стрельба (ЛКМ) через Input Actions controls.Player.Attack.started += ctx => isShootingInput = true; controls.Player.Attack.canceled += ctx => isShootingInput = false; } 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(); } } HandleShooting(); HandleMouseLook(); HandleCameraAndBobbing(); } private void FixedUpdate() { CheckGrounded(); CalculateCustomGravity(); if (jumpBufferTimer > 0f && isGrounded) { ExecuteJump(); } ApplyMovement(); rb.linearVelocity = new Vector3(horizontalVelocity.x, verticalVelocity, horizontalVelocity.z); wasGrounded = isGrounded; } #region Механика Стрельбы (Weapons) private void HandleShooting() { if (shootTimer > 0f) shootTimer -= Time.deltaTime; // Проверяем ввод: либо через Input Action, либо напрямую через Mouse (для надежности) bool shootPressed = isShootingInput || (Mouse.current != null && (isAutomatic ? Mouse.current.leftButton.isPressed : Mouse.current.leftButton.wasPressedThisFrame)); if (shootPressed && shootTimer <= 0f) { Shoot(); } } private void Shoot() { shootTimer = fireRate; // 1. Звук выстрела if (shootSound != null) audioSource.PlayOneShot(shootSound, 0.8f); // 2. Вспышка из ствола if (muzzleFlash != null) muzzleFlash.Play(); // 3. Отдача (вздергивание прицела вверх) cameraPitch -= recoilKick; // 4. Рейкаст строго из центра камеры Ray ray = new Ray(cameraPosition.position, cameraPosition.forward); if (Physics.Raycast(ray, out RaycastHit hit, weaponRange, hitLayers, QueryTriggerInteraction.Ignore)) { // Нанесение урона врагу if (hit.collider.TryGetComponent(out var damageable)) { damageable.TakeDamage(damage); } // Если попали в физ. объект — отталкиваем его импульсом пули else if (hit.rigidbody != null) { hit.rigidbody.AddForceAtPosition(-hit.normal * (damage * 10f), hit.point, ForceMode.Impulse); } // Спавн декали / эффекта удара пули if (impactPrefab != null) { GameObject impact = Instantiate(impactPrefab, hit.point + (hit.normal * 0.001f), Quaternion.LookRotation(hit.normal)); Destroy(impact, 3f); } } } #endregion 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() { 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; 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() { 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); 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); } } // Интерфейс для любых целей/врагов, принимающих урон public interface IDamageable { void TakeDamage(float amount); }