Godot Version
v4.7.stable.official
Question
I’m making a game where I will be managing a lot of characters, items, etc. which must be saved to file, so I need a save/load system that’s elegant and scalable. I want to use a collection of resources nested within resources. For example, this would allow me to change Stats in a way that affects anything which has Stats, whether it be a Creature, a Tree, a Wall, etc. (I’m well aware that making my Characters with Dictionaries or Arrays would make it much easier to save and load to files, but I think it would cause problems with scalability down the road.) Right now, I’m facing the problem of how to save and load resources which are attached to each other.
class character extends Resource:
var identity:Identity
var profession:Profession
var stats:Stats
var portrait:CompressedTexture2D
func _init( famName:String, persName:String, ):
identity = Identity.new( famName, persName )
class Identity extends Resource:
var personalName:="Nameless"
var familyName:= "Vagabond"
func _init( family:String, personal:String ):
personalName = personal
familyName = family
func _ready() -> void:
var grandma = Characters.character.new( "Kinoti", "Arrow" )
print( grandma.identity.familyName)
ResourceSaver.save( grandma, "grandma.tres")
var grandma2 = ResourceLoader.load( "res://grandma.tres" )
print( grandma2.identity.familyName)
The first print statement works as expected, while the second returns, “Invalid access to property or key ‘identity’ on a base object of type ‘Resource’.” The file grandma.tres was created successfully. Examining it in notepad, we find:
[gd_resource type="Resource" format=3]
[sub_resource type="GDScript" id="GDScript_h742f"]
[resource]
script = SubResource("GDScript_h742f")
Obviously this file is much more simple than expected, and it does not include the values that were supposed to be saved. It seems to be a collection of pointers. So, what am I doing wrong here? What is the correct way to store a world-state which may be massive and complex?
Don’t use inner classes. Have a separate script file for each of your resource classes.
Okay, so after I make that change, how does it effect how I invoke save/load?
You can either save every resource object, or just save the top object. The latter will embed nested resources into that one file.
All right, I’ll try that.
Okay, I’m still having the same (or similar) problem. I simplified the system a bit for testing. Here is character.gd:
class_name character extends Resource
@export var familyName = "gramma test"
func _init( fName:String ):
familyName = fName
And here is my main function:
func _ready() -> void:
var grandma = character.new( "Kinoti" )
print( grandma.familyName)
ResourceSaver.save( grandma, "grandma.tres")
var grandma2 = ResourceLoader.load( "res://grandma.tres" )
print( grandma2.familyName )
This returns the errors, “Error constructing a GDScriptInstance: ‘Resource(character.gd)::_init’: Method expected 1 argument(s), but called with 0” and “_ready: Invalid access to property or key ‘familyName’ on a base object of type ‘Resource’.” Now, grandma.tres looks like this:
[gd_resource type="Resource" script_class="character" format=3]
[ext_resource type="Script" path="res://scripts/character.gd" id="1_v21og"]
[resource]
script = ExtResource("1_v21og")
familyName = "Kinoti"
So clearly grandma.tres does know what familyName is supposed to be. That means I’m making progress. But why isn’t ResourceLoader.load() able to construct the object properly with that information? I guess I could solve the problem by ensuring that all savable/loadable objects have _init() funcs with 0 arguments, but there must be a more elegant solution.
Supply default values for all arguments to _init().
Okay, I dealt with that. But the real problem is that ResourceSaver.save() still isn’t working recursively. For example, I now have stats.gd with:
class_name Stats extends Resource
var endurance:=100
and character.gd with
class_name character extends Resource
@export var familyName = "gramma test"
@export var stats:Stats
but grandma.tres still looks like
[gd_resource type="Resource" script_class="character" format=3]
[ext_resource type="Script" path="res://scripts/character.gd" id="1_jqp7l"]
[ext_resource type="Script" path="res://scripts/stats.gd" id="2_6l576"]
[sub_resource type="Resource" id="Resource_4ia38"]
script = ExtResource("2_6l576")
[resource]
script = ExtResource("1_jqp7l")
familyName = "Kinoti"
stats = SubResource("Resource_4ia38")
Looks fine to me. How do you think it should look?
Okay, so if I do
print(grandma2.stats.endurance)
The output is “100” as expected. But the number 100 and the contents of Stats are not visible anywhere in grandma.tres, nor is another file created. I was a little confused because I didn’t think it would work, since I can’t tell where the info is being stored. But where is the number 100 actually being kept?
In Stats class initializing code. It’s the default value for the property so it doesn’t need to be stored again in the resource file. Try changing its value after object creation and see what happens in the resource file.
Okay, so now if I write
var grandma = character.new( )
grandma.setup( "Kinoti" )
grandma.stats.endurance = 127
print( grandma.familyName)
print( grandma.stats.endurance)
ResourceSaver.save( grandma, "grandma.tres")
var grandma2 = ResourceLoader.load( "res://grandma.tres" )
print( grandma2.familyName )
print( grandma2.stats.endurance )
The output is:
Kinoti
127
Kinoti
100
and the contents of grandma.tres looks the same as it did before.
Before saving, check if the file already exists. If yes, call take_over_path() on the resource object.
That doesn’t seem to change the result.
This version has several different workarounds that I found mentioned in different places:
if FileAccess.file_exists("res://grandma.tres"):
grandma.take_over_path("")
grandma.resource_local_to_scene = true
ResourceSaver.save( grandma, "grandma.tres")
if ResourceLoader.has_cached("res://grandma.tres"):
print("Grandma cached")
var grandma2 = ResourceLoader.load( "res://grandma.tres" )
print( grandma2.familyName )
print( grandma2.stats.endurance )
No matter which combination is enabled/disabled, the result is still the same as the output before.
Umm, you need to supply the actual path to take_over_path()
Okay, that does make the output statement read 127 correctly. (Sorry, I saw an example of someone using take_over_path(“”) somewhere.) But the .tres file still doesn’t contain the number 127. I tried commenting out the save part of my code and using only the load part, like this:
var grandma2 = ResourceLoader.load( "res://grandma.tres" )
print( grandma2.familyName )
print( grandma2.stats.endurance )
And the output was
Kinoti
100
The original process is like this.
var grandma = character.new( )
grandma.setup( "Kinoti" )
grandma.stats.endurance = 127
print( grandma.familyName)
print( grandma.stats.endurance)
grandma.take_over_path("res://grandma.tres")
ResourceSaver.save( grandma, "grandma.tres")
var grandma2 = ResourceLoader.load( "res://grandma.tres" )
print( grandma2.familyName )
print( grandma2.stats.endurance )
Trying to load it without saving first looks like this:
var grandma2 = ResourceLoader.load( "res://grandma.tres" )
print( grandma2.familyName )
print( grandma2.stats.endurance )
The point is to make a save file which the player can keep between play sessions, so a method of writing saves isn’t useful if it requires the game to keep running until you load.