Resources in a game are missing properties set in the inspector. What is the "Godot" way to handle this data?

Godot Version

Godot 4.2.1

Question

I am new to Godot and trying some smaller projects based on existing games to help myself learn. Currently I am trying to build a Pokemon battler, which would be a game that allows me to preset different custom Pokemon for myself and an AI opponent, and have us battle like a normal game.

Currently, I have a Battle scene, which is attached to a script to handle the battle process. This script exports and Opponent field, which I can assign a predefined resource to. This simple base class will be extended to subclasses such as a battle against a wild Pokemon, or single or double battles against Trainers. Opponents essentially export a single array called their Team, which I instantiate individual Pokemon. These Pokemon are also Resources, and this class represents an actual individual Pokemon in the world or on your team, as opposed to the PokemonSpecies resource which contains data shared by all pokemon of the same species.

This is my problem: The Pokemon on my opponent’s team do not have data related to their base stats, such as speed, attack, health, etc that should be calculated when the Pokemon is created. Since this Pokemon Resource is not a Node, and is not a part of the scene tree, I can’t do these calculations in _ready(). When I moved this logic to an _init() function, I found that the details I had specified in my editor were not present in the debug session when the code ran, i.e. the Species of these pokemon are null even though I have set them in the editor.

My question: What is the right way to fix this? Is there a Godot preference? I’ve come up with some potential solutions but I was hoping there was another lifecycle method I don’t know of, or an annotation like @onready that I can add to the _init() function.

I could 1) Replace many of my resources with nodes, add the _ready() function, and have an opponent’s Pokemon be nodes in their scene tree, but this would make each individual opponent much more time consuming to set up I think, and it would require me to add logic that knows about the scene structure, which I thought I should avoid. Currently, I have one battle scene and many opponent resources, rather than many opponent scenes that would be added to a base battle scene.

Or 2) I could add logic which calls my own initStats() function on every Pokemon in the scene from the Battle script. This would call a similarly named function in my opponent class, and it would be up to each specific opponent subtype to call this function manually on every Pokemon they are responsible for.

Which approach would you recommend? Have I overlooked an easier option? Should individual of a species in the world be represented as Resources or Nodes?

I will add some relevant code/pictures in a comment, it seems easier to do that way.

My scene structure:

Battle (has a script attached)
→ Background (A sprite2d)
→ Controls (All of my ui controls, buttons, etc)
→ Player (Script attached, invisible)

Player is a node because it will eventually walk around an overworld maybe? It is invisible because it has a player sprite attached to it. Similar to Opponent, it exports an array called Team which I add Pokemon to, which mean individual Pokemon are still just resources, as they don’t do anything besides holding data. This example gives my team a single pokemon named Grassy, which is an existing resource full of data.

When I reach my _init() script however, all of these values are defaults (Species is null, level is 1, the moves array is empty).

class_name Pokemon 
extends Resource

@export var species: PokemonSpecies
... more variables exported to the editor ...

func _init():
	self.xp = xpToLevel(self.level, self.species.growthRate)
	self.currentAbility = self.species.abilities[self.abilitySlot]
	self.currentTypes = self.species.types
	
	# stats
	self.maxHP = (((2 * species.hp) * level) / 100) + level + 10
	... it goes on like this for a while.

You can load a resource by using the path. Let’s say you use classname myItem.

var mystats
ready():
mystats = load(path to item) as myItem.

There are tutorials around on youtube that might help.
Not sure if the code is right, but should be close.

I’m not sure if I will be able to use this solution? In this example, is that “path to item” a hardcoded string or another variable that is exposed to the editor?

For instance, in my ready script for an opposing trainer, I won’t be able to hard code a path to a specific “item”, or Pokemon in this case, because I want to be able to use that opponent script for multiple trainers with different inputs. Are you suggesting that I replace the exported variable that accepts a pokemon resource with a string that represents the path to that resource? I don’t really understand why that would be a better solution.

I also can’t use ready() because this script extends Resource, not Node. Is the suggestion here to convert these files to Nodes as well?

I’m not sure what you are doing, but the resource is just to get the initial stats. You load it into a variable at the start of the game and then you can change it whenever you want. If multiple nodes need it, you load it into an autoload script. I don’t really understand what you are doing though, so maybe it doesn’t apply. I mainly use resources for inventory items which have stats that go with them.

@fireside I don’t think he wants to do that. @notnearlymad Did you change the code in between the pictures, or move the scenes in the file manager.

That can change and make the paths null values, at least I think that is how that works.

Everything in this answer is up to date and I will try to keep it that way until I find a solution.

For resources that depend on sub-resources you may want to implement a computed properties or getter pattern, listen for changes, or like you have posted, call a separate initialization method.

Computed properties and getters

class_name Pokemon 
extends Resource

@export var species: PokemonSpecies
... more variables exported to the editor ...

# Computed properties
var xp:
	set(v): xp = v # Do you allow setting xp?
	get(): return xpToLevel(level, species.growthRate)

var currentAbility:
	set(v): assert(currentAbility in species.abilities); currentAbility = v
	get(): return species.abilities[abilitySlot]

# Getters
func get_xp(): return xpToLevel(level, species.growthRate)
func get_currentAbility(): return species.abilities[abilitySlot]

Listen for changes

Another route is to listen for changes on setting the species property:

@export var species: PokemonSpecies:
	set(v):
		if species != v:
			if species != null:
				species.changed.disconnect(update_species_data)
			species = v
			species.changed.connect(update_species_data)
			update_species_data() # Manually update since new species was set.

func update_species_data():
	xp = xpToLevel(level, species.growthRate)
	currentAbility = species.abilities[abilitySlot]

This pattern will update when species is set. To update when PokemonSpecies data changes you must emit PokemonSpecies.changed.emit() when it’s properties change like:

# PokemonSpecies.gd
@export var growthRate:
	set(v):
		if growthRate != v:
			growthRate = v
			changed.emit()

Separate init

Ensure the data is initialized by delegating the call to the containing node.

# Pokemon.gd
extends Resource

func update_species_data():
	xp = xpToLevel(level, species.growthRate)
	# ...
# PokemonNode.gd
extends Node

@export var data: Pokemon

func _ready():
	data.update_species_data()

These can be combined as desired to keep data in sync.

Ideally the container accesses the resource data at a point when the resources have been properly initialized like by Godot when loading the resources from storage memory.

The computed properties, getters, and change listener patterns have the benefit of changes being propagated to owners/parents/containers. Which is a form of reactive programming.


Let me know if this helps.

1 Like

At this point, if it isn’t too hard, you should just delete the resource and create a new one with the same data. If you haven’t tried it already.

This looks very helpful, thank you. I will test the getters and setters, I have seen this pattern in other languages but have not used it much, so I may have overlooked this approach. Otherwise, I think the final example here is perfect for what I need to do, and I was suspicious that the resource simply had not been initialized yet when I was trying to run this logic. Thanks again for the help.

1 Like

Ok, using getters I have confirmed that this data exists and is properly accessible during runtime, the issue was trying to run the logic in the _init() step before the properties I set in the editor were accessible to the code. Converting a top level Resource (used to be my opponent base class, which I was using a bit like an interface) to a Node and putting this logic in the _ready() step I believe solved my issue.

1 Like

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