We prepared a mouse-based menu for you. There, the players can select a hand color and a hand pose. That way, we can focus our attention on resources.
Open the UICustomizeCharacter.tscn
scene in the
ObstacleCourse_Part3/
folder and run the scene.
You can click the white arrows to select between the two characters and change options in the drop-down menus. However, that currently has no effect.
In this lesson, we will add properties to our
PlayerSettings
resource and complete the menu’s code to
change the characters’ looks. We’ll later integrate them into the
obstacle course and add save support.
Let’s briefly look at key aspects of the scene we prepared for you.
Notice how we have two PlayerCharacter instances on the scene’s side.
This allows the players to move around while seeing their characters’ looks change in real time. It will also allow us to reuse the same code to update the characters’ looks in the obstacle course.
Our menu’s layout relies on a
, the Menu node. It most notably contains three nodes to create the character selection row and the drop-down buttons.Nesting box containers like this is handy for making user interfaces in Godot.
Let’s move on to the code. Open the UICustomizeCharacter node’s script.
There is a bunch of code to update the menu items. We’ll focus on the
parts that deal with the PlayersSettings
resource.
First, in the _ready()
function, we wrote code to load
the PlayerSettings
resources and give them to each
character.
We only do that when running this scene with F6 because we will load the settings from the player’s save file in the obstacle course scene.
func _ready() -> void:
# If we're running this scene with F6, we give each character a settings
# resource.
#
# The owner property is null for a scene's root node when running the scene
# in isolation.
if owner == null:
= preload("player_1_settings.tres")
player_1.settings = preload("player_2_settings.tres")
player_2.settings # ...
Then, we connect the drop-down buttons to the same callback function. That function is currently empty, but we will complete it in this lesson.
func _ready() -> void:
# ...
# When selecting a new option in either drop-down menu,
# we call _on_hands_option_changed() to update the character's look.
connect("item_selected", self, "_on_hands_option_changed")
hand_pose_option.connect("item_selected", self, "_on_hands_option_changed") hand_color_option.
There is more code in the script to make the menu work, but it is beyond the scope of this series.
We wrote some code comments for you if you want to learn more.
Let’s make it so changes in the menu update the character’s hands.
Observe the hand textures’ names again:
They follow a pattern: hand_[color]_[pose].png
. Our
drop-down options match the names.
Instead of writing down all possible combinations, we can get the “pose” from the first dropdown, the “color” from the second, and rebuild the file name.
When the player selects a new option in one of the drop-down buttons, we need to:
settings.hand_texture
property.We’ll work in the _on_hands_option_changed()
callback
function, as it gets called whenever the player selects a new drop-down
option.
Let’s start by getting the displayed text of each drop-down button.
We use their text
property for that.
func _on_hands_option_changed(item: int) -> void:
var color: String = hand_color_option.text
var pose: String = hand_pose_option.text
We named every file in the assets/
directory with the
same pattern: hand_color_pose.png
. That way, we can rebuild
the file names from the options displayed in the drop-downs.
To achieve that, we could add strings, like so:
"hand_" + color + "_" + pose + ".png"
.
But here, we use a more flexible
feature: string templates.func _on_hands_option_changed(item: int) -> void:
# ...
# %s is a placeholder we can replace using the % sign followed by an
# array of values.
var filename := "hand_%s_%s.png" % [color, pose]
templates allow you to build text strings using placeholders that you can replace later.
You mark placeholders with %s
in your text. You can
replace the placeholders using the %
sign after the string
followed by an array of values. In this array, you need one value per
placeholder.
The replacements get automatically converted to text, making this a powerful feature.
We can now calculate file names, but we need the full path to the files to load the hand textures.
Unfortunately, we cannot use relative file paths like
assets/hand_red_pointing.png
in this case. We need an
absolute path, that starts with res://
.
We could hard code the assets/
directory’s path like
so:
var folder_path = "res://ObstacleCourse_Part3/assets/"
Instead, we want to show you a powerful trick: how to calculate file paths relative to your scripts at runtime.
This technique will allow you to move folders in your projects without having errors due to hard-coded paths.
We need to know which folder our script is in, and then append our
assets/
folder and file name to it.
We will first find the current script’s file path. From there, we can calculate its parent directory.
As we’ve seen, every Resource
has a
resource_path
property. This is the path to the resource’s
file.
As it turns out, scripts are resources attached to a node. We can get
a script resource in code by calling the Node.get_script()
function.
Then, we can get the path to the script we are editing like so.
get_script().resource_path
The resource_path
property is a . We can call the
String.get_base_dir()
function to get the directory from a
that looks like a file path, and
String.plus_file()
to append filenames.
# All of the statements below are true:
"dir1/filename.ext".get_base_dir() == "dir1/"
"dir1/dir2/dir3".get_base_dir() == "dir1/dir2"
"dir1/dir2".plus_file("new_dir") == "dir1/dir2/new_dir"
"dir1/dir2".plus_file("filename.ext") == "dir1/dir2/filename.ext"
By calling String.get_base_dir()
once, we can get the
resource’s directory path. Then, we can call
String.plus_file()
to append the assets/
directory to it.
func _on_hands_option_changed(item: int) -> void:
# ...
# We get the directory path corresponding to this script.
var folder_path: String = get_script().resource_path.get_base_dir()
# And we append the "assets/" folder to it.
= folder_path.plus_file("assets/") folder_path
Finally, we append the file name to the path, using the
String.plus_file()
function once more.
func _on_hands_option_changed(item: int) -> void:
# ...
var texture_path := folder_path.plus_file(filename)
We can now load the texture at runtime.
Until now, we loaded files using the preload()
function.
It allowed us to load a file at the start of the game with a path we
knew in advance.
Here, we need to load textures on the fly with a path we do not
know in advance. To do so, we use the load()
function.
The advantage of the load()
function is that we can use
a path calculated at runtime, as we just did.
We need to update the selected player’s hand texture to complete the function. We do it like so.
func _on_hands_option_changed(item: int) -> void:
# ...
# We prepared a function to get the selected PlayerCharacter instance in the
# menu.
var selected_player := _get_selected_player()
# We then load the resource at the path we calculated and assign it to the
# PlayerSettings.hand_texture property.
= load(texture_path) selected_player.settings.hand_texture
Because we defined a setter function in the PlayerCharacter
script, we will trigger the PlayerCharacter.set_settings()
function every time we try to load a new hand texture.
Now, run the menu scene with F6. When you change options in the drop-downs, and you should see the character hands update accordingly.
In the next lesson, we will add the menu and the two characters to the obstacle course. We will then add support for saving and loading the players’ data.