Unity中rb.MovePosition的误区和相关物理系统知识详解
在开发游戏的过程中,大家肯定有时候想直接设置物理位置同时保证不穿墙,上网一搜,全都是用rb.MovePosition。排名前几的文章也是这么说的,包括ai也被污染了,但是这根本是错的!
rb.MovePosition根本不能防止穿墙!
在unity的官方文档中,从来没有说过rb.MovePosition能够防止穿墙,甚至从官方文档中还能看到rb.MovePosition只有勾选了isKinematic的时候才能发挥它的真正作用,就是位置过渡。
我之前面对穿墙的时候上网搜全是用rb.MovePosition,结果发现越用越不对,不得不感慨,真的是以讹传讹。
现在开始具体解释。
首先,这个文档中有几个信息。
一、他只是让位置平滑过渡罢了,这适用于和渲染步长不一样的时候用来同步位置。比如默认每秒物理移动50次,而渲染帧数有120甚至240帧的时候。如果还是按照每秒50次来移动位置,那再高的帧数移动看起来都不会变流畅,甚至如果相机按照LateUpdate进行移动的话,和fixedUpdate不同步还会导致抖动效果。所以unity开放了这个,如果开启平滑过渡,那么物理还是计算50次,但是渲染位置会自动过渡为120次或者240次,看起来就会流畅的多。LateUpdate同步也就没问题了。
二、在使用MovePosition的时候必须勾选IsKinematic后才能启用这个插值效果。如果不勾选,那么效果就是直接设置位置,不会有任何过度。
什么是IsKinematic?IsKinematic就是外界的物理系统不会再对自身产生任何位移旋转等效果,但自己的移动还能作用与外界。代码可以对其进行完整的控制。而且MovePosition和MoveRotation还可以提供对应的插值过渡效果。
unity的刚体默认是不勾选IsKinematic的,也就是默认的模拟真实世界的物理表现。
要注意的是,并非只有MovePosition才能使用过渡动画,在使用rb.velocity=velocity的时候也可以启用这个来开启过渡效果。
关于防止穿墙的误解
上面说了,官方文档并没有写能穿墙。
实际测试的写法也是直直的穿过去了。
先看看默认情况下的物理
默认情况下的物理,其实就是非Kinematic情况下的物理。也就是非动力学,也就是模拟现实的情况。
我们有两种情况来进行物理移动,
一种是通过velocity,包括直接设置velocity和AddForce方法。
因为这这种情况下的速度是连续的,unity内部能够正确的检测并防止穿墙,也可以和其他动态刚体正常互动。但是速度过快的话一个物理帧之间移动的距离过大直接跳过了某些碰撞体的话也防止不了穿墙,这时就需要设置检测模式为continous,unity内部会通过算法来进行连续监测,简单来说,在移动后从上一帧发射射线到这里的位置来检测是否跳过了碰撞体,检测到就把位置移动回去。
另一种是直接设置rb.Position或者设置transform.position。
这样是可以在速度较慢的情况下防止穿墙。因为unity的物理系统有一定的补偿效果,当物体进入到了其他的碰撞体后unity的物理系统会给出一个弹出的力。如果移动速度慢,通过rb.position或者transform.position把物体设置到墙内,unity会自动弹出一点距离,但这个力度是有限的。而且根据位置不同可能直接弹出到另一边去了。当然了,直接设置位置不会与途中的刚体产生交互,自然更不可能防止穿墙了。
常见物理系统撞墙抖动原因
在游戏开发过程中,经常会遇到刚体撞到墙面抖动的情况。
如何解决呢?这里只说非Kinematic的情况,因为Kinematic默认情况下是不可阻挡的。
其实撞墙抖动很简单,就是因为用了rb.position和transform.position来进行物理移动。详情见我的另一篇文章:Unity撞墙抖动原因https://blog.csdn.net/qq_37724011/article/details/146218767?fromshare=blogdetail&sharetype=blogdetail&sharerId=146218767&sharerefer=PC&sharesource=qq_37724011&sharefrom=from_link
用rb.velocity进行移动其实就不会产生问题了,在制作角色控制的时候建议用rb.velocity进行移动,同时忽略掉y轴对刚体的控制,y轴保留自由落体,然后勾选continous,就可以很流畅的进行移动,碰到墙也可以正确处理不会抖动。但是上面说了,这还是每秒50次,所以勾选interplote,就可以和渲染帧数同步。其实某些相机跟随刚体的抖动效果就是因为位移帧数和相机帧数不同步导致的。要么把相机放到fixedUpdate里要么刚体勾选continous都可以解决。
MovePosition测试
非运动学刚体MovePosition测试
官方文档说过,Moveposition只在IsKinematic中提供过渡效果,非运动学等于rb.position=pos。那么结果显而易见,使用MovePosition等于直接设置位置,和上面的解析一样,如果速度较慢不会穿墙,否则,则会穿墙。
首先,用以下移动逻辑测试:
void FixedUpdate()
{rb.MovePosition(rb.position + transform.forward * (speed * Time.fixedDeltaTime));//写法A
}
小黄和小兰把速度设置为3时:
小黄小蓝把速度设置为1时:
可以清楚的看到,速度过快时直接穿墙,即使墙比较厚但是速度较快的时候物理系统的纠正力度小于位移速度时也无法阻止穿墙了。
由此可见,MovePosition是无法防止穿墙的,但是MovePosition大家还爱用,这是为什么呢?其实是因为它的另一个特性,在非运动学刚体情况下,它的代码等于rb.position=pos。就是它设置的是刚体位置而不是transform.position。
这两者又有什么区别呢?之前说过,渲染和物理跟新频率是不一致的,一般渲染频率多于物理频率。假设我们用transform.position或者动画来移动物体,那么fxiedUpdate因为更新次数少,所以会出先物理位置慢于渲染位置的情况。这种情况下就会出现一些射线不准确的问题或者碰撞检测不准确的问题。这个时候可以调用Physics.SyncTransforms()来强行进行一次物理更新把物理位置放置到渲染位置。
然后我们要说的是rb.Position=pos;和transform.position=pos;的区别。他们也和物理更新和渲染更新的帧率不同有关系。假设在fixedUpdate中调用这两个方法,他们是没有区别的,因为本身就是在物理帧调用。但是在Update中调用那就有区别了。
在update中设置rb.position,rb的位置会改变,但是碰撞体的位置并不会改变。我们只是设置了他的rb位置,但是物理系统并没有更新,要等到下一物理帧,rb.position和碰撞体之类的才会跟着更新。可以通过降低物理步长来测试这一点。
那么基于上面的理论,以下的代码你能否看出不同呢?
void FixedUpdate()
{rb.MovePosition(rb.position + transform.forward * (speed * Time.fixedDeltaTime));//写法A
}void Update()
{rb.MovePosition(rb.position + transform.forward * (speed * Time.deltaTime));//写法Brb.position+=transform.forward * (speed * Time.deltaTime);//写法Ctransform.position = transform.position + transform.forward * (speed * Time.deltaTime);//写法D
}
这四种写法,其实ABC的效果都是一样的,在update中设置rb.position,最后真正的更新也是fixedUpdate中才会更新,他们和物理更新的频率一直,所以撞墙了也不会抖动。
但是写法D就会导致撞墙抖动。当然这里说的是速度很慢的情况下。如果速度快上面四种方法都会穿墙。
写法D撞墙抖动的原因还是在这个链接里能看到:
Unity撞墙抖动原因https://blog.csdn.net/qq_37724011/article/details/146218767?fromshare=blogdetail&sharetype=blogdetail&sharerId=146218767&sharerefer=PC&sharesource=qq_37724011&sharefrom=from_link
运动学刚体碰撞
现在开始说勾选IsKinematic后的运动情况。
IsKinematic不接受其他刚体的影响,但是能影响其他刚体。
这种情况下如果用transform.position来移动和rb.position的方法来移动区别不大,因为对其他刚体的影响都是在fixedUpdate中结算,而自己又是不可阻挡的,根本不会有撞墙抖动的问题。除非高级用法中自己编写角色控制器,这就很复杂了,大家完全不用考虑。
但是要注意的是它的性质。
我们用这种方式进行移动:
void FixedUpdate()
{rb.position+=transform.forward * (speed * Time.deltaTime);
}
可以看到,对方块产生了影响,但并不能推着走,然后也直接穿墙了。和上面说的一样。这是因为他用的是直接设置位置,物理系统判断重叠只是给了周围弹出的力。
我们要让他能推着走,代码就需要用rb.MovePosition了。
void FixedUpdate()
{rb.MovePosition(rb.position + transform.forward * (speed * Time.fixedDeltaTime));//写法A
}
这下就可以推着走了,简单来说,他的内部用的应该是力来计算。
MovePostion和rb.velocity不一样,它传入的是位置而不是力,但是它任仍然可以表现为力的形式,这里比较特殊,需要谨记
此外,它还有些奇特的特性需要了解。
我新弄了一个例子,通过MovePosition在两个点来回MovePosition,然后它的中间有很多动态方块。
正常情况下,会直接位移过去不会有影响。这很正常。
但是MovePosition还有个功能,把检测方式设置为continous的话,就可以对途中的刚体的也造成影响,而它的力则会根据速度和质量自动计算。