CharacterBody2D only shows up in Exports if Editable Children is checked

Godot Version

4.4.1-stable

Question

I’m making a metroidvania game for Metroidvania Month 28 Game Jam. (It ends today.) When I export my game, if a CharacterBody2D in a level does not have Editable Children checked in the editor, the characters are not visible in the game. It’s really weird behavior, and I’m wondering if anyone else has seen it or know what causes it?

The biggest problem this causes, is I cannot initially spawn in characters like the player and bosses, because setting the value of editable children in code before or after adding them to the scene tree isn’t enough.

My only guess is that I have a tool script attached to the AnimatedSprite2D nodes that loads all the images for me from a directory so I don’t have to. It loads all the sprite frames for a character in about a second, instead of me doing it by hand. It has saved me immeasurable time.

@tool
class_name ChibiAnimatedSprite2D extends AnimatedSprite2D

const DEATH_ANIMATION = "dying"
const FALL_ANIMATION = "falling_down"
const HURT_ANIMATION = "hurt"
const IDLE_ANIMATION = "idle"
const IDLE_BLINKING_ANIMATION = "idle_blinking"
const JUMP_ANIMATION = "jump_loop"
const JUMP_START_ANIMATION = "jump_start"
const KICK_ANIMATION = "kicking"
const RUN_SLASH_ANIMATION = "run_slashing"
const RUN_THROW_ANIMATION = "run_throwing"
const RUN_ANIMATION = "running"
const ATTACK_ANIMATION = "slashing"
const JUMP_ATTACK_ANIMATION = "slashing_in_the_air"
const SHOOT_ANIMATION = "shooting"
const JUMP_SHOOT_ANIMATION = "shooting_in_the_air"
const SLIDE_ANIMATION = "sliding"
const THROW_ANIMATION = "throwing"
const JUMP_THROW_ANIMATION = "throwing_in_the_air"
const WALK_ANIMATION = "walking"

@export_dir var sprite_root_folder:
	set(value):
		sprite_root_folder = value
		if sprite_frames:
			sprite_frames = null
		if value:
			_load_sprite_frames()


## Returns true or false if this sprite has this animation.
func has_animation(animation_name: StringName) -> bool:
	return sprite_frames.has_animation(animation_name)


func _load_sprite_frames() ->void:
	#if sprite_frames is null, create a new instance
	if not sprite_frames:
		sprite_frames = SpriteFrames.new()
	#delete default if it exists
	sprite_frames.remove_animation(&"default")
	#iterate through folders
	var dir = DirAccess.open(sprite_root_folder)
	if not dir:
		return
	var animation_list = dir.get_directories()
	for animation_dir in animation_list:
		var animation_name = animation_dir.get_file().to_snake_case()
		sprite_frames.add_animation(animation_name)
		sprite_frames.set_animation_speed(animation_name, 24.0)
		#Set looping for looping animations
		if animation_name.contains(IDLE_ANIMATION) or \
				animation_name.contains("falling") or \
				animation_name.contains("loop") or \
				animation_name.contains("running") or \
				animation_name.contains("sliding") or \
				animation_name.contains("walking"):
			sprite_frames.set_animation_loop(animation_name, true)
		else:
			sprite_frames.set_animation_loop(animation_name, false)
		#change to the specific animation directory
		dir.change_dir(animation_dir)
		var file_list = dir.get_files()
		var path = dir.get_current_dir() + "/"

		#skip over .import files and uids and add the animation frames
		for file in file_list:
			if file.ends_with(".png"):
				sprite_frames.add_frame(animation_name, load(path + file))
		#drop back down a directory for the next loop
		dir.change_dir("..")
	#set the default animations
	set_autoplay(IDLE_ANIMATION)
	animation = IDLE_ANIMATION

Anyway, I have 11 hours until the jam concludes, and if anyone has any ideas on solving this problem, please let me know. Right now I’m working on a staging area for the player to be in when the game starts to pull it into the first level. (Needed so I don’t have two players when the player re-enters the first level from the second level.)

The exported game will have different files in "res://" paths than testing in editor. Because your tool script applies both in-editor and in-game, you may be trying to read animation data when the game launches and the export has clobbered the file paths you want.

Try applying the animations through a @export_tool_button and saving the changes; otherwise you can re-write your loading script to use ResourceLoader.list_directory() instead of dir access.

1 Like

Thanks @gertkeno I will look into that after the game jam. Right now, it’s hacked and working.

Now I’m having the same problem now with loading pictures of keyboard keys. My gamepad buttons all show up because I exported them as variables. This seems to be the same problem. I’m not sure what to do to fix the code. Since I’m not using DirAccess here, but the ResourceLoader.

extends Node


@onready var icon_path: String = "res://addons/dragonforge_controller/assets/key_icons/"


## Returns the Texture2D representation of the keyboard key event passed
func get_key_icon(event: InputEventKey) -> Texture2D:
	var keyname = event.as_text().trim_suffix(" (Physical)")
	keyname = keyname.trim_prefix("Kp ")
	var filename = icon_path + "keyboard_" + keyname + "_outline.png"
	if ResourceLoader.exists(filename):
		return load(filename)
	return

Yes load should work for export and in-editor alike; maybe drop the exists check before hand as load will return null on failure anyways

1 Like

I dropped the exists() call and it still fails gracefully, but it’s still not loading the files on export, and still loads them when I run the game from the editor.

Is there any reason your files would be excluded from the export? Are you on Windows, if so did you capitalize the path the exact same way as the files are named?

1 Like

Nope, in fact I specifically included them with a filter just to check.

Yes and . . . I thought so.

Turns out I thought surely this isn’t something I did because I always check. But once I removed the exists() call, it turned out that the keyname is actually capitalized, so I needed to convert it to lowercase. I went with snake_case just in case.

var filename = icon_path + "keyboard_" + keyname.to_snake_case() + "_outline.png"

So that solves that issue. Thank you!

I’ll get back to you on the other one later.

Final code on the first issue for reference. (Turns out snake_case was the wrong choice.)

extends Node

const icon_path = "res://addons/dragonforge_controller/assets/key_icons/"


## Returns the Texture2D representation of the keyboard key event passed
func get_key_icon(event: InputEventKey) -> Texture2D:
	var keyname = event.as_text().trim_suffix(" (Physical)")
	keyname = keyname.trim_prefix("Kp ").to_lower()
	var filename = icon_path + "keyboard_" + keyname + "_outline.png"
	return load(filename)