Pausing

Taking a short break while playing.

This is the fourteenth tutorial in a series that covers the creation of a simple true top-down 2D game with pixel graphics, using the Godot Engine, version 4. It follows Conveyors and adds the ability to pause to the game.

This tutorial uses Godot 4.5, the regular version, but you could also use the .NET version.

Pause Menu

This time we'll add pause functionality to our game. We'll implement it by summoning a pause menu. This menu will allow us to resume playing the current map or restart it. As it's another two-button menu let's duplicate main_menu.tscn and name it pause_menu.tscn. Rename the nodes and change their text so we have a resume and a restart button.

Pause menu.

Unlike the main menu the pause menu will be displayed on top of the current map scene, like the HUD scene. To make this work correctly we have to make a few changes to its scene structure. First, change the root node type to CanvasLayer. Increase its CanvasLayer › Layer › Layer to 5 so it gets drawn on top of everything else. Also set its Node › Process › Mode to Always so it will update whether the game is paused or not. Then remove its root node script and give it a new pause_menu.gd script. Disconnect the now invalid pressed() signals of the buttons and make new connections.

extends CanvasLayer


func _on_resume_button_pressed() -> void:
	pass


func _on_restart_button_pressed() -> void:
	pass

Next, add a ColorRect node as the first child of the root. Set its Control › Layout › Anchors Preset to Full Rect so it covers the entire window. We'll give it the same gray color as Godot's default background color, but we make it slightly transparent so the current map is still visible. Set its ColorRect › Color to 4d4d4de6.

Node hierarchy.
Where is the default background color set?

In the project settings, via Rendering › Environment › Default Clear Color.

Add the scene to Globals with PauseMenu for its name. Just like the map transition scene, we'll make the scene remove itself from the root window in _ready().

var root_window : Window


func _ready() -> void:
	root_window = get_tree().root
	root_window.remove_child.call_deferred(self)

And we give it an activate() function that adds itself back to the root window and pauses the game.

func activate() -> void:
	root_window.add_child(self)
	get_tree().paused = true

Opening the Pause Menu

The pause menu will be summoned by pressing the Esc key. We check this in main.gd by giving it a process() function that calls Input.is_action_just_pressed() and if so activates the pause menu. We can use the built-in ui_cancel action to detect that the Esc key was pressed.

func _process(_delta: float) -> void:
	if Input.is_action_just_pressed("ui_cancel"):
		PauseMenu€.activate()

But we only want to summon the pause menu when we're currently playing a map. We do not want it to overlay the main menu. So let's add a playing_map variable to indicate whether we're currently playing a map. It is false by default and we set it to true at the end of map_loaded(). Then we only check the input if we're playing a map.

var playing_map := false

…

func _process(_delta: float) -> void:
	if playing_map and Input.is_action_just_pressed("ui_cancel"):
		PauseMenu.activate()

…

func map_loaded(map: GameMap) -> void:
	…
	playing_map = true
Pause menu overlay.

Now we can open the pause menu and it overlays the current map. But although gameplay is stopped the conveyors and dotted teleporter lines still appear to move. This happens because those are shader animations, which are not affected by the paused state of the node tree. To also freeze these animations we have to stop the shader time. We could do this by using a custom shader variable instead of TIME, but the simplest solution is to set Engine.time_scale to 0.0, which suffices for our simple game. Do this in the activate() function of pause_menu.gd.

func activate() -> void:
	root_window.add_child(self)
	get_tree().paused = true
	Engine.time_scale = 0.0

Now the whole map is frozen while the pause menu is open.

Resuming and Restarting

When we dismiss the pause menu we need to set everything back to normal. The tree must be unpaused, the time scale must be set back to 1.0, and the pause menu must be removed from the root window. Let's create a _close_pause_menu() function for this.

func _close_pause_menu() -> void:
	get_tree().paused = false
	Engine.time_scale = 1.0
	root_window.remove_child(self)

Now we can call that function when the resume button is pressed. Let's also close the menu when the Esc key is pressed again, by checking for it in a _process() function.

func _process(_delta: float) -> void:
	if Input.is_action_just_pressed("ui_cancel"):
		_close_pause_menu()


func _on_resume_button_pressed() -> void:
	_close_pause_menu()

To restart the current map let's add a restart_map() function to main.gd that simply loads the current map path.

func restart_map() -> void:
	load_map(game_save_data[KEY_MAP_PATH])

Then in pause_menu.gd when restart is pressed we close the menu and restart the map.

func _on_restart_button_pressed() -> void:
	_close_pause_menu()
	Main€.restart_map()

We have added a little quality of life to our game by supporting the ability to pause and restart the current map. We'll add more gameplay elements in the future.