AnimatedSprite2D is visible in the editor but doesn't appear in-game

Godot Version

Godot 4.7

Question

Hi, I’m using Godot 4.7.

I have a Blue_Golem scene that looks like this:


extends CharacterBody2D

@onready var anim = $AnimatedSprite2D

func _ready():
	anim.play("idle")

func _physics_process(delta):
	pass

I instance blue_golem.tscn into my main.tscn, and both the player and the golem are at (0, 0).

The strange part is that the CollisionShape2D appears in-game, but the AnimatedSprite2D is completely invisible.

Here’s my player.gd script:

extends CharacterBody2D

const SPEED = 300.0

var last_direction: Vector2 = Vector2.RIGHT
var is_attacking: bool = false
var use_second_attack: bool = false # Toggle for the attack combo
var hitbox_offset: Vector2

# --- NODES ---
@onready var animated_sprite_2d: AnimatedSprite2D = $AnimatedSprite2D
@onready var swing_sword: AudioStreamPlayer2D = $SwingSword
@onready var hitbox: Area2D = $Hitbox

func _ready() -> void:
	# Initialize hitbox offset
	hitbox_offset = hitbox.position
	
# --- SOUNDS ---
const SLASH_1 = preload("uid://dajfxthjtexbv")
const SLASH_2 = preload("uid://dicth32opmm58")
const SLASH_3 = preload("uid://52tgn8a60r1p")

# Put the sounds in an array so we can easily pick a random one later
var slash_sounds: Array = [SLASH_1, SLASH_2, SLASH_3]


#------------------------
# MOVEMENT AND ANIMATION
#-------------------------
func _physics_process(_delta: float) -> void:
	
	if Input.is_action_just_pressed("attack") and not is_attacking:
		attack()
		
	# Check this EVERY frame, not just when the button is clicked
	if is_attacking:
		velocity = Vector2.ZERO
	else:
		# Only process movement and animation if we are NOT attacking
		process_movement()
		process_animation()
		
	# move_and_slide should always run at the very end so physics update properly
	move_and_slide()
	
func process_movement() -> void:
	var direction := Input.get_vector("left", "right","up","down")
	
	if direction != Vector2.ZERO:
		velocity = direction * SPEED
		last_direction = direction
		update_hitbox_offset()
	else:
		velocity = Vector2.ZERO
		
func process_animation() -> void:
	if is_attacking:
		return
	if velocity != Vector2.ZERO:
		play_animation("run", last_direction)
	else:
		play_animation("idle",last_direction)

func play_animation(prefix: String, dir: Vector2) -> void:
	if dir.x > 0:
		animated_sprite_2d.play(prefix + "_right")
	elif dir.y < 0:
		animated_sprite_2d.play(prefix + "_up")
	elif dir.y > 0:
		animated_sprite_2d.play(prefix + "_down")
	elif dir.x < 0:
		animated_sprite_2d.play(prefix + "_left")
	
#-------------------
# ATTACK
#-------------------

func attack() -> void:
	# Ignore input if we are already in the middle of an attack animation
	if is_attacking:
		return
		
	is_attacking = true
	
	# Load a random sound from our array and play it using the video's variable name
	swing_sword.stream = slash_sounds.pick_random()
	swing_sword.play()
	
	# Alternate between 'attack' and 'attack2' animations
	if use_second_attack:
		play_animation("attack2", last_direction)
	else:
		play_animation("attack", last_direction)
		
	# Flip the boolean so the next click plays the other animation
	use_second_attack = !use_second_attack
	
func _on_animated_sprite_2d_animation_finished() -> void:
	if is_attacking:
		is_attacking = false

#---------
# HITBOX
#---------

func update_hitbox_offset() -> void:
	# Calculate the total distance of the original offset you set in the editor
	var distance := hitbox_offset.length()
	
	# This perfectly matches how play_animation() chooses directions!
	if last_direction.x > 0:
		hitbox.position = Vector2(distance, 0)
	elif last_direction.y < 0:
		hitbox.position = Vector2(0, -distance)
	elif last_direction.y > 0:
		hitbox.position = Vector2(0, distance)
	elif last_direction.x < 0:
		hitbox.position = Vector2(-distance, 0)

func _on_hitbox_body_shape_entered(body_rid: RID, body: Node2D, body_shape_index: int, local_shape_index: int) -> void:
	pass # Replace with function body.

What else could cause an AnimatedSprite2D to be invisible in-game while its CollisionShape2D is still visible? Thanks.

Could you share the error shown in the Debugger?

Hello! The debugger warning was caused by the Area2D hitbox signal that I connected. It was pointing to this function:

func _on_hitbox_body_entered(body: Node2D) -> void:
		pass # Replace with function body.

I have already implemented the function, so that warning is now resolved.

Here’s my player.gd script currently:

extends CharacterBody2D

const SPEED = 300.0

var last_direction: Vector2 = Vector2.RIGHT
var is_attacking: bool = false
var use_second_attack: bool = false # Toggle for the attack combo
var hitbox_offset: Vector2

# --- NODES ---
@onready var animated_sprite_2d: AnimatedSprite2D = $AnimatedSprite2D
@onready var swing_sword: AudioStreamPlayer2D = $SwingSword
@onready var hitbox: Area2D = $Hitbox

func _ready() -> void:
	# Initialize hitbox offset
	hitbox_offset = hitbox.position
	
# --- SOUNDS ---
const SLASH_1 = preload("uid://dajfxthjtexbv")
const SLASH_2 = preload("uid://dicth32opmm58")
const SLASH_3 = preload("uid://52tgn8a60r1p")

# Put the sounds in an array so we can easily pick a random one later
var slash_sounds: Array = [SLASH_1, SLASH_2, SLASH_3]


#------------------------
# MOVEMENT AND ANIMATION
#-------------------------
func _physics_process(_delta: float) -> void:
	# Disable Hitbox until Attacking
	hitbox.monitoring = false
	
	if Input.is_action_just_pressed("attack") and not is_attacking:
		attack()
		
	# Check this EVERY frame, not just when the button is clicked
	if is_attacking:
		velocity = Vector2.ZERO
	else:
		# Only process movement and animation if we are NOT attacking
		process_movement()
		process_animation()
		
	# move_and_slide should always run at the very end so physics update properly
	move_and_slide()
	
func process_movement() -> void:
	var direction := Input.get_vector("left", "right","up","down")
	
	if direction != Vector2.ZERO:
		velocity = direction * SPEED
		last_direction = direction
		update_hitbox_offset()
	else:
		velocity = Vector2.ZERO
		
func process_animation() -> void:
	if is_attacking:
		return
	if velocity != Vector2.ZERO:
		play_animation("run", last_direction)
	else:
		play_animation("idle",last_direction)

func play_animation(prefix: String, dir: Vector2) -> void:
	if dir.x > 0:
		animated_sprite_2d.play(prefix + "_right")
	elif dir.y < 0:
		animated_sprite_2d.play(prefix + "_up")
	elif dir.y > 0:
		animated_sprite_2d.play(prefix + "_down")
	elif dir.x < 0:
		animated_sprite_2d.play(prefix + "_left")
	
#-------------------
# ATTACK
#-------------------

func attack() -> void:
	# Ignore input if we are already in the middle of an attack animation
	if is_attacking:
		return
		
	is_attacking = true
	hitbox.monitoring = true
	
	# Load a random sound from our array and play it using the video's variable name
	swing_sword.stream = slash_sounds.pick_random()
	swing_sword.play()
	
	# Alternate between 'attack' and 'attack2' animations
	if use_second_attack:
		play_animation("attack2", last_direction)
	else:
		play_animation("attack", last_direction)
		
	# Flip the boolean so the next click plays the other animation
	use_second_attack = !use_second_attack
	
func _on_animated_sprite_2d_animation_finished() -> void:
	if is_attacking:
		is_attacking = false

#---------
# HITBOX
#---------

func update_hitbox_offset() -> void:
	# Calculate the total distance of the original offset you set in the editor
	var distance := hitbox_offset.length()
	
	# This perfectly matches how play_animation() chooses directions!
	if last_direction.x > 0:
		hitbox.position = Vector2(distance, 0)
	elif last_direction.y < 0:
		hitbox.position = Vector2(0, -distance)
	elif last_direction.y > 0:
		hitbox.position = Vector2(0, distance)
	elif last_direction.x < 0:
		hitbox.position = Vector2(-distance, 0)

func _on_hitbox_body_entered(body: Node2D) -> void:
	if is_attacking:
		print("hit")
		pass # Replace with function body.

As for the AnimatedSprite2D not showing, I managed to fix it, although I’m not sure why it works. I had to run my scenes in this order:

  1. Press F6 on player.tscn
  2. Press F6 on blue_golem.tscn
  3. Press F6 on player.tscn again
  4. Press F6 on main.tscn

After doing that, both the player and the golem appeared correctly in the main scene. I’m not sure why this sequence fixes it, but that’s what solved the issue for me. Thanks for your help!

Run the RESET animation and see if it changes the position

Glad you got it working!

That F6 sequence normally shouldn’t be required. It may simply have forced Godot to save or reload the scene resources.

If it happens again, try saving all scenes, restarting the editor, and checking whether the AnimatedSprite2D properties are overridden in main.tscn.

He using an AnimatedSprite2D with SpriteFrames, not an AnimationPlayer, so there is no RESET animation in this scene.

The node position also appears correct; only the sprite was missing at runtime.