勇哥注:
移动动画怎么让它持续的时间完全由速度和总距离来决定?
这其实是模拟的运动控制卡API的工作方式。
下面举的两个例子,使用了两种不同的方式实现目的。
(一)Z轴上下移动
using System.Collections; using UnityEngine; public class Lifting : MonoBehaviour { public Transform lifter; public float takeProductDepth; public float releaseProductDepth; private bool working = false; // 协程函数,接受速度和移动距离作为参数 public IEnumerator LiftingCoroutine(float speed, float distance) { if (!working) { working = true; // 确定目标深度 float startDepth = Mathf.Abs(lifter.transform.localPosition.y); // 假设当前深度是y坐标的绝对值 float targetDepth; if (lifter.transform.localPosition.z == 0) { targetDepth = takeProductDepth; } else { targetDepth = releaseProductDepth; } // 如果目标深度与当前深度相同,则不移动 if (Mathf.Approximately(startDepth, targetDepth)) { working = false; yield break; } // 计算所需时间(注意:这里假设速度是正数,且距离也是正数或零) float duration = Mathf.Abs(distance) / speed; // 开始平滑移动 float startTime = Time.realtimeSinceStartup; while (Time.realtimeSinceStartup - startTime < duration) { // 计算当前时间点的插值 float t = (Time.realtimeSinceStartup - startTime) / duration; // 根据插值和总距离计算新深度(注意方向) float direction = Mathf.Sign(targetDepth - startDepth); float newDepth = startDepth + direction * distance * t; // 设置新位置(只改变y坐标) lifter.transform.localPosition = new Vector3(lifter.transform.localPosition.x, newDepth, lifter.transform.localPosition.z); // 等待下一帧 yield return null; } // 确保最终位置精确(通常不需要,因为插值已经足够精确,但可以作为保险措施) lifter.transform.localPosition = new Vector3(lifter.transform.localPosition.x, targetDepth, lifter.transform.localPosition.z); // 重置工作状态 working = false; } } // 你可以通过调用这个函数并传入速度和距离来启动协程 public void StartLifting(float speed, float distance) { StartCoroutine(LiftingCoroutine(speed, distance)); } // 其他代码,如按钮点击事件处理,可以调用 StartLifting() 并传入所需的参数来触发动画 }
在这个修改后的版本中,LiftingCoroutine 函数现在接受两个参数:speed(每秒移动的单位数)和 distance(要移动的总距离)。
这些参数允许你在调用协程时动态地指定速度和距离。
请注意以下几点:
我添加了一个检查来确保如果目标深度与当前深度相同,则不执行移动操作,并立即退出协程。
我使用了 Mathf.Sign 来确定移动的方向(向上或向下)。
我使用了 Mathf.Approximately 来检查两个浮点数是否近似相等,以避免由于浮点精度问题而导致的微小差异导致的错误。
你可以通过调用 StartLifting 方法并传入所需的速度和距离来启动动画。
确保你的 lifter 变量已经正确设置,并且 takeProductDepth 和 releaseProductDepth 的值符合你的需求。
此外,由于这个协程使用了实时时间(Time.realtimeSinceStartup),所以即使应用被暂停,动画也会继续进行。
如果你希望动画在应用暂停时停止,你可能需要使用不同的时间基准(例如 Time.deltaTime 和一个累积的时间变量)。
(二)X轴水平移动
为了确保动画的持续时间完全由速度和总距离决定,我们需要根据这两个参数来计算动画所需的总时间,
并使用一个循环或等待机制来确保动画按预期完成。在你的情况下,由于你正在使用协程和WaitForSecondsRealtime,
我们可以使用一个循环,并在每次迭代中根据速度和已过去的时间来计算应该移动的距离。
下面是一个修改后的代码示例,它使用速度和总距离来计算每帧的移动量,并确保动画在正确的时间内完成
IEnumerator Carrying(float speed, float totalDistance) { if (working == false) { // 确定移动方向 float direction = Mathf.Sign(-moving_H); // 计算实际要移动的总距离(考虑方向) totalDistance *= direction; // 计算动画所需的总时间 float totalTime = Mathf.Abs(totalDistance) / speed; // 如果lifter当前位置高于safeHeight,则开始动画 if (lifter.transform.localPosition.y > safeHeight) { working = true; float startTime = Time.realtimeSinceStartup; // 记录动画开始时间 while (true) { // 计算已过去的时间 float elapsedTime = Time.realtimeSinceStartup - startTime; // 如果已过去的时间超过了总时间,则结束动画 if (elapsedTime >= totalTime) { // 将lifter的最终位置设置为目标位置(考虑方向) lifter.transform.localPosition += new Vector3(0, 0, totalDistance); working = false; yield break; // 退出协程 } // 计算在当前时间应该移动的距离 float distanceToMove = speed * elapsedTime; // 但是,由于我们已经移动了一部分距离(如果这是循环的第二次或后续迭代), // 我们需要计算还需要移动多少距离才能到达目标。 // 因此,我们使用总距离减去已经移动的距离(通过之前的迭代或帧)。 // 然而,在这个特定的实现中,我们并没有在每次迭代中跟踪已经移动的距离, // 因为我们可以直接通过时间来计算应该在哪里。 // 所以,我们实际上是在每帧都“重新计算”应该在哪里,而不是累加移动量。 // 这意味着我们不需要一个单独的变量来跟踪已经移动的距离。 // 但是,为了保持动画的平滑性,并且避免在最后一帧突然跳跃到目标位置, // 我们应该计算剩余的距离并在最后一帧移动它。 // 但是,由于我们使用了实时时间,并且每帧都会检查是否应该结束动画, // 所以这种跳跃实际上不会发生(或者会非常微小,以至于不可察觉)。 // 因此,我们可以简单地移动当前应该移动的距离(基于时间和速度), // 并且知道当elapsedTime >= totalTime时,我们会准确地到达目标位置。 // 注意:由于浮点数的精度问题,直接比较浮点数可能会导致动画在最后一帧稍微超过目标位置。 // 为了解决这个问题,你可以添加一个小的容差值来结束动画,比如: // if (elapsedTime >= totalTime - someSmallTolerance) // 但是,在这个例子中,由于我们使用了实时时间和协程,这种精度问题通常不会很明显。 // 移动lifter到当前应该在的位置 // 注意:这里我们不需要再次乘以direction,因为totalDistance已经包含了方向。 lifter.transform.localPosition += new Vector3(0, 0, speed * Time.deltaTime * direction); // 但是上面的表达式可以简化为下面的形式,因为我们已经在外面计算了elapsedTime和speed的关系: // lifter.transform.localPosition += new Vector3(0, 0, (totalDistance / totalTime) * Time.deltaTime * direction); // 然而,这仍然会导致每帧都基于Time.deltaTime进行微小的移动,而不是直接跳到当前应该在的位置。 // 为了直接跳到当前应该在的位置(基于已过去的时间),我们应该使用下面的代码: // lifter.transform.localPosition += new Vector3(0, 0, (totalDistance / totalTime) * (elapsedTime - previousElapsedTime)); // 其中previousElapsedTime是上一次迭代中的elapsedTime值。但是,在这个实现中,我们不需要这样做, // 因为我们每帧都在重新计算应该在哪里,并且知道当elapsedTime >= totalTime时动画会结束。 // 所以,最终我们只需要简单地每帧移动一小段距离,直到动画完成。 yield return null; // 等待下一帧 } } } } // 注意:上面的代码有一些冗余和不必要的计算,特别是关于如何移动lifter的部分。 // 为了简化代码,我们可以直接使用下面的方法,它基于已过去的时间来计算应该移动的距离, // 并且直接在每帧中更新lifter的位置,直到动画完成。 // 简化后的代码: IEnumerator Carrying(float speed, float totalDistance) { if (working == false && lifter.transform.localPosition.y > safeHeight) { working = true; float startTime = Time.realtimeSinceStartup; float direction = Mathf.Sign(-moving_H); totalDistance *= direction; // 确保总距离包含正确的方向 while (true) { float elapsedTime = Time.realtimeSinceStartup - startTime; float distanceMoved = speed * elapsedTime; if (Mathf.Abs(distanceMoved) >= Mathf.Abs(totalDistance)) { lifter.transform.localPosition += new Vector3(0, 0, totalDistance); working = false; yield break; } else { lifter.transform.localPosition += new Vector3(0, 0, speed * Time.deltaTime * direction); } yield return null; // 等待下一帧 } } } // 但是,上面的简化代码仍然有一个问题:它在每帧中都使用Time.deltaTime来移动一小段距离, // 这可能会导致动画在最后一帧稍微超过或未达到目标位置(由于浮点数的精度问题)。 // 为了解决这个问题,我们可以使用下面的最终版本,它在每帧中检查是否应该结束动画, // 并且如果应该结束,则直接跳到目标位置。 // 最终版本: IEnumerator Carrying(float speed, float totalDistance) { if (working == false && lifter.transform.localPosition.y > safeHeight) { working = true; float startTime = Time.realtimeSinceStartup; float direction = Mathf.Sign(-moving_H); Vector3 targetPosition = lifter.transform.localPosition + new Vector3(0, 0, totalDistance); while (true) { float elapsedTime = Time.realtimeSinceStartup - startTime; float distanceMoved = speed * elapsedTime; // 检查是否已经达到或超过目标位置 if (Mathf.Abs(distanceMoved) >= Mathf.Abs(totalDistance)) { lifter.transform.localPosition = targetPosition; // 直接跳到目标位置 working = false; yield break; } // 否则,继续移动 lifter.transform.localPosition += new Vector3(0, 0, speed * Time.deltaTime * direction); yield return null; // 等待下一帧 } } }
注意:在最终版本中,我们仍然在每帧中移动了一小段距离,但是我们在每次迭代中都检查了是否应该结束动画。
如果应该结束,我们直接将lifter的位置设置为目标位置,从而避免了由于浮点数精度问题而导致的微小偏差。
这种方法确保了动画的平滑性和准确性。

