Camera jitter while moving player in grid based movement with Tween

Godot Version

4.3

Question

So, I am trying to make a grid based movement system. Currently I have 4 way movement that is controlled by InputEvent queuing the direction which is then run through the physics function which wil call my move function. This is regulated by a basic player state enum system. {MOVE, IDLE}

My move function will then calculate direction based on the input event, and then will tween the players position to +32 pixels in said direction. I have already tried the solution in another locked post “Grid movement jittering when used with tweens and camera following player”, since my camera also is attached to the player, however, making the tween process in physics did not solve the issue.

The issue is I have test box sprites that are Area2D, they jitter 1 pixel horizontally or vertically in relation to the direction I am moving. I have tried also to move my character in the physics_process directly using lerp, however, the jitter still persists.

This is frustrating, since my game design requires a grid based movement system, and I have tried various methods to move my characters in these 32 pixel increments, so far all have been jittery. If the camera isn’t attached to the player it isn’t jittery.

func _unhandled_key_input(event: InputEvent) -> void:
	var move_names : Array[String] = ["north","south","west","east"] 
# Key map strings
	for action_name in move_names: 
# Iterates through input options to populate move_queue
		if event.is_action_pressed(action_name): 
# Checks if input is a movement input
			if not move_queue.has(move_names.find(action_name)): 
# Checks if this is a duplicate
				move_queue.push_front(move_names.find(action_name)) 
# Moves input to front of queue
				break
		elif event.is_action_released(action_name): 
# Checks if event is input release
			move_queue.erase(move_names.find(action_name)) 
# Removes movement from queue
			break

# Runs a loop through input_queue; 
#checks if input direction is moveable, and if so moves
func _physics_process(_delta: float) -> void: 
# Controls smooth movement calls
	for action_dir in move_queue: 
# Iterates through the move queue
		if not state == State.MOVE: 
# Prevents rapid execution of movement
			if not move_dir[action_dir]: 
# Checks if direction is moveable;
# if not sets IDLE and continues
				state = State.IDLE
				_motion(action_dir)
				continue
			state = State.MOVE 
# Sets state to MOVE to prevent further movement calls
			_motion(action_dir)
			move._move(self,action_dir) 
# Calls _move function in player movement noded

#MOVEMENT CODE CALLED
enum {NORTH,SOUTH,WEST,EAST} # Matches player direction enums

var move_dist : int = 32 
# Clarity for what the number is used for

func _move(player:Node2D,dir:int) -> void: 
# Controls players movement
	var start = player.position
	var destination : Vector2
	
	if dir < 2: 
# Sets direction to == direction of player input
		if dir == 0: 
#Value == NORTH
			destination = start + Vector2.UP * move_dist
		else: 
#Value = 1 == SOUTH
			destination = start + Vector2.DOWN * move_dist
	elif dir == 2: 
#Value == WEST
		destination = start + Vector2.LEFT * move_dist
	else: 
#Value = 3 == EAST
		destination = start + Vector2.RIGHT * move_dist

	var tween = get_tree().create_tween() 
# Creates new tween to be used for player movement

	tween.set_process_mode(Tween.TWEEN_PROCESS_PHYSICS)
	tween.tween_property(player,"position",destination,player.speed)
	tween.tween_callback(func(): player.state = 0)
	tween.tween_callback(func(): player._motion(dir))

You’r problem is rounding the positions to whole positions (i think) and did some other tweaks…

extends CharacterBody2D  # Or Node2D depending on your needs

enum State { IDLE, MOVE }
enum Direction { NORTH, SOUTH, WEST, EAST }

@export var grid_size: int = 32
@export var move_speed: float = 0.15  # Duration of movement in seconds

var state: State = State.IDLE
var move_queue: Array = []
var is_moving: bool = false

# Called when handling input
func _unhandled_input(event: InputEvent) -> void:
	if is_moving:
		return
		
	var move_actions = {
		"move_north": Direction.NORTH,
		"move_south": Direction.SOUTH,
		"move_west": Direction.WEST,
		"move_east": Direction.EAST
	}
	
	for action in move_actions.keys():
		if event.is_action_pressed(action):
			if not move_queue.has(move_actions[action]):
				move_queue.push_back(move_actions[action])
		elif event.is_action_released(action):
			move_queue.erase(move_actions[action])

# Called during the physics processing
func _physics_process(_delta: float) -> void:
	if is_moving or move_queue.is_empty():
		return
		
	var direction = move_queue[0]
	move_in_direction(direction)

func move_in_direction(direction: Direction) -> void:
	var target_pos = position
	
	match direction:
		Direction.NORTH:
			target_pos.y -= grid_size
		Direction.SOUTH:
			target_pos.y += grid_size
		Direction.WEST:
			target_pos.x -= grid_size
		Direction.EAST:
			target_pos.x += grid_size
	
	# Round the positions to prevent floating-point errors
	target_pos = target_pos.round()
	position = position.round()
	
	# Create tween for smooth movement
	var tween = create_tween()
	tween.set_trans(Tween.TRANS_LINEAR)  # Use linear interpolation
	tween.set_ease(Tween.EASE_IN_OUT)
	is_moving = true
	
	# Ensure movement happens in physics process
	tween.set_process_mode(Tween.TWEEN_PROCESS_PHYSICS)
	
	# Move to target position
	tween.tween_property(self, "position", target_pos, move_speed)
	
	# Handle movement completion
	tween.tween_callback(func():
		is_moving = false
		move_queue.pop_front()
		state = State.IDLE
	)

# Add this to your camera node
func _process(_delta: float) -> void:
	# Ensure camera position is always rounded to whole pixels
	position = position.round()

and add this to your camera node

func _process(_delta: float) → void:
position = position.round()

1 Like

Hey, thank you for looking at the code and giving me that advice, it was helpful as far as considering other ways of formatting my movement parameters. Unfortunately, it didn’t solve the pixel jitter. My boxes still move by what seems to be 1 pixel. I only know this because they have a black border that is one pixel, and it will disappear on the right side and bottom when I am moving horizontally and vertically respectively.

However, your usage of the dictionary and the match statement was helpful in streamlining my implementation. Alas, the problem still persists.

Although some of your suggestions had to be tweaked because it didn’t provide the functionality that I was looking for. Specifically, that I had it set up so that new inputs would override the direction the character was moving. Also, with how you had it set up it wasn’t continually moving. So, I implemented the dictionary and the match statement in my code to streamline how I was doing things.

I did implement your code exactly as you suggested, and there was still the jitter though, so after I tweaked it using some of your suggestions but retaining the functionality that I was looking for.

One example that seemed redundant was the var is_moving, since I was already using the State.MOVE in the same fashion it didn’t seem necessary to have both, but maybe I am not understanding why to have both.

Update Edit:
So, I found a partial solution that creates another problem and is related to your solution. If you make the camera a sibling not a child of the player, and have its position = player.position.round() and after force_scroll_update() it removes the other sprites jitter, however, the player then jitters.

So, I think this confirms your analysis. So I need a way to move the player sprite or the player by rounded pixels. Otherwise either the other sprites will jitter, or the player will jitter.

So, I found the solution by asking a more specific question about moving character with rounded pixels. It is really simple, in project settings → rendering → 2D, you check the Snap 2D Transforms to Pixels. This fixed the issue for me, now neither the player or the other sprites jitter, I don’t need any extra code in my camera node either.

This is the image that was posted in my other question that showed to solution.

1 Like

Hahaha… cooll… i’m just to 3D minded i guess :wink:

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.