Criando um Pong

Criando a estrutura de arquivos do jogo

O primeiro passo no desenvolvimento do seu jogo é a criação do arquivo-fonte. Após escolher a pasta onde irá armazenar os arquivos do jogo, crie um arquivo chamado pong.py. Você pode criar esse arquivo utilizando um editor de texto ou IDE de sua preferência. Em seguida, crie uma pasta chamada assets dentro da mesma pasta do projeto, e coloque nela as duas imagens que usaremos no jogo: bola.png (a bolinha) e pad.png (a raquete, usada tanto pelo jogador quanto pela IA).

Importações

Aqui começaremos de fato a escrever o código. Primeiramente importaremos tudo que será necessário para construir nosso jogo.

from PPlay.sprite import Sprite
from PPlay.window import Window
from PPlay.collision import Collision

A classe Sprite será usada para criar a bola e os pads. A classe Window será responsável por criar e controlar a janela onde o jogo é exibido. Já a classe Colision será usada para detectar a colisão entre a bola e os pads.

Além das classes do PPlay, também vamos precisar da biblioteca random do Python, que usaremos para sortear a direção da bola a cada início de jogada:

import random

Criando a janela

Aqui criamos a janela, já passando altura e largura como parâmetros para o construtor: 800 pixels de largura por 600 de altura.

window = Window(800, 600)

Criando os Sprites e posicionando os pads

Com a janela criada, é hora de criar os três elementos visuais do jogo: a bola e os dois pads. Repare que os dois pads compartilham a mesma imagem (pad.png) — o que muda entre eles é apenas a posição na tela e quem controla cada um:

bola = Sprite('assets/bola.png')
pad_e = Sprite('assets/pad.png')
pad_d = Sprite('assets/pad.png')

Agora vamos posicionar os pads. O pad esquerdo (pad_e), controlado pelo jogador, fica a 10 pixels da borda esquerda da janela. O pad direito (pad_d), controlado pela IA, fica a 10 pixels da borda direita, para isso, subtraímos a própria largura do pad da largura da janela, evitando que ele fique parcialmente fora da tela. Os dois pads começam na metade da altura da janela:

pad_e.set_position(10, window.altura / 2)
pad_d.set_position(window.largura - pad_d.width - 10, window.altura / 2)

Pontuação

Agora criamos as variáveis para que seja possível armazenar a pontuação de cada lado: pts_player, para o jogador, e pts_ia, para a IA:

pts_player = 0
pts_ia = 0

Constantes de velocidade

Nesse passo, criamos as constantes que controlam a velocidade e o comportamento do jogo. Concentrar esses valores em constantes, em vez de espalhá-los pelo código, facilita bastante o alterá-las mais tarde:

VEL_JOGADOR = 400   # pixels por segundo
VEL_IA = 300        # um pouco mais lenta que o jogador, para a IA ser vencível
ZONA_MORTA = 10     # evita a IA tremer quando já está alinhada com a bola

VEL_BOLA_INICIAL = 300
VEL_BOLA_MAX = 1000
GANHO_REBATIDA = 25  # quanto a bola acelera a cada rebatida no pad

VEL_JOGADOR e VEL_IA controlam a velocidade de movimento vertical de cada pad. ZONA_MORTA é a margem de tolerância que evita que a IA fique tremendo para cima e para baixo quando já está praticamente alinhada com a bola. Já VEL_BOLA_INICIAL, VEL_BOLA_MAX e GANHO_REBATIDA controlam a bola: a velocidade em que ela começa cada jogada, o limite máximo de velocidade que ela pode atingir, e o quanto ela acelera a cada vez que é rebatida por um pad.

resetar_bola()

Essa função reposiciona a bola no centro da tela e sorteia uma nova direção para ela. É chamada tanto no início do jogo quanto toda vez que um dos lados marca um ponto:

def resetar_bola():
    bola.set_position(
        window.largura / 2 - bola.width / 2, #posiciona o centro da
        window.altura / 2 - bola.height / 2  #bola no centro da tela
    )

    direcao_x = random.choice([-1, 1])   #escolhe uma direção para a bola (esquerda/direita)
    direcao_y = random.uniform(-1, 1)    #escolhe uma direção para a bola(quão para cima ou baixo ela desloca-se)

    # Normaliza o vetor de direção
    comprimento = (direcao_x ** 2 + direcao_y ** 2) ** 0.5

    direcao_x /= comprimento
    direcao_y /= comprimento

    # Aplica a velocidade normalizada
    bola.vx = direcao_x * VEL_BOLA_INICIAL
    bola.vy = direcao_y * VEL_BOLA_INICIAL

Primeiro, posicionamos a bola exatamente no centro da janela, compensando metade da largura e da altura da própria bola. Em seguida, sorteamos uma direção horizontal (esquerda ou direita, com igual probabilidade) e uma direção vertical qualquer entre -1 e 1.

direcao_x e direcao_y formam um vetor, e esse vetor precisa ser normalizado, ou seja, ter comprimento igual a 1, antes de virar velocidade de fato. Fazemos isso dividindo cada componente pelo comprimento do vetor, calculado com o teorema de Pitágoras (comprimento). Sem essa normalização, uma bola lançada na diagonal se moveria mais rápido do que uma lançada na horizontal, deixando o jogo inconsistente. Por fim, multiplicamos o vetor já normalizado por VEL_BOLA_INICIAL, obtendo vx e vy, dois atributos novos que estamos criando agora no objeto bola, para guardar sua velocidade em cada eixo.

rebater_no_pad(pad)

Essa função calcula o que acontece quando a bola bate em um dos pads: o novo ângulo (baseado em qual parte do pad foi atingida) e o aumento de velocidade da bolinha.

def rebater_no_pad(pad):
    # Calcula o ponto de impacto da bola em relação ao pad
    centro_pad = pad.y + pad.height / 2
    centro_bola = bola.y + bola.height / 2

    # -1 = topo do pad bola vai para cima
    #  0 = centro bola vai reta
    #  1 = base do pad bola vai para baixo

    direcao_y = (centro_bola - centro_pad) / (pad.height / 2)

    # Calcula a nova velocidade
    velocidade_atual = (bola.vx ** 2 + bola.vy ** 2) ** 0.5
    nova_velocidade = min(velocidade_atual + GANHO_REBATIDA,VEL_BOLA_MAX) #limitamos a nova velocidade pela
                                                                          #velocidade máxima

    # Define a nova direção x
    direcao_x = 1 if bola.vx < 0 else -1 # se a bola vai para esquerda passa a ir para direita e vice-versa

    # Normaliza o vetor de direção
    comprimento = (direcao_x ** 2 + direcao_y ** 2) ** 0.5

    direcao_x /= comprimento
    direcao_y /= comprimento

    # Aplica a velocidade normalizada
    bola.vx = direcao_x * nova_velocidade
    bola.vy = direcao_y * nova_velocidade

resetar_bola()

A parte mais interessante dessa função é o cálculo de direcao_y. Comparamos o centro da bola com o centro do pad no momento da colisão. Se a bola acertou bem no meio do pad, a diferença é 0 e ela volta reta; se acertou perto do topo, a diferença é negativa e a bola sobe; se acertou perto da base, a diferença é positiva e a bola desce. Dividir pela metade da altura do pad normaliza esse valor para o intervalo aproximado de -1 a 1.

Em seguida, calculamos a velocidade atual da bola (de novo com o teorema de Pitágoras, agora sobre vx e vy) e somamos GANHO_REBATIDA, sem deixar passar de VEL_BOLA_MAX, isso faz o jogo ficar mais rápido a cada rebatida. A direção horizontal simplesmente se inverte: se a bola vinha da direita, agora vai para a esquerda, e vice-versa. Por fim, assim como em resetar_bola(), normalizamos o vetor (direcao_x, direcao_y) antes de multiplicá-lo pela nova velocidade, garantindo que a bola sempre se mova na velocidade correta, não importa o ângulo de saída.

Pré Game Loop

Com as duas funções auxiliares prontas, chamamos resetar_bola() uma única vez, fora de qualquer loop, para posicionar e lançar a bola antes da primeira jogada:

resetar_bola()

Game Loop

Esse é o coração do nosso jogo: um while True onde toda a lógica do jogo está contida.

Movimento do Pad do Jogador

O pad esquerdo é controlado pelo jogador através das setas Cima e Baixo do teclado, usando o método move_key_y() do Sprite. Em seguida, limitamos a posição do pad para que ele não ultrapasse o topo ou a base da janela:

pad_e.move_key_y(VEL_JOGADOR)
pad_e.y = max(0, min(pad_e.y, window.altura - pad_e.height))#Limita o quanto o pad pode se mover para cima ou baixo

Movimento do Pad da IA

O pad direito é controlado por uma IA simples, que persegue verticalmente o centro da bola. Comparamos o centro do pad com o centro da bola e movemos o pad na direção correspondente, respeitando a ZONA_MORTA para evitar tremulação quando os dois já estão praticamente alinhados:

centro_pad_d = pad_d.y + pad_d.height / 2
centro_bola = bola.y + bola.height / 2

if centro_bola < centro_pad_d - ZONA_MORTA:
    pad_d.move_y(VEL_BOLA_INICIAL * -1)
elif centro_bola > centro_pad_d + ZONA_MORTA:
    pad_d.move_y(VEL_BOLA_INICIAL)

pad_d.y = max(0, min(pad_d.y, window.altura - pad_d.height))

Assim como no pad do jogador, a posição do pad da IA também é limitada com o mesmo padrão max/min, para que ela nunca saia da área jogável.

Movimento da bola

Movemos a bola de acordo com sua velocidade atual, os valores vy e vx:

bola.move_x(bola.vx)
bola.move_y(bola.vy)

Colisão com o Topo e a Base da Tela

A bola precisa quicar ao tocar a borda superior ou inferior da janela. Verificamos a posição da bola e, ao detectar o contato, invertemos o sinal de vy.

if bola.y <= 0:
   bola.y = 0
   bola.vy *= -1
elif bola.y + bola.height >= window.altura:
   bola.y = window.altura - bola.height
   bola.vy *= -1

Além de inverter vy, também reposicionamos a bola exatamente na borda (bola.y = 0 ou bola.y = window.alturabola.height). Isso evita que a bola fique presa fora da área visível caso ela tenha avançado um pouco além do limite entre um quadro e o outro.

Colisão com os Pads

Aqui usamos a classe Colision, importada lá no início do arquivo, para verificar se a bola colidiu com algum dos pads. Collision.collided() é um método estático, então chamamos passando os dois sprites que queremos comparar. Ao detectar colisão, reposicionamos a bola encostada no pad (para não atravessá-lo) e chamamos rebater_no_pad() para calcular o novo ângulo e a nova velocidade:

if Collision.collided(bola, pad_e):
    bola.x = pad_e.x + pad_e.width  # evita grudar/atravessar o pad
    rebater_no_pad(pad_e)
elif Collision.collided(bola, pad_d):
    bola.x = pad_d.x - bola.width
    rebater_no_pad(pad_d)

Pontuação e Reset da Bola

Por fim, verificamos se a bola saiu completamente da tela pela esquerda ou pela direita. Se saiu pela esquerda, a IA marca ponto; se saiu pela direita, o jogador marca ponto. Nos dois casos, chamamos resetar_bola() para recomeçar a jogada:

if bola.x + bola.width < 0:
    pts_ia += 1
    resetar_bola()

if bola.x > window.largura:
    pts_player += 1
    resetar_bola()

Desenhando os Elementos na Tela

Agora que toda a lógica do quadro já foi calculada, chamamos as funções de draw dos Sprites

bola.draw()
pad_e.draw()
pad_d.draw()

HUD: FPS e Placar

Usando o draw_text(), desenhamos na tela contador de FPS e Placar. Vale ressaltar que o draw_text() é uma função relativamente cara e é bom evitar chamá-la mais que o necessário.

window.draw_text(f"FPS: {int(window.get_fps())}", 10, 10, cor="green")
window.draw_text(f"Player: {pts_player}", 10, 30, cor="green")
window.draw_text(f"IA: {pts_ia}", window.largura - 30, 30, cor="green")

Atualizando a Janela

Para fechar o loop, chamamos window.update(), que redesenha a tela e processa os eventos do jogo a cada quadro:

window.update()

Pronto! Seu Pong está completo.

Se você chegou até esta parte do tutorial e o seu jogo funcionou corretamente, parabéns! Você acabou de finalizar o seu primeiro Pong!

O que fazer agora?

Agora que seu jogo está pronto, está na hora de melhorá-lo! Faça uma lista com os melhoramentos que você deseja fazer em seu jogo e mãos à obra. Caso você não tenha muitas ideias, eis algumas sugestões de adições que você pode fazer ao seu Pong:

  • adicionar uma tela de menu inicial para o jogo;
  • adicionar efeitos sonoros (rebatida na raquete, ponto marcado, quique na parede etc);
  • criar uma condição de vitória, com uma tela de fim de jogo quando um dos lados atingir uma certa pontuação;
  • transformar o jogo em modo 2 jogadores, com o pad direito controlado por outro conjunto de teclas (por exemplo, W e S);
  • criar níveis de dificuldade para a IA, ajustando sua velocidade de reação e o tamanho da zona morta;

As possibilidades são infinitas! Utiliza a sua criatividade e teste os seus limites melhorando esse jogo. Agora só depende de você!


Créditos do Tutorial: Miguel Faria
Tutorial baseado no tutorial do Space Invaders do PPlay original.