Undestanding special case handling in the move_and_slide implementation (and a potential bug?)

Godot Version

4.4.1.stable

Question

I’m currently working on a 2.5D top-down character controller; it involves implementing a custom version of move_and_slide as part of it that snaps to the 2D gameplay plane as best as it could, so I’m referencing the official implementation of move_and_slide to ensure I don’t miss any edge cases along the way. It’s generally pretty well documented, but I’m having trouble understanding why reaction to the first collision is handled differently:

if (first_slide) {
	Vector3 motion_slide_norm = result.remainder.slide(wall_normal).normalized();
	motion = motion_slide_norm * (motion.length() - result.travel.length());
} else {
	motion = result.remainder.slide(wall_normal);
}

Okay, so normally you project remaining motion onto the wall you’ve collided with. Makes sense. However, for the first collision remaining motion’s magnitude is calculated as motion.length() - result.travel.length() rather than the more obvious result.remainder.length(). I’m having trouble understanding why it does what it does:

First, vector’s magnitude is set to motion.length() - result.travel.length() instead of result.remainder.length() - my best guess is that it has something to do with the case of recovering from already being inside of an object. Am I correct?

Second, it applies magnitude to the vector after doing projection, which means motion magnitude is fully preserved and not projected. This part seems like a bug to me, with the correct implementation being

if (first_slide) {
	real_t remainder_magnitude = motion.length() - result.travel.length();
	Vector3 remainder_adjusted = result.remainder.normalized() * remainder_magnitude
	motion = remainder_adjusted.slide(wall_normal);
} else {
	motion = result.remainder.slide(wall_normal);
}

It looks like the current implementation would cause issues like CharacterBody2D (only in Floating Motion Mode) accelerated by slides · Issue #101052 · godotengine/godot · GitHub - although that one is 2D, the underlying calculation is identical.

1 Like

Small addition: it seems that there is a case where you want to skip projection entirely - namely, if you’ve recovered from being inside of an object in the general direction of your movement (i.e. won’t collide into the wall you’ve been ejected from on further movement). So my proposed code becomes

if (first_slide) {
	real_t remainder_magnitude = motion.length() - result.travel.length();
	Vector3 remainder_adjusted = result.remainder.normalized() * remainder_magnitude
	if (result.remainder.dot(wall_normal) <= 0.0) {
		motion = remainder_adjusted.slide(wall_normal);
	} else {
		motion = remainder_adjusted
	}
} else {
	motion = result.remainder.slide(wall_normal);
}