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
相关产品推荐
相关产品推荐

