【Unity】Rigidbody.velocity 的陷阱

说在前面

本人还是Unity初学者,文章中若有错误恳请大家能够指正。如果有其他的想法也欢迎来交流,大家共同学习,共同进步。

正文

我们常在Unity开发中直接使用Rigidbody.velocity属性来获取刚体的当前速度,这在大多数情况下是没有问题的。但在某些情况下这么做就可能得不到我们想要的结果。比如通过transform.Translate(), transform.RotateAround(), rigidbody.MovePosition(), Vector3.MoveTowards() 等方法 “强制” 改变刚体的运动状态时,此时物体速度的改变并不会引起Rigidbody.velocity的改变。


不信?那我们先在Unity中做个实验,看看是不是这样。

为了探讨Rigidbody.velocity到底准不准确,我们必须找到一种能够相对准确地计算速度的方式来作为对照。我这里采取的方式是定义一个Vector3变量记录刚体在上一帧的position,计算速度时在Update()中将当前帧物体的位置减去上一帧存储的物体位置,再除以帧的刷新间隔deltaTime,得到的就是物体的瞬时速度,最后在Update()结尾更新Vector3变量的值供下一帧进行计算。我们姑且将这种方法得到的值作为游戏场景中的 “ 真实速度 ”


先构建一个这样的场景,如图所示。场景中有水平的地面,以及并排放置在地面上的红黄蓝三个小球,三个小球都被赋予了刚体属性。

我给这三个小球分别创建并绑定了red.cs,yellow.cs,blue.cs 这三个脚本,下面放代码:

// red.cs
using UnityEngine;
public class red : MonoBehaviour
{
    private Transform m_transform;
    private Rigidbody body;
    private Vector3 lastPos;
    private int frameNum = 0; //记录当前是第几帧

    void Awake()
    {
        m_transform = transform;
        body = GetComponent();
        lastPos = m_transform.position;//通过求相邻两帧物体的位移长度和时间间隔
                                       //deltaTime的比值计算物体瞬时速率
    }

    void FixedUpdate()
    {
        //使用rigidbody.MovePosition()移动物体
        body.MovePosition(transform.position + Vector3.forward * Time.deltaTime);

        float realspeed = (m_transform.position - lastPos).magnitude / Time.deltaTime;
        lastPos = m_transform.position;
        frameNum++;

        Debug.Log("第" + frameNum + "帧 " + "【仅移动】" + "真实速率 :" + realspeed +"Rigidbody.velocity:" + body.velocity.magnitude);
    }
}

由于另外两个脚本只有FixedUpdate()与上面不同,下面只贴出这一部分:

//blue.cs   
void FixedUpdate()
{
    //使用rigidbody.AddForce()添加力的作用
    body.AddForce(Vector3.forward * 5);

    float realspeed = (m_transform.position - lastPos).magnitude / Time.deltaTime;
    lastPos = m_transform.position;
    frameNum++;

    Debug.Log("第" + frameNum + "帧 " + "【仅受力】" + "真实速率 :" + realspeed +"Rigidbody.velocity:" + body.velocity.magnitude);
}   
//yellow.cs 
void FixedUpdate()
{
    //使用rigidbody.MovePosition()移动物体
    //同时使用rigidbody.AddForce()添加力的作用
    body.MovePosition(transform.position + Vector3.forward * Time.deltaTime);
    body.AddForce(Vector3.forward * 5);

    float realspeed = (m_transform.position - lastPos).magnitude / Time.deltaTime;
    lastPos = m_transform.position;
    frameNum++;

    Debug.Log("第" + frameNum + "帧 " + "【混合变换】" + "真实速率 :" + realspeed +"Rigidbody.velocity:" + body.velocity.magnitude);
}

由此可见,三个代码的不同处仅仅在于,red.cs 使用 MovePosition() 方法使红球运动,blue.cs 使用 AddForce() 方法使蓝球运动,yellow.cs 将这两种方法共同作用于黄球。除此之外,它们都用相同的方法计算瞬时速度。将相似的功能分成三个脚本来写是为了能自定义脚本执行的顺序,从而方便在Console面板上提取信息。在脚本中,我们将各自经过计算得到的瞬时速度大小和Rigidbody.velocity的大小在Console面板上输出进行比较。运行后结果如下:

【Unity】Rigidbody.velocity 的陷阱_第1张图片
【Unity】Rigidbody.velocity 的陷阱_第2张图片

在【仅移动】的脚本中,MovePosition(transform.position + Vector3.forward * Time.deltaTime)这一语句使红色球在deltaTime的时间内移动 Vector3.forward * Time.deltaTime的距离,即真实速率为1,这与该脚本在Console中的输出一致。但与此同时,它的Rigidbody.velocity却一直显示为0,这验证了我之前说的结论。

除此之外,还能发现一些有趣的现象:

  • 在小球【仅受力】的作用时,Rigidbody.velocity能和真实速率保持一致
  • 当小球既受力又做MovePosition变换时, 真实速率是同一帧【仅移动】和【仅受力】中真实速率的和,Rigidbody.velocity和【仅受力】一致。

而当我们把所有球的刚体组件中的Is Kinematic属性勾选上后,我们再来看看运行结果:
【Unity】Rigidbody.velocity 的陷阱_第3张图片
OMG!【仅移动】小球的Rigidbody.velocity居然又和真实速率一致了!

本场景中使用MovePosition()作为【仅移动】变换的函数,事实上transform.Translate(), transform.RotateAround(), rigidbody.MovePosition(), Vector3.MoveTowards() 等等亦能产生相同的结果,这里不再重复实验。

分析

我并不了解Rigidbody.velocity这个属性在内部是如何被定义的,官方文档没有相关的说明,网上也没有找到相关的资料,我个人只能根据这些现象做如下的一些推测。

当刚体的Is Kinematic没有被勾选时,刚体的运动就被Unity的物理引擎所掌控,物体的运动和状态都会遵循真实世界的物理定律。我们知道,在牛顿力学中,要改变一个物体的运动状态必须要对其施加力,Unity也为我们提供了AddForce()方法。然而像MovePosition()这样的方法似乎可以让物体的运动随心所欲,能够以任意速度到达任意位置,可以让物体瞬间加到一个非常大的速度。显而易见,这种对运动状态的 “ 强制 ” 改变必定不能通过加力的方式实现,这就已经脱离了真实世界的物理定律了。被物理引擎控制的物体擅自进行了不按套路的操作,Rigidbody.velocity就不会记录这种 “非法” 操作带来的速度改变,或者将这种非法操作对velocity的改变视为0。

反之,当刚体的Is Kinematic被勾选时,刚体的运动就脱离了Unity的物理引擎控制。风水轮流转,天道好轮回,这种情况下MovePosition()成了合法操作,AddForce()成了非法操作了。想要报仇雪恨的MovePosition()积攒了多年的怨气,对非法操作的限制变得更为严格,之前的情况还允许非法操作对物体运动状态的改变,这次已经完全屏蔽了AddForce()的作用。从上一张截图就可看出,这次即便加力物体也始终保持静止。此时此刻MovePosition()终于作为合法操作被Rigidbody.velocity认可,使其能够反映物体真实速率。

总结

通过以上案例,我的想法就是最好不要对未勾选Is Kinematic的刚体使用transform.Translate(), transform.RotateAround(),rigidbody.MovePosition(),Vector3.MoveTowards()等等这些方法,毕竟这些非常规操作必定会对物理模拟的真实性产生影响。如果你不得不使用时,也请注意Rigidbody.velocity并不是物体在场景和游戏视图中的真实速度,不要滥用这些方法和这个属性而不小心掉入它的 “ 陷阱 ”。

由于不清楚实现机制所以无法总结更多,欢迎大家踊跃发言,建言献策。

你可能感兴趣的:(【Unity】Rigidbody.velocity 的陷阱)