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/VBoxContainercom 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.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
2) PauseMenu.tscn
Estrutura sugerida:
Control (PauseMenu)Panel/VBoxContainercom 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/VBoxContainercom 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 estadosCanvasLayer (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 = nullPausa, 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:
| Estado | Mouse | Motivo |
|---|---|---|
| MENU | Input.MOUSE_MODE_VISIBLE | clicar em botões |
| PLAYING | Input.MOUSE_MODE_CAPTURED (opcional) | controle/mira sem sair da janela |
| PAUSED | Input.MOUSE_MODE_VISIBLE | interagir com menu |
| GAME_OVER | Input.MOUSE_MODE_VISIBLE | escolher 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
MENUe mostraMainMenu. - Botão “Jogar” emite
start_game. - Main recebe e troca para
PLAYING: instanciaWorld, remove menu, despausa.
2) Pausar
- Durante
PLAYING, pressionarui_cancel(Esc) chamachange_state(PAUSED). - Main pausa a árvore e mostra
PauseMenucomWHEN_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 mostraGameOver.
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_PAUSEDno PauseMenu/GameOver. - Ao sair do pause, o jogo continua pausado: garanta
get_tree().paused = falseao entrar emPLAYINGe ao sair dePAUSED. - 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_cancelestá mapeado e que você usa_unhandled_inputno Main.