Souljack/Assets/_Project/Scripts/PlayerController.cs

547 lines
18 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("Оружие и Стрельба (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<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();
// Стрельба (ЛКМ) через 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<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();
}
}
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<IDamageable>(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);
}