• 2D
  • Pixel perfect movement and rounding

This is an issue I've been having with Godot since I've started using it for a pixel perfect game project: https://godotforums.org/discussion/26396/jitter-in-player-kb2d-over-moving-platform-kb2d#latest

I think I've understood completely the problem, now I need to find a good way to fix it.

It can be best exaplained with an example:

There's a platform (KinematicBody2D) moved by an AnimationPlayer.

There's a player (KinematicBody2D) moved with move_and_slide.

Both use fractional increments for the movement because that's how AnimationPlayer and move_and_slide work.

Everything gets rounded to the pixels in the screen in the last step of rendering into the viewport. Stretch mode is set to "viewport" and Aspect is set to "keep".

The issue happens when the platform is at a fractional value and the player is over the platform following its movement.

Say platform is at (50.2, 40), the player is at (50.3, 30) moved by the platform. On the screen, it gets rounded and the platform is at (50, 40) and the player at (50, 30).

Next frame the platform moves to (50.4, 40) and the player moves to (50.5, 30). Now the platform is at (50, 40) and the player at (51, 30) on the screen. The player is one pixel to the right respect to the platform since last frame because of the rounding at the rendering stage.

The same can happen with any two objects moving next to each other whenever the fractional parts aren't the same.

I'm trying to find a solution but I don't know of any way to force the rounding to be done before the physics processing.

I can try to match the fractional part of the player's position with the fractional part of the moving platform but I think there should be a better way. If the rounding was done before the physics came into play, this issue wouldn't exist.

Thanks for reading.

Edit: Test project in a comment below.

I don't think you need to round the positions, this may be messing with the physics. Just enable "GPU pixel snap" in the project settings and everything should work.

@cybereality said: I don't think you need to round the positions, this may be messing with the physics. Just enable "GPU pixel snap" in the project settings and everything should work.

I tried and it does nothing.

What is the problem you are trying to solve?

Sorry, if I didn't explained it very well but it was my best attempt. I'll make an example project and post it.

This is a minimal project showing the issue. I hope this helps better understand my first post.

You can see here that the sprite on top (player) is jittering respect to the bottom sprite (platform). The AnimationPlayer changes the platform position in sub-pixels, in turn this changes the player's position by the same amount, then both positions get rounded in the rendering stage.

When using higher resolutions like the most common ones today this is practically unnoticeable. But for really pixel perfect games, or if we were programming for a device with a very low pixel density we would get this effect.

I think this could be fixed by doing the rounding somewhere before the physics engine kicks in, maybe in the AnimationPlayer, or forcing the KinematicBody2D to round the position set by AnimationPlayer. In any case, I would like to know more about this issue, how is it affecting others, how do you fix it, if there's any plans to improve the way it works, etc.

Thanks.

EDIT: I have updated the project to enable "sync with physics" in the platform and AnimationPlayer since I think they should be enabled. I've also tweaked the initial positions and animation to make sure the issue is visible. I said the platform is not jittering, I really meant the player doesn't move in sync with the platform, I've changed this in the comment.

Okay, it is late for me here. I will check out your project tomorrow.

If you change the platform to a RigidBody2D in mode Kinematic, the jitter goes away. There is a slight offset as the platform bounces off the way, but this is somewhat realistic given the momentum. Better than jitter for sure.

@cybereality said: If you change the platform to a RigidBody2D in mode Kinematic, the jitter goes away. There is a slight offset as the platform bounces off the way, but this is somewhat realistic given the momentum. Better than jitter for sure.

Thank you very much. This looked promising but after trying to make it work, it seems it doesn't really fix it, it just changes the way it shows up. I've modified the project to better see the issues with this change implemented.

I've enabled GPU snap this time.

It can be paused with the Intro key to see it more clearly.

I got it pretty much working. The trick was to use a KinematicBody2D with Physics Sync On. I also changed the test resolution of the window. I think that might have helped.

@cybereality said: I got it pretty much working. The trick was to use a KinematicBody2D with Physics Sync On. I also changed the test resolution of the window. I think that might have helped.

It breaks as soon as the top square is positioned between pixels. Set the top square position to (40.2, 121.5) and it breaks again. So it takes me back at my original assumption that the issue is in the coordinate rounding.

Setting a lower test resolution makes it harder to see but just because the pixels are smaller.

It looks pretty close to me. Honestly, I don't think anyone could notice once you have the real game graphics, it's like a difference of a pixel or less.

@cybereality said: It looks pretty close to me. Honestly, I don't think anyone could notice once you have the real game graphics, it's like a difference of a pixel or less.

With real game graphics it looks like crap. The difference can't be less than a pixel, it's a pixel that comes and goes. The amount of jittering can be even worse than in the example, it depends on the fractional parts being used in the platform and player, but I thought this was enough to illustrate the issue. This might not be an issue for a prototype or demo but it is for any attempt to do a serious game.

What you could try doing to work around the issue and have pixel-perfect graphics is take the visual child nodes of the KinematicBody, and make their global positions be rounded to the nearest pixel. Something like this, for example:

# Make all sprite child nodes use pixel-perfect positioning
for child_node in get_children():
	if child_node is Sprite:
		child_node.global_position = global_position.round()

Then the physics could still be in between pixels and work like normal, but visually everything will be 100% pixel perfect. The code above assumes none of the sprites have an offset relative to the KinematicBody2D. If there are offsets, what I would recommend doing is storing them in an array or dictionary, and then applying the offset after rounding the position.

var node_offsets = {}
func _ready():
	for child_node in get_children():
		if child_node is Sprite:
			node_offset[child_node.name] = child_node.position

# Then when applying pixel perfect positioning
for child_node in get_children():
	if child_node is Sprite:
		child_node.global_position = global_position.round() + node_offsets[child_node.name]

@TwistedTwigleg said: What you could try doing to work around the issue and have pixel-perfect graphics is take the visual child nodes of the KinematicBody, and make their global positions be rounded to the nearest pixel. Something like this, for example: ... Then the physics could still be in between pixels and work like normal, but visually everything will be 100% pixel perfect. The code above assumes none of the sprites have an offset relative to the KinematicBody2D. If there are offsets, what I would recommend doing is storing them in an array or dictionary, and then applying the offset after rounding the position.

Where would I put that code?

I think that's what's already happening. The physics work with the floats, and when rendering it gets rounded to the nearest pixel. And that's exactly the problem I'm trying to avoid or solve.

Two objects that are only 0.2 pixels apart can get rendered in the same pixel or 1 pixel apart depending on the exact position. If they're at (50, 50) and (50.2, 50.2) they will show at the same pixel, but if they're at (50.4, 50.4) and (50.6, 50.6) they will be at different pixel positions in the X and Y axes.

It's only an issue when an object is pushing or dragging one another, since the rounding is done after the interaction between objects.

@berarma said: Where would I put that code?

Ideally right after the movement of the KinematicBody2D, so after any movement of the node, the Sprite nodes get updated as well. You could also put it at the end of _physic_process and it'd probably also work.

I think that's what's already happening. The physics work with the floats, and when rendering it gets rounded to the nearest pixel. And that's exactly the problem I'm trying to avoid or solve.

Two objects that are only 0.2 pixels apart can get rendered in the same pixel or 1 pixel apart depending on the exact position. If they're at (50, 50) and (50.2, 50.2) they will show at the same pixel, but if they're at (50.4, 50.4) and (50.6, 50.6) they will be at different pixel positions in the X and Y axes.

It's only an issue when an object is pushing or dragging one another, since the rounding is done after the interaction between objects.

Hmm, what you may want to do then floor the value instead of rounding it, so positions (50, 50), (50.2, 50.2), and (50.6, 50.6) all would be rendered as (50, 50). So instead of global_position.round() you'd want to use global_position.floor().

@TwistedTwigleg said: Hmm, what you may want to do then floor the value instead of rounding it, so positions (50, 50), (50.2, 50.2), and (50.6, 50.6) all would be rendered as (50, 50). So instead of global_position.round() you'd want to use global_position.floor().

And here you have the same problem again: (50, 50),(50.2, 50.2) and (49.9, 49.9), (50.1, 50.1).

It only changes the breaking point from .5 to .0.

Edit: I've tested it now in the example project and it's the same, it doesn't solve the issue using round nor floor.

I'm sorry, but how this supposed to help me? The position values are already rounded in the rendering stage.

If you make sure your nodes/children are in the correct position in the first place then the render stage rounding not only might not be an issue but might not be necessary at all?