Godot Version
v4.3.stable.official [77dcf97d8]
Question
I’m trying to move characters in a low-res pixel-art scene (sub-pixel movement, scaled up). But every way I tried to do that has jittering or stuttering.
I know how to do smooth movement with a camera and a shader, but a camera will complicate my life because I just want to move some things in the world, and have those collide with other things.
I tried a similar approach with a shader on a sprite, with and without rounding. I tried no shader. I tried various combinations of “Snap 2D” for all of that. But none of these produced smooth movement.
(The gif may make “smooth camera” look less smooth, but in game itself, it’s perfectly smooth, unlike any of the others.)
Yes, I did see the article on jitter and stutter and that wasn’t really applicable to my issue.
How do I get it to move smoothly?
If there isn’t a direct solution, I guess my options are:
- Scale up the sprites, not the viewport container, and then pixel-perfect movement would be much smoother, given the much greater number of pixels. Then I guess I’d take that end result and scale it again (or let the game engine do that for me) to handle different resolutions.
- Use a camera and do manual collision detection
- I briefly tried using different viewports with a shared world, but that didn’t seem like it would work.
Would #1 be the best option?
Scene tree and code
Code:
extends Control
var position_unrounded = null
var new_pos = null
const MOVE_DELAY = 0.04
var move_timer = MOVE_DELAY
var MOVE_AMOUNT = 0.4
func update_pos(delta):
if position_unrounded == null:
position_unrounded = new_pos
else:
position_unrounded = lerp(position_unrounded, new_pos, 5*delta)
var cam_subpixel_pos = round(position_unrounded) - position_unrounded
%SpriteSR.material.set_shader_parameter("cam_offset", cam_subpixel_pos)
%SpriteSU.material.set_shader_parameter("cam_offset", cam_subpixel_pos)
%SpriteSnap.material.set_shader_parameter("cam_offset", cam_subpixel_pos)
%SpriteSR.position.x = round(position_unrounded)
%SpriteSU.position.x = position_unrounded
%SpriteSnap.position.x = position_unrounded
func _ready():
new_pos = %SpriteSR.position.x
update_pos(0)
_global.viewport_container = $SubViewportContainerCamera
new_camera_pos = %SubViewport.get_visible_rect().position + %SubViewport.get_visible_rect().size / 2
update_camera(0)
func _process(delta):
move_timer -= delta
if move_timer < 0:
new_pos += MOVE_AMOUNT
move_timer += MOVE_DELAY
%SpriteRaw.position.x += MOVE_AMOUNT
%SpriteSnapRaw.position.x += MOVE_AMOUNT
new_camera_pos.x -= MOVE_AMOUNT
update_pos(delta)
update_camera(delta)
# Camera
var new_camera_pos
var actual_cam_pos = null
func update_camera(delta):
if actual_cam_pos == null:
actual_cam_pos = new_camera_pos
else:
actual_cam_pos = lerp(actual_cam_pos, new_camera_pos, 5*delta)
var cam_subpixel_pos = actual_cam_pos.round() - actual_cam_pos
$SubViewportContainerCamera.material.set_shader_parameter("cam_offset", cam_subpixel_pos)
%Camera2D.global_position = actual_cam_pos.round()