Setting Character Values with Button Press

Godot Version 4.4.1

Question

Hello! I’m brand new to Godot. I’m working on my character creator. I would like to change character values when the player selects an option from an ItemList and hits the “Next” button. I have a general understanding of signals, but my issue is that ideally, my Character Creation scene and my Player scene would be separate. I can’t figure out how to link a signal from one scene tree to another. Is there a way to do this? Otherwise my Character Creation node will have to be a child of the Player, and that doesn’t feel right (though I know next to nothing at this stage).

I appreciate any guidance the community can provide! I imagine this is a pretty simple problem, but is one I’m struggling to find an answer to.

It’s actually not a simple problem, as there are multiple valid answers. However, there are a few simple solutions.

The first is to create an exported variable on your CharacterCreation scene:

@export var player: Player

Then as long as both scenes exist in your main scene, you can assign the actual player to the exported player variable of your CharacterCreation scene in that scene. Then you can just reference the player variable in your code.

The second option is to create an Autoload. Create a script and call it something clever like operator.gd or conductor.gd or signals.gd.

#signals.gd
signal need_player_reference
signal player_reference(player: Player)

Then add this script as an Autoload in Project settings.

In CharacterCreation script:

var player: Player

func _ready() -> void:
	Signals.player_reference.connect(_on_player_reference)

func _on_player_reference(player_ref: Player) -> void:
	player = player_ref

In Player script:

func _ready() -> void:
	Signals.need_player_reference.connect(_on_need_player_reference)

func _on_need_player_reference() -> void:
	player_reference.emit(self)

I would prefer the first option over the second in this case.

@dragonforge-dev Wonderful! Thank you for your help. I’ll make sure to study exported variables further; I’m hazy on the “player: Player” use case, and that seems important to know.

1 Like

That’s just good practice for typing variables.

player is the name of the variable. Followed by a colo (:slight_smile: it says the next thing is going to be the type of the variable. In this case your Player class. If that code isn’t working for you, add this to the second line of your player.gd file:

class_name Player

Gotcha. Since my game doesn’t have multiple copies of any given entity, I wasn’t sure if classes were going to end up being relevant, but here they are!

IMO, they are always relevant. You never know when you want to refer to something in code. For example lets say you want to look at all the children of a node but filter out only the things you want. Having a class_name makes that easy.

for node in get_children():
	if node is Weapon:
		#do stuff here

There are exceptions. For example when you are doing UI and make a one-off script for a button or something.

However as a new developer, I think you would benefit from the practice of naming everything. Reason being that practice makes you better at it. Cause you’re going to give things names that are too generic and come back and wonder what you meant or which thing was the thing you wanted, and you can only learn that kind of stuff if you actually practice naming them.

It will also help you see when you would benefit from refactoring. If you are having trouble naming things differently between say your enemies and your NPCs and your players - it’s a good time to think about how they might all share the same code, and maybe that HitBox script could actually be the same on all of them (for example).

1 Like

@dragonforge-dev Okay, last question. I’m closer, but even though I can access the Player script variables, I don’t know how to permanently alter those variables from the character creation script depending on what option the player selects. “Trader” is the name of the player.

# From the Character Creation script
@export var trader: Trader
func _on_goal_list_item_selected(chosen_goal) -> void:
	button_next_from_goal.visible = true
	if chosen_goal == 0:
		goal_desc_save.visible = true
		var goal = trader.goal_is.SAVE_YOUR_LOVED_ONE
	else:
		goal_desc_save.visible = false
	if chosen_goal == 1:
		goal_desc_buy.visible = true
		var goal = trader.goal_is.BUY_YOUR_FREEDOM
	else:
		goal_desc_buy.visible = false
	if chosen_goal == 2:
		goal_desc_get.visible = true
		var goal = trader.goal_is.GET_YOUR_SHIP_BACK
	else:
		goal_desc_get.visible = false

I’m currently trying to keep the choices organized in an enum. Is that a mistake?

# From the 'Player' script
enum goal_is {SAVE_YOUR_LOVED_ONE, BUY_YOUR_FREEDOM, GET_YOUR_SHIP_BACK}

The end goal is to permanently set a Trader.gd variable value (the “goal”) after it’s been selected on this screen in the character creation scene:


I really cannot find solutions to this anywhere. It seems to me that everything I do with a signaled function (the chosen selection from ItemList) will not “stick” after the function does its job.

Ok, so first thing, there is a Style Guide and following it will make your code more readable as you read it, and more readable to others when they read it.

Case in point, Enum should always be PascalCase. Because when I’m reading your code, that lack of PascalCase had me scratching my head. Not the end of the world, but still - helpful.

So let’s rework your Enum. The name should be in PascalCase, and it’s just a list of things it could be not what it is. Also, we are going to put each entry on its own line for readability. Notice the last line is followed by a comma. It’s not needed, but if you add a new line, you don’t have to add it later.

Then we are going to add a goal (lowercase) variable to store the value for the player. When we declare the variable, we are going to say it is of type Goal, which means it has to have one of those Enums as a value and nothing else. This helps us find bugs. If we pass it the wrong thing, we get an error.

#player.gd
enum Goal {
	SAVE_YOUR_LOVED_ONE,
	BUY_YOUR_FREEDOM,
	GET_YOUR_SHIP_BACK,
}

var goal: Goal

Next let’s take a look at your character creation script. You have three things going on, and it’s a bit convoluted.

First we are going to turn off all three descriptions, then turn on the one we want. This gets rid of all your if/else statements. However, if you can, just go into your scene, hide them all and remove those 3 lines. If that’s not possible, no sweat.

Then, let’s use a match statement. It will be easier to read.

Finally I’m skipping assigning a local goal variable, and assigning it right to the player instance, but the last line will also set a local version of the variable if you need it for some reason (though you should just read it from player.goal)

#character_creation.gd
@export var trader: Trader


func _on_goal_list_item_selected(chosen_goal) -> void:
	button_next_from_goal.visible = true
	goal_desc_save.visible = false
	goal_desc_buy.visible = false
	goal_desc_get.visible = false

	match(chosen_goal):
		0:
			goal_desc_save.visible = true
			trader.goal = trader.Goal.SAVE_YOUR_LOVED_ONE
		1:
			goal_desc_buy.visible = true
			trader.goal = trader.Goal.BUY_YOUR_FREEDOM
		2:
			goal_desc_get.visible = true
			trader.goal = trader.Goal.GET_YOUR_SHIP_BACK
	goal = trader.goal #If you need it

As you can see, if you can hide those descriptions in the editor and remove the local goal variable, the code gets even more concise.

1 Like

When I run the scene with the above code and select a Goal, it throws this error:
Invalid assignment of property or key ‘goal’ with value of type ‘int’ on a base object of type ‘Nil’.

Looks like this also happens even when I try a simpler set-up with @export var goal = '' in the Player.gd, then try to assign it with trader.goal = "Save Your Loved One" in the ChCreation.gd. In that case, it throws:
Invalid assignment of property or key ‘goal’ with value of type ‘String’ on a base object of type ‘Nil’.

Yeah if you export the trader variable, you have to assign it to the trader instance in the editor. Here’s an example from a multiplayer tutorial I’ve been following lately. The architecture is horrible, but the guy did like his exported variables.

See how the camera and animated sprite are exported then assigned?

Strange issue: I am able to assign @export var trader: Trader to the Trader node in the inspector when I have selected the ChCreation node in the Main tree; however, I’m still getting the error when running the scene and selecting an option that tries to set the enum value, perhaps because I cannot assign the variable to the Trader node when I view the ChCreation node as a parent. I really don’t understand the distinction here.

Working under Main:

Not working under ChCreation (Trader not available because it’s not in the tree):

I’m really sorry for all these issues. I’m about ready to just use Autoload and be done with it. The Autoload documentation even says that “Godot’s scene system, while powerful and flexible, has a drawback: there is no method for storing information (e.g. a player’s score or inventory) that is needed by more than one scene.”

Edit: Can confirm I avoid this error by storing the Trader.gd script as an Autoload.

# TraderStats script stored as a Singleton
var trader = TraderStats
# No longer getting errors when making selections
func _on_goal_list_item_selected(chosen_goal) -> void:
	button_next_from_goal.visible = true
	match(chosen_goal):
		0:
			goal_desc_save.visible = true
			trader.goal = trader.Goal.SAVE_YOUR_LOVED_ONE
		1:
			goal_desc_buy.visible = true
			trader.goal = trader.Goal.BUY_YOUR_FREEDOM
		2:
			goal_desc_get.visible = true
			trader.goal = trader.Goal.GET_YOUR_SHIP_BACK

# The Next button confirms the goal enum has been stored
func _on_button_next_from_goal_pressed() -> void:
	print(trader.goal)

Yeah that’s exactly where you want to set it - in Main. The problem you’re having is most likely that your code is running before Trader is finished being created. Try moving Trader up in the tree before CHCreation. An Autoload won’t solve this problem. The data just isn’t there yet. You could also put in a time and wait a second or two.

Put this in front of whatever code is throwing the error - see if it goes away.

await get_tree().create_timer(2.0).timeout

I made sure Trader is above ChCreation in the Main tree. Trader node is assigned for ChCreation in Main tree. Added the await right before the problem code. Still getting the error. Here’s the whole ChCreation script; failing at line 19 when I select that option, even after delay:

Since an Autoload script did in fact avoid this error, could you elaborate on why I shouldn’t use that to store player stats?

If the Autoload works, use it. I didn’t think it would. But at this point, I think it’s better to get your code working.

Beats me. But this was certainly a useful education in using classes for the future. Thank you again for all your help!

1 Like

You’re welcome.

After hashing things out with @dragonforge-dev, it seems the best solution to my particular problem is to use a Singleton script that allows player variables to be globally accessible, whose values can then be altered in the signal functions of character creation buttons. In my case, I’m setting the value of an enum once an ItemList option is selected.

There may be a better and more extensible way to do this with a dedicated class Node, but I was unable to figure out how to do so using a character creation node separate from the player stats node without getting the following error:
Invalid assignment of property or key ‘goal’ with value of type ‘int’ on a base object of type ‘Nil’.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.