Tutorial Space Shooter Godot: Player e Projéteis - Parte 3
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.
Tá legal desenvolvedor, nos encontramos mais uma vez, certo? Pelo jeito você está gostando do material disponibilizado aqui, senão não voltava, né? Se for isso mesmo, curta esse post, comente e indique o site para seus conhecidos.
Vamos fazer uma retomada rápida aqui: No primeiro post da série falamos sobre planejamento, definimos em grandes linhas o que seria construído (link aqui), baixamos o Godot, Assets do Kenney e também do OpenGameArt.
Criamos a estrutura base para nosso jogo, definindo as proporções da tela, mascara padrão de apresentação e mapeamos algumas ações que serão utilizadas no jogo.
No post anterior (link aqui) nós mapeamos mais 2 ações, criamos nosso primeiro script com o mapeamento de tais ações e também criamos um fundo em Parallax com duas camadas que se mexem em velocidades diferentes.
No post de hoje, vamos mostrar esse Parallax funcionando melhor, para isso tá faltando incluir uma nave espacial e ter um gostinho à mais de como vai ficar o nosso jogo. Então sem mais atrasos, vamos lá!
Passo 3 - Criação do Player
O nosso Player será criado fora da cena principal do nosso jogo, isso vai nos permitir trabalhar todas as características dele e somente no final incluir ele como parte da árvore no Godot.
Para começar, vamos criar uma nova cena, clicando no +
ao lado do nome da cena do jogo aberta, outra opção é no menu suspenso selecionar Nova Cena
ou então usar o atalho Ctrl + N
pelo teclado.
Essa cena estará completamente vazia, então precisamos selecionar qual será o nó principal dessa cena, vamos utilizar o nó tipo CharacterBody2D
que já possui alguns parametros e funções úteis para nosso personagem.
Para selecionar o nó, você pode clicar em + Other Node
ou usar o atalho de teclado ctrl + A
, na caixa de seleção que se abre, procure na lista pelo nó específico que vamos utilizar.
Renomeie este nó para Player e inclua como filho deste nó dois outros nós:
- Sprite2D;
- CollisionShape2D.
O Sprite 2D será responsável pela textura que será apresentada em tela, já o Collision Shape será responsável por identificar as colisões com outros objetos. Na aula anterior criamos a pasta assets onde incluímos as imagens do Parallax, vamos usar a mesma pasta para incluir as imagens do Player.
Para o Sprite devemos definir sua propriedade de textura com a imagem que vamos usar para o jogador, eu escolhi uma nave verde, mas vc pode escolher a que achar mais interessante.
Para o Collision Shape, vou usar uma cápsula de 20 x 80 definida na propriedade shape deste nó. Novamente, você pode usar outros formatos caso ache que ficam mais adequados.
Uma alternativa ao CollisionShape2D é utilizar um CollisionPoligon2D caso queira deixar as áreas de colisão melhor definidas. Lembre-se de ajustar seu código de acordo com a seleção.
E vamos salvar nossa cena na pasta de cenas criada anteriormente para não perder o trabalho feito até aqui, né? Afinal, nosso bebê já tá ficando bonitinho, não acha?
Muito bem, agora precisamos fazer um ajuste pontual no nó do Player, como estamos utilizando um nó do tipo CharacterBody2D, ele vem com algumas configurações padrão, entre elas a configuração de modo de movimento.
No nosso caso precisamos alterar esta configuração para Floating pois não estamos fazendo um jogo de plataforma que utilizaria a opção Grounded. Deixe seu comentário com um #plataforma se vc quiser um tutorial de jogo plataforma posteriormente.
E feito isso é hora de partir para a definição do script que fará a movimentação da nave na tela. Vamos clicar no pergaminho para associar um script ao nó raiz da nossa cena e vamos salvar este script na pasta de scripts criada anteriormente.
Para o nó CharacterBody2D (nó principal da nossa cena), o Godot 4.1 indica um script de movimentação básico como template, mas esse script tem características para jogo plataforma e portanto não vamos utilizá-lo.
Para a movimentação do Player, vamos disparar as ações dentro na função de processamento de físicas (physics_process), avaliando se o jogador está apertando as teclas correspondentes às ações de esquerda e direita que mapeamos anteriormente. Você vai incluir as seguintes informações no seu script:
- Variável Speed acessível via Inpetor;
- Variável Direction para definir a direção de movimento;
Também vamos usar a função nativa get_axis que vai nos apoiar à definir um valor positivo ou negativo de acordo com as teclas esquerda e direita pressionadas. Como nossa nave não vai se mexer verticalmente, vamos assumir que a direção terá seu componente vertical sempre como zero.
Seu código deve ficar parecido com isso aqui:
@export var speed = 300.0
func _physics_process(delta):
var direction = Vector2(Input.get_axis("left", "right"), 0)
velocity = direction * speed
move_and_slide()
Se você executar esta cena (apertando o F6
), já vai conseguir ver a sua nave se mexendo horizontalmente de acordo com as teclas esquerda e direita que você configurou. Mas espera, e como fica o fundo animado que fizemos no último post?
Lembra que lá no começo falamos que iríamos definir o Player separadamente e depois incluir ele na cena principal? Então chegou a hora de fazer isso!!!
Na cena do Player, através do inspetor, vamos definir:
- O player como parte do grupo player.
Na cena e script do jogo vamos fazer alguns ajustes:
- Incluir a cena do Player na cena do jogo.
- Um nó para referenciar a posição em que o Player tem que ser apresentado;
- Uma variável para armazenar a cena do Player;
- Ajustes no código para posicionar o Player no local esperado.
Vamos por partes, né? Primeiro incluir o Player em um grupo:
Depois incluir um novo nó Marker2D na cena do game para ter a referência do local em que o Player será incluído (na parte de baixo da tela, centralizado):
Agora incluir a variável e o código para incluir o Player como parte da cena principal do jogo, com as seguintes informações:
...
@onready var player_spawn_pos = $PlayerSpawnPos
...
var player = null
func _ready():
player = get_tree().get_first_node_in_group("player")
assert(player !=null)
player.global_position = player_spawn_pos.global_position
...
Para incluir a cena do Player na cena do jogo, basta arrastar uma para dentro da outra e ela será apresentada na árvore de cenas.
Legal, agora já temos o jogo com o parallax de fundo e nosso jogador à frente se movimentando horizontalmente de acordo com as teclas pressionadas, para testar basta pressionar F5
.
Bora partir para a criação dos projéteis? Vou passar um pouco mais rápido por essa parte, indo mais devagar somente quando tiver algum ponto diferente do que já definimos, tá legal?
Passo 4 - Criação do Projetil
Implementaremos a lógica para disparar projéteis e criar emocionantes batalhas no espaço.
Os projéteis serão também uma nova cena, então já sabe né? Tem que clicar no sinal de +
junto das abas das cenas para criar uma nova.
Para esta cena, vamos utilizar um novo tipo de nó raíz, um nó do tipo Area2D, porque só queremos identificar quando o projétil tocar alguma coisa diferente e uma área 2D é simples o suficiente para isso. Renomeie o nó para Laser e salve a cena.
Vamos incluir dois nós filhos à este nó principal, um Sprite2D e um CollisionShape2D para armazenar a imagem do nosso laser e definir a área de colisão deste laser com os inimigos ou objetos. Selecionei uma das imagens dos assets do Kenney que me pareceu mais interessante e ajustei a área de colisão com um retângulo do tamanho da imagem.
Vamos incluir também um nó do tipo VisibleOnScreenNotifier2D que vamos utilizar para saber quando o laser sair da tela do jogador. Esse nó pode ser bem pequeno e ficar na parte mais baixa da cena pois queremos identificar quando o laser sair da cena.
Vamos incluir o script no nosso Laser para fazer a movimentação dele de baixo para cima na tela e também excluir ele quando não estiver mais visivel.
Como a intenção é que sempre que um laser seja criado ele automaticamente se mova em direção ao topo da tela, vamos incluir a movimentação desse nó sem dependência de nada, simplesmente iniciando no momento em que as regras de física deste nó aconteçam na função _physics_process
. Seu código deve ficar mais ou menos assim:
@export var speed = 600
func _physics_process(delta):
global_position.y -= speed * delta
Também vamos utilizar um dos sinais nativos do nó VisibleOnScreenNotifier2D para detectar quando ele sair da tela. Para isso com este nó selecionado, vá até a aba Node no Inspector, clique duas vezes no sinal screen_exited e conecte ao script que vc criou, incluindo na função que foi criada o comando queue_free()
para que elimine o nó quando ele não estiver mais na tela do jogador.
Essa é a tela para conectar o sinal ao script criado:
E o seu código vai deve ficar deste jeito:
...
func _on_visible_on_screen_notifier_2d_screen_exited():
queue_free()
Lembre-se de remover do seu jogo as cenas que não façam mais sentido permanecerem na memória para garantir uma performance adequada ao seu jogo.
Agora precisamos ir à cena do Player para incluir a posição em que o tiro será gerado com um Marker2D, assim como fizemos para identificar onde o Player aparecerá no mundo. Vou renomear este nó como Turret, mas vc pode usar o que achar mais adequado, e posicioná-lo logo à frente da nave.
Também será necessário ajustar o script do Player para incluir uma variável que armazenará a cena do tiro, uma para referenciar a posição em que o tiro será disparado, uma função para controlar os disparos e um sinal customizado quando o jogador pressionar o tiro (mapeado para o botão de espaço, lembra?) que passará a cena e a posição para ser criada no nó do Game.
Como os nós filhos tem posição relativa em relação aos nós pais, não podemos criar o laser como filho do Player, ou ele vai se mover junto com a movimentação do Player. O ideal neste caso é criar a instância do laser como filho do nó Game, assim ele não sofrerá influencia do Player depois de criado.
Vamos incluir os seguintes códigos no script do player:
...
signal laser_shot(laser_scene, location)
...
@onready var turret = $Turret
var laser_scene = preload("res://scenes/laser.tscn")
func _process(delta):
if Input.is_action_just_pressed("shoot"):
shoot()
...
func shoot():
laser_shot.emit(laser_scene, turret.global_position)
O script vai ficar desta forma:
No nó do Game, precisaremos conectar o sinal que criamos quando o tiro for disparado para poder criar uma instância do tiro como filho deste nó. Na verdade, vamos incluir um nó para guardar os disparos e não poluir demais o nó principal.
Basta criar um novo nó Node2D que vou chamar de LaserContainer, e incluir uma referência para encontrar esse nó mais facilmente dentro do script do Game.
Como já temos uma referência para o player na cena Game, vamos fazer a conexão do sinal diretamente via código, mas também poderia ser feito via Inspector.
Conectar o sinal é simples, precisamos dizer ao Godot que função do script atual vamos chamar quando o sinal gerado pelo player for recebido. Para isso vamos criar os seguintes itens:
- Um código de conexão do sinal;
- Uma função para criar o laser na cena.
Não se esqueça de criar a variável para o seu nó LaserContainer também. Seu código ficará assim:
...
@onready var laser_container = $LaserContainer
...
func _ready():
...
player.laser_shot.connect(_on_player_laser_shot)
...
...
func _on_player_laser_shot(laser_scene, location):
var laser = laser_scene.instantiate()
laser.global_position = location
laser_container.add_child(laser)
A função add_child que foi incluída na última linha, inclui a cena do laser recém instanciada como filha do LaserContainer, assim todos os tiros gerados vão sempre ser criados como irmãos, filhos deste container.
O seu código ficará assim com as novas variáveis, funções e conexão de sinais:
Vamos fazer uma revisão rápida do que foi feito aqui:
- Criamos a cena do jogador, definimos seu script de movimentação com base nas teclas e ações que havíamos mapeado;
- Definimos no Game onde o Player será apresentado inicialmente através de um novo nó chamado Marker2D;
- Criamos a cena do projétil Laser, definimos seu script de movimentação (masi simples que o do Player), área de colisão e notificação de visibilidade em tela;
- Definimos no Player uma referência de posição para que o Laser seja disparado de uma posição coerente em relação ao Player;
- Definimos um sinal customizado que é enviado junto com a cena do laser e sua posição à partir do Player para o Game;
- Criamos um nó filho do Game para armazenar os disparos gerados pelo Player sem bagunçar os nós existentes;
- Criamos uma função para instanciar o Laser na posição recebida à partir do sinal gerado no Player.
Ah e chegamos ao final do dia de hoje. Espero que tenha gostado bastante do que viu até aqui e que nos acompanhe na próxima semana, onde vamos criar os nossos inimigos e randomizar sua geração na tela do jogador.
Deixe seu comentário, duvidas e sugestões de melhoria. Vou ler e responder à todos comentários que estiverem acompanhando.