Like many other game engines that include editors, Godot allows you to expose variables declared in a script, that can then be modified through the editor. While GDScript is dynamically typed, when you export
a variable to be modifiable through the editor, you must define a type the editor recognizes.
export(float) var floatValue = 0.5 # Default value used in the editor
export(NodePath) var pathToANode # No Default value
NodePath
is the variable type to use if you want to link to another Node
in the scene, without resorting to hard-coding a path e.g. onready var anotherNode = get_node("a/node/path")
. But that example doesn't retrieve a NodePath
, it retrieves the actual Node
itself. It's a distinction that's important to make--NodePath
only points to a Node
, and it must be resolved to obtain the actual Node
.
When a script has the need to reference multiple other Node
s in a scene, resolving these NodePath
s can become cumbersome.
export(NodePath) var nodePathA
export(NodePath) var nodePathB
onready var resolvedNodePathA = get_node(nodePathA)
var resolvedNodePathB
func _ready():
resolvedNodePathB = get_node(nodePathB)
Using Reflection
By taking advantage of reflection, we can reduce the code above down to:
# These will be resolved in-place by Utils.LinkNodePaths(forNode)
export(NodePath) var nodePathA
export(NodePath) var nodePathB
onready var _ready = Utils.LinkNodePaths(self)
This magic is made possible by Object.get_property_list
. As the method name suggests, when given a Script
, it returns all the properties declared in that script and its parents.
A Singleton
First things first, we need to create a singleton to hold our reflection methods, as well as store our cached properties. Reflection isn't free, but we can amortize the cost and make it cheap by caching the properties we're interested in, for each different Script
we feed our reflection methods.
Creating the singleton is easy, and covered in so many different guides on GDScript, that I'll only glance over the steps:
- Create a new script, name it and save it somewhere.
- Go into
Scene->Project Settings->AutoLoad
and add the newly created script. - Enable the
Singleton
checkbox if not already, and check the name. For this article, I'm naming itUtils
.
We should now have a singleton available to us in all our other scripts, which we can access through Utils
, or the name you used, if different.
Our Property Cache
Before we can do anything else, in our singleton script, we need to add and handle our cache.
var _cachedNodePathVariables = {}
func CacheNodePathVariables(node):
var links = []
var propertyList = node.get_property_list()
for property in propertyList:
if property.type == TYPE_NODE_PATH and property.usage & PROPERTY_USAGE_SCRIPT_VARIABLE:
links.append(property.name)
_cachedNodePathVariables[node.get_script()] = links
return links
Breaking it down, _cachedNodePathVariables
is where we'll be storing our cache. It will be a dictionary where every key::pair is a Script
and an array of properties for that Script
, that we wish to cache.
We only wish to cache
NodePath
variables, that are explicitly defined in the script.
We get a list of all the properties stored within a Script
, using Object.get_property_list()
. We're calling this on a Node
, but it will retrieve them from the Script
attached to that Node
. Even though every Node
has its own instance of its attached Script
, the properties will not change between instances.
Each property retrieved carries a lot of information along with it, but for our purposes, we only care about the type and usage. These are only used to filter the properties, as in the end, we'll only cache the names of the properties we don't filter out.
Because we only care about NodePath
properties, we filter property.type
against TYPE_NODE_PATH
. We also only care about properties explicitely defined in the script, so we filter property.usage
against PROPERTY_USAGE_SCRIPT_VARIABLE
. property.usage
is a bit-flag, so we use bitwise and between it and PROPERTY_USAGE_SCRIPT_VARIABLE
, and check that it's !=
0. GDScript implicitly casts numbers to bool
when used in a conditional expression, which is why there's no explicit comparison to 0 in the example.
Unfortunately the documentation is quite vague on the exact coverage of PROPERTY_USAGE_SCRIPT_VARIABLE
, and it's poorly named to boot, but in practice, it does seem to only refer to properties actually defined in the script. That is, it will not refer to a property that is defined internally by Godot.
Once we've filtered the properties using this criteria, we're left with only the properties we care about. We then store the names of these properties in the cache.
Of note is the Object.get_script()
method, which we use to obtain the attached Script
we use as our key in the cache. This does not retrieve an instance of the Script
, but the base Script
itself, which means that every Node
we pass with the same base Script
, will use the same cached data.
Resolving the NodePaths
We now have a way to properly cache and retrieve NodePath
properties, so we can focus on actually resolving them.
func LinkNodePaths(node):
var nodeScript = node.get_script()
var links
if _cachedNodePathVariables.has(nodeScript):
links = _cachedNodePathVariables[nodeScript]
else:
links = CacheNodePathVariables(node)
for link in links:
var propertyValue = node.get(link)
if propertyValue:
node.set(link, node.get_node(propertyValue))
else:
node.set(link, null)
return true
This method is as simple as it looks. We retrieve the Script
attached to node
, and use that as the key for our cache. If nothing has been cached for that key, we go ahead and cache and retrieve the properties. If it has already been cached before, we just retrieve the properties from the cache.
It's now only a matter of iterating through the retrieved properties. Object
has two methods we're interested in, get
and set
. They allow us to retrieve and assign the value of a property, by name. As before with Object.get_property_list()
, calling these methods on a Node
will act upon its attached Script
, but in this case, we're acting upon the instance of the Script
attached to Node
.
For each property, we retrieve the stored NodePath
. We then resolve the value of that NodePath
to get the actual Node
it pointed to, and set the value of the property to our newly resolved Node
. This is how from within the target Script
, it appears that the value of the property has been resolved in-place.
Targeting Single Properties
Sometimes you only want to resolve a single NodePath
, and don't need the added overhead of the cache and property reflection. In that case, we can write a much simpler helper method.
func LinkNodePath(node, propertyName):
var sharedInfo = node.get(propertyName)
if sharedInfo and typeof(sharedInfo) == TYPE_NODE_PATH:
node.set(propertyName, node.get_node(sharedInfo))
return true
This method is so simple, it almost doesn't need any explanation. Given node
to act upon, and the name of the property to resolve as propertyName
, we retrieve the value from the property. After a quick check that it isn't null
and that it is a NodePath
, we can resolve the Node
it points to and set the property to that Node
.
When Ready
Something important to remember about these methods, is that they can only be called as the assignment to an onready
variable or property, or after the target Node
is ready. This is because a Node
cannot access other Node
s in a scene until it has been added to the scene.
If you'll notice, both methods always return true. Due to the nature of when they can be called, and because it's a common idiom to keep a ready variable in a script, this makes a handy shortcut.
export(NodePath) var nodePathA
export(NodePath) var nodePathB
onready var _ready = Utils.LinkNodePaths(self)
Both of our NodePath
properties will be resolved for us as soon as our Node
is ready, and for free we'll gain a flag that the rest of the code can check to see if the Node
is ready.
Wrap-Up
Overall, in comparison to other languages, GDScript has limited reflection capabilities, but with smart use of it, we can make development faster and easier. Our primary goal here, has been to help alleviate the need for extremely fragile hard-coded paths to other Node
s in the scene.