← Назад к вопросам

Реализовать движение персонажа с плавной интерполяцией

1.0 Junior🔥 211 комментариев
#C# и ООП#Unity Core#Анимация#Физика и математика

Условие

Реализуйте скрипт движения персонажа от третьего лица с плавным поворотом в направлении движения.

Требования

  1. Персонаж управляется клавишами WASD или стрелками
  2. Персонаж поворачивается плавно в направлении движения (используйте Quaternion.Slerp)
  3. Скорость движения должна быть независима от FPS (используйте Time.deltaTime)
  4. Добавьте возможность бега при зажатой клавише Shift
  5. Персонаж должен использовать CharacterController или Rigidbody

Бонус

  • Добавьте анимации ходьбы и бега через Animator
  • Реализуйте торможение при остановке

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Подход к движению персонажа

Плавное движение от третьего лица требует координации трёх компонентов: ввода, движения и поворота. Важно использовать 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);

Эта реализация обеспечивает плавное, отзывчивое и натуральное движение персонажа.