unity3d学习(十六)轴运动动画的持续时间完全由速度和总距离决定


勇哥注:

移动动画怎么让它持续的时间完全由速度和总距离来决定?

这其实是模拟的运动控制卡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的位置设置为目标位置,从而避免了由于浮点数精度问题而导致的微小偏差。

这种方法确保了动画的平滑性和准确性。





本文出自勇哥的网站《少有人走的路》wwww.skcircle.com,转载请注明出处!讨论可扫码加群:
本帖最后由 勇哥,很想停止 于 2024-10-29 17:03:19 编辑

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

会员中心
搜索
«    2025年4月    »
123456
78910111213
14151617181920
21222324252627
282930
网站分类
标签列表
最新留言
    热门文章 | 热评文章 | 随机文章
文章归档
友情链接
  • 订阅本站的 RSS 2.0 新闻聚合
  • 扫描加本站机器视觉QQ群,验证答案为:halcon勇哥的机器视觉
  • 点击查阅微信群二维码
  • 扫描加勇哥的非标自动化群,验证答案:C#/C++/VB勇哥的非标自动化群
  • 扫描加站长微信:站长微信:abc496103864
  • 扫描加站长QQ:
  • 扫描赞赏本站:
  • 留言板:

Powered By Z-BlogPHP 1.7.2

Copyright Your skcircle.com Rights Reserved.

鄂ICP备18008319号


站长QQ:496103864 微信:abc496103864