In this lesson, we will code a resource to represent a character’s stats, like strength or endurance.
To get started, open the scene UICharacterStats.tscn
. To
quickly open a scene, you can press
Ctrl+Shift+O. This shortcut opens a
pop-up listing all the scene files in your project.
You can search for the filename to quickly find it.
This scene is a little character creation menu like the ones you sometimes see in hack-and-slash and role-playing games. Well, if you get past the barebones visuals!
This menu has a list of spin-boxes: one for each character stat that we want to support. There are also buttons to simulate punching, getting hit, and lockpicking.
You can change the numbers by clicking the arrows next to each spin box. Then, try to push the buttons on the left. Notice changing the values does not change the results.
Then, close the scene and rerun it again. The numbers are all back to their original values.
We then have two problems:
Let’s solve both.
Create a new script named CharacterStats.gd
in the
CharacterStatsDemo/
folder.
For each stat, we define a new variable and export it.
We also make it extend Resource
and give the script a
class name. More on that below
class_name CharacterStats
extends Resource
export var strength := 5
export var endurance := 5
export var intelligence := 5
This is not a Resource
; this is a Resource
class. Just like a node’s class is a
blueprint for creating nodes, a Resource
’s
class is a blueprint for creating more of that
resource.
We will use the CharacterStats
script to create a
CharacterStat
resource.
You can save, load, and manipulate this file in the editor.
Thanks to the ’ export ’ keyword, Godot knows which data to save.
Godot will only save the value of exported variables when writing the
resource to the disk. Any variable that does not have
export
will not be saved.
Note: A resource is not a node! Until now, you’ve only worked with scripts that extended descendants of
: , , , etc.Nodes and resources are in different branches of Godot’s class tree. It has many of the same functions as a node: it can dispatch signals, for example. However, you cannot use it as a node.
We added a new keyword at the top of the script:
class_name
.
class_name CharacterStats
extends Resource
The class_name
keyword registers your script globally in
your Godot project.
To use it, you write class_name
followed by the desired
name, typically the same as the script file. By convention, we write the
name in PascalCase: a sequence of capitalized words without
spaces.
You can then use the class name as a type hint and create new copies of your resource from other scripts, like so.
var stats := CharacterStats.new()
This is similar to how you created nodes from code in the previous series.
var label := Label.new()
Reminder:
new()
function to create fresh objects from
a script or a class name.instance()
to create duplicates of a
scene.Registering a resource with the class_name
keyword also
allows us to create new resources of that type from the editor.
In the FileSystem dock, right-click on the
CharacterStatsDemo/
directory and select New
Resource.
Search for CharacterStats
and create it.
You can name the new file something like stats.tres
.
You can double-click the new resource file to edit its values in the Inspector. You should see three properties that match our source code.
Just like that, you created a file that can save data! You can use the same technique to store any sort of data you’d like; names, items, numbers, and even other resources.
We are ready to update the character stats from our menu.
We’ll make it so that changing the spinners modifies the values in the resource first. After that, we’ll learn how to save the changed values to the disk.
At the moment, our spinner boxes don’t do anything useful. We will
make it so changing them changes the CharacterStats
resource.
We will:
CharacterStats
resource
file.CharacterStats
’ values when changing the
spin box numbers.Open the UICharacterStats scene, then open the
UICharacterStats.gd
script. You can click on the script
icon in the scene dock to do so.
We defined a variable named characters_stats
to hold our
stats file and save you some typing in the script.
# We export the variable to drag character stat files into the Inspector and
# test them with the menu.
export var character_stats: Resource = null
The rest of the script’s code holds references to each spin box in
the scene and connects their value_changed
signals to three
callback functions.
onready var strength_spinbox := $HBoxContainer/Stats/HBoxContainer/StrengthSpinBox
onready var endurance_spinbox := $HBoxContainer/Stats/HBoxContainer2/EnduranceSpinBox
onready var intelligence_spinbox := $HBoxContainer/Stats/HBoxContainer3/IntelligenceSpinBox
func _ready() -> void:
# The value_changed signal of the spin box emits along with the new value
# displayed in the interface.
connect("value_changed", self, "_on_StrengthSpinBox_value_changed")
strength_spinbox.connect("value_changed", self, "_on_EnduranceSpinBox_value_changed")
endurance_spinbox.connect("value_changed", self, "_on_IntelligenceSpinBox_value_changed")
intelligence_spinbox.connect("pressed", self, "_on_ResetButton_pressed")
reset_button.
# Each of these functions updates the character state matching the spin box.
func _on_StrengthSpinBox_value_changed(new_value: int) -> void:
= new_value
character_stats.strength
func _on_EnduranceSpinBox_value_changed(new_value: int) -> void:
= new_value
character_stats.endurance
func _on_IntelligenceSpinBox_value_changed(new_value: int) -> void:
= new_value character_stats.intelligence
It makes it so that changing a value in a spin box updates the
corresponding stat on the CharacterStats
resource.
To test this, head back to the scene, select the
UICharacterStats node, and drag your stats.tres
file into the Character Stats property in the
Inspector.
Try the “Punch”, “Take Hit” and “Lockpick” buttons, and notice that
now, the output changes depending on the strength
,
endurance
, and intelligence
values. Neat!
Open the practice Fortune cookies.
We have a FortuneCookie scene that can display sentences nicely when pressing a button.
Unfortunately, right now, it doesn’t have anything to show! You’ll make a resource that provides random lines to the interface.
Select the UICharacterStats node again and in the Inspector, click the stats resource to expand it. Change the starting values to whatever you’d like.
Note: Editing the resource directly updates the
contents of the stats.tres
file, even if attached to a
node.
Run the scene: the initial values do not correspond to your values.
We need to update each SpinBox.value
property in the
UICharacterStats.gd
script. Locate the
_update_spinboxes()
function, and write the updating code
in it
func _update_spinboxes() -> void:
= character_stats.strength
strength_spinbox.value = character_stats.endurance
endurance_spinbox.value = character_stats.intelligence intelligence_spinbox.value
Rerun the scene to see the spin boxes display the correct value.
Congrats!
We solved the first half of the issue: our spinners actually change a value. But notice that when you close the scene and reopen it, the values aren’t saved.
To save a resource, we use Godot’s ResourceSaver
object.
Its save()
function takes two arguments: a file path and
the resource to save. It works with any resource.
We’re going to implement the save directly in the
CharacterStats
script. Reopen
CharacterStats.gd
where we add a save function.
func save() -> void:
save(resource_path, self) ResourceSaver.
This function takes two arguments:
Resource.resource_path
property is the path to stats.tres
in the project.self
means
“this resource.” In practice, it will be the stats.tres
resource.When we want to save, all we have to do is to call the
save()
function. We could call it when pressing a button,
or at any moment we want.
In this case, we want to save every time a value changes. How do we do that? Is there a way to run a function every time a value changes?
We call a function that runs every time a variable changes a “setter”. And we call a function that runs every time we read a variable “getter”.
Here’s an example of a setter:
# Person.gd
var age := 0 setget set_age
func set_age(new_age: int) -> void:
if new_age <= 5 or new_age > 100:
print("too young or too old!")
return
= new_age age
if we used person.age = 101
, the age
variable wouldn’t change. Godot invisibly runs the
set_age()
function we specified.
Here’s an example of a setter and a getter:
# Person.gd
var age := 0 setget set_age, get_age
func set_age(new_age: int) -> void:
= new_age
age
func get_age() -> int:
if age <= 5 or age > 100:
return -1
return age
In this example, we can set any age
value, but if we do
var x = person.age
, then Godot invisibly runs the
get_age()
function we specified.
Note: setters and getters are only called if we
access variables from another script. From inside the same
script, they do not; otherwise, calling age = new_age
from
the script itself would trigger an infinite loop.
We will use setters to run save()
any time one of the
resource’s values changes.
Update the three variable definitions like so.
# Changing the strength variable from the menu will trigger a call to the
# set_strength() function.
export var strength := 2 setget set_strength
export var endurance := 2 setget set_endurance
export var intelligence := 2 setget set_intelligence
After each variable, we add the setget
keyword followed
by a function name.
You will get an error we’ll clear in a second: let’s define the first
function, set_strength()
.
# A setter function will always be called with the new value we're trying to
# assign to the variable.
func set_strength(new_strength: int) -> void:
# We must always update the variable manually.
= new_strength
strength # We can then call any code that we want, like our save() function.
save()
With the code above, every time we change the spin box in the menu,
it will both update the strength
variable and call the
save()
function. Handy!
You can name the function parameter however you want, but you need
one. Here, we named it new_strength
, but
new_value
or value
would also work.
Important: You must always update the variable manually when using setter functions. It’s common to forget when you start using them, and it will cause your variable not to update.
Let’s add the remaining two setter functions. They work the same way.
func set_endurance(new_endurance: int) -> void:
= new_endurance
endurance save()
func set_intelligence(new_intelligence: int) -> void:
= new_intelligence
intelligence save()
Rerun the scene and change the stats using the spin boxes.
When you stop the scene, double-click the stats.tres
file to open it in the Inspector. Its values changed!
The next time you open the menu, it will display the character’s new stats.
You successfully saved player data!
The next lesson covers the difference between types, objects, and classes, which we introduced earlier.
We’ll then add a two-player local co-op to the obstacle course.
Currently, we’re saving the resources in our project directory.
We generally do this for project resources like a theme or an animation. But we don’t do that for resources the player can change in-game.
We write options and save files to a special user-directory Godot
provides us. Instead of res://
, you use the
user://
path prefix to access that directory.
For example, for the PlayerSettings
resource, instead
of:
save(resource_path, self) ResourceSaver.
You would write a path like:
save("user://player_settings1.tres", self) ResourceSaver.
However, the settings would not appear in the project files if we did this now. They would be hard to edit and inspect.
It’s common to save to the project directory during development and then change the paths once we’re sure of our design.
On desktop platforms, the user://
directory is:
OS | directory path |
---|---|
Windows | %APPDATA%\Godot\app_userdata\[project_name] |
macOS | ~/Library/Application Support/Godot/app_userdata/[project_name] |
Linux | ~/.local/share/godot/app_userdata/[project_name] |