Реализовать движение персонажа с плавной интерполяцией
Условие
Реализуйте скрипт движения персонажа от третьего лица с плавным поворотом в направлении движения.
Требования
- Персонаж управляется клавишами WASD или стрелками
- Персонаж поворачивается плавно в направлении движения (используйте Quaternion.Slerp)
- Скорость движения должна быть независима от FPS (используйте Time.deltaTime)
- Добавьте возможность бега при зажатой клавише Shift
- Персонаж должен использовать CharacterController или Rigidbody
Бонус
- Добавьте анимации ходьбы и бега через Animator
- Реализуйте торможение при остановке
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Подход к движению персонажа
Плавное движение от третьего лица требует координации трёх компонентов: ввода, движения и поворота. Важно использовать Time.deltaTime для независимости от FPS, Quaternion.Slerp для плавного поворота и правильное разделение логики физики (FixedUpdate) и контроля (Update). Я предоставлю решение с Rigidbody, которое более гибко для игр с физикой.
Реализация с Rigidbody
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
[Header("Movement")]
[SerializeField] private float walkSpeed = 5f;
[SerializeField] private float runSpeed = 8f;
[SerializeField] private float groundDrag = 5f;
[Header("Rotation")]
[SerializeField] private float rotationSmoothTime = 0.1f; // Для Slerp интерполяции
[Header("Ground Check")]
[SerializeField] private float groundDrag_Ground = 5f;
[SerializeField] private float groundDrag_Air = 1f;
[SerializeField] private LayerMask groundLayer;
[SerializeField] private float groundCheckDistance = 0.2f;
[Header("Components")]
[SerializeField] private Animator animator;
[SerializeField] private Transform cameraTransform;
// Компоненты
private Rigidbody rb;
private CapsuleCollider capsuleCollider;
// Состояние движения
private Vector3 moveDirection;
private Vector3 currentVelocity;
private float currentSpeed;
private bool isRunning;
private bool isGrounded;
// Поворот
private Quaternion targetRotation;
private float rotationVelocity; // Для SmoothDamp угла поворота
private float currentYaw;
// Хеши Animator параметров
private int speedHash = Animator.StringToHash("Speed");
private int isRunningHash = Animator.StringToHash("IsRunning");
void Awake()
{
rb = GetComponent<Rigidbody>();
capsuleCollider = GetComponent<CapsuleCollider>();
// Настройка Rigidbody для управления
rb.freezeRotation = true; // Не позволяем физике вращать персонажа
}
void Update()
{
// Читаем инпут
HandleInput();
// Проверяем, находимся ли на земле
CheckGroundedStatus();
// Обновляем визуальный поворот
UpdateRotation();
// Обновляем анимацию
UpdateAnimation();
}
void FixedUpdate()
{
// Применяем движение (физика)
ApplyMovement();
}
/// <summary>Обработка инпута игрока</summary>
private void HandleInput()
{
// Получаем инпут с клавиатуры
float inputX = Input.GetAxis("Horizontal");
float inputZ = Input.GetAxis("Vertical");
// Создаём вектор движения относительно направления камеры
if (cameraTransform != null)
{
// Движение относительно камеры (камера смотрит в этом направлении)
Vector3 cameraForward = cameraTransform.forward;
Vector3 cameraRight = cameraTransform.right;
// Обнуляем Y компоненту для движения по земле
cameraForward.y = 0f;
cameraRight.y = 0f;
cameraForward.Normalize();
cameraRight.Normalize();
moveDirection = (cameraForward * inputZ + cameraRight * inputX).normalized;
}
else
{
// Движение в мировых координатах (если камеры нет)
moveDirection = new Vector3(inputX, 0f, inputZ).normalized;
}
// Проверяем зажата ли клавиша бега
isRunning = Input.GetKey(KeyCode.LeftShift) && moveDirection.magnitude > 0.1f;
// Вычисляем целевую скорость
currentSpeed = isRunning ? runSpeed : walkSpeed;
currentSpeed *= moveDirection.magnitude; // Снижаем скорость если направление неполное
}
/// <summary>Проверка касания земли</summary>
private void CheckGroundedStatus()
{
// Castline из дна капсули вниз
Vector3 rayStart = transform.position + Vector3.up * (capsuleCollider.radius - groundCheckDistance);
isGrounded = Physics.Raycast(
rayStart,
Vector3.down,
groundCheckDistance,
groundLayer
);
}
/// <summary>Применить движение через Rigidbody</summary>
private void ApplyMovement()
{
// Целевая скорость
Vector3 targetVelocity = moveDirection * currentSpeed;
targetVelocity.y = rb.velocity.y; // Сохраняем гравитацию
// Плавное переходит к целевой скорости
rb.velocity = Vector3.Lerp(rb.velocity, targetVelocity, Time.fixedDeltaTime * 10f);
// Применяем сопротивление при отсутствии инпута
if (moveDirection.magnitude < 0.1f && isGrounded)
{
rb.velocity = Vector3.Lerp(
rb.velocity,
new Vector3(0, rb.velocity.y, 0),
Time.fixedDeltaTime * groundDrag
);
}
}
/// <summary>Плавный поворот в сторону движения</summary>
private void UpdateRotation()
{
// Если нет движения, не поворачиваемся
if (moveDirection.magnitude < 0.1f)
{
return;
}
// Желаемый поворот в направлении движения
targetRotation = Quaternion.LookRotation(moveDirection);
// Интерполируем поворот Slerp (сферическая линейная интерполяция)
// Это обеспечивает плавный и естественный поворот
float rotationSpeed = 1f / rotationSmoothTime;
transform.rotation = Quaternion.Slerp(
transform.rotation,
targetRotation,
Time.deltaTime * rotationSpeed
);
}
/// <summary>Обновление параметров анимации</summary>
private void UpdateAnimation()
{
if (animator == null)
return;
// Нормализованная скорость (0-1) для анимации
float animationSpeed = currentSpeed / runSpeed;
animator.SetFloat(speedHash, animationSpeed);
animator.SetBool(isRunningHash, isRunning);
}
/// <summary>Получить текущую скорость персонажа</summary>
public float GetCurrentSpeed() => currentSpeed;
/// <summary>Проверить движется ли персонаж</summary>
public bool IsMoving() => moveDirection.magnitude > 0.1f;
/// <summary>Проверить находится ли на земле</summary>
public bool IsGrounded() => isGrounded;
}
Вариант с CharacterController
Если ты предпочитаешь CharacterController (более лёгкий и простой):
using UnityEngine;
public class PlayerMovementCharacterController : MonoBehaviour
{
[Header("Movement")]
[SerializeField] private float walkSpeed = 5f;
[SerializeField] private float runSpeed = 8f;
[SerializeField] private float groundDrag = 0.5f;
[Header("Jumping")]
[SerializeField] private float jumpHeight = 1f;
[SerializeField] private float gravity = -9.81f;
[Header("Rotation")]
[SerializeField] private float rotationSmoothTime = 0.1f;
[Header("Components")]
[SerializeField] private CharacterController characterController;
[SerializeField] private Animator animator;
[SerializeField] private Transform cameraTransform;
// Состояние
private Vector3 moveDirection;
private Vector3 velocity;
private float currentSpeed;
private bool isRunning;
private Quaternion targetRotation;
// Хеши Animator
private int speedHash = Animator.StringToHash("Speed");
private int isRunningHash = Animator.StringToHash("IsRunning");
void Awake()
{
if (characterController == null)
characterController = GetComponent<CharacterController>();
}
void Update()
{
HandleInput();
UpdateRotation();
ApplyMovement();
UpdateAnimation();
}
private void HandleInput()
{
float inputX = Input.GetAxis("Horizontal");
float inputZ = Input.GetAxis("Vertical");
// Движение относительно камеры
if (cameraTransform != null)
{
Vector3 cameraForward = cameraTransform.forward;
Vector3 cameraRight = cameraTransform.right;
cameraForward.y = 0f;
cameraRight.y = 0f;
cameraForward.Normalize();
cameraRight.Normalize();
moveDirection = (cameraForward * inputZ + cameraRight * inputX).normalized;
}
else
{
moveDirection = new Vector3(inputX, 0f, inputZ).normalized;
}
isRunning = Input.GetKey(KeyCode.LeftShift) && moveDirection.magnitude > 0.1f;
currentSpeed = isRunning ? runSpeed : walkSpeed;
currentSpeed *= moveDirection.magnitude;
}
private void UpdateRotation()
{
if (moveDirection.magnitude < 0.1f)
return;
targetRotation = Quaternion.LookRotation(moveDirection);
float rotationSpeed = 1f / rotationSmoothTime;
transform.rotation = Quaternion.Slerp(
transform.rotation,
targetRotation,
Time.deltaTime * rotationSpeed
);
}
private void ApplyMovement()
{
// Горизонтальное движение
Vector3 horizontalVelocity = moveDirection * currentSpeed;
// Вертикальное движение (гравитация)
velocity.y += gravity * Time.deltaTime;
if (characterController.isGrounded)
{
velocity.y = -0.1f; // Маленький отрицательный импульс для удержания на земле
}
// Применяем движение
characterController.Move((horizontalVelocity + new Vector3(0, velocity.y, 0)) * Time.deltaTime);
// Торможение при остановке
if (moveDirection.magnitude < 0.1f)
{
velocity.x = Mathf.Lerp(velocity.x, 0, Time.deltaTime * groundDrag);
velocity.z = Mathf.Lerp(velocity.z, 0, Time.deltaTime * groundDrag);
}
}
private void UpdateAnimation()
{
if (animator == null)
return;
float animationSpeed = currentSpeed / runSpeed;
animator.SetFloat(speedHash, animationSpeed);
animator.SetBool(isRunningHash, isRunning);
}
}
Настройка Animator
Для правильной работы анимации создай параметры в Animator:
1. Speed (Float) — нормализованная скорость (0-1)
2. IsRunning (Bool) — бежит ли персонаж
Транзиции:
- Idle → Walk — Speed > 0.1
- Walk → Run — Speed > 0.5 AND IsRunning = true
- Run → Walk — IsRunning = false
- Walk/Run → Idle — Speed < 0.05
Ключевые особенности
1. Time.deltaTime — все движения независимы от FPS.
2. Quaternion.Slerp — плавный поворот без рывков:
transform.rotation = Quaternion.Slerp(from, to, time);
3. Движение относительно камеры — персонаж движется в направлении камеры, а не мировых координат.
4. Плавное торможение — при остановке скорость постепенно снижается.
5. Разделение Update/FixedUpdate — инпут в Update, физика в FixedUpdate.
Дополнительные улучшения
Прыжок:
if (Input.GetKeyDown(KeyCode.Space) && isGrounded)
{
velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
}
Кривая скорости:
AnimationCurve speedCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
float smoothSpeed = speedCurve.Evaluate(currentSpeed / maxSpeed);
Эта реализация обеспечивает плавное, отзывчивое и натуральное движение персонажа.