Files
Learning-UnityNGO/Assets/CONTENT/FEATURES/DEV-TEST/Scripts/PlayerMovement.cs
T
2026-07-02 17:04:29 +03:00

222 lines
9.2 KiB
C#

using UnityEngine;
/// <summary>
/// Отзывчивый аркадный FPS-контроллер на CharacterController.
/// Move() вызывается в Update — движение плавное на частоте кадров без интерполяции.
/// Step offset и slope limit — встроенные в CharacterController.
/// Init() вызывается из PlayerFacade только у владельца.
/// </summary>
[RequireComponent(typeof(CharacterController))]
public class PlayerMovement : MonoBehaviour
{
[Header("Speed")]
[SerializeField] private float walkSpeed = 8f;
[SerializeField] private float sprintSpeed = 12f;
[Header("Acceleration")]
[SerializeField] private float groundAcceleration = 80f; // как быстро набираем скорость на земле
[SerializeField] private float airAcceleration = 30f; // управление в воздухе
[SerializeField] private float groundDeceleration = 60f; // трение/торможение при отпускании клавиш
[Header("Jump")]
[SerializeField] private float jumpHeight = 1.6f; // желаемая высота прыжка в метрах
[SerializeField] private float coyoteTime = 0.12f; // окно прыжка после схода с края
[SerializeField] private float jumpBufferTime = 0.12f; // окно "запоминания" нажатия прыжка
[Header("Gravity")]
[SerializeField] private float gravity = -28f; // выше реального -9.81 = более аркадно/отзывчиво
[SerializeField] private float fallMultiplier = 1.6f; // падаем тяжелее — меньше "плавучести"
[SerializeField] private float lowJumpMultiplier = 2.5f; // отпустил прыжок раньше = короткий прыжок
[SerializeField] private float maxFallSpeed = -40f; // ограничение скорости падения
[Header("Ground / Steps / Slopes")]
[SerializeField] private float stepOffset = 0.4f; // высота ступеньки на которую можно зайти
[SerializeField] private float slopeLimit = 50f; // макс. угол склона
[SerializeField] private float groundStickForce = -4f; // прижим к земле чтобы не отрывало на спусках
[Header("Look")]
[SerializeField] private float mouseSensitivity = 2f;
[SerializeField] private Transform cameraRoot;
[SerializeField] private float minPitch = -89f;
[SerializeField] private float maxPitch = 89f;
private CharacterController controller;
private PlayerHealth playerHealth;
private bool isActive;
// Горизонтальная скорость (x, z) — храним между кадрами для инерции
private Vector3 horizontalVelocity;
private float verticalVelocity;
// Look
private float pitch;
// Jump timers
private float coyoteCounter;
private float jumpBufferCounter;
public void Init()
{
controller = GetComponent<CharacterController>();
playerHealth = GetComponent<PlayerHealth>();
// Встроенные возможности CharacterController — задаём из инспектора
controller.stepOffset = stepOffset;
controller.slopeLimit = slopeLimit;
// // Прячем курсор — только у владельца, Init для не-владельца не вызывается
// Cursor.lockState = CursorLockMode.Locked;
// Cursor.visible = false;
isActive = true;
}
private void Update()
{
if (!isActive) return;
// Esc — освободить курсор (удобно в редакторе)
// if (Input.GetKeyDown(KeyCode.Escape))
// {
// Cursor.lockState = CursorLockMode.None;
// Cursor.visible = true;
// }
if (IsMovementBlocked())
{
// Мёртвый — CharacterController выключен (как коллайдер), двигать нельзя
horizontalVelocity = Vector3.zero;
verticalVelocity = 0f;
return;
}
HandleLookYaw(); // поворот тела по горизонтали — здесь, камера-ребёнок следует за ним
UpdateJumpTimers();
HandleHorizontal();
HandleJumpAndGravity();
// Контроллер могли выключить (смерть) между кадрами — Move на выключенном кинет ошибку
if (!controller.enabled) return;
// Один общий Move за кадр — и горизонталь, и вертикаль
Vector3 velocity = horizontalVelocity + Vector3.up * verticalVelocity;
controller.Move(velocity * Time.deltaTime);
}
private void LateUpdate()
{
if (!isActive) return;
// Наклон камеры (pitch) — после движения, чтобы не было микро-дрожи
cameraRoot.localRotation = Quaternion.Euler(pitch, 0f, 0f);
}
private bool IsMovementBlocked()
{
return playerHealth != null && playerHealth.isDead.Value;
}
// ── LOOK ──────────────────────────────────────────────
private void HandleLookYaw()
{
float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity;
transform.Rotate(Vector3.up * mouseX);
float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity;
pitch = Mathf.Clamp(pitch - mouseY, minPitch, maxPitch);
}
// ── HORIZONTAL MOVEMENT ───────────────────────────────
private void HandleHorizontal()
{
Vector3 input = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
input = Vector3.ClampMagnitude(input, 1f);
Vector3 wishDirection = transform.right * input.x + transform.forward * input.z;
bool isSprinting = Input.GetKey(KeyCode.LeftShift);
float targetSpeed = isSprinting ? sprintSpeed : walkSpeed;
bool grounded = controller.isGrounded;
if (wishDirection.sqrMagnitude > 0.01f)
{
// Есть ввод — разгоняемся к целевой скорости
float acceleration = grounded ? groundAcceleration : airAcceleration;
Vector3 targetVelocity = wishDirection * targetSpeed;
horizontalVelocity = Vector3.MoveTowards(
horizontalVelocity,
targetVelocity,
acceleration * Time.deltaTime
);
}
else if (grounded)
{
// Нет ввода на земле — тормозим до нуля (трение)
horizontalVelocity = Vector3.MoveTowards(
horizontalVelocity,
Vector3.zero,
groundDeceleration * Time.deltaTime
);
}
// В воздухе без ввода — сохраняем импульс (момент инерции)
}
// ── JUMP & GRAVITY ────────────────────────────────────
private void UpdateJumpTimers()
{
if (controller.isGrounded)
coyoteCounter = coyoteTime;
else
coyoteCounter -= Time.deltaTime;
if (Input.GetButtonDown("Jump"))
jumpBufferCounter = jumpBufferTime;
else
jumpBufferCounter -= Time.deltaTime;
}
private void HandleJumpAndGravity()
{
bool grounded = controller.isGrounded;
// Прижим к земле — иначе на спусках и ступеньках вниз отрывает в "полёт"
if (grounded && verticalVelocity < 0f)
verticalVelocity = groundStickForce;
// Прыжок: есть буфер нажатия И есть coyote-окно
if (jumpBufferCounter > 0f && coyoteCounter > 0f)
{
jumpBufferCounter = 0f;
coyoteCounter = 0f;
// Скорость из желаемой высоты: v = sqrt(2 * h * g)
verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);
}
ApplyGravity(grounded);
}
private void ApplyGravity(bool grounded)
{
if (grounded && verticalVelocity <= 0f)
return; // на земле гравитацию не копим (кроме прыжка вверх)
float g = gravity;
if (verticalVelocity < 0f)
{
g *= fallMultiplier; // падаем — тяжелее
}
else if (verticalVelocity > 0f && !Input.GetButton("Jump"))
{
g *= lowJumpMultiplier; // отпустил прыжок — короткий хоп
}
verticalVelocity += g * Time.deltaTime;
verticalVelocity = Mathf.Max(verticalVelocity, maxFallSpeed);
}
}