Tutorial Space Shooter Godot: Inimigos e Spawner - Parte 4

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: Inimigos e Spawner - Parte 4
Photo by Brian McGowan / Unsplash

Opa, tá de volta por aqui, né? E aí, tá gostando da série tutorial? Deixa seus comentários do que dá pra gente melhorar, tá legal? O feedback de vocês é importante pra gente.

Nos posts anteriores vimos:

  • Planejamento e Preparação - Definimos o estilo de jogo, baixamos o Godot e os assets que vamos usar. Também configuramos o tamanho da tela, máscara de apresentação das texturas e mapeamos algumas ações;
  • Parallax - Criamos um fundo com movimento infinito com duas camadas, simulando o espaço sideral e dando a impressão de estrelas em distâncias diferentes. Criamos nosso primeiro script;
  • Player e Projéteis - Criamos o jogador, definimos seu script de movimento e disparo de projéteis. Criamos a cena dos projéteis, seu script de movimento e instanciamos esta cena com base em um sinal emitido pela cena do jogador.

No post de hoje, vamos criar a cena dos nossos inimigos e fazer eles serem gerados de forma aleatória para a tela do usuário. Então mãos à obra.

Passo 5 - Criação do Template do Inimigo e Inimigos

Desenvolveremos modelos de inimigos alienígenas e multiplicaremos suas ameaças.

Vamos começar simples e vamos complicar um pouquinho dessa vez, ok?

Para a cena do inimigo, vamos criar uma cena bastante parecida com a cena do Laser que criamos no post anterior, precisamos ter uma cena nova, que terá como nó raíz uma Area2D e como filhos desse nó temos um Sprite2D, um ColisionShape2D e um VisibleOnScreenNotifier2D.

Dessa vez o VisibleOnScreenNotifier2D ficará no topo da cena, pois a movimentação do inimigo será para baixo.

Cena do Inimigo, com o nó raíz Area2D e seus nós filhos.

Em relação ao script, também será bastante parecido com o script do Laser, com a diferença de que o movimento é feito no sentido contrário, ou seja com o valor de y crescente. Seu código deve ficar assim:

extends Area2D

@export var speed := 150.0

func _physics_process(delta):
	global_position.y += speed * delta


func _on_visible_on_screen_notifier_2d_screen_exited():
	queue_free()

Inclua a cena do inimigo na sua cena principal e veja o inimigo descendo a tela. Ele ainda não pode ser atingido nem pelo tiro, nem em uma colisão com o Player, mas vamos ajeitar isso na sequência.

Precisamos definir as camadas em que cada uma das entidades do nosso jogo existe e como estas camadas interajem entre si. Hein? Como é? Não entendi...

Basicamente, vamos dizer que o Player, o Laser e o Inimigo estão cada um em uma camada e que o Laser não atinge o Player, só o Inimigo.

Para fazer esta configuração primeiro vamos às configurações do projeto definir nomes para as camadas conforme eu fiz aí nessa imagem:

Definindo o nome das Layers de Física.

Depois para cada uma das cenas que criamos, temos que indicar à qual camada ela pertence e com que camadas ela interage. Isso é feito para cada uma das cenas, no seu nó raíz, dentro do Inspector selecionando as informações de Layer e Mask dentro do grupo Colision:

Definição de Layer e Mask para a cena do Player.

Com isto configurado podemos começar à verificar as colisões entre Laser e Inimigo ou Inimigo e Player. Vamos verificar a colisão do Laser com o Inimigo dentro do script do Laser utilizando o sinal padrão de area_entered.

Lembra como se conecta um sinal? Com o nó selecionado, vá à aba Node, selecione Signals e dê um clique duplo no sinal area_entered. Na tela que se abre, selecione o nó raíz da cena que possui o script de movimento do Laser e conclua a conexão:

Conectando o sinal ao script do Laser.

Sabemos que o Laser só interage com a camada de inimigos, mas para garantir que o que o Laser atingiu seja um inimigo, é interessante dar um nome para a classe criada no script do inimigo. Inclua na primeira linha do script do inimigo o seguinte código:

class_name Enemy
...

E também precisamos criar uma função no script dos inimigos para eliminar eles da cena quando tomarem dano. Seu script do inimigo vai ficar desse jeito:

Script do Inimigo ajustado.

E no script do Laser, na função criada com a conexão do sinal, vamos colocar a validação de que atingiu um inimigo, chamar a função die deste inimigo e também excluir o Laser da cena. Tudo isso com este código:

...
func _on_area_entered(area):
	if area is Enemy:
		area.die()
		queue_free()

E pronto, quando o Laser atingir o inimigo tanto o Laser quanto o inimigo serão removidos do jogo. Agora inclua o inimigo na cena do jogo (arrastando a cena para a árvore de cenas do jogo) e faça um teste você mesmo.

Agora é hora de fazermos algo similar para quando o inimigo atingir o jogador. Ambos deverão ser excluídos da cena do jogo nesta situação. A principal diferença neste caso será o sinal, que ao invés de area_entered deve ser utilizado um body_entered pois o Player é do tipo CharacterBody2D.

💡
Importante:
Ainda não incluímos pontuação para cada inimigo destruído, efeitos visuais de explosão ou efeitos sonoros no nosso jogo, isso será feito em posts posteriores. Neste momento o importante é que você consiga entender a lógica de como as cenas interagem entre si através de sinais, chamadas de funções e instanciamento de cenas.

Altere o script do player incluindo um nome de classe e uma função die conforme foi feito para o script do inimigo.

Seu script do Player vai ficar assim:

Script do Player com nome de classe e função die.

No script do inimigo conecte o sinal e inclua na função criada a validação do body que entrou em contato, bem como a chamada para a função de eliminar o player e o inimigo da cena. E o script do inimigo vai ficar dessa forma aqui:

Script do inimigo com o sinal de body_entered conectado.

Inclua 2 inimigos na cena do jogo e faça os testes de atirar em um deles e colidir no outro, em ambos casos o inimigo deve ser excluído junto com o Laser ou Player que colidiu com ele.

Mas espera, o objetivo desse passo não era criar um inimigo e sim um modelo para qualquer dos inimigos que tenhamos no jogo, certo? SIM!!! Agora que temos a cena do inimigo criada, podemos criar outros inimigos que seguem a mesma estrutura utilizando esta cena como base e criando uma cena herdada.

Bora criar um segundo inimigo então? No menu superior selecione Scene > New Inherited Scene... e selecione a cena do inimigo como sua cena base:

Criando uma cena herdada com base na cena do inimigo.

Note que esta cena terá exatamente as mesmas coisas que a cena original: mesmos nós, mesmos scripts, mesmas texturas, mesmos valores para as propriedades, etc. Você não consegue excluir os nós de uma cena herdada, mas pode alterar os valores de suas propriedades e também incluir novos nós definindo comportamentos para eles independente da cena original da qual se herdaram os demais componentes.

No nosso caso vamos alterar o nome do nó para Kamikaze, pois será um inimigo que se move muito mais rapidamente para atingir o jogador, alteramos a variável speed que criamos para refletir isso e também vamos alterar a textura que estamos utilizando para esta cena.

Novo inimigo Kamikaze feito à partir de uma cena herdada.

Um terceiro inimigo que atire ou se mova de forma diferente também pode ser criado como uma cena herdada, fica como desafio para você fazer mais tarde. Só fique atento quando precisar fazer alterações na cena principal para não impactar as demais cenas herdadas.

💡
Atenção:
Caso você altere a cena da qual sua cena herdou as propriedades, a cena herdada vai ser afetada junto então tenha atenção quando precisar realizar este tipo de ação.

E nosso próximo passo será gerar inimigos automaticamente, de tempos em tempos, em locais diferentes da tela para que o jogador tenha um desafio de destruí-los um a um.


Passo 6 - Spawner de Inimigos

Criaremos um sistema para gerar inimigos de forma dinâmica durante nossa jornada espacial.

Vamos criar um nó que vai armazenar os Inimigos criados no jogo, similar ao que fizemos para os tiros Laser, vamos criar um nó do tipo Node2D como um container de inimigos na cena principal do jogo. Vai ficar desse jeito:

Cena Game com o EnemyContainer criado.

Adicionalmente, como podemos ter mais de uma cena de inimigos, vamos criar no script do game uma variável de lista para armazenar as cenas que queremos que sejam instanciadas durante a execução do jogo. Queremos esta variável com acesso pelo Inspector, então precisa ser uma variável exportada.

...
@export var enemy_scenes : Array[PackedScene] = []
...

Dessa forma, é possível incluir novas cenas de inimigos no seu array sem precisar ficar alterando o tamanho dessa lista no código, isso poderá ser feito via Inspector. O seu código vai ficar desse jeito:

Criação da lista vazia com PackedScenes como objetos da lista.

E no inspetor, vc pode incluir as cenas dos inimigos. No nosso caso como criamos 2 cenas, vamos incluir ambas, caso você crie novos inimigos, também irá incluí-los aí, alterando a quantidade de itens da sua lista:

Definindo as cenas do seu Array à partir do Inspector.

Ótimo, já temos como armazenar as cenas, agora precisamos criar a lógica para geração destas instâncias de cenas durante o jogo. Para começar, vamos definir um Timer que à cada 1 segundo vai liberar um novo inimigo em tela.

Crie um novo nó do tipo Timer, defina o tempo dele no inspetor e também defina ele como Autostart. Renomeie como quiser, vou chamá-lo de EnemySpawnTimer.

💡
Importante:
As cenas no Godot são criadas em memória de acordo com sua posição na árvore de cenas, então coloque o Timer no início caso queira utilizar ele em alguma lógica no seu script.

Conecte o sinal de timeout deste Timer ao script do jogo. Seu código deve ficar parecido com isso aqui:

Nó Timer criado, colocado no topo da lista na árvore de cenas e com o timeout conectado.

Nossa lógica será de instanciar uma cena da lista de cenas de inimigos aleatoriamente sempre que ocorrer um sinal de timeout. Para isso vamos precisar fazer o seguinte:

  • criar uma variável referenciando o container de inimigos no script do jogo;
  • criar uma instância da cena do inimigo na função criada com o sinal timeout;
  • definir uma posição deste inimigo de forma aleatória no fora da tela;
  • incluir esta instância como filha do container de inimigos;

Seu código deve ficar dessa forma:

...
@onready var enemy_container = $EnemyContainer
...
func _on_enemy_spawn_timer_timeout():
	var e = enemy_scenes.pick_random().instantiate()
	e.global_position = Vector2(randf_range(60, 480), -50)
	enemy_container.add_child(e)

E com isso já temos um Spawner de Inimigos funcionando. Ao rodar o jogo, os inimigos vão começar à descer pela tela desse jeito:

Inimigos gearados em posições aleatórias na tela do jogador.

Já estou pensando em algumas melhorias para os próximos posts:

  • Fazer com que os inimigos tenham quantidade diferente de pontos de vida;
  • Criar inimigos que atiram ou que se movem de forma diferente;
  • Criar tiros diferentes para o Player e um tiro especial;
  • Fazer o Player se mover en outras direções;
  • Reduzir o tamanho do Player, Inimigos e Laser;
  • Criar Power-ups para o jogador.

O que acha? Tem alguma outra sugestão? Deixe seu comentário para irmos melhorando nos próximos posts. Por hoje é isso aí... Te vejo na semana que vem.