02.coding-character-stats

Coding character stats

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:

  1. The spinners do not update any value. They’re just detached numbers that don’t affect anything
  2. The values are never saved

Let’s solve both.

Coding your first custom resource

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 Node: Area2D, Sprite, KinematicBody2D, 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.

Registering your resource type in the project

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:

Creating a character stats file

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.

Using the character stats file in the menu

At the moment, our spinner boxes don’t do anything useful. We will make it so changing them changes the CharacterStats resource.

We will:

  1. Provide the menu with our CharacterStats resource file.
  2. Update the initial spin boxes’ value based on the resource’s starting values.
  3. Change the 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.
    strength_spinbox.connect("value_changed", self, "_on_StrengthSpinBox_value_changed")
    endurance_spinbox.connect("value_changed", self, "_on_EnduranceSpinBox_value_changed")
    intelligence_spinbox.connect("value_changed", self, "_on_IntelligenceSpinBox_value_changed")
    reset_button.connect("pressed", self, "_on_ResetButton_pressed")


# Each of these functions updates the character state matching the spin box.
func _on_StrengthSpinBox_value_changed(new_value: int) -> void:
    character_stats.strength = new_value


func _on_EnduranceSpinBox_value_changed(new_value: int) -> void:
    character_stats.endurance = new_value


func _on_IntelligenceSpinBox_value_changed(new_value: int) -> void:
    character_stats.intelligence = new_value

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!

Practice: Fortune cookies

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.

Synchronizing the initial state

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:
    strength_spinbox.value = character_stats.strength
    endurance_spinbox.value = character_stats.endurance
    intelligence_spinbox.value = character_stats.intelligence

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.

Writing a save function

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:
    ResourceSaver.save(resource_path, self)

This function takes two arguments:

  1. A path to the file to save. The Resource.resource_path property is the path to stats.tres in the project.
  2. A reference to the resource to save. Here, 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?

Introducing setter functions

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
    age = new_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:
  age = new_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.
    strength = new_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:
    endurance = new_endurance
    save()

func set_intelligence(new_intelligence: int) -> void:
    intelligence = new_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.

A note on paths

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:

ResourceSaver.save(resource_path, self)

You would write a path like:

ResourceSaver.save("user://player_settings1.tres", self)

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]