Godot do Zero: Menus e navegação com estados de jogo

Capítulo 13

Tempo estimado de leitura: 4 minutos

+ Exercício

Objetivo: menus e navegação com estados de jogo

Neste capítulo, você vai organizar o fluxo do jogo usando cenas separadas para Menu Principal, Pause e Game Over, e um gerenciador de estados (uma “state machine” simples) no nó principal para alternar entre: MENU, PLAYING, PAUSED e GAME_OVER.

A ideia central é: um único nó “dono do fluxo” decide qual tela está ativa e quando trocar, evitando que cada tela “mande” no jogo de forma desorganizada. Isso reduz bugs de transição, evita telas duplicadas e facilita voltar ao menu, reiniciar e pausar.

Arquitetura recomendada (cenas e responsabilidades)

Cenas

  • Main.tscn: cena raiz do jogo (gerencia estados, instancia/desinstancia telas e o mundo).
  • MainMenu.tscn: UI do menu principal (botões: Jogar, Sair).
  • PauseMenu.tscn: UI do pause (botões: Retomar, Voltar ao Menu).
  • GameOver.tscn: UI de fim de jogo (botões: Tentar de novo, Voltar ao Menu).
  • World.tscn: seu “mundo” jogável (fase + player + inimigos). Essa cena emite sinais como “player morreu”.

Regras de ouro para transições seguras

  • Somente o Main troca de estado. Menus apenas emitem sinais (ex.: “start_pressed”).
  • Instancie e libere cenas com cuidado: add_child() para mostrar, queue_free() para remover.
  • Pause: use get_tree().paused, mas garanta que a UI continue respondendo (process mode adequado).
  • Mouse: capture/solte conforme o estado (jogando captura; menus soltam).

Passo a passo: criando as cenas de menu

1) MainMenu.tscn

Estrutura sugerida:

  • Control (MainMenu)
  • Panel/VBoxContainer com botões: PlayButton, QuitButton

No script do MainMenu, emita sinais ao invés de trocar cenas diretamente:

extends Control

signal start_game
signal quit_game

func _on_play_button_pressed() -> void:
	start_game.emit()

func _on_quit_button_pressed() -> void:
	quit_game.emit()

Conecte os pressed() dos botões para as funções acima.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

2) PauseMenu.tscn

Estrutura sugerida:

  • Control (PauseMenu)
  • Panel/VBoxContainer com botões: ResumeButton, BackToMenuButton

Script:

extends Control

signal resume
signal back_to_menu

func _ready() -> void:
	# Importante: quando a árvore estiver pausada, este menu precisa continuar processando.
	process_mode = Node.PROCESS_MODE_WHEN_PAUSED

func _on_resume_button_pressed() -> void:
	resume.emit()

func _on_back_to_menu_button_pressed() -> void:
	back_to_menu.emit()

3) GameOver.tscn

Estrutura sugerida:

  • Control (GameOver)
  • Panel/VBoxContainer com botões: RetryButton, BackToMenuButton

Script:

extends Control

signal retry
signal back_to_menu

func _on_retry_button_pressed() -> void:
	retry.emit()

func _on_back_to_menu_button_pressed() -> void:
	back_to_menu.emit()

Passo a passo: World emitindo “morte do jogador”

Para o Main saber quando ir para Game Over, o World deve emitir um sinal. No World.tscn (ou no script do nó raiz do mundo):

extends Node2D

signal player_died

func _on_player_died() -> void:
	player_died.emit()

Você pode conectar o sinal do Player (ex.: died) ao método _on_player_died do World. O importante é: o Main vai escutar o World, não o Player diretamente (isso mantém o fluxo centralizado).

Gerenciador de estados no Main (state machine simples)

1) Estrutura do Main.tscn

Estrutura sugerida:

  • Node (Main) com script de estados
  • CanvasLayer (UIRoot) para adicionar menus
  • (opcional) Node (GameRoot) para adicionar o World

Ter nós separados para UI e jogo ajuda a não misturar camadas e facilita limpar/instanciar.

2) Script do Main: enum de estados e referências

extends Node

enum GameState { MENU, PLAYING, PAUSED, GAME_OVER }

@onready var ui_root: CanvasLayer = $UIRoot
@onready var game_root: Node = $GameRoot

var state: GameState = GameState.MENU

var world: Node = null
var main_menu: Control = null
var pause_menu: Control = null
var game_over: Control = null

const MAIN_MENU_SCENE := preload("res://ui/MainMenu.tscn")
const PAUSE_MENU_SCENE := preload("res://ui/PauseMenu.tscn")
const GAME_OVER_SCENE := preload("res://ui/GameOver.tscn")
const WORLD_SCENE := preload("res://game/World.tscn")

func _ready() -> void:
	change_state(GameState.MENU)

3) Função central: change_state

Essa função é o “cérebro” do fluxo. Ela desliga o que não deve existir no estado atual e liga o que deve existir.

func change_state(new_state: GameState) -> void:
	# Evita trabalho desnecessário
	if state == new_state:
		return

	# Saída do estado atual (limpeza/ajustes)
	match state:
		GameState.PAUSED:
			# Ao sair do pause, sempre despausar a árvore
			get_tree().paused = false

	state = new_state

	# Entrada no novo estado
	match state:
		GameState.MENU:
			enter_menu()
		GameState.PLAYING:
			enter_playing()
		GameState.PAUSED:
			enter_paused()
		GameState.GAME_OVER:
			enter_game_over()

Implementando cada estado (entrada/saída)

Estado MENU

No menu, você normalmente quer: sem mundo carregado (ou mundo limpo), mouse solto, e o menu visível.

func enter_menu() -> void:
	# Garantir que o jogo não está pausado
	get_tree().paused = false

	# Mouse livre para clicar
	Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

	# Limpar mundo e telas de jogo
	free_world()
	free_pause_menu()
	free_game_over()

	# Criar menu principal se necessário
	if main_menu == null:
		main_menu = MAIN_MENU_SCENE.instantiate()
		ui_root.add_child(main_menu)
		main_menu.start_game.connect(_on_start_game)
		main_menu.quit_game.connect(_on_quit_game)

Handlers do menu:

func _on_start_game() -> void:
	change_state(GameState.PLAYING)

func _on_quit_game() -> void:
	get_tree().quit()

Estado PLAYING

Ao iniciar/retomar o jogo: esconder menus, garantir mundo instanciado, despausar, e capturar mouse (se fizer sentido no seu jogo).

func enter_playing() -> void:
	get_tree().paused = false

	# Captura opcional (útil se você usa mira/tiro com mouse)
	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

	# Remover menus de UI
	free_main_menu()
	free_pause_menu()
	free_game_over()

	# Instanciar mundo se ainda não existe
	if world == null:
		world = WORLD_SCENE.instantiate()
		game_root.add_child(world)
		# O World emite player_died
		world.player_died.connect(_on_player_died)

Quando o jogador morre:

func _on_player_died() -> void:
	change_state(GameState.GAME_OVER)

Estado PAUSED

No pause: pausar a árvore e mostrar o menu de pause. O ponto crítico é: UI precisa continuar respondendo mesmo com a árvore pausada. Você já configurou o process_mode do PauseMenu para WHEN_PAUSED.

func enter_paused() -> void:
	# Soltar mouse para interagir com UI
	Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

	# Pausar o jogo (física, scripts, etc.)
	get_tree().paused = true

	# Criar pause menu
	if pause_menu == null:
		pause_menu = PAUSE_MENU_SCENE.instantiate()
		ui_root.add_child(pause_menu)
		pause_menu.resume.connect(_on_resume)
		pause_menu.back_to_menu.connect(_on_back_to_menu)

Handlers do pause:

func _on_resume() -> void:
	change_state(GameState.PLAYING)

func _on_back_to_menu() -> void:
	change_state(GameState.MENU)

Estado GAME_OVER

No game over: normalmente você quer parar o mundo (pode pausar a árvore) e mostrar a tela. Aqui há duas abordagens comuns:

  • Pausar a árvore e deixar apenas a UI processar (mais simples).
  • Não pausar e apenas desabilitar controles/IA (mais flexível).

Vamos usar a primeira, consistente com o pause:

func enter_game_over() -> void:
	Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
	get_tree().paused = true

	free_pause_menu()
	free_main_menu()

	if game_over == null:
		game_over = GAME_OVER_SCENE.instantiate()
		ui_root.add_child(game_over)
		# Garanta que o GameOver também processe quando pausado
		game_over.process_mode = Node.PROCESS_MODE_WHEN_PAUSED
		game_over.retry.connect(_on_retry)
		game_over.back_to_menu.connect(_on_back_to_menu_from_game_over)

Handlers do game over:

func _on_retry() -> void:
	# Reinicia o mundo do zero
	free_world()
	change_state(GameState.PLAYING)

func _on_back_to_menu_from_game_over() -> void:
	change_state(GameState.MENU)

Funções utilitárias para criar/remover telas com segurança

Centralize a limpeza para evitar “menus duplicados” e referências penduradas.

func free_world() -> void:
	if world != null and is_instance_valid(world):
		world.queue_free()
	world = null

func free_main_menu() -> void:
	if main_menu != null and is_instance_valid(main_menu):
		main_menu.queue_free()
	main_menu = null

func free_pause_menu() -> void:
	if pause_menu != null and is_instance_valid(pause_menu):
		pause_menu.queue_free()
	pause_menu = null

func free_game_over() -> void:
	if game_over != null and is_instance_valid(game_over):
		game_over.queue_free()
	game_over = null

Pausa, UI responsiva e “process mode” (o detalhe que mais causa bugs)

Quando você faz get_tree().paused = true, por padrão a maioria dos nós para de processar e de receber input. Para menus de pause/game over funcionarem:

  • Defina o menu como process_mode = Node.PROCESS_MODE_WHEN_PAUSED (no _ready() do menu ou no Inspector).
  • Se algum nó específico precisar continuar (ex.: animação de UI), também deve estar em WHEN_PAUSED.
  • Evite colocar lógica de jogo dentro de nós que continuam processando durante pause.

Captura e soltura do mouse por estado

Um padrão simples:

EstadoMouseMotivo
MENUInput.MOUSE_MODE_VISIBLEclicar em botões
PLAYINGInput.MOUSE_MODE_CAPTURED (opcional)controle/mira sem sair da janela
PAUSEDInput.MOUSE_MODE_VISIBLEinteragir com menu
GAME_OVERInput.MOUSE_MODE_VISIBLEescolher retry/menu

Se seu jogo não precisa capturar o mouse, mantenha sempre visível e apenas garanta que o foco de UI esteja correto.

Fluxo completo exigido (do início ao retorno ao menu)

1) Iniciar jogo

  • Main inicia em MENU e mostra MainMenu.
  • Botão “Jogar” emite start_game.
  • Main recebe e troca para PLAYING: instancia World, remove menu, despausa.

2) Pausar

  • Durante PLAYING, pressionar ui_cancel (Esc) chama change_state(PAUSED).
  • Main pausa a árvore e mostra PauseMenu com WHEN_PAUSED.

3) Retomar

  • Botão “Retomar” emite resume.
  • Main troca para PLAYING: remove pause menu, get_tree().paused = false.

4) Morrer

  • World emite player_died.
  • Main troca para GAME_OVER: pausa a árvore e mostra GameOver.

5) Retornar ao menu

  • No Game Over, “Voltar ao Menu” emite back_to_menu.
  • Main troca para MENU: remove world e telas, garante árvore despausada, mostra menu principal.

Checklist de depuração rápida

  • Ao pausar, a UI não clica: verifique process_mode = WHEN_PAUSED no PauseMenu/GameOver.
  • Ao sair do pause, o jogo continua pausado: garanta get_tree().paused = false ao entrar em PLAYING e ao sair de PAUSED.
  • Menus duplicados: use funções free_* e mantenha referências (main_menu, pause_menu, game_over) atualizadas.
  • Tecla Esc não funciona: confirme que ui_cancel está mapeado e que você usa _unhandled_input no Main.

Agora responda o exercício sobre o conteúdo:

Ao implementar menus com uma máquina de estados no nó Main, qual prática ajuda a manter transições seguras e evitar telas duplicadas?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Quando o Main controla mudança de estados e instancia/queue_free das cenas, o fluxo fica centralizado. Menus só emitem sinais, reduzindo bugs de transição e evitando múltiplas instâncias de telas.

Próximo capitúlo

Godot do Zero: Áudio 2D e mixagem básica (SFX e música)

Arrow Right Icon
Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.
  • Leia este curso no aplicativo para ganhar seu Certificado Digital!
  • Ouça este curso no aplicativo sem precisar ligar a tela do celular;
  • Tenha acesso 100% gratuito a mais de 4000 cursos online, ebooks e áudiobooks;
  • + Centenas de exercícios + Stories Educativos.