CharacterBody2D staircase jitter when moving diagonally

Godot Version

Godot 4.3

Question

Hello! This is a top-down pixel art game where the player can move in 8 directions. I am moving a CharacterBody2D with the move_and_slide() function. Here is my player movement code:

extends CharacterBody2D

const SPEED := 60.0
const ACCELERATION := 800.0

func _physics_process(delta: float) -> void:
	# Gets direction input and normalises it
	var direction : Vector2
	direction.x = Input.get_axis("ui_left", "ui_right")
	direction.y = -Input.get_axis("ui_down", "ui_up") # Flip y-axis
	direction = direction.normalized()
	
	# Moves with acceleration
	velocity = velocity.move_toward(direction * SPEED, ACCELERATION * delta)
	move_and_slide()

When moving at 60 speed in any direction (without normalisation of the direction), there is no jitter. This is because the player is moving 60 pixels per second, which matches the physics ticks per second and my refresh rate. However when the direction is normalised, the player does not move at 60 pixels per second when moving diagonally. This should be fine, however there is a strange staircase effect that happens.

What happens (at 60 speed, normalised):
gif1

Here it is slower so it’s easier to see (at 4 speed, normalised):
gif2

This is what’s happening:

If I do NOT normalise the direction (so it remains at 60), then it moves as intended:
gif3

The stretch mode of the game is set to viewport, and “Snap 2D Transforms to Pixels” is enabled. Setting the stretch mode to canvas_items and disabling “Snap 2D Transforms to Pixels” obviously fixes the issue, however then it would not be a true pixel art game.

Putting global_position = round(global_position) after the move_and_slide() also fixes the problem, however then the acceleration of the movement is nullified because it rounds the velocity to 0 when trying to move less then 0.5 pixels. This is clear when decreasing the acceleration constant so that the acceleration is amplified with the move_toward() function.

I initially thought that the move_and_slide function was the problem, however the issue still occurs when moving the player by adjusting it’s position directly, like with position += direction.

The issue would be fixed if there’s a way to move the player both up/down and across on the same frame instead of different frames, but I’m not sure how I would go about implementing that. Again, the issue occurs when the direction is normalised (or not at speed of 60 pixels per second). Help would be greatly appreciated!

Hmm, not sure how to fix that, you could have an if else that limits the diagonal movement to once every two frames?

So if direction == Vector2(1,1):
if not iteration:
set velocity
iteration var = true
move_and_slide
else:
iteration = false
Else:
Do non diagonal movement

I think the problem is your diagonal movement means moving in non-integer values, while pixels are drawn at precise integer values on screen.
If you print your direction_normalized, you’ll see the diagonals are some positive and negative combination of (0.707107, 0.707107). Thus changing your objects position to a non-integer value.

May also be worth including the settings on the sprite and the body.

If you want to check the path and make sure nothings going unexpectedly there, you can put the following code into the base Node2D of the scene and it’ll draw the path of the object:

extends Node2D
var coord_array = []

func _process(delta: float) -> void:
	coord_array.push_back($CharacterBody2D.position)
	queue_redraw()
	pass

func _draw() -> void:
	draw_polyline(coord_array,Color(1,1,1,1))
	pass

If you see diagonal lines being drawn that aren’t perfectly straight, you’ll know it’s a coordinate issue. If the diagonals show up perfectly straight, then hopefully that narrows it down another way.

2 Likes

Yes, it seems it’s because the position of the player begins to move at non-whole values when moving diagonally. As shown in my post above it works fine when the speed is 60px per second moving diagonally, because it moves exactly one pixel per frame consistently. However when moving at any other speed (except for multiples of 60), you can see that the position updates to non-whole numbers, which causes the staircase effect.

gif4

Here is my code for the gif above. I commented out the normalized() function and replaced it with moving the player at 40px per second when moving diagonally. I also commented out the acceleration to simplify things for this example.

extends CharacterBody2D

const WALK_SPEED := 60.0
const DIAGONAL_SPEED := 40.0
#const ACCELERATION := 800.0

var speed : float = WALK_SPEED

func _physics_process(delta: float) -> void:
	# Gets direction input
	var direction : Vector2
	direction.x = Input.get_axis("left", "right")
	direction.y = -Input.get_axis("down", "up") # Flip y-axis
	#direction = direction.normalized()
	
	if direction.x * direction.y != 0:
		speed = DIAGONAL_SPEED # If moving diagonally, then move at 40px per second
	else:
		speed = WALK_SPEED # If not moving diagonally, then move at 60px per second
	
	# Moves with acceleration
	#velocity = velocity.move_toward(direction * speed, ACCELERATION * delta)
	velocity = direction * speed
	move_and_slide()
	
	print(global_position)

The only way I could see solving this is to keep the moving 60px per second while moving diagonally, but then to somehow slow down the rate (without changing the speed), while keeping it so that it moves one pixel up/down and one pixel across in the same frame. However, changing the actual timescale would then mess with future stuff, like animation.

I’ve tried doing stuff like moving the sprite independently from the CharacterBody2D and instead snapping to it’s global_position rounded, but that hasn’t worked.

Also with the pathing, most of the time it moves with the staircase effect, but sometimes it does move diagonally perfectly. This would be because of the sub-pixel values. Despite this, there’s unfortunately still some jitter, because it’s not moving at a consistent pixel rate. (Sorry, I can’t put another gif to show this because Godot forums isn’t letting me have more then one in this reply for some reason).

Anyway thank you for your response! If I can’t find a solution, then I will likely just switch to the canvas_item stretch mode and fake snap to the pixel grid when not moving for smoother movement.

Hi! Unfortunately that doesn’t work, but thank you for your reply though!!!

Can you use position = position.snapped(Vector2(1,1)) after your move slide call to ensure each frame the player ends on a whole pixel. Though the problem with this is it would make diagonal movement faster.

Yea, that would work. But like you said, the normalisation, as well as the acceleration of the movement, would no longer work. This seems to be the simplest solution while keeping the stretch mode set to viewport, however I would prefer to keep the normalisation and acceleration.

For reference: I’m using Godot 4.3 through Steam.
Do you have any code attached to the root node of the scene?
I copied your code into a scene that has a Node2D root, a Characterbody2D, and a Sprite2D. I put the draw code in the Node2D and then copied your code into Characterbody2D.
I had to set up actions for the axes, and using my controller, got pretty straight diagonals. The printout, though, seemed to show input fluctuation with what felt like holding the control stick perfectly steady.

Using the keyboard, I got perfect diagonal lines without any of the curvature you’re getting. This is a fresh project, so maybe try running your own test on a fresh project- if it behaves different in a fresh project, you know some kind of settings are different between the two and causing problems.

I’d try switching the Input.get_axis for some if-thens checking ui_up,ui_down,etc., just as a debug test.

It’s also weird that your down-right lines are drawing curves, while the up-right one is kind of a double thick pattern.

I created a new project and copied my code from my original post, however the staircase jitter still happens. Just to be as clear as I can, my settings for this new project was:

Viewport width/weight: 480x360
Stretch mode: viewport
Scale mode: integer
Snap 2D transforms to Pixel: true

The scene consists of a root Node2D with a CharacterBody2D (which the script is attached to), and a Sprite2D as a child of the CharacterBody2D. The texture of the Sprite2D is the default icon.svg, and the scale of the Sprite2D is 0.2. Also the position of the Sprite2D starts at 100.0, 100.0. I am using Godot 4.3 on Windows, downloaded from the website.

I also tried using the Godot 4.4 (beta1) that just released recently (with the same setup), however it produced the same results.