You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity立方体单步移动精度问题求助:无法精准移动指定距离

单步移动解谜立方体的位置精度问题排查与解决

问题背景

开发类似《劳拉GO》(Lara Croft Go)的单步移动解谜机制,核心对象为可旋转、弹跳的立方体,使用Unity新输入系统实现PlayerMovement功能。目前存在移动精度问题:例如立方体当前位置为(1,0,1)时,按下右键应精准移动至(3,0,1),实际却出现(3.8377,0,1)这类非预期超距偏移。

当前实现代码:

public class PlayerMovement : MonoBehaviour
{
    [SerializeField] InputAction singleMove;

    [Header("Player attributes")]
    [SerializeField] float moveDistance = 1f;
    [SerializeField] float moveSpeed = 1f;
    [SerializeField] float rotateSpeed = 1f;
    [SerializeField] float bounceForce = 1f;
    [SerializeField] float bounceHeight = 1f;

    [SerializeField] float defaultFallSpeed = -10;
    [SerializeField] float maxFallSpeed = -20f;
    Rigidbody rb;
    Vector3 targetPos;
    Vector3 lastTargetPosition;
    CollisionHandler collisionHandler;
    float fallSpeed = 0f;
    float currentBounce;
    bool isMoving = false;
    bool canBounce = false;
    bool isRotating = false;


    void OnEnable()
    {
        singleMove.Enable();
    }
    void Start()
    {
        rb = GetComponent<Rigidbody>();
        collisionHandler = GetComponent<CollisionHandler>();

        targetPos = transform.position;
        lastTargetPosition = transform.position;
    }

    void Update()
    {
        HandleMove();
        HandleBounce();
        HandleFall();

        if (isMoving)
        {
            bool positionReached = Vector3.Distance(new Vector3(transform.position.x, 0, transform.position.z),
                                                    new Vector3(targetPos.x, 0, targetPos.z)) < 0.01f;

            float bounceOffset = currentBounce;
            var nextPos = new Vector3(targetPos.x, transform.position.y + bounceOffset, targetPos.z);

            transform.position = Vector3.MoveTowards(transform.position, nextPos, moveSpeed * Time.deltaTime);

            if (positionReached)
            {
                transform.position = new Vector3(Mathf.Round(targetPos.x * 100) / 100, transform.position.y, Mathf.Round(targetPos.z * 100) / 100);
                lastTargetPosition = transform.position;
                Debug.Log(lastTargetPosition);
                isMoving = false;
            }
        }
    }


    void HandleMove()
    {
        Vector3 input = singleMove.ReadValue<Vector3>();
        input.Normalize();

        float moveX = input.x * moveDistance;
        float moveZ = input.z * moveDistance;


        if (singleMove.WasPressedThisFrame() && collisionHandler.isGrounded && !isMoving && (input.x != 0 || input.z != 0))
        {
            isMoving = true;
            canBounce = true;

            currentBounce = bounceForce;

            //Restrict player diagonal movement
            if (Mathf.Abs(input.x) > Mathf.Abs(input.z))
            {
                input.z = 0;
                targetPos = lastTargetPosition + new Vector3(moveX, 0, 0);

                if (isRotating == false)
                    StartCoroutine(RotateCube(Vector3.right * moveX));
            }
            else if (Mathf.Abs(input.z) > Mathf.Abs(input.x))
            {
                input.x = 0;
                targetPos = lastTargetPosition + new Vector3(0, 0, moveZ);

                if (isRotating == false)
                    StartCoroutine(RotateCube(Vector3.forward * moveZ));
            }
        }
    }

    IEnumerator RotateCube(Vector3 direction)
    {
        isRotating = true;

        float remainingAngle = 90;
        Vector3 rotationCenter = transform.position + direction / 2 + Vector3.down / 2;
        Vector3 rotationAxis = Vector3.Cross(Vector3.up, direction);

        while (remainingAngle > 0)
        {
            float rotationalAngle = Mathf.Min(Time.deltaTime * rotateSpeed, remainingAngle);
            transform.RotateAround(rotationCenter, rotationAxis, rotationalAngle);
            remainingAngle -= rotationalAngle;
            yield return null;
        }

        isRotating = false;
    }

    void HandleBounce()
    {
        if (canBounce && isMoving)
        {
            currentBounce -= bounceHeight * Time.deltaTime;

            if (currentBounce <= 0f)
            {
                currentBounce = 0f;
                canBounce = false;
            }
        }
    }

    void HandleFall()
    {
        if (!collisionHandler.isGrounded)
        {
            fallSpeed += defaultFallSpeed * Time.deltaTime;
            Mathf.Clamp(fallSpeed, defaultFallSpeed, maxFallSpeed);
        }
        else if (collisionHandler.isGrounded)
        {
            fallSpeed = 0f;
        }

        //Apply the fall speed to y axis
        float fallingDir = fallSpeed * Time.deltaTime;
        transform.Translate(0f, fallingDir, 0f, Space.World);
    }

}

排查思路

  • 多逻辑位置修改冲突:代码同时通过RotateAround旋转带动位置、Vector3.MoveTowards主动移动、HandleFall的Translate三种方式修改位置,多源操作叠加导致位置偏移。
  • 浮点误差累积:RotateAround逐帧计算产生的微小浮点误差,会通过lastTargetPosition传递到下一次移动,逐渐累积成明显偏移。
  • 目标位置计算漏洞:input.Normalize()会改变输入向量的长度,导致moveX/moveZ的实际移动距离偏离设定的moveDistance。
  • 位置到达判断不严谨:用Vector3.Distance+0.01阈值判断到达,无法完全消除浮点误差;到达后仅对targetPos做四舍五入,未强制对齐网格坐标。

解决方案

1. 统一位置控制逻辑

移除Update中Vector3.MoveTowards的移动代码,完全依赖RotateAround完成旋转+移动的联动(《劳拉GO》的移动本质是立方体绕边缘旋转,旋转完成后自然到达目标位置);取消HandleFall中Translate修改位置的方式,改用Rigidbody物理模拟下落(开启Rigidbody重力,锁定X/Z轴的旋转约束)。

2. 强制对齐网格坐标

旋转完成后直接将位置强制设置为网格步长的整数倍,彻底消除浮点误差:

IEnumerator RotateCube(Vector3 direction)
{
    isRotating = true;
    float remainingAngle = 90;
    Vector3 rotationCenter = transform.position + direction / 2 + Vector3.down / 2;
    Vector3 rotationAxis = Vector3.Cross(Vector3.up, direction);

    while (remainingAngle > 0)
    {
        float rotationalAngle = Mathf.Min(Time.deltaTime * rotateSpeed, remainingAngle);
        transform.RotateAround(rotationCenter, rotationAxis, rotationalAngle);
        remainingAngle -= rotationalAngle;
        yield return null;
    }

    // 旋转完成后强制对齐网格
    Vector3 gridPos = new Vector3(
        Mathf.Round(transform.position.x / moveDistance) * moveDistance,
        Mathf.Round(transform.position.y / moveDistance) * moveDistance,
        Mathf.Round(transform.position.z / moveDistance) * moveDistance
    );
    transform.position = gridPos;
    lastTargetPosition = gridPos;
    isRotating = false;
    isMoving = false; // 移动完成状态放在这里,确保准确
}

3. 修正目标位置计算逻辑

取消input.Normalize(),直接根据输入方向的正负计算移动向量,确保每次移动距离严格等于moveDistance:

void HandleMove()
{
    Vector3 input = singleMove.ReadValue<Vector3>();
    // 直接取输入方向的正负,避免归一化导致的距离偏差
    int dirX = input.x > 0.1f ? 1 : (input.x < -0.1f ? -1 : 0);
    int dirZ = input.z > 0.1f ? 1 : (input.z < -0.1f ? -1 : 0);

    if (singleMove.WasPressedThisFrame() && collisionHandler.isGrounded && !isMoving && (dirX != 0 || dirZ != 0))
    {
        isMoving = true;
        canBounce = true;
        currentBounce = bounceForce;

        // 限制斜向移动,优先X或Z方向
        if (Mathf.Abs(input.x) > Mathf.Abs(input.z))
        {
            dirZ = 0;
            Vector3 moveDir = Vector3.right * dirX * moveDistance;
            targetPos = lastTargetPosition + moveDir;

            if (!isRotating)
                StartCoroutine(RotateCube(moveDir));
        }
        else if (Mathf.Abs(input.z) > Mathf.Abs(input.x))
        {
            dirX = 0;
            Vector3 moveDir = Vector3.forward * dirZ * moveDistance;
            targetPos = lastTargetPosition + moveDir;

            if (!isRotating)
                StartCoroutine(RotateCube(moveDir));
        }
    }
}

4. 移除冗余的位置到达判断

删除Update中isMoving分支内的位置到达判断与移动代码,状态切换完全由旋转协程控制,避免多逻辑冲突。

内容的提问来源于stack exchange,提问作者Mazen Hassan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 19:57:02