刚体控制器在移动平台上滑动

编程语言 2026-07-09

我有一个非常基础的刚体玩家控制器和一个移动平台脚本。当玩家站在移动平台上时,玩家会随着平台一起移动。然而,当平台迅速改变方向时,玩家在改变方向前的移动方向上滑动几帧。

我希望无论平台的速度有多快,都能让玩家在平台上的同一位置保持不变。我已经应用了一个物理材质来消除所有摩擦,但无论摩擦系数如何,当平台速度超过某个阈值时,仍会发生滑动。

using System;
using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerController : MonoBehaviour
{
    private Rigidbody _rigidbody;
    private CapsuleCollider _capsule;

    private RaycastHit _ground;

    private bool _isGrounded = false;
    private Vector3 _direction = Vector3.zero;

    private Vector3 _velocity = Vector3.zero;
    private Vector3 _gravity = Vector3.zero;
    private Vector3 _platform = Vector3.zero;
    private Vector3 _lastPlatformVelocity = Vector3.zero;

    void Start()
    {
        _capsule = GetComponent<CapsuleCollider>();
        _rigidbody = GetComponent<Rigidbody>();
        _rigidbody.constraints = RigidbodyConstraints.FreezeRotation;
    }

    public void OnMove(InputAction.CallbackContext context)
    {
        var value = context.ReadValue<Vector2>();
        _direction.x = value.x;
        _direction.z = value.y;
    }

    public void OnJump(InputAction.CallbackContext context)
    {
        if (_isGrounded && context.performed)
        {
            _velocity.y = 10f;
        }
    }

    private void FixedUpdate()
    {
        _isGrounded = CheckGrounded();

        Gravity();
        Platform();
        Movement();


        _rigidbody.linearVelocity = _velocity + _platform;
    }


    private void Platform()
    {

        if (_isGrounded && _ground.rigidbody != null)
        {
            _platform = _ground.rigidbody.GetPointVelocity(_ground.point);
            // Shift _velocity into the new platform's reference frame.
            // This handles both landing AND mid-platform direction changes.
            _velocity -= Vector3.ProjectOnPlane(_platform - _lastPlatformVelocity, Vector3.up); ;
        }
        else
        {
            // Leaving the platform — eject any stored platform velocity into _velocity
            // so the player carries their momentum through the air correctly.
            _velocity += _lastPlatformVelocity;
            _platform = Vector3.zero;
        }

        _lastPlatformVelocity = _platform;
    }

    private void Movement()
    {
        var acceleration = 15f;
        const float baseSpeed = 10f;

        Vector3 facing = transform.TransformDirection(_direction);
        float verticalVelocity = _velocity.y;

        if (_isGrounded)
        {
            _velocity = Vector3.Lerp(_velocity, facing * baseSpeed, acceleration * Time.deltaTime);
        }
        else
        {
            Vector3 horizontalVelocity = Vector3.ProjectOnPlane(_velocity, Vector3.up);

            // Preserve inertia in air
            float speedCap = Mathf.Max(horizontalVelocity.magnitude, baseSpeed);

            horizontalVelocity += facing * acceleration * Time.deltaTime;

            _velocity = Vector3.ClampMagnitude(horizontalVelocity, speedCap);
        }

        Debug.Log(Vector3.ProjectOnPlane(_velocity, Vector3.up));

        _velocity.y = verticalVelocity;
    }

    private void Gravity()
    {
        if (!_isGrounded)
        {
            _velocity += Physics.gravity * 2f * Time.deltaTime;
        }
    }

    private bool CheckGrounded()
    {
        Vector3 center = transform.TransformPoint(_capsule.center);

        float radius = _capsule.radius * Mathf.Max(transform.lossyScale.x, transform.lossyScale.z);

        return Physics.SphereCast(
            center,
            radius * 0.95f,
            Vector3.down,
            out _ground,
            maxDistance: 0.55f
        );
    }
}

奇怪的是,这段平台方法中的这一部分让玩家滑动得更严重,但修复了在玩家跳跃时将平台速度应用给玩家的另外一个问题。

_velocity -= Vector3.ProjectOnPlane(_platform - _lastPlatformVelocity, Vector3.up); ;

注意:你可能会问“为什么不使用CharacterController呢?”CharacterController有一个重大缺陷,导致无法在X 轴和Z 轴上旋转玩家。我正在尝试引入重力井的想法。

解决方案

根本原因在于 GetPointVelocity 总是滞后一帧。你读取平台的速度、将其应用,然后下一个物理步骤发生——平台已经继续移动了。在低速时错误不可见;在高速时(尤其是方向反转时),那一帧的滞后就会变成可见的滑动。

解决办法是完全不再用速度空间来处理平台移动。改为跟踪平台的位置增量,并用 MovePosition 来应用。位置增量是精确的。没有近似,没有滞后。

Rewrite Platform()

private Rigidbody _platformRb;
private Vector3 _lastPlatformPos;
private Quaternion _lastPlatformRot;
private bool _wasOnPlatform;

private void Platform()
{
    if (_isGrounded && _ground.rigidbody != null)
    {
        Rigidbody platformRb = _ground.rigidbody;

        if (_wasOnPlatform && platformRb == _platformRb)
        {
            Vector3 posDelta = platformRb.position - _lastPlatformPos;
            _rigidbody.MovePosition(_rigidbody.position + posDelta);
        }

        _lastPlatformPos = platformRb.position;
        _lastPlatformRot = platformRb.rotation;
        _platformRb = platformRb;
        _wasOnPlatform = true;

        // Only needed for jump handoff
        _platform = platformRb.GetPointVelocity(_ground.point);
    }
    else
    {
        if (_wasOnPlatform)
            _velocity += _lastPlatformVelocity;

        _platform = Vector3.zero;
        _wasOnPlatform = false;
        _platformRb = null;
    }

    _lastPlatformVelocity = _platform;
}
Since MovePosition now handles platform movement, remove _platform from the velocity line:


// was: _rigidbody.linearVelocity = _velocity + _platform;
_rigidbody.linearVelocity = _velocity;

如果平台也会旋转,就把增量扩展以考虑旋转:

if (_wasOnPlatform && platformRb == _platformRb)
{
    Quaternion rotDelta = platformRb.rotation * Quaternion.Inverse(_lastPlatformRot);
    Vector3 pivotOffset = _rigidbody.position - _lastPlatformPos;
    Vector3 posDelta = (platformRb.position - _lastPlatformPos)
                     + (rotDelta * pivotOffset - pivotOffset);
    _rigidbody.MovePosition(_rigidbody.position + posDelta);
}

一个需要注意的点:如果你的平台的 FixedUpdate 发生在玩家之后执行,platformRb.position 将读取上一帧的值——这样滞后就会重新出现。请检查Edit → Project Settings → Script Execution Order,并确保平台脚本在玩家控制器之上。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章