How to fix sub-pixel moving platform jitter without breaking physics

Godot Version

4.3

Question

I’m getting very close to releasing my first game, Unicopia, and have found an annoying bug causing moving platforms jitter.

As I was chasing the pixel perfect dream, I have Snap 2D Vertices to Pixel and
GPU Pixel Snap turned on, which I have to admit I don’t fully understand, although I was under the impression they’d lock my sprites to pixels.

However, I’ve still got an issue with moving platforms jittering, which I’ve realised is because sub-pixel physic calculations mean when my player sprite is standing still on a platform the pixel snapping can oscillate in and out of phase, making the player movement jittery.

Once easy fix I’ve come across is to round the positions of the platform and the player after moving them, which thankfully removes the jitter. However, rounding the positions makes some significant changes to my jump physics, unbalancing a load of my levels.

One variation of rounding the platform and player positions is to only round the player positions when they’re standing still, meaning the jitter disappears when they aren’t moving, is impossible to detect when they are moving, and the physics works when they’re moving as well. That said it causes a knock-on and rather subtle collision problem when the player’s touched by a moving platform, which is making me nervous, as I’m not sure what else might have broken.

While launching with a little jitter won’t be the end of the world, can anyone suggest how I can fix the jitter while minimizing the effect on overall physics movements?

Faking pixel perfect positioning with moving platforms is a tricky subject (though moving boxes is moreso) that is difficult to solve because it’s working correctly, just the “correct” result isn’t desirable.

Note: Before anything else, you shouldn’t turn on both Snap 2D Vertices to Pixel and Snap 2D Transforms to Pixel at the same time. The docs recommend only using Snap 2D Transforms to Pixel. That should at least make some of the following consistent.

The classic “moving platforms issue” stems from the fact that while the renderer is rounding the display of your sprites to whole pixels, you have two objects with their own different float positions, but moving at the same time. That means if your character’s position is in-between pixels, like x 450.5, and your platform is at x 460.9, they’ll both snap up (451, and 461), but as they move left, even 0.1 pixel, the character rounds down while the platform rounds up (450, 461). This results in a one pixel gap that keeps appearing and disappearing as they move, making the objects appear to jitter.

The good news is there is something you can do. Rather than controlling when your character crosses that threshold, you can make sure your platforms are consistent, never crossing the threshold at all! In your platform code, you can hold the “real position” to calculate its movement like normal, but use a rounded position to display:

extends AnimatableBody2D

var real_pos : Vector2

func _ready():
    real_pos = position

func _process(delta):
    # your movement code using real_pos goes here
    
    real_pos = some_new_position
    position = real_pos.round()

This allows your platform to move consistently only in whole numbers, meaning your character will follow in whole number increments while on the platform, eliminating any jitter.

Good luck!

1 Like

@markdibarry you are a superstar, thank you.

I’d already realised rounding the platform position could help, and had naively assumed rounding the position after the physics calculations would do the trick. However, it clearly didn’t work, and your answer gives me an inkling why.

Every physics frame I was rounding the position at the end of my calculations, but there was nothing to constrain the position to integers during the calculations. Presumably when the platform’s movement was transferred to the player it was transferring across the floating point component, leaving the player to oscillate across the rounding boundary, while the platform didn’t.

By doing the platform’s physics calculations using a separate floating point position and then just updating the position at the end, the floating point component is no longer transferred to the player, making everything just as it should be.

As you mentioned, the real challenge is working against how things should work (e.g. using nice “accurate” floating point physics) and then trying to jimmy them into a pixel perfect world. Although keeping everything rounded could also help with pixel perfection, it removed the increased accuracy of floating point physics, which would have been a disaster for the game.

Again, thank you so much.

1 Like