Saving Instanced Child with Config File Save/Load System

Godot Version

Godot_v4.4.1-stable_win64

Question

Can I save and Instanced Child to Load later using a Config File?

I’m making an avatar creator and I want to be able to save and load all the things a player chooses for their character to load into another scene eventually. I was following a tutorial on using Config Files to save player data and it works perfectly for the name value but I am stuck on getting it to save the hairstyles and their color that instance in on top of a node 3D that holds the position of where the hair should sit on the player model. (Pics below for better context!)

Remote Debug Example of Hair Mesh being instanced (No children when no instance which is default) :

Saving data works but loading the data I get this error :

Trying to assign value of type 'Array' to a variable of type 'Node3D'.

on this line :

hair_instance = config.get_value("PalSettings", "HairInst")

Save/Load Section of Script :

#Hair :
@export var hair_instance : Node3D
@export var hair_color : ColorPicker

#Facial Features : 

 
func _on_save_pressed():
	var config = ConfigFile.new()
	config.set_value("PalSettings", "PalName", pal_name.text)
	config.set_value("PalSettings", "HairInst", hair_instance.get_children())
	#config.set_value("PalSettings", "HairColor", hair_color.color)
	config.save("user://PalSettings.cfg")
	
	print("Pal Saved!")

func _on_load_pressed():
	var config = ConfigFile.new()
	var result = config.load("user://PalSettings.cfg")
	
	if result == OK:
		pal_name.text = config.get_value("PalSettings", "PalName")
		hair_instance = config.get_value("PalSettings", "HairInst")
		#hair_color.color = config.get_value("PalSettings", "HairColor")
		
		printerr("Error while loading save file! May be corrupted or missing!")#Save Data Variables
#Name : 
@export var pal_name : LineEdit

Example of Hair Instance script (There is a button for every hair style with this function on pressed) :

func rogerhair_inst():
	var RogerHairScene = preload("res://Meshes/Angels/Hair/HairInstScenes/RogerHair.tscn")
	var RHinst = RogerHairScene.instantiate()
	var RogerHPos = $"../../../../Base_Angel/Armature/Skeleton3D/NeckBone/HairPos1"
	#Remove Existing Children 
	for child in RogerHPos.get_children():
		child.queue_free()
	#Add Child 
	RogerHPos.add_child(RHinst)
	#Checking for Instance
	if is_instance_valid(RHinst):
		pass
#----------------------------------------------------------------------------------------------------------------------------
func _on_roger_hair_btn_pressed():
	#Runs the Roger Hair Instance func 
	rogerhair_inst()
#Playing Animation
	AP.play("Posing001")

I’m also wondering if I have to set the config within each instance button instead of the save/load section? Thank you for reading!

In this line you save an array with childs of hair_instance instead you can do this:

config.set_value("PalSettings", "HairInst", hair_instance)

(I’ve never tried saving a node in a config file, so it might not work, in which case please tell me to give you a better method)

1 Like

Updated Script!

#Hair :
@export var hair_instance : MeshInstance3D
@export var hair_color : ColorPicker

#Facial Features : 

 
func _on_save_pressed():
	var config = ConfigFile.new()
	config.set_value("PalSettings", "PalName", pal_name.text)
	config.set_value("PalSettings", "HairInst", hair_instance)
	#config.set_value("PalSettings", "HairColor", hair_color.color)
	config.save("user://PalSettings.cfg")
	
	print("Pal Saved!")

func _on_load_pressed():
	var config = ConfigFile.new()
	var result = config.load("user://PalSettings.cfg")
	
	if result == OK:
		pal_name.text = config.get_value("PalSettings", "PalName")
		hair_instance = config.get_value("PalSettings", "HairInst")
		#hair_color.color = config.get_value("PalSettings", "HairColor")
		
		printerr("Error while loading save file! May be corrupted or missing!")#Save Data Variables
#Name : 
@export var pal_name : LineEdit

but got this error :

E 0:00:11:483   AlterCreationUI.gd:1657 @ _on_load_pressed(): Couldn't find the given section "PalSettings" and key "HairInst", and no default was given.
  <C++ Error>   Condition "p_default.get_type() == Variant::NIL" is true. Returning: Variant()
  <C++ Source>  core/io/config_file.cpp:85 @ get_value()
  <Stack Trace> AlterCreationUI.gd:1657 @ _on_load_pressed()

In the tutorial I was using they assigned the variables to look for here in the root node of the scene where the script was attached, I’m now realizing I don’t have anything to put here because it doesn’t technically exist until the player makes it exist. Before I put the Node3D that is called HairPos and that wasn’t working either unfortunately :

Not sure if it will help but this is the tutorial I was using :

Saving nodes/instances as text is scary business, where numbers, text, and colors are well-defined as text, what would your instance be? It has tons of data packed into it, do you mean to save the entire mesh, material, scale, node name, metadata, visual layers, etc?

Chances are there is a way to save how you got the instance instead of the instance itself, maybe just the scene or mesh file path? If your hair is always a scene like RogerHair appears to be, then you can save hair_instance.scene_file_path and load it like so

var hair_path: String = config.get_value("PalSettings", "HairInst")
var hair_scene: PackedScene = load(hair_path)
hair_instance = hair_scene.instantiate()
# and you will need to `add_child()` the instance too
1 Like

Updated it to this, think I’m misunderstanding though.

#Name : 
@export var pal_name : LineEdit

#Skin Tone : 

#Hair :
@export var hair_instance : MeshInstance3D
@export var hair_color : ColorPicker


#Facial Features : 

 
func _on_save_pressed():
	var config = ConfigFile.new()
	
	#Hair Instance Save 
	hair_instance.scene_file_path
	
	config.set_value("PalSettings", "PalName", pal_name.text)
	#config.set_value("PalSettings", "HairColor", hair_color.color)
	config.save("user://PalSettings.cfg")
	
	print("Pal Saved!")

func _on_load_pressed():
	var config = ConfigFile.new()
	var result = config.load("user://PalSettings.cfg")
	
	if result == OK:
		
		var hair_path: String = config.get_value("PalSettings", "HairInst")
		var hair_scene: PackedScene = load(hair_path)
		hair_instance = hair_scene.instantiate()
		
		pal_name.text = config.get_value("PalSettings", "PalName")
		#hair_color.color = config.get_value("PalSettings", "HairColor")
		
		printerr("Error while loading save file! May be corrupted or missing!")#Save Data Variables



Got this error when trying to save :

Invalid access to property or key 'scene_file_path' on a base object of type 'Nil'.

Does this mean I need to add something to the instance buttons too for it to work?

# and you will need to `add_child()` the instance too

For saving there’s two apparent problems, first is that your hair_instance is null, so there’s no way we’re able to save it in the first place. Second is that you aren’t doing anything with the scene_file_path, you probably get a warning along the lines of “statement has no effect”. You must save the scene_file_path for it to be useful later in the load function.

func _on_save_pressed():
	var config = ConfigFile.new()
	
	#Hair Instance Save 
	if hair_instance: # confirm hair exists
		# save hair scene path
		config.set_value("HairPath", hair_instance.scene_file_path)
	else:
		print("No hair to save!")
	
	config.set_value("PalSettings", "PalName", pal_name.text)
	config.save("user://PalSettings.cfg")

During loading I mention add_child because creating a new instance of the hair doesn’t automatically add it to the scene, and I do not know where you want the hair to be added relative to this save function.

1 Like

This is where I’m at now :

#Save Data Variables

#Name : 
@export var pal_name : LineEdit

#Skin Tone : 

#Hair :
@onready var hair_path = $"../../../../Base_Angel/Armature/Skeleton3D/NeckBone/HairPos1".get_children()
@onready var hair_instance = $"../../../../Base_Angel/Armature/Skeleton3D/NeckBone/HairPos1".get_children()
@export var hair_color : ColorPicker


#Facial Features : 

 
func _on_save_pressed():
	var config = ConfigFile.new()
	
	#Hair Instance Save 
	if hair_instance: #confirm hair instance exists!
		#Saving hair scene path!
		config.set_value("PalSettings", "HairPath", hair_instance.scene_file_path)
	
	else: 
		print("No hair to save!")
	
	config.set_value("PalSettings", "PalName", pal_name.text)
	#config.set_value("PalSettings", "HairColor", hair_color.color)
	config.save("user://PalSettings.cfg")
	
	print("Pal Saved!")

func _on_load_pressed():
	var config = ConfigFile.new()
	var result = config.load("user://PalSettings.cfg")
	
	if result == OK:
		
		var hair_path: String = config.get_value("PalSettings", "HairPath")
		var hair_scene: PackedScene = load(hair_path)
		hair_instance = hair_scene.instantiate()
		
		pal_name.text = config.get_value("PalSettings", "PalName")
		#hair_color.color = config.get_value("PalSettings", "HairColor")
		
		printerr("Error while loading save file! May be corrupted or missing!")

I think I am confused on how to define the variables in this section :

#Hair :
@onready var hair_path = $"../../../../Base_Angel/Armature/Skeleton3D/NeckBone/HairPos1".get_children()
@onready var hair_instance = $"../../../../Base_Angel/Armature/Skeleton3D/NeckBone/HairPos1".get_children()

I’m also wondering if using a configfile isn’t the best option for saving this data, all the cosmetics are instanced in like this. I’m hoping to be able to load the last saved options in this same scene along with pulling the options that make up the avatar into other game scenes in the future.

Using @onready and get_children() will only capture the children when this node is added to the scene, that may not be what you need. Maybe using a node path instead of getting the direct node, then grabbing the node and/or it’s children during the save function. Is there more than one hair at a time? Do you need to save multiple hair instances?

1 Like

I’m thinking if I add more than one hair instance it’ll be added separately on a different “Pos” node, from a different tab in the tab container. Almost like a separate category.

Would add a Hair 2 or something like that to these tabs.

and then it’s own “Position” node here :

However I save the hair, I’ll have to repeat for the rest of the different types of “cosmetics”.

The Pos nodes are fantastic for organizing, I’d recommend adding all of them to a group, maybe “CosmeticPosition” so you can iterate through them and use some of their properties for a more dynamic script.

After adding groups to your Pos nodes, try this script. It makes heavy use of the group and node name to save data without hard coding so many strings, only using the grouped nodes as references.

func _on_save_pressed() -> void:
	var config := ConfigFile.new()
	var cosmetics: Array[Node] = get_tree().get_nodes_in_group("CosmeticPosition")

	for cosmetic in cosmetics:
		# skip any cosmetic Pos without children
		if cosmetic.get_child_count() == 0:
			continue
		# Get the first child's scene path, this method only supports one
		# cosmetic child per Pos
		var path: String = cosmetic.get_child(0).scene_file_path
		# Save the cosmetic Pos name with it's scene path, it will result in:
		# (PalSettings, HairPos1, res://Hairs/RogersHair.tscn)
		config.set_value("PalSettings", cosmetic.name, path)

	config.save("user://PalSettings.cfg")


func _on_load_pressed() -> void:
	var config := ConfigFile.new()
	var result := config.load("user://PalSettings.cfg")
	if result == OK:
		# Now we can iterate through all of the PalSettings keys, for the Pos names.
		var keys: Array[String] = config.get_section_keys("PalSettings")
		var cosmetics: Array[Node] = get_tree().get_nodes_in_group("CosmeticPosition")

		# Sadly we will have to find the cosmetic by name with lots of loops.
		for key in keys:
			for cosmetic in cosmetics:
				if cosmetic.name == key:
					var path: String = config.get_value("PalSettings", key, "")
					# load the saved resouce path and immediately add it to the
					# same cosmetic we found by name.
					var new_instance: Node3D = load(path).instantiate()
					cosmetic.add_child(new_instance)

					# stop looping after first find, an optimization
					break

You may have to implement a “remove all cosmetics” function as this example loading function does not abide by the one child per Pos rule, thus breaking functionality if there are already children.

1 Like

Alright this is the updated script :

func _on_save_pressed() -> void:
	var config = ConfigFile.new()
	var cosmetics: Array[Node] = get_tree().get_nodes_in_group("CosmeticPosition")
	
	#Cosmetic Handling
	for cosmetic in cosmetics:
		#Skip Pos Nodes w/o children
		if cosmetic.get_child_count() == 0:
			continue
			
		#Getting the first child's scene path
		var path: String = cosmetic.get_child(0).scenefilepath
		#Saving CosmeticPos with it's Child's Scene path
		config.set_value("PalSettings", cosmetic.name, path)
	
	
	#Cosmetic Colors 
	#config.set_value("PalSettings", "HairColor", hair_color.color)
	
	#Entered Data 
	config.set_value("PalSettings", "PalName", pal_name.text)
	
	
	#Saving Data to Config FIle
	config.save("user://PalSettings.cfg")
	
	print("Pal Saved!")


#Loading Function
func _on_load_pressed():
	var config = ConfigFile.new()
	var result = config.load("user://PalSettings.cfg")
	
	#Loading Saved Data
	if result == OK:
		var keys: Array[String] = config.get_section_keys("PalSettings")
		var cosmetics: Array[Node] = get_tree().get_nodes_in_group("CosmeticPositions")
		
		for key in keys:
			for cosmetic in cosmetics:
				if cosmetic.name == key:
					var path: String = config.get_value("PalSettings", key, "")
					#load the saved resource path and add it to the cosmetic 
					var new_instance: Node3D = load(path).instantiate()
					cosmetic.add_child(new_instance)
		
		#hair_color.color = config.get_value("PalSettings", "HairColor")
		pal_name.text = config.get_value("PalSettings", "PalName")
		
		printerr("Error while loading save file! May be corrupted or missing!")

After testing, I got this error :

Invalid access to property or key 'scenefilepath' on a base object of type 'MeshInstance3D'.

on this line :

		var path: String = cosmetic.get_child(0).scenefilepath

It looks like it’s detecting the instance now, I think!

Also, at this point :

# Sadly we will have to find the cosmetic by name with lots of loops.
		for key in keys:
			for cosmetic in cosmetics:
				if cosmetic.name == key:
					var path: String = config.get_value("PalSettings", key, "")

Will I need to write this in for each CosmeticPosition type in those quotes or is this alright as is?

Thank you for the help so far, learning a lot from this thread, it’s been hard finding resources on saving/loading!

The property is scene_file_path, with underscores.


I’m not sure what you mean by “CosmeticPositions type”, it will work for Node3D in group “CosmeticPosition” with a child that has a saved packed scene.

We haven’t discussed much about the cosmetic color options so I don’t have a great solution for streamlining that kind of data, sorry.


For your save and load function you are getting two different groups, one with s and one without

1 Like
func _on_save_pressed() -> void:
	var config = ConfigFile.new()
	var cosmetics: Array[Node] = get_tree().get_nodes_in_group("CosmeticPosition")
	
	#Cosmetic Handling
	for cosmetic in cosmetics:
		#Skip Pos Nodes w/o children
		if cosmetic.get_child_count() == 0:
			continue
			
		#Getting the first child's scene path
		var path: String = cosmetic.get_child(0).scene_file_path
		#Saving CosmeticPos with it's Child's Scene path
		config.set_value("PalSettings", cosmetic.name, path)
	
	
	#Cosmetic Colors 
	#config.set_value("PalSettings", "HairColor", hair_color.color)
	
	#Entered Data 
	config.set_value("PalSettings", "PalName", pal_name.text)
	
	
	#Saving Data to Config FIle
	config.save("user://PalSettings.cfg")
	
	print("Pal Saved!")


#Loading Function
func _on_load_pressed():
	var config = ConfigFile.new()
	var result = config.load("user://PalSettings.cfg")
	
	#Loading Saved Data
	if result == OK:
		var keys: Array[String] = config.get_section_keys("PalSettings")
		var cosmetics: Array[Node] = get_tree().get_nodes_in_group("CosmeticPosition")
		
		for key in keys:
			for cosmetic in cosmetics:
				if cosmetic.name == key:
					var path: String = config.get_value("PalSettings", key, "")
					#load the saved resource path and add it to the cosmetic 
					var new_instance: Node3D = load(path).instantiate()
					cosmetic.add_child(new_instance)
					
					break
		
		#hair_color.color = config.get_value("PalSettings", "HairColor")
		pal_name.text = config.get_value("PalSettings", "PalName")
		
		printerr("Error while loading save file! May be corrupted or missing!")

Updated script, kind of silly errors on my part :sweat_smile: As for colors I think I have some things I’m gonna try out to save their data.

It saved the data so neatly in the config file, I honestly thought I’d have to type out each and every type of position node we had to do this! :

[PalSettings]

HairPos1="res://Meshes/Angels/Hair/HairInstScenes/RogerHair.tscn"
EyePos="res://Meshes/Angels/Eyes/StatiaEyes.glb"
BrowsPos="res://Meshes/Angels/Brows/TSCN/roger_brows.tscn"
FacialHairPos="res://Meshes/Angels/FacialHair/FacialHairInstScenes/DavidFacialHair.tscn"
FacePos="res://Meshes/Angels/Face/FaceScenes/agna_freckles.tscn"
PalName="Test"

But, when I press the load button, I get this error :

Trying to assign a value of type "PackedStringArray" to a variable of type "Array[String]".

Here :

		var keys: Array[String] = config.get_section_keys("PalSettings")

After messing around a little I changed it to this and it loads perfectly!

	var keys: PackedStringArray = config.get_section_keys("PalSettings")

I see what you mean now about having a “Remove all Cosmetics” option and I’m going to try to fix that up. Thank you again, it works great! :grinning:

Full updated script just for reference :

#Save Data Variables
#Name

@export var pal_name : LineEdit

#CosmeticColors
@export var hair_color : ColorPicker

 
func _on_save_pressed() -> void:
	var config = ConfigFile.new()
	var cosmetics: Array[Node] = get_tree().get_nodes_in_group("CosmeticPosition")
	
	#Cosmetic Handling
	for cosmetic in cosmetics:
		#Skip Pos Nodes w/o children
		if cosmetic.get_child_count() == 0:
			continue
			
		#Getting the first child's scene path
		var path: String = cosmetic.get_child(0).scene_file_path
		#Saving CosmeticPos with it's Child's Scene path
		config.set_value("PalSettings", cosmetic.name, path)
	
	
	#Cosmetic Colors 
	#config.set_value("PalSettings", "HairColor", hair_color.color)
	
	#Entered Data 
	config.set_value("PalSettings", "PalName", pal_name.text)
	
	
	#Saving Data to Config FIle
	config.save("user://PalSettings.cfg")
	
	print("Pal Saved!")


#Loading Function
func _on_load_pressed():
	var config = ConfigFile.new()
	var result = config.load("user://PalSettings.cfg")
	
	#Loading Saved Data
	if result == OK:
		var keys: PackedStringArray = config.get_section_keys("PalSettings")
		var cosmetics: Array[Node] = get_tree().get_nodes_in_group("CosmeticPosition")
		
		for key in keys:
			for cosmetic in cosmetics:
				if cosmetic.name == key:
					var path: String = config.get_value("PalSettings", key, "")
					#load the saved resource path and add it to the cosmetic 
					var new_instance: Node3D = load(path).instantiate()
					cosmetic.add_child(new_instance)
					
					break
		
		#hair_color.color = config.get_value("PalSettings", "HairColor")
		pal_name.text = config.get_value("PalSettings", "PalName")
		
		printerr("Error while loading save file! May be corrupted or missing!")

1 Like