Main Menu Scripts Not Loading

Godot Version

v4.6.1.stable.official [14d19694e]

Question

My game has a ‘pause menu’. One of the buttons in these menus is connected to this function:

const menuScn:PackedScene = preload(“res://menu.tscn”)
func on_savequit_button_pressed():
    ...
    currentScene.queue_free()
    currentScene = menuScn.instantiate()
    get_tree().root.add_child(currentScene)

The menu loads, but none of the buttons on the main menu work. According to my debugging with print() in the _ready() function of the script attached to the menu, _ready() isn’t run by the menu either.

You need to set the buttons or the whole menu to either Always run or run When Paused.

It’s in the Inspector under Node.

I recommend you just change the root node to When Paused and see how that goes.

2 Likes

In addition to what @dragonforge-dev, is there any reason you’re not using the get_tree().change_scene_to*() methods?

1 Like

I tried that before and it yielded the same result so I changed it back to this. It works with @dragonforge-dev’s solution, though. Changed the menu to ‘always run’ and now it works.

1 Like

TBH I hate those methods. I don’t think they’re a good way to handle switching levels.

Why so? And how do you handle scene switching then?

Because then each level needs to have more info in it than it needs to. It either needs the Main/Pause menu as part of it, or it needs to know how to switch to that scene. Either way, a level doesn’t need to know about the rest of the game, menus, etc. So it’s poor encapsulation.

I prefer a main scene that handles everything with a state machine.

I have a Game autoload that has a signal. It takes a level name which can actually be a name to search for, a path, or UID. Then a player which can be any type of Node (typically a CharacterBody2D/3D). And finally the name of a transition area which is typically the name of a Marker2D/3D node where the player spawns.

signal load_level(level_name: String, player: Node, target_transition_area: String)

The Loading and Gameplay states listen for that signal. When it goes off, the Loading state kicks off and uses the ResourceLoader to load a threaded request. It puts up a customizable progress bar.

Loading State
class_name GameStateLoading extends State

var progress_amount := 0.0
var level_path: String

@onready var loading_screen: CanvasLayer = $"Loading Screen"
@onready var progress_bar: ProgressBar = %ProgressBar
@onready var precentage_label: Label = %PrecentageLabel


func _activate_state() -> void:
	super()
	Game.load_level.connect(_on_load_level)
	precentage_label.text = "0 %"
	loading_screen.hide()


func _enter_state() -> void:
	super()
	Game.pause()
	loading_screen.show()
	print_rich("[color=purple][b]Level Loading[/b][/color]: %s" % [level_path])
	progress_bar.value = 0.0
	set_process(true)
	ResourceLoader.load_threaded_request(level_path)


func _exit_state() -> void:
	super()
	Game.unpause()
	loading_screen.hide()


func _process(delta: float) -> void:
	var progress: Array = []
	var status = ResourceLoader.load_threaded_get_status(level_path, progress)
	
	if progress[0] > progress_amount:
		progress_amount = progress[0]
	
	if progress_bar.value < progress_amount:
		progress_bar.value = lerp(progress_bar.value, progress_amount, delta * 60)
	progress_bar.value += delta * 0.2 * (3.0 if progress_amount >= 1.0 else clamp(0.95 - progress_bar.value, 0.0, 1.0))
	
	precentage_label.text = str(int(progress_bar.value * 100.0)) + " %"
	
	if status == ResourceLoader.THREAD_LOAD_LOADED:
		set_process(false)
		progress_bar.value = 1.0
		precentage_label.text = "100 %"


func _on_load_level(level_name: String, _player: Node, _target: String) -> void:
	if level_name.contains("uid://"):
		level_path = level_name
	else:
		level_path = "res://levels/" + level_name + ".tscn"
	switch_state()

At the same time, when that signal kicks off, the Gameplay state starts monitoring the progress of the ResourceLoader’s threaded loading. When it is complete, it takes over and loads the level, making it a child of itself. If there’s an old level, it then deletes that level once the player has been moved over.

Gameplay State
class_name GameStateGameplay extends State

var level_path: String
var player: Node
var current_level: Node
var target_transition_area
var level_loading = false


func _activate_state() -> void:
	super()
	Game.load_level.connect(_on_load_level)
	set_process_input(true)
	

func _process(delta: float) -> void:
	var status = ResourceLoader.load_threaded_get_status(level_path)
	if status == ResourceLoader.THREAD_LOAD_LOADED:
		set_process(false)
		await get_tree().create_timer(0.5).timeout
		switch_state()


func _input(event: InputEvent) -> void:
	if not Game.is_paused():
		return
	if event.is_action_pressed("pause"):
		switch_state()
		get_viewport().set_input_as_handled()


func _enter_state() -> void:
	super()
	if Game.is_paused():
		Game.unpause()
	if level_loading:
		_start_level()


func _on_load_level(level_name: String, player: Node, transition_area: String) -> void:
	set_process(true)
	if level_name.contains("uid://"):
		level_path = level_name
	else:
		level_path = "res://levels/" + level_name + ".tscn"
	self.player = player
	target_transition_area = transition_area
	level_loading = true


func _start_level() -> void:
	var scene = ResourceLoader.load_threaded_get(level_path)
	var new_level = scene.instantiate()
	add_child(new_level)
	if player != null:
		player.reparent(self)
	if new_level != current_level and current_level != null:
		current_level.queue_free()
	current_level = new_level
	current_level.start(player, target_transition_area)
	level_loading = false

You can see working examples for 2D and 3D in my Game Template Plugin.

1 Like