06.customizing-the-character-in-a-menu

Customizing the character’s look using a menu

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.

The UICustomizeCharacter scene and code

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 VBoxContainer, the Menu node. It most notably contains three HBoxContainer 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:
        player_1.settings = preload("player_1_settings.tres")
        player_2.settings = preload("player_2_settings.tres")
    # ...

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.
    hand_pose_option.connect("item_selected", self, "_on_hands_option_changed")
    hand_color_option.connect("item_selected", self, "_on_hands_option_changed")

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.

Changing the hands when selecting new menu options

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:

  1. Get the displayed text in each of the drop-down buttons.
  2. Convert that into a file name.
  3. Calculate the path to the hand texture file.
  4. Load the file at that path and assign it to the player’s 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 String 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]

String 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.

Calculating relative paths at runtime

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 String. We can call the String.get_base_dir() function to get the directory from a String 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 = folder_path.plus_file("assets/")

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.

Loading files on the fly

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.
    selected_player.settings.hand_texture = load(texture_path)

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.