Sprite flickering when its frame_coords are changed in code

Godot v 4.11

Greetings!

In my project, I’ve set up a Sprite3D with a spritesheet as its texture that has 64 frames - 8 columns for every animation frame and 8 rows for 8 directions.

I’ve attached animation player to it to animate the frames and created the animation that cycles through frames 0-7 of the first row.

To skip the monotony and save time, I tried to update the animation based on direction through script, where I take the “facing” value that represents the current direction of the sprite and switch the Sprite3D’s frame_coords.y based on it, to a corresponding row in the spritesheet.

Everything appears to work correctly, except for a strange graphical bug. When the sprite’s direction is equal to the initial direction and the animation that was set up through animation player plays - everything is fine, but when the direction changes and the frame_coords.y updates, the sprite starts to flicker every frame. It looks like there’s an out of place frame somewhere in the animation that appears for fraction of a second, except there isn’t one.

I’ve tested it, printed frames and values of facing to the console and evrything appears to be working correctly. There are no out of place frames and the facing always correctly corresponds with the direction. But the flickering happens anyway.

I’ve tried making the animation frames last longer - 0.5 seconds instead of 0.1 like it was set up originally. What it showed is that the rate of flickering also slows down to 0.5. So the flickering happens in every frame of animation.

Here’s a video demonstrating what is happening. The video’s framerate isn’t very good, so you can’t see flickering as often as I do in the editor, but it’s still visible every time the character faces any direction but right.

Here’s a code of CharacterBody3D the sprite is attached to that handles the animation switching logic.

Code

extends CharacterBody3D

@onready var anim_player = $AnimationPlayer

@export var move_speed : float = 5.0

var ismoving := false
var facing = “right”
@onready var sprite = $Sprite3D

var anim_rows : Dictionary = {
“right”:0,
“backright”:1,
“back”:2,
“backleft”:3,
“left”:4,
“frontleft”:5,
“front”:6,
“frontright”:7
}

func _process(delta):
print(facing)
var movement_vector = get_movement_vector()
var direction = movement_vector.normalized()
var target_velocity = direction * move_speed

velocity.x = direction.x * move_speed
velocity.z = direction.y * move_speed

update_facing(direction)
move_and_slide()


if direction:
	anim_player.play("run")
	ismoving = true
else:
	anim_player.play("idle")
	ismoving = false

func get_movement_vector():

var x_movement = Input.get_action_strength("move_right") - Input.get_action_strength("move_left")
var y_movement = Input.get_action_strength("move_down") - Input.get_action_strength("move_up")

return Vector2(x_movement, y_movement)

func update_facing(direction):
sprite.frame_coords.y = anim_rows[facing]

if direction.y <- 0.5:
	facing = "back"
	
elif direction.x <-0.5:
	facing = "left"
	
elif direction.x > 0.5:
	facing = "right"
	
elif direction.y > 0.5:
	facing = "front"
	
elif direction.y > 0 and direction.x > 0:
	facing = "frontleft"
	
elif direction.y > 0 and direction.x < 0:
	facing = "frontright"
	
elif direction.y < 0 and direction.x > 0:
	facing = "backleft"
	
elif direction.y < 0 and direction.x < 0:
	facing = "backright"
	
else:
		return

I am not sure what the causes of this problem might be and need help figuring it out. Or, at the very least, advice on how else can I automate the creation of animation variations for each of the 8 directions. Considering I already have 6 characters in the project with at least 4 animations each, manually creating 8 animation variations for every direction for every action (that’s 192 animations already!) sounds very impractical.

Take a look at this:

https://www.reddit.com/r/godot/comments/12gwafr/sprite2d_frame_coord_y_value_gets_reset_to_0_when/

Happened to me some time ago.

Thank you for the link, but this isn’t what my issue is.
I am already changing specifically only frame_coords.y value in code without touching frames.

Where is facing actually being used to choose the animation?

Here:

func update_facing(direction):
sprite.frame_coords.y = anim_rows[facing]

For some reason, it isn’t parsing as code so it’s difficult to see. I am not sure how to format the text as code manually.

Two problems, then. First, you are updating the facing with the info from the previous frame, because you do it at the start of _process. Second, you are using move_and_slide() during _process, which is fine, but it would be better to use physics functions inside _physics_process so the rest of the processing happens after your character has moved.

I changed the script, moved move_and_slide() to _physics_process and moved update_facing(direction) into if statement to only update when the player is moving

Here’s the code:

	var movement_vector = get_movement_vector()
	var direction = movement_vector.normalized()
	var target_velocity = direction * move_speed
	direction_updated = direction
	
	velocity.x = direction.x * move_speed
	velocity.z = direction.y * move_speed
	
	move_and_slide()


func _process(delta):
	
	if direction_updated:
		anim_player.play("run")
		ismoving = true
		update_facing(direction_updated)
	else:
		anim_player.play("idle")
		ismoving = false

I’ve also tried moving things around in other ways. Updating facing at the end of process, moving the change of value of frame_coords.y to the end of update facing fucntion, after all the if checks.

The result is the same. The flickering happens in all the variations.
For some reason, now changing frame_coords.y also doesn’t affect idle animation. No matter what the facing value is, when the character is idle, the sprite is always looking to the right. The funny thing is, it happens even if I revert the script to what it was before all the changes. Even though you can clearly see on the video that it was updating the animation correctly before. I must have accidentaly changed something else, too.

Luckily, I’ve made a backup of the project and everything works as before in it.

Here’s the full updated script just in case

Full Code
extends CharacterBody3D

@onready var anim_player = $AnimationPlayer

@export var move_speed : float = 5.0

var direction_updated = 0.0

var ismoving := false
@onready var sprite = $Sprite3D


var anim_rows : Dictionary = {
	"right":0,
	"backright":1,
	"back":2,
	"backleft":3,
	"left":4,
	"frontleft":5,
	"front":6,
	"frontright":7
}

func _physics_process(delta):
	var movement_vector = get_movement_vector()
	var direction = movement_vector.normalized()
	var target_velocity = direction * move_speed
	direction_updated = direction
	
	velocity.x = direction.x * move_speed
	velocity.z = direction.y * move_speed
	
	move_and_slide()


func _process(delta):
	
	if direction_updated:
		anim_player.play("run")
		ismoving = true
		update_facing(direction_updated)
	else:
		anim_player.play("idle")
		ismoving = false


func get_movement_vector():
	
	var x_movement = Input.get_action_strength("move_right") - Input.get_action_strength("move_left")
	var y_movement = Input.get_action_strength("move_down") - Input.get_action_strength("move_up")
	
	return Vector2(x_movement, y_movement)

func update_facing(direction):
	var facing = "right"
	if direction.y <- 0.5:
		facing = "back"
		
	elif direction.x <-0.5:
		facing = "left"
		
	elif direction.x > 0.5:
		facing = "right"
		
	elif direction.y > 0.5:
		facing = "front"
		
	elif direction.y > 0 and direction.x > 0:
		facing = "frontleft"
		
	elif direction.y > 0 and direction.x < 0:
		facing = "frontright"
		
	elif direction.y < 0 and direction.x > 0:
		facing = "backleft"
		
	elif direction.y < 0 and direction.x < 0:
		facing = "backright"
		
	else:
			return
	sprite.frame_coords.y = anim_rows[facing]

Additionally, I got rid of move and slide altogether and wrote a different script that updates the facing value based on the camera’s global basis. And set up a scene with a camera rotating around the CharacterBody3D.

The result is also the same. The sprite flickers in the same way.

Here’s the video.

And here’s the code:

Summary
extends CharacterBody3D

var camera = null
var facing = "right"
var anim_rows : Dictionary = {
	"right":0,
	"backright":1,
	"back":2,
	"backleft":3,
	"left":4,
	"frontleft":5,
	"front":6,
	"frontright":7
}

func set_camera(c):
	camera = c

func _process(delta):
	
	$AnimationPlayer.play("idle")
	camera_facing()
	
func camera_facing():
	if camera == null:
		return
 
	var p_fwd = -camera.global_transform.basis.z
	var fwd = global_transform.basis.z
	var left = global_transform.basis.x
 
	var l_dot = left.dot(p_fwd)
	var f_dot = fwd.dot(p_fwd)
	
	if f_dot < -0.85:
		facing = "front"
	elif f_dot > 0.85:
		facing = "back"
	else:
		if abs(f_dot) < 0.3 and l_dot > 0:
			facing = "right" # left sprite
		elif abs(f_dot) < 0.3 and l_dot < 0:
			facing = "left" # left sprite
		elif f_dot < 0 and l_dot > 0:
			facing = "frontright" # forward left sprite
		elif f_dot < 0 and l_dot < 0:
			facing = "frontleft" # forward left sprite
		else:
			if l_dot < 0:
				facing = "backleft" # back left sprite
			else:
				facing = "backright"
	
	$Sprite3D.frame_coords.y = anim_rows[facing]

I can go even simpler.

extends CharacterBody3D

var anim_rows = 0

func _process(delta):
	$AnimationPlayer.play("idle")
	$Sprite3D.frame_coords.y = anim_rows


func _on_timer_timeout():
	anim_rows +=1

The script simply updates frame_coords.y each time the timer runs out (every second).

The flickering is reproduced in the same way.

I suspect the problem might be with the fact that I update the frame_coords.y value on its own, and not through the animation player. But I am not sure how to update it using the animation player, so I can’t test it, unless somebody can help me with that.

Or… the problem might be in the engine side itself?

Ah, I didn’t notice before you were using an animation player. It may be that it is reaching the end of the animation and going back to the default state before you start playing it again. It is not a synchronous process. To solve this race condition you could set your animations to loop, I think. That way it will stay on the same animation track until your code changes it.

The animation is already set to be looping.
Your suggestion gave me an idea to try and tweak other properties of the animation itself, though. And I’ve found out that updating frame_coords.y does exactly nothing when the animation is set to continuous update mode. It does change the spritesheet row when the animation is set to discrete update mode.
Not sure if this is helpful in any way, though.

Yeah, the AnimationPlayer is complicated and im not surprised changing it has no effect under some conditions. Honestly, if all you’re doing with animation is looping on the UV of an atlas, it might be easier and less of a headache to do it in your own code than managing it with the AP…
But if you’re gonna be doing finite state machines to drive the animation, maybe it’s time to start doing that instead of using code to change the animation track.
Whichever you choose, the point is to either do it on your own code, or not, but not half-and-half

Thanks. You are probably right and it’s something to keep in mind for the future. For now I just wanted a quick set up to prototype a bunch of things, which I didn’t get to because I ended up stuck on this problem.

But, the good news is, my assumption was correct. I figured out how to change the frame_coords.y property through the animation player instead of directly changing the Sprite3Ds property.

Here’s what I did

func animation_change():
	var animation = anim_player.get_animation(anim_player.current_animation)
	
	if animation:
		animation.track_set_key_value(2, 0, anim_rows[facing]) #the track under index 2 is frame_coords:y with a key value of 0)

Changing it in this way stopped the flickering. So I guess this is solved.

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