Tutorial Space Shooter Godot: Menu e Game Over - Parte 9

Aprenda a criar um jogo Space Shooter de forma simples e prática com a Godot. Aprenda as funcionalidades da Engine e faça seu jogo um sucesso.

Tutorial Space Shooter Godot: Menu e Game Over - Parte 9
Photo by Sigmund / Unsplash

Calma que ainda não acabou. Todo tipo de feedback ao jogador é interessante, seja ele um feedback positivo ou um feedback negativo. Mas o feedback que os jogadores menos gostam é o game over, a tela final do seu jogo, onde o jogador entende que não conseguiu avançar além daquele ponto, daquele inimigo, daquela fase. Enfim, o jogo acabou.

Hoje vamos criar uma tela de Game Over para apresentar ao jogador a sua pontuação final e a pontuação máxima já obtida no jogo.

Além disso também vamos criar o menu inicial do nosso jogo, pois afinal o jogador não vai abrir o jogo e sair jogando sem ver uma tela de preparação antes. Bora começar pra valer então?

Passo 11 - Menu / Game Over

Criaremos um menu interestelar, um arquivo para armazenar o high-score do jogador e uma tela de Game Over customizada.

Vamos começar pelo fim, assim a gente já tira da frente este tabú que é a tela de game over, né? Vamos fazer o seguinte:

  • crie uma nova cena com o nó raiz do tipo Control;
    • renomeie este nó como GameOverScreen
    • altere a propriedade Z Index para 1
  • inclua como filhos desse nó Control um nó do tipo ColorRect
    • ajuste as dimensões desse nó pela âncora, para cobrir o tamanho completo da tela
    • ajuste a cor desse nó para preto e com 50% de transparência.
  • inclua um segundo filho ao nó Control do tipo Panel
    • defina as dimensões deste nó para que fique com alguma distância das bordas conforme a sua preferência.
    • alinhe este painel centralizado através das âncoras.
    • altere a cor e arredondamento das bordas através das propriedades correspondentes do grupo Theme Override (incluindo um StyleBoxFlat como o painel)
  • como filhos do nó Panel inclua 3 nós do tipo Label nomeados:
    • GameOver
    • HighScore
    • Score
  • inclua também um nó do tipo Button renomeado como RestartButton
  • ajuste o tipo de fonte para os labels e botão conforme sua preferência de fonte e tamanhos.

Você deve ficar com uma cena similar à esta aqui:

Cena de Game Over

Associe um script ao nó raíz dessa nova cena e conecte o click do botão à este script. Adicionalmente crie também as funções para setar os valores de score e high-score.

Seu código deve ficar assim:

extends Control

func _on_restart_button_pressed():
	get_tree().reload_current_scene()

func set_score(value):
	$Panel/Score.text = "Score: " + str(value)

func set_high_score(value):
	$Panel/HighScore.text = "High Score: " + str(value)

Precisamos incluir esta cena na nossa cena principal do jogo mas deixando ela por enquanto sem ser apresentada, para isto basta clicar no ícone de olho na árvore de cenas e a cena ficará invisível.

O próximo passo será fazer com que esta tela seja apresentada logo que o player morrer (vamos incluir um delay para não ser algo muito brusco para o jogador).

E também precisamos chamar as funções para atualizar os valores de score e high-score nesta tela de game over. Este último ponto será uma coisa nova, pois vamos criar um arquivo para armazenar o high-score e manter esta informação ao longo de todas as seções do jogo.

Vamos por partes então, certo? Incluíndo a cena e deixando ela oculta:

Cena Game Over incluída com visibilidade desativada.

Criando um sinal customizado na cena do jogador para identificar o momento em que ele morre e conectando tal sinal na função die do player:

...
signal player_killed
...
func die():
	player_killed.emit()
	queue_free()

Conectando este sinal na função ready do game e chamando uma função para ativar a visibilidade da cena de GameOver quando este sinal for recebido:

...
func _ready():
	player.player_killed.connect(_on_player_killed)
...
func _on_player_killed():
	$GameOverScreen.visible = true

Legal, com estes ajustes a tela de Game Over já será apresentada ao jogador quando a nave for destruída, mas ainda precisamos atualizar os valores de Score, High-Score e fazer com que a tela demore um tempinho para aparecer.

Para ter um delay entre a morte do jogador e a tela se tornar visivel, vamos criar um timer temporário de 1.25 segundos já conectando seu sinal de timeout à função nativa await dessa forma:

...
func _on_player_killed():
	await get_tree().create_timer(1.25).timeout
	$GameOverScreen.visible = true

Atualmente estamos definindo o score no HUD, para que fique mais fácil compartilhar a informação entre as cenas de HUD e Game Over vamos criar uma variável score na cena do Game e gerenciar a informação de forma única.

Na cena do HUD, vamos remover a variável score, a conexão do sinal do script global on_score_increment e a função set_score criada anteriormente. O script da HUD ficará assim:

extends Control

@onready var life_container = $LifeContainer
@onready var score_label = $ScoreLabel

var life_icon_scene = preload("res://scenes/life_icon.tscn")

func _ready():
	clear_lives()
	Signals.on_player_life_change.connect(set_lives)

func clear_lives():
	for child in life_container.get_children():
		child.queue_free()

func set_lives(lives: int):
	clear_lives()
	for i in range(lives):
		life_container.add_child(life_icon_scene.instantiate())

E no script do Game, vamos incluir o que removemos da HUD e adicionalmente criar uma variável referência para a HUD para simplificar a atualização do dado lá também. Os ajustes no script ficarão desse jeito:

...
@onready var hud = $HUD
...
var score := 0
...
func _ready():
...
    Signals.on_score_increment.connect(set_score)
...
func set_score(amount: int):
	score += amount
	hud.score_label.text = str(score)

Legal, com esse ajuste agora conseguimos chamar a função de set_score do Game Over antes de apresentar a tela ao jogador. Para isso na função _on_player_killed vamos ajustar da seguinte maneira:

...
func _on_player_killed():
	await get_tree().create_timer(1.25).timeout
	$GameOverScreen.set_score(score)
	$GameOverScreen.visible = true

A tela de game over deve ser apresentada corretamente, com o valor de Score refletindo a pontuação do jogador, conforme apresentado aí em baixo:

Tela de Game Over apresentando o score do jogador

Para o High Score, vamos fazer o seguinte:

  • criar uma variável na cena do game para gravar o high score
  • validar se o valor do score é maior ou igual ao high score e atualizar tal informação
  • chamar a função para atualizar tal dado na tela de game over
  • salvar o high score em um arquivo para que o dado não se perca entre seções do jogo

Os ajustes no script do game serão estes aqui:

...
var high_score
...
func _ready():
	var save_file = FileAccess.open("user://save.data", FileAccess.READ)
	if save_file != null:
		high_score = save_file.get_32()
	else:
		high_score = 0
		save_game()
	player = get_tree().get_first_node_in_group("player")
	assert(player != null)
	player.global_position = player_spawn_pos.global_position
	player.laser_shot.connect(_on_player_laser_shot)
	player.player_killed.connect(_on_player_killed)
	Signals.on_score_increment.connect(set_score)

func save_game():
	var save_file = FileAccess.open("user://save.data", FileAccess.WRITE)
	save_file.store_32(high_score)

func set_score(amount: int):
	score += amount
	if score >= high_score:
		high_score = score
	hud.score_label.text = str(score)

func _on_player_killed():
	await get_tree().create_timer(1.25).timeout
	$GameOverScreen.set_score(score)
	$GameOverScreen.set_high_score(high_score)
	save_game()
	$GameOverScreen.visible = true

Muito bom, agora é testar para ver se tudo deu certo, do meu lado tudo rodou 100% e por aí? Valide que ao iniciar o jogo pela primeira vez o high score será qualquer pontuação que você obtenha na primeira partida e que tal valor será atualizado sempre que sua pontução final for maior que a anterior.

Com relação ao Game Over, é isso aí... Agora vamos partir para o Menu do jogo. No nosso Jogo, vamos fazer um menu bem simples, apenas uma tela inicial com o nome do Jogo um botão de Iniciar, um botão para mostrar Informações sobre o jogo e um botão de Sair.

Crie uma nova cena do tipo Control, nesta cena inclua um TextureRect e um MarginContainer como filhos, um VBoxContainer como filho deste MarginContainer, um Label e três botões como filhos do VBoxContainer. Renomeie os nós conforme a imagem abaixo:

Cena do Menu com os nós renomeados e com textos ajustados

Inclua o mesmo fundo utilizado na cena do Game para o Parallax, com as mesmas características de repetição na largura e altura. A diferença é que nesta cena não vamos deixar o fundo animado.

Brinque com as propriedades de separação, ancoragem, fonte e tamanho de fonte conforme ache interessante para que fique visualmente interessante, vou deixar desta forma aí.

Crie um novo script associado ao nó raiz dessa nova cena e conecte os sinais de pressed dos três botões conforme já fizemos várias vezes anteriormente. Seu script ficará desta forma:

Sinal pressed dos botões associados ao script do Menu.

Agora vamos incluir as ações para cada um desses botões. No botão Play, vamos alterar a cena para a cena principal do jogo que definimos anteriormente. No botão Quit vamos disparar a função nativa de sair do jogo. No botão About vamos mandar o jogador para uma nova cena que criaremos já já. O seu código deve ficar similar a isto:

extends Control

func _on_play_pressed():
	get_tree().change_scene_to_file("res://scenes/game.tscn")

func _on_about_pressed():
	get_tree().change_scene_to_file("res://scenes/about.tscn")

func _on_quit_pressed():
	get_tree().quit()

Não se esqueça de alterar a cena raiz no menu de configuração do projeto para que a partir de agora o seu jogo inicie com a cena do Menu e não mais com a cena do Game. Isso é feito nessa parte aqui:

Alteração da cena inicial nas configurações do projeto.

E pra fechar o post de hoje, vamos criar a cena de informações também será uma cena do tipo Control, tendo como filhos um TextureRect um RichTextLabel e um Button.

Incluí um texto "Loren Ipsum" somente como exemplo e criei um script associado à cena raíz. Estou fazendo assim só para simplificar, mas você pode fazer como quiser, incluindo informações dos comandos, explicação sobre você ou sobre como jogar, imagens da nave e inimigos, etc.

Sua cena ficará mais ou menos dessa forma:

Cena Sobre com a imagem de fundo, texto e botão de retorno.

Assim como feito para o menu, inclua a textura com as propriedades de largura e altura de acordo com o tamanho da tela e a função de repetição da textura ativada.

Conecte o sinal no botão de voltar apontando para a cena Menu, assim como fizemos na cena menu para esta e para a cena do game. Assim óh:

extends Control

func _on_return_pressed():
	get_tree().change_scene_to_file("res://scenes/menu.tscn")

E por hoje foi isso, ficamos por aqui. Falta muito pouco para concluirmos nossa série tutorial, não deixe de acompanhar os próximos posts pois eles serão nosso fechamento. Deixe seus comentários, sugestões e críticas para podermos melhorar sempre.

Abraço e nos vemos na semana que vem.