勇哥注:
移动动画怎么让它持续的时间完全由速度和总距离来决定?
这其实是模拟的运动控制卡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的位置设置为目标位置,从而避免了由于浮点数精度问题而导致的微小偏差。
这种方法确保了动画的平滑性和准确性。


少有人走的路


















