GG Pong por dentro: a CPU que segue a bola e o multiplayer peer-to-peer com PeerJS
Pong é o jogo perfeito para aprender a fazer jogos. As regras cabem numa frase, a tela tem só três objetos, e mesmo assim ele toca em quase tudo que importa: movimento, colisão, pontuação, um adversário controlado pelo computador e, se você quiser ir além, jogo em rede.
O GG Pong é a versão do portal. Este artigo abre o código e explica as decisões por trás de cada parte: como a bola ganha ângulo, por que a CPU erra de propósito e como duas pessoas jogam uma contra a outra pela internet sem que exista um servidor de jogo no meio.
O que tem em uma partida
O menu tem dois caminhos: Single Player (vs CPU) e Multiplayer Lobby.
- Você controla a paleta da esquerda. No teclado, com as setas para cima e para baixo. No celular ou com o mouse, tocando na metade de cima da tela para subir e na metade de baixo para descer.
- A paleta da direita é a CPU ou o outro jogador.
- Quem fizer 10 pontos primeiro vence. Aparece o nome do vencedor e um botão para começar de novo.
- Há botões de pausa e de voltar ao menu na própria tela.
O jogo é feito com Phaser 3, numa tela de 1024 por 768 que se ajusta ao tamanho da janela. O visual é proposital: retângulos brancos, uma linha central cinza e o placar em fonte grande, como no original.
A física da bola
Movimento independente da taxa de quadros
O Phaser chama a função update a cada quadro e informa quanto tempo passou desde o anterior. O jogo converte esse tempo para segundos e multiplica todas as velocidades por ele:
update(time, deltaMs) {
const dt = deltaMs / 1000; // segundos desde o último quadro
ball.x += ball.vx * dt;
ball.y += ball.vy * dt;
}Com isso as velocidades ficam em pixels por segundo, não por quadro. A bola atravessa a tela no mesmo tempo num monitor de 60 Hz e num de 144 Hz. Sem esse cuidado, quem tem uma tela mais rápida jogaria uma partida mais rápida.
O saque
Depois de cada ponto, a bola volta ao centro e sai numa direção sorteada, com velocidade horizontal de 400 px/s e vertical de 200 px/s:
resetBall() {
ball.x = width / 2;
ball.y = height / 2;
ball.vx = (Math.random() > 0.5 ? 1 : -1) * BALL_SPEED_START; // 400
ball.vy = (Math.random() > 0.5 ? 1 : -1) * BALL_SPEED_START * 0.5; // 200
}Sortear o lado evita que quem acabou de perder ponto receba sempre o mesmo saque.
O ângulo da rebatida
Este é o detalhe que transforma Pong em um jogo de habilidade. Se a bola sempre voltasse no mesmo ângulo, não haveria o que fazer além de ficar na frente dela. No GG Pong, o ponto da paleta onde a bola bate decide para onde ela vai:
// Paleta com 100 px de altura: offset vai de -1 (topo) a +1 (base)
const offset = (paddle.y - ball.y) / 50;
ball.vy = -offset * 350;
ball.vx = Math.abs(ball.vx) * 1.05; // e acelera 5%- Bater no centro devolve a bola quase reta.
- Bater perto da ponta de cima manda a bola para cima, com até 350 px/s de velocidade vertical.
- Bater perto da ponta de baixo manda para baixo.
Cada rebatida também aumenta a velocidade horizontal em 5%. É uma regra simples que dá ritmo à partida: as trocas começam calmas e vão ficando tensas até alguém errar. Depois de dez rebatidas seguidas, a bola já está cerca de 60% mais rápida do que no saque.
Colisões
As paletas medem 20 por 100 pixels e a bola é um quadrado de 20 por 20. Como tudo é retângulo, a colisão é um teste de sobreposição entre dois retângulos, que o Phaser oferece pronto (Phaser.Geom.Intersects.RectangleToRectangle). A bola também quica nas bordas de cima e de baixo, invertendo a velocidade vertical.
A CPU: imperfeita de propósito
A forma mais fácil de fazer uma CPU imbatível em Pong é colocar a paleta exatamente na altura da bola em todos os quadros. Ela nunca erra, e o jogo fica chato em segundos. O trabalho de quem faz o adversário é o contrário: fazer a CPU perder de um jeito que pareça justo.
O GG Pong faz isso com três regras:
const diff = ball.y - cpu.y;
const DEAD_ZONE = 35; // px
const CPU_SPEED = PADDLE_SPEED * 0.5; // 300 px/s, metade do jogador
if (Math.abs(diff) > DEAD_ZONE) {
cpu.y += Math.sign(diff) * CPU_SPEED * dt;
}- Ela segue a altura atual da bola, não o lugar onde a bola vai chegar. Quando a bola vem inclinada, a CPU corre atrás dela em vez de esperá-la no ponto certo.
- Ela se move na metade da velocidade do jogador. Você anda a 600 px/s, ela a 300. Uma bola rápida, rebatida perto da ponta, pode ser simplesmente mais rápida que ela.
- Ela tem uma zona morta de 35 pixels. Se a bola está a menos de 35 px do centro da paleta, a CPU não se mexe. Isso evita que a paleta trema a cada quadro e cria uma pequena folga que você pode explorar.
O resultado é um adversário que devolve as bolas fáceis e perde nas difíceis. Para ganhar dele, a estratégia é usar o ângulo: rebater perto da ponta da paleta, mandando a bola para longe de onde a CPU está.
Ideia para uma próxima versão: prever a trajetória
Uma CPU mais forte poderia calcular onde a bola vai cruzar a linha da paleta, incluindo os quiques nas paredes, e ir esperá-la lá. Isso não está no jogo hoje. Seria algo assim:
// Não implementado no GG Pong: exemplo de como seria
function predictY(ball, targetX, height) {
const t = (targetX - ball.x) / ball.vx; // tempo até chegar
let y = ball.y + ball.vy * t;
const span = height * 2;
y = ((y % span) + span) % span; // "desdobra" os quiques
return y > height ? span - y : y;
}Com essa previsão, a dificuldade passaria a ser controlada somando um erro sorteado ao ponto previsto: quanto maior o erro, mais fácil a CPU. É um caminho natural para ter níveis de dificuldade, que hoje o jogo não tem.
Multiplayer sem servidor de jogo
Jogar Pong pela internet tem um problema clássico: os dois jogadores precisam ver a mesma bola no mesmo lugar. A solução comum é ter um servidor que calcula a partida e manda o resultado para os dois. O GG Pong evita esse servidor com uma conexão peer-to-peer: os dois navegadores conversam diretamente.
As três peças
- WebRTC, a tecnologia dos navegadores que permite a conexão direta entre dois computadores. O jogo usa a biblioteca PeerJS, que simplifica o WebRTC.
- Um servidor de sinalização, que só apresenta um navegador ao outro. Depois que a conexão direta está estabelecida, ele sai do caminho. O GG Pong usa o servidor público do PeerJS e um servidor STUN do Google, que ajuda cada navegador a descobrir seu endereço na internet.
- Uma lista de salas no Firebase Firestore, para que um jogador encontre o outro.
Criar e entrar numa sala
Quem cria a sala escolhe um nome (até 15 caracteres, o jogo sugere algo como "Strawberry"). O jogo grava no Firestore o nome da sala e o identificador PeerJS de quem a criou, com o status "esperando".
Quem quer entrar vê a lista de salas disponíveis e clica em uma. O jogo lê o identificador gravado na sala e conecta direto no navegador de quem a criou. Ninguém precisa copiar ou digitar código nenhum.
Para a lista não ficar cheia de salas abandonadas, quem criou a sala manda um sinal de vida a cada 10 segundos. Salas sem sinal há mais de 30 segundos somem da lista.
Quem manda na bola
Com dois navegadores calculando a física ao mesmo tempo, os resultados acabariam diferentes. Por isso só um deles calcula:
- Quem criou a sala (host) fica com a paleta da esquerda, calcula toda a física e, a cada quadro, envia o estado completo da partida: bola, paletas e placar.
- Quem entrou (cliente) fica com a paleta da direita e só envia a altura da própria paleta, e só quando ela se move.
// No host, a cada quadro
peer.send({ type: 'update', state: gameState });
// No cliente, quando a paleta se move
peer.send({ type: 'input', y: myPaddle.y });Esse modelo é simples e evita trapaça e divergência: só existe uma bola de verdade, a do host. O custo é que o cliente vê a partida com o atraso da conexão. Entre dois jogadores na mesma cidade isso é quase imperceptível. Em conexões lentas, a bola do cliente pode dar pequenos saltos, porque ele mostra a última posição recebida, sem suavizar entre uma atualização e outra.
Resumo
- Phaser 3, tela de 1024 por 768, velocidades em pixels por segundo
- Partida até 10 pontos; a bola acelera 5% a cada rebatida
- O ângulo de saída depende de onde a bola bate na paleta
- CPU que segue a bola na metade da sua velocidade, com zona morta de 35 px
- Multiplayer P2P com PeerJS, salas listadas no Firestore e física calculada só no host
