囿里有条小咸鱼 发表于 2023-1-26 22:38:56

【技术帖】Godot中修复钝角凹槽抖动和摩擦误判问题

本帖最后由 电童·Isamo 于 2023-1-30 13:33 编辑

RT,阅读本帖前,请确保你有一定的Godot开发经验,并熟悉KinematicBody2D的操作

我们都知道,Godot的物理可以说非常牛b,对于KinematicBody2D节点来说,基本上只需要一条move_and_slide(_with_snap)()代码,就可以帮助我们解决基本的带上下坡和重力的运动问题。
然而即便是这样强大的物理函数,却依然存在几个问题。本帖就针对move_and_slide()系函数会产生的一些问题来进行讨论,并给出较为稳妥的修复方法。
static/image/hrline/2.gif1.钝角凹槽运动抖动这个bug得名于使该问题出现的地形的形状,如下图所示:https://s1.ax1x.com/2023/01/25/pStjeA0.png
上图中斜坡与墙壁相交,正好形成了一个钝角,这便是钝角凹槽,当一个KinematicBody2D调用以下代码时:
extends KinematicBody2D


var velocity:Vector2

func _physics_process(delta: float) -> void:
      # 持续提供速度
      velocity.x = 600
      
      # 重力设置
      velocity.y += 60
      
      # 运动,同时使最终速度为物理滑动碰撞后的结果
      velocity = move_and_slide(velocity,Vector2.UP)在物体碰到墙壁后,如果调用的运动方法是move_and_slide_with_snap(),且参数snap的方向指向地面并且其模长足够大,那么物体将会在这个钝角凹槽处不停地上下抽搐运动。如果snap参数过小或者snap的指向与地面垂直或者snap为零向量,亦或是所调用的运动方法为上例中的move_and_slide(),那么这个物体将会沿着墙壁向上运动一小段距离后下落
原因是:这个物体在碰到这种钝角凹槽后,因为处理运动的方法是滑动碰撞,即碰到物体后沿物体的接触面方向继续运动而不是立即停下,所以其在滑动运动碰撞后时Y速度分量不为0,导致物体沿墙壁向上运动。同时,由于我们一直提供有效的x分量,导致物体每帧都要向墙壁的方向运动,使得每帧都必然会执行卡墙判定。由于涉及到子像素运算,且不够精确,因此就会出现水平方向抽搐的现象。
在我发现这个问题后,我赶紧使用print大法打印了每次运动完毕后所得到的速度,发现这个速度的x分量的方向总是远离墙壁的,y分量不用说,一定是向上的。所导致的结果就是物体在执行完这一物理帧的滑动碰撞后脱离理论上应该到达的坐标。
需要注意的是:Godot中诸如move_and_collide()、move_and_slide()等物理操作方法都是针对一个节点的全局坐标而言的。
解决方法也很简单:在物体沿斜面运动钝角凹槽处时,我们只需要把物体的全局坐标按反向速度位移即可。
extends KinematicBody2D


var velocity:Vector2

func _physics_process(delta: float) -> void:
      # 持续提供速度
      velocity.x = 600
      
      # 重力设置
      velocity.y += 60
      
      # 运动,同时使最终速度为物理滑动碰撞后的结果
      velocity = move_and_slide(velocity,Vector2.UP)
      
      # 如果物体滑动到斜面上,则velocity.y一定是小于0的
      # 物体沿斜面运动至钝角凹槽处时,将其全局坐标按反向速度位移即可。
      # 需要注意的是,这个速度的单位是px/s,如果要改成px则需要乘上delta,delta的单位为s
      if is_on_wall() && is_on_floor() && velocity.y < 0:
                global_position -= velocity * delta然后你再运行一下就会发现:物体的抽搐程度明显减轻了许多,但还是存在轻微的抽搐。这实际上是因为子像素渲染的锅。我们只需要将子像素整化即可。这里我推荐用向下取整来得到最佳效果:
extends KinematicBody2D


var velocity:Vector2

func _physics_process(delta: float) -> void:
      # 持续提供速度
      velocity.x = 600
      
      # 重力设置
      velocity.y += 60
      
      # 运动,同时使最终速度为物理滑动碰撞后的结果
      velocity = move_and_slide(velocity,Vector2.UP)
      
      # 如果物体滑动到斜面上,则velocity.y一定是小于0的
      # 物体沿斜面运动至钝角凹槽处时,将其全局坐标按反向速度位移即可。
      # 需要注意的是,这个速度的单位是px/s,如果要改成px则需要乘上delta,delta的单位为s
      if is_on_wall() && is_on_floor() && velocity.y < 0:
                global_position = (global_position - velocity * delta).floor()这样问题就解决了……吗?
实际上,如果重力方向是正上/下/左/右,那么上面的代码确实没有问题,然而,如果重力方向并不是刚刚所说的那四个方向,那么该问题依然存在。
经过了一些对比测试,我发现:如果物体在地面上时,把重力计算关掉,那么它就不会出现上述问题,同时还能节省一部分性能。
以下是修改后的代码:
extends KinematicBody2D

export var velocity:Vector2
export var gravity:float
export var stop_on_slope:bool = true # 开启后,物体在斜坡上不会往下滑


func _physics_process(delta: float) -> void:
      # 测试用,持续提供速度
      velocity.x = 300
      
      # 当物体在地面上时,取消重力计算,既能修复钝角凹槽bug,又能节省一部分性能
      # 同时建议先查看is_able_slope_down()方法的注释
      if !is_on_floor() || is_able_slope_down():
                velocity.y += gravity
      
      velocity = move_and_slide(velocity,Vector2.UP,stop_on_slope)


# 负责检测是否可以向下滑下斜坡
# 可以滑下斜坡时,启用重力,让move_and_slide自动计算斜坡速度即可
func is_able_slope_down() -> bool:
      if stop_on_slope: return false
      # 这里dot()方法里的是重力方向
      var dot:float = get_floor_normal().dot(Vector2.DOWN)
      # 由于get_floor_normal()方法本身要求is_on_floor(),因此我们无需再添加is_on_floor()条件
      # 这里必须保证碰墙时也不能启用重力,否则效果白搭。
      # 因为is_on_wall()检测的最近一次发生move_and_slide()后的结果,所以当输入的x速度不能满足碰墙时,is_on_wall()自动失效,使物体受重力影响下滑
      return !is_on_wall() && dot < 0 && !is_equal_approx(dot,-1)但即便如此,有些情况下还是会出现抖动,这我就真的没有办法了,已经是最优解了QwQ。
https://www.marioforever.net/static/image/hrline/2.gif2.摩擦误判这个问题本身涉及到godot的碰撞算法精度的问题。像我们这种不敢深入API层面去研究修复的中下等gd程序员来说,我们就只能用节点层面的东西来解决这个问题。
这个问题仅当重力方向角不为90°的整数倍(以下我们称这个重力为非正交重力)时才会出现,因此,如果重力方向为正下/上/左/右,则不会出现此问题。
这个问题是这样的:当一个物体的重力为非正交重力时,如果此时这个物体的重力与其受到的地面的支持力互为反向向量(但支持力的方向有微小偏差,不满足严格的共线条件)时,若该物体在做水平滑动运动,则该物体会有一定概率被误判为“碰墙”。上述情况如下图所示:
https://s1.ax1x.com/2023/01/25/pStvExe.png
如图,红色箭头为该物体的重力,淡蓝色箭头为物体受到的支持力。当且仅当物体在地面上做水平滑动运动时(不上下坡),该物体就有一定概率被判为碰墙,即is_on_wall()方法返回true。
这种误判实际上是因为move_and_slide()里有一个叫up的参数(类型为Vector2)的运算不精确导致的。它的作用就是判断物体在运动时如果碰到障碍物,根据碰撞的位置来决定碰到的是墙壁、地面还是天花板。但这个参数up在非正交情况(即其方向角不为90°的整数倍)下会在物体在相对水平的地面上水平滑行时导致计算结果不精确,从而使有些情况下本不应该判定为碰墙的结果变成了碰墙。这种情况下,我们就需要解决物体与水平面紧贴的情况下up方向的选择了。经本人实践证明:当这种情况下将up参数取为上下左右四个方向中最贴近这个up向量的方向即可修复这个问题。下面是修复代码:
extends KinematicBody2D


var velocity:Vector2
var global_gravity:Vector2

func _physics_process(delta: float) -> void:
      # 定义上方向和重力方向
      var up:Vector2 = Vector2.UP.rotated(global_rotation)
      global_gravity = Vector2.DOWN.rotated(global_rotation)
      
      # 建议先看这个方法的注释
      if is_on_floor_approx():
                # 如果与地面紧密贴合,则对参数up取整
                # 注:当up的方向角为±45°、±135°时,该方法失效
                up = up.round()
      
      velocity = move_and_slide(velocity,up)

# 这里需要自己构建一个方法来判断是否大致位于地面上
# 判断方法就是利用重力的单位向量(即重力方向向量)与的地面法向量(单位向量)的点乘是否等于-1,也就是两向量夹角的余弦值是否为-1
# 但当参数up为非正交情况时,与地面的法向量会与重力方向的单位向量的反向向量有一定偏离,从而导致精确等于不恒成立
# 为使其能够恒成立,我们需要判断其点乘是否大致为-1
# 由于get_floor_normal()方法本身要求is_on_floor(),因此我们无需再添加is_on_floor()条件
func is_on_floor_approx() -> bool:
      return is_equal_approx(get_floor_normal().dot(global_gravity.normalized()),-1)当参数up的方向角为±45°、±135°时,在某些情况下碰墙检测直接失效。如果你对这个修复方法仍不满意,我建议手动修改参数up(在Godot 3.X中用export变量来实现,Godot 4.0中已将参数up_direction作为自带属性,可直接在编辑器内修改)
static/image/hrline/line6.png
以上就是Godot中修复KinematicBody2D节点在某些情况下调用move_and_slide()系列方法时运动bug的方法,如果你还有更好的方法,也欢迎在本帖讨论交流。
页: [1]
查看完整版本: 【技术帖】Godot中修复钝角凹槽抖动和摩擦误判问题