本人还是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面板上输出进行比较。运行后结果如下:
在【仅移动】的脚本中,MovePosition(transform.position + Vector3.forward * Time.deltaTime)这一语句使红色球在deltaTime的时间内移动 Vector3.forward * Time.deltaTime的距离,即真实速率为1,这与该脚本在Console中的输出一致。但与此同时,它的Rigidbody.velocity却一直显示为0,这验证了我之前说的结论。
除此之外,还能发现一些有趣的现象:
而当我们把所有球的刚体组件中的Is Kinematic属性勾选上后,我们再来看看运行结果:
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并不是物体在场景和游戏视图中的真实速度,不要滥用这些方法和这个属性而不小心掉入它的 “ 陷阱 ”。
由于不清楚实现机制所以无法总结更多,欢迎大家踊跃发言,建言献策。