235 lines
9.5 KiB
C#
235 lines
9.5 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())
|
|
{
|
|
// Мёртвый стоит — гасим горизонталь, но гравитацию оставляем
|
|
horizontalVelocity = Vector3.zero;
|
|
ApplyGravityOnly();
|
|
return;
|
|
}
|
|
|
|
HandleLookYaw(); // поворот тела по горизонтали — здесь, камера-ребёнок следует за ним
|
|
UpdateJumpTimers();
|
|
HandleHorizontal();
|
|
HandleJumpAndGravity();
|
|
|
|
// Один общий 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);
|
|
}
|
|
|
|
// Используется когда движение заблокировано (смерть) — только падение
|
|
private void ApplyGravityOnly()
|
|
{
|
|
if (controller == null) return;
|
|
|
|
if (controller.isGrounded && verticalVelocity < 0f)
|
|
verticalVelocity = groundStickForce;
|
|
else
|
|
{
|
|
verticalVelocity += gravity * Time.deltaTime;
|
|
verticalVelocity = Mathf.Max(verticalVelocity, maxFallSpeed);
|
|
}
|
|
|
|
controller.Move(Vector3.up * verticalVelocity * Time.deltaTime);
|
|
}
|
|
} |