Mini Tutorial Pong na Godot

Transforme sua paixão por jogos em realidade! Aprenda a criar o clássico Pong na Godot neste tutorial fácil de seguir. Desenvolva suas habilidades de programação e divirta-se construindo seu próprio jogo! 🎮✨ #Godot #DesenvolvimentoDeJogos

Mini Tutorial Pong na Godot
Photo by Feelfarbig Magazine / Unsplash

Fala Game Dev, tudo certo? Tá por aqui pra ver mais um tutorial, né? Clássico.

Antes de entrarmos no tutorial, você sabia que o Pong é um dos primeiros jogos criados pela Atari? Clássico é clássico, desde 1972 ele vem sendo copiado arduamente por vários desenvolvedores para entendimento de mecânicas e aprendizado. Não vamos fazer nada diferente disso, certo?

Vamos primeiro definir quais são os elementos que precisamos ter no nosso jogo:

  • Uma bola quadrada (O Quico iria adorar essa parte);
  • Duas raquetes retangulares, uma para o jogador e outra para o computador;
  • Dois displays para apresentar a pontuação do jogador e oponente;
  • Lógica de movimentação vertical do jogador com base em botões pressionados;
  • Lógica de movimentação do oponente de acordo com a posição da bola;
  • Lógica de movimentação da bola.

Para começar, vamos criar um novo Projeto chamado Pong (ou se quiser usar outro nome, fique à vontade), não vou incluir nenhum asset específico, mas vc pode criar ou baixar assets para a bola, raquetes e fontes de textos diferentes para incluir no seu jogo.

Selecionando a pasta para o novo projeto

Com o projeto criado, na seção File System, inclua 2 ou 3 pastas para melhor organização:

  • scripts - vai armazenar os scripts do seu jogo;
  • scenes - vai armazenar as cenas do seu jogo;
  • assets - pasta opcional caso vá utilizar algum asset específico no seu jogo.

Apesar de ser um jogo simples, você pode incrementar algum nível de complexidade posteriormente e por isso é sempre importante tentar deixar tudo bem organizado desde o início.

Vamos começar com os elementos móveis e depois nós criamos o tabuleiro do nosso jogo, ok? Comecemos pela criação da raquete (só vamos precisar criar uma como modelo e depois duplicamos para ter ambas raquetes no jogo).

Para a criação da raquete, vamos criar um Nó do tipo Area2D que terá como filhos um CollissionShape2D e um Sprite2D. Como não vamos incluir nenhuma imagem específica, na configuração de textura deste Sprite, vamos incluir um New PlaceholderTexture2D, com o tamanho que vc achar interessante (eu defini com o tamanho de 40px x 250px).

Criação de uma textura placeholder para a raquete.

Inclua um Shape de Retângulo no seu Colision Shape e ajuste o tamanho para cobrir toda área do seu Sprite, esse elemento será responsável por identificar as colisões entre a raquete e a bola. A lógica desta colisão não estará definida nesta cena, mas tenha em mente que tal elemento é relevante para isso.

Altere o nome do nó raiz e salve a cena, eu dei o nome de Paddle, só por uma convenção de manter em inglês, mas você pode salvar como raquete, barrinha, rebatedor ou qualquer outro nome que ache interessante. Sua cena ficará mais ou menos desse jeito:

Cena Paddle (Area2D) com os elementos Colision e Sprite configurados

O próximo passo é criar o segundo elemento que se move em nosso jogo. O processo será muito similar, nossa bola também será uma Area2D com um ColisionShape2D e um Sprite2D como filhos.

Como é uma bola (apesar de quadrada), eu coloquei o shape do colisor como um círculo. Mas no sprite eu fiz da mesma forma que para a raquete, só incluí um New PlaceholderTexture2D (fiz o raio do colisor com 10px e o quadrado do sprite com 18px em cada lado).

Novamente, alterei o nome do nó raiz desta cena e salvei. Usei o nome Ball, mas vc pode chamar de peteca, bolinha, bola ou qualquer outro nome que ache interessante. A cena ficou dessa forma:

Cena Ball com os elementos configurados.

Agora vou incluir as lógicas para a movimentação da bola, esta cena será responável pela movimentação dela mesma, então vamos fazer com bastante atenção. Primeiro, vamos definir algumas variáveis:

  • ball_speed: para termos maior controle da velocidade da bola;
  • ball_direction: para sabermos em que direção ela se move;
  • ball_position: para saber a posição instantânea da bola;
  • ball_radius: para saber qual o tamanho da bola.

Na função ready, vamos posicionar a bola no centro da tela e definir uma direção aleatória para iniciar. Sempre que usamos aleatoriedade é importante chamar a função randomize para garantir que não usemos sempre a mesma aleatoriedade toda vez que iniciarmos o jogo.

💡
Curiosidade:
Em computadores não há uma aleatoriedade real, mas sim uma sequência pseudo-aleatória que pode ser utilizada. Quando usamos o randomize, mudamos a ordem dessa sequência ou o ponto inicial desta sequência que estamos utilizando, dando a idéia de que o resultado será aleatório em cada novo jogo.

Na função process, vamos calcular a posição da bola com base na direção e velocidade definidas e atualizar esta informação à cada frame, dando a idéia do movimento. Seu código até aqui ficará da seguinte forma:

extends Area2D

var ball_speed = 300
var ball_direction = Vector2(1, 1).normalized()
var ball_position = Vector2.ZERO
var ball_radius = 10

func _ready():
	randomize()
	ball_position = Vector2(get_viewport_rect().size.x/2, get_viewport_rect().size.y/2)
	ball_direction = Vector2(randi_range(-1,1),randi_range(-1,1))

func _process(delta):
	ball_position += ball_direction * ball_speed * delta
	position = ball_position

O próximo passo é garantir que a bola não saia da área do jogo quando for em direção ao topo ou base da tela, é como se houvessem paredes nestes lados para garantir que a bola não ultrapasse tais paredes. E também definir que quando a bola ultrapassar as laterais direita ou esquerda um dos jogadores receba um ponto por ter marcado gol.

A bola não vai ser responsável por alterar o score do jogador na tela principal, mas será responsável por indicar à tela principal que o score precisa ser alterado, isso é feito através da emissão de um sinal de incremento de pontuação para o jogador ou oponente.

Considerando estes pontos, vamos incluir os sinais:

...
signal enemy_scored
signal player_scored
...

E na função process as regras de colisão com topo, base, lógicas de emitir o sinal correspondente e retornar a bola para o centro da tela:

...
    if ball_position.y - ball_radius <= 0:
		ball_direction.y *= -1
	elif ball_position.y + ball_radius >= get_viewport_rect().size.y:
		ball_direction.y *= -1

	if ball_position.x - ball_radius <= 0:
		enemy_scored.emit()
		ball_position = Vector2(get_viewport_rect().size.x/2, get_viewport_rect().size.y/2)
	elif ball_position.x + ball_radius >= get_viewport_rect().size.x:
		player_scored.emit()
		ball_position = Vector2(get_viewport_rect().size.x/2, get_viewport_rect().size.y/2)
...

O raio da bola foi utilizado nestas lógicas para apresentar a movimentação mais coerente quando quicar nas paredes ou sair da tela pelo gol direito ou esquerdo.

Por último vamos conectar um sinal pré-existente da Area2D para identificar que a bola e uma das raquetes se encontraram. Este sinal é o sinal on_area_entered, e vamos incluir uma regra para mudar a direção horizontal quando a bola bater na raquete. A conexão do sinal é feita aqui:

Conexão do sinal area_entered ao método _on_area_entered

E a lógica será bem simples, essa aqui:

...
func _on_area_entered(area):
	if area.name == "PlayerPaddle" or area.name == "EnemyPaddle":
		ball_direction.x *= -1
...

Veja que coloquei os nomes PlayerPaddle e EnemyPaddle (que serão os nomes que vou definir na tela principal), mas também poderia definir que a raquete pertence à um grupo específico e validar contra isso. Não há uma solução única para tal funcionalidade, usei a que me pareceu mais simples de explicar. É importante que você estude e veja qual é a melhor regra para a situação que você esteja atuando.

Enfim, agora vamos para a última parte, vamos criar a tela principal do jogo, incluíndo os elementos criados (bola e raquetes), pontuação dos jogadores e mais algumas regras.

Para nossa tela de jogo, vamos usar um nó do tipo Node2D, para isso você pode selecionar o nó 2D Scene, clicar Other Node ou com o atalho de teclado Ctrl+A o nó correspondente:

Criação do nó Node2D como nó principal do jogo

Com tal nó criado, vamos incluir como nós filhos as cenas das raquetes e da bola, arrastando as cenas à partir do FileSystem sob tal nó raiz ou clicando com o botão direito e selecionando Instantiate Child Scene e selecionando as cenas necessárias.

Adicionalmente, precisamos incluir dois nós do tipo Label para a pontuação do jogador e oponente, posicionar tais labels na tela e colocar um valor inicial como 0 (zero). Para os nós das raquetes e bola, o posicionamento dos mesmos será feito via código.

A tela deve ficar mais ou menos desta forma:

Cena principal com as cenas filhas e Labels adicionados

O posicionamento das labels, tamanho dos mesmos, cores e outras configurações vou deixar à vontade do desenvolvedor. Lembrando que vc pode incluir fontes específicas para ficar mais (ou menos) parecido com o Pong original.

O próximo e último passo é configurar regras nesta cena, para:

  • Posicionar as raquetes do jogador e oponente;
  • Controlar a movimentação do jogador com as teclas (cima e baixo);
  • Controlar a movimentação do inimigo com base na posição da bola;
  • Atualizar o score do jogador ou inimigo com base em sinal de gol recebido.

Parece muita coisa, mas vai ser simples. Precisamos de algumas variáveis e referências para elementos do jogo:

...
@onready var PlayerPaddle = $PlayerPaddle
@onready var EnemyPaddle = $EnemyPaddle
@onready var Ball = $Ball
@onready var PlayerScoreLabel = $PlayerScoreLabel
@onready var EnemyScoreLabel = $EnemyScoreLabel

var ball_position = Vector2.ZERO
var paddle_speed = 400
var player_score = 0
var enemy_score = 0
...

Na função ready, vamos inicializar a posição das raquetes e conectar os sinais de score da bola:

...
func _ready():
	PlayerPaddle.position = Vector2(50, get_viewport_rect().size.y/2)
	EnemyPaddle.position = Vector2(get_viewport_rect().size.x - 50, get_viewport_rect().size.y/2)
	Ball.enemy_scored.connect(update_enemy_score)
	Ball.player_scored.connect(update_player_score)
...

Na função process, vamos identificar se os botões foram apertados e mover o jogador, também vamos fazer a movimentação da raquete do inimigo com base na posição da bola:

...
func _process(delta):
	if Input.is_action_pressed("ui_up"):
		PlayerPaddle.position.y -= paddle_speed * delta
	if Input.is_action_pressed("ui_down"):
		PlayerPaddle.position.y += paddle_speed * delta
	ball_position = Ball.position
	EnemyPaddle.position.y = lerp(EnemyPaddle.position.y, ball_position.y, 0.01)
...

E por último as 2 funções para atualizar o score do jogador ou computador. Seria possível otimizar para que esta lógica ficasse em uma função única (se tiver curiosidade, dá uma olhada no post do Jokenpô na parte que fala de passar argumentos em um sinal para ver como poderíamos fazer isso):

...
func update_player_score():
	player_score += 1
	PlayerScoreLabel.text = str(player_score)
		
func update_enemy_score():
	enemy_score += 1
	EnemyScoreLabel.text = str(enemy_score)

E com isso terminamos o nosso mini tutorial... Espero que tenha gostado do que te apresentamos até aqui, mas se ficou com alguma dúvida, se cadastra no site para ter acesso ao vídeo que disponibilizamos para os usuários cadastrados aqui na Game Guild.

O vídeo está aqui em baixo, mas é exclusivo para usuários registrados, ok?