Hora de colocar classes, módulos e refatoração à prova construindo um jogo 2D completo a 60 FPS com Pygame — da janela principal e disparo de projéteis até frotas coordenadas, detecção de colisões, escalonamento dinâmico de velocidade e placar com recorde.
Ouça o episódio complementar direto por aqui antes ou depois das oficinas para conectar o game loop de 60 FPS, a composição de sprites em grupos, o gerenciamento de memória de projéteis e a evolução de dificuldade do jogo.
Fluxo contínuo do game loop: captura de eventos, física, colisões e renderização.
OFICINA 01 · CAPÍTULO 12
1. Loop Pygame, Clock e Classe Settings
Todo jogo em Pygame gira em torno de um laço infinito (while True:) que captura entradas do usuário, atualiza posições e redesenha a tela. Antes de escrever código, planeje os módulos separados (alien_invasion.py, settings.py, ship.py, bullet.py) e instale o Pygame com python -m pip install --user pygame.
Em vez de espalhar números mágicos pelo código, centralize dimensões, cores e velocidades na classe Settings. No arquivo principal, métodos auxiliares prefixados com sublinhado (como _check_events() e _update_screen()) mantêm run_game() legível e fácil de evoluir, enquanto self.clock.tick(60) trava a taxa em 60 quadros por segundo.
settings.py: centraliza parâmetros estáticos da janela e velocidades iniciais para ajuste rápido em um único lugar.
Métodos auxiliares (_metodo): a convenção com underscore inicial indica métodos internos da classe AlienInvasion usados para refatorar o loop.
2. Classe Ship, Movimento Contínuo e Grupo de Bullet
No Pygame, a origem (0, 0) fica no canto superior esquerdo, crescendo para a direita (x) e para baixo (y). O atributo rect permite alinhar objetos de forma declarativa, como self.rect.midbottom = self.screen_rect.midbottom para posicionar a nave na base da tela.
Para obter movimento fluido ao segurar a seta, usamos flags booleanas (moving_right e moving_left) alternadas em _check_keydown_events() e _check_keyup_events(), armazenando a posição decimal em self.x = float(self.rect.x) e limitando às bordas da tela. Já os projéteis herdam de pygame.sprite.Sprite, vivem em um pygame.sprite.Group() e devem ser removidos quando saem do topo (bullet.rect.bottom <= 0) iterando sobre self.bullets.copy().
Por que iterar sobre self.bullets.copy()? Python não permite alterar o tamanho de uma lista ou grupo enquanto o percorre diretamente. Usar .copy() possibilita chamar self.bullets.remove(bullet) com segurança e evita vazamento de memória.
Python · Movimento contínuo e limpeza de projéteis
class Ship:
def update(self):
if self.moving_right and self.rect.right < self.screen_rect.right:
self.x += self.settings.ship_speed
if self.moving_left and self.rect.left > 0:
self.x -= self.settings.ship_speed
self.rect.x = self.x
def _update_bullets(self):
self.bullets.update()
for bullet in self.bullets.copy():
if bullet.rect.bottom <= 0:
self.bullets.remove(bullet)
Salvo localmente.
OFICINA 03 · CAPÍTULO 13
3. Frota Alienígena, Laços Aninhados e Bordas
A classe Alien também herda de Sprite e expõe check_edges(), retornando True quando o alien toca a borda direita (self.rect.right >= screen_rect.right) ou esquerda (self.rect.left <= 0).
Para montar a frota em _create_fleet(), criamos um alien modelo para ler alien.rect.size (alien_width, alien_height) e usamos dois laços while aninhados: o externo avança current_y até screen_height - 3 * alien_height, enquanto o interno preenche current_x até screen_width - 2 * alien_width. A cada frame, _check_fleet_edges() verifica se algum membro encostou na borda; se sim, _change_fleet_direction() desce toda a frota em fleet_drop_speed e inverte fleet_direction *= -1.
Python · _create_fleet() e _change_fleet_direction()
O Pygame resolve colisões entre grupos inteiros com uma única chamada: pygame.sprite.groupcollide(self.bullets, self.aliens, True, True) compara retângulos e remove ambos os sprites atingidos. Quando not self.aliens: se torna verdadeiro, esvaziamos os projéteis restantes, aumentamos a velocidade e geramos uma nova frota.
Já a derrota de uma vida ocorre quando pygame.sprite.spritecollideany(self.ship, self.aliens) retorna um alien ou quando _check_aliens_bottom() detecta alien.rect.bottom >= self.settings.screen_height. Em _ship_hit(), GameStats decrementa ships_left; se ainda houver naves, o jogo limpa a tela, centraliza a nave e pausa sleep(0.5). Ao zerar ships_left, self.game_active = False encerra a partida.
No Capítulo 14, o jogo passa a iniciar inativo (self.game_active = False) aguardando o clique no botão Play. A classe Button em button.py renderiza texto como imagem usando pygame.font.SysFont(None, 48) dentro de _prep_msg(). Ao detectar pygame.MOUSEBUTTONDOWN, verificamos self.play_button.rect.collidepoint(mouse_pos) and not self.game_active para impedir cliques acidentais na área do botão invisível durante o combate — e ocultamos o cursor com pygame.mouse.set_visible(False).
Para criar progressão de dificuldade, dividimos Settings entre configurações estáticas (no __init__) e variáveis que reiniciam a cada partida em initialize_dynamic_settings(). A cada frota destruída, increase_speed() multiplica ship_speed, bullet_speed e alien_speed por speedup_scale = 1.1, além de multiplicar alien_points por score_scale = 1.5.
A classe Scoreboard em scoreboard.py transforma estatísticas numéricas em elementos gráficos: pontuação atual no canto superior direito, recorde (high_score, inicializado em GameStats.__init__ para nunca zerar entre partidas) centralizado no topo, nível logo abaixo do placar e vidas restantes no canto superior esquerdo como um Group() de miniaturas Ship.
Dois detalhes elegantes do Capítulo 14: primeiro, round(self.stats.score, -1) arredonda o placar para o múltiplo de 10 mais próximo e f"{rounded_score:,}" formata milhares. Segundo, ao pontuar colisões, iteramos sobre collisions.values() multiplicando alien_points * len(aliens) para garantir que dois aliens atingidos no mesmo frame contem integralmente.
Python · Pontuação de colisões e Scoreboard.prep_score()
if collisions:
for aliens in collisions.values():
self.stats.score += self.settings.alien_points * len(aliens)
self.sb.prep_score()
self.sb.check_high_score()
def prep_score(self):
rounded_score = round(self.stats.score, -1)
score_str = f"{rounded_score:,}"
self.score_image = self.font.render(
score_str, True, self.text_color, self.settings.bg_color
)
Salvo localmente.
LABORATÓRIO INTERATIVO · CAPÍTULO 14
Simulador de Dificuldade & Pontuação
Ajuste a onda atual, o fator de aceleração (speedup_scale) e o multiplicador de pontos (score_scale) para prever o ritmo real da classe Settings.
Alien Speed1.00 px
Bullet Speed2.50 px
Alien Points50 pts
Nível inicial equilibrado: velocidade base de 1.00 px/frame e 50 pontos por alienígena abatido.
VERIFICAÇÃO DE DOMÍNIO · 4 QUESTÕES
Quiz de Consolidação Lógica
Teste seus conhecimentos sobre Pygame, sprites, colisões e Scoreboard.
1. Por que _update_bullets() percorre self.bullets.copy() ao remover projéteis fora da tela?
2. O que faz pygame.sprite.groupcollide(self.bullets, self.aliens, True, True)?
3. Por que high_score deve ser inicializado em GameStats.__init__() em vez de reset_stats()?
4. Qual é o efeito de round(self.stats.score, -1) na classe Scoreboard?
CONCLUSÃO DA ETAPA 4
Checklist de Prontidão & Exportação
Marque cada marco concluído; seu progresso fica salvo no navegador.
PRÓXIMA PARADA · ETAPA 5 DE 5
Projetos de Aplicação Real — Dados e Web
Transforme dados em gráficos com Matplotlib e Plotly, consuma APIs reais e construa uma aplicação web completa com Django nos Capítulos 15 a 20.