Nesse tutorial iremos construir o jogo PyLadies Invaders, onde nossa PyLady luta heroicamente contra os invasores alienígenas!
Vamos utilizar a biblioteca Pygame, e exercitar alguns conceitos de Programação Orientada à Objetos (POO).
Pygame é uma biblioteca popular para desenvolvimento de jogos em Python. Ela fornece funcionalidades para criar janelas, desenhar gráficos, tocar sons e gerenciar eventos de entrada do usuário.
Para começar, você precisa ter o Python instalado em seu computador. Você pode baixar a versão mais recente do Python em python.org.
Depois de instalar o Python, você pode verificar se ele está instalado corretamente executando o seguinte comando no terminal:
python --versionSe você estiver usando o Windows, pode ser necessário usar python3 em vez de python.
Após isso, crie um diretório para o seu projeto e navegue até ele no terminal. Por exemplo:
mkdir pyladies-invaders
cd pyladies-invadersPara criar projetos em Python, é muito importante ter um ambiente virtual configurado. Assim, bibliotecas utilizadas em um projeto não irão interferir em projetos futuros.
Você pode criar um ambiente virtual com o seguinte comando:
python -m venv venvPara ativar o ambiente virtual, use:
- No Windows:
venv\Scripts\activate- No macOS/Linux:
source venv/bin/activateDepois de ativar o ambiente virtual, você pode instalar o Pygame usando o pip:
pip install pygameCrie um arquivo chamado main.py no diretório do projeto. Esse será o ponto de entrada do nosso jogo.
Para executar o código, você pode usar o seguinte comando no terminal:
python main.pySe você estiver usando o Windows, pode ser necessário usar python3 em vez de python.
Caso precise parar o código em execução, você pode usar Ctrl + C no terminal.
Iremos começar criando o jogo de uma forma simples, para termos uma introdução ao funcionamento do Pygame e como ele lida com eventos e gráficos.
Vamos criar uma janela de jogo simples para começar. A seguir, você verá como configurar o Pygame e criar uma janela onde o jogo será exibido.
Iremos exibí-la por 5 segundos antes de fechá-la. Para isso, vamos utilizar o método pygame.time.delay().
import pygame
# Inicializando o Pygame
pygame.init()
# Definindo as dimensões da tela
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
# Criando a janela do jogo
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
# Definindo o título da janela
pygame.display.set_caption("PyLadies Invaders")
# Definindo a cor de fundo
BACKGROUND_COLOR = (0, 0, 0) # Preto
# Preenchendo a tela com a cor de fundo
screen.fill(BACKGROUND_COLOR)
# Atualizando a tela
pygame.display.flip()
# Aguardando 5 segundos
pygame.time.delay(5000)
# Fechando o Pygame
pygame.quit()Para manter a janela aberta e responder a eventos, precisamos de um loop principal do jogo. Esse loop irá verificar eventos, atualizar o estado do jogo e desenhar os elementos na tela.
No Pygame temos diversos eventos que podemos capturar, como o fechamento da janela e teclas pressionadas. Vamos criar um loop que irá manter a janela aberta e responder ao evento de fechar a janela.
Para isso, remova as últimas duas linhas do código anterior (pygame.time.delay(5000) e pygame.quit()) e adicione o seguinte código:
# Loop principal do jogo
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = FalsePara tornar o jogo interativo, precisamos ler eventos do teclado. Isso nos permitirá mover a PyLady e disparar projéteis.
No Pygame, podemos capturar quando uma tecla é pressionada ou liberada. Vamos adicionar um evento para verificar se a tecla "espaço" foi pressionada e trocar a cor de fundo da tela quando isso acontecer.
Adicione o seguinte código ao seu script, dentro do loop do jogo:
# Verificando eventos do teclado
pressed_keys = pygame.key.get_pressed()
# Verificando se a tecla "espaço" foi pressionada
if pressed_keys[pygame.K_SPACE]:
# Trocar a cor de fundo
BACKGROUND_COLOR = (255, 255, 255) # Branco
# Preenchendo a tela com a cor de fundo
screen.fill(BACKGROUND_COLOR)
# Atualizando a tela
pygame.display.flip()Agora que temos uma janela de jogo e eventos do teclado, vamos adicionar a PyLady. Iremos chamá-la de pylady.
Para desenhar a PyLady, precisamos de uma imagem que a represente. Crie uma pasta img dentro do seu projeto. Você pode baixar a imagem da PyLady aqui e salvá-la na pasta img com o nome pylady.png.
O tamanho da imagem é 50x50 pixels, então vamos posicioná-la na parte inferior da tela, centralizada horizontalmente.
Para isso, iremos utilizar o tamanho da tela e a largura da imagem para calcular a posição inicial da PyLady.
No Pygame, o sistema de coordenadas começa no canto superior esquerdo da tela, onde o eixo x aumenta para a direita e o eixo y aumenta para baixo.
Portanto, para centralizar a PyLady horizontalmente, precisamos dividir a largura total da tela por 2. Para posicioná-la na parte inferior, vamos subtrair a altura da imagem da altura total da tela.
Adicione as inicializações da PyLady ao seu script, na parte onde você já configurou o Pygame e a janela do jogo:
PYLADY_WIDTH = 50
PYLADY_HEIGHT = 50
PYLADY_IMG = "img/pylady.png"
# Carregando a imagem da PyLady
pylady_image = pygame.image.load(PYLADY_IMG)
# Definindo a posição inicial da PyLady
pylady_x = (SCREEN_WIDTH - PYLADY_WIDTH) // 2 # Centraliza horizontalmente
pylady_y = SCREEN_HEIGHT - PYLADY_HEIGHT # Posiciona na parte inferiorApós definir a PyLady, vamos desenhá-la na tela. Para isso, utilizaremos o método blit do Pygame, que desenha uma superfície (neste caso, a imagem da PyLady) em outra superfície (a tela do jogo).
Adicione o seguinte código dentro do loop principal do jogo, logo após a leitura dos eventos e antes de atualizar a tela:
# Desenhando a PyLady na tela
screen.blit(pylady_image, (pylady_x, pylady_y))Para tornar o jogo interativo, precisamos permitir que o jogador mova a PyLady para a esquerda e para a direita usando as teclas de seta.
Para isso, vamos adicionar eventos de teclado que irão atualizar a posição da PyLady com base nas teclas pressionadas.
Vamos definir uma velocidade para a movimentação da PyLady. Adicione a seguinte linha logo após a definição da PyLady:
PYLADY_SPEED = 5 # Velocidade de movimentação da PyLadyVamos adicionar o seguinte código dentro do loop principal do jogo, entre a leitura dos eventos e o desenho da PyLady, para permitir a movimentação da PyLady:
# Movendo a PyLady para a esquerda
if pressed_keys[pygame.K_LEFT]:
pylady_x -= PYLADY_SPEED # Move 5 pixels para a esquerda
# Movendo a PyLady para a direita
if pressed_keys[pygame.K_RIGHT]:
pylady_x += PYLADY_SPEED # Move 5 pixels para a direitaPara corrigir a PyLady aparecendo diversas vezes conforme vamos movimentando, precisamos adicionar uma linha para limpar a tela antes de desenhar a PyLady novamente. Isso pode ser feito logo antes de desenhar a PyLady, preenchendo a tela com a cor de fundo:
# Preenchendo a tela com a cor de fundo
screen.fill(BACKGROUND_COLOR)Para adicionar a mecânica de tiro, vamos permitir que o jogador dispare projéteis pressionando a tecla "espaço". Vamos desenhar um projétil que se move para cima a partir da posição da PyLady.
Iremos definir a velocidade do projétil e sua imagem. Você pode baixar a imagem
aqui e salvá-la na pasta img com o nome projectile_pylady.png.
Vamos adicionar o seguinte código para definir a imagem do projétil e sua posição inicial, logo após a definição da PyLady:
PROJECTILE_WIDTH = 3
PROJECTILE_HEIGHT = 5
PROJECTILE_PYLADY_IMG = "img/projectile_pylady.png"
PROJECTILE_PYLADY_SPEED = 10
# Carregando a imagem do projétil
projectile_pylady_image = pygame.image.load(PROJECTILE_PYLADY_IMG)
# Definindo a posição inicial do projétil
projectile_x = pylady_x + (PYLADY_WIDTH // 2) # Posiciona o projétil na mesma posição horizontal da PyLady
projectile_y = SCREEN_HEIGHT - PYLADY_HEIGHT - PROJECTILE_HEIGHT # Posiciona o projétil logo acima da PyLadyPara que o projétil seja exibido na tela e se mova corretamente, precisamos:
-
Detectar quando a tecla "espaço" for pressionada para disparar.
-
Atualizar a posição do projétil a cada quadro.
-
Gerenciar o estado do projétil (se está ativo ou pronto para ser disparado).
-
- Detectando o disparo do projétil Para detectar quando a tecla "espaço" é pressionada, vamos adicionar uma variável que controla se o projétil está ativo ou não. Quando a tecla for pressionada, o projétil será disparado e começará a se mover para cima.
# Variável para controlar se o projétil está ativo
projectile_active = FalseAgora, vamos modificar o código que lê os eventos do teclado para verificar se a tecla "espaço" foi pressionada e ativar o projétil:
# Verificando se a tecla "espaço" foi pressionada
if pressed_keys[pygame.K_SPACE] and not projectile_active:
projectile_active = True # Ativa o projétil
projectile_x = pylady_x + (PYLADY_WIDTH // 2) # Centraliza o projétil na PyLady
projectile_y = pylady_y - PROJECTILE_HEIGHT # Posiciona o projétil logo acima da PyLadyNo entanto, diferente da PyLady, o projétil deve se mover para cima continuamente.
-
- Atualizando a posição do projétil Os elementos do jogo, incluindo o projétil, precisam ser atualizados a cada quadro. Para isso, o Pygame possui o mecanismo de clock, onde é passado uma taxa de atualização de quadros (FPS) para controlar a velocidade do jogo.
Defina o FPS no começo do seu script:
# Definindo a taxa de quadros (FPS)
FPS = 60Vamos adicionar o seguinte código logo após a inicialização do Pygame, para definir o clock do jogo:
# Definindo o clock do jogo
clock = pygame.time.Clock()Dentro do loop principal do jogo, vamos atualizar a taxa de quadros e controlar a velocidade do projétil.
# Atualizando o clock do jogo
clock.tick(FPS) # Controla a taxa de quadros do jogoPodemos então atualizar a posição do projétil a cada quadro. Para isso, vamos utilizar a velocidade definida para o projétil e verificar se ele está ativo. Se estiver ativo, vamos mover sua posição vertical para cima.
Adicione o seguinte código logo após a verificação de eventos do teclado e antes de desenhar a PyLady:
# Atualizando a posição do projétil
if projectile_active:
projectile_y -= PROJECTILE_PYLADY_SPEED # Move o projétil para cima
# Verifica se o projétil saiu da tela
if projectile_y < 0:
projectile_active = False # Desativa o projétil quando sair da telaDesse modo, o projétil se moverá para cima quando disparado e será desativado quando sair da tela.
-
- Desenhando o projétil na tela Adicione o seguinte código logo após desenhar a PyLady:
# Desenhando o projétil na tela
if projectile_active:
screen.blit(projectile_pylady_image, (projectile_x, projectile_y))Quando você pressionar a tecla "espaço", o projétil será disparado e se moverá para cima. Ao sair da tela, ele será reposicionado na posição inicial, pronto para ser disparado novamente.
O código completo até agora deve se parecer com o arquivo parte_1.py:
Até agora, o nosso código está espalhado e repetitivo. Para melhorar a organização, podemos encapsular a lógica de cada elemento do jogo em funções. Isso nos permitirá visualizar melhor o fluxo do jogo e facilitar a manutenção.
Precisamos de funções para cada uma das responsabilidades do jogo, como desenhar a PyLady, mover a PyLady, desenhar o projétil, mover o projétil e lidar com os eventos do teclado.
Veremos abaixo como essa abordagem se torna complexa rapidamente, e como a Programação Orientada a Objetos (POO) pode nos ajudar a organizar melhor o código.
Para a PyLady, precisamos de funções para desenhar a PyLady na tela e para mover a PyLady de acordo com os eventos do teclado.
Podemos criar uma função draw_pylady() que será responsável por desenhar a PyLady na tela. Essa função receberá a tela e a posição da PyLady como parâmetros.
def draw_pylady(screen, pylady_x, pylady_y):
screen.blit(pylady_image, (pylady_x, pylady_y))Vamos criar uma função move_pylady() que será responsável por atualizar a posição da PyLady para o valor recebido como parâmetro.
Será necessário receber a posição atual e a posição alvo, fazer a verificação para não permitir que a PyLady saia da tela e retornar a nova posição.
def move_pylady(pylady_x, x):
if x < 0:
pylady_x = 0 # Não permite que a PyLady saia da tela pela esquerda
elif x >= SCREEN_WIDTH - PYLADY_WIDTH:
pylady_x = SCREEN_WIDTH - PYLADY_WIDTH # Não permite que a PyLady saia da tela pela direita
else:
pylady_x = x # Atualiza a posição da PyLady
return pylady_xPara o projétil, precisamos de funções para desenhar o projétil na tela e para mover o projétil de acordo com sua velocidade.
Vamos criar uma função draw_projectile() que será responsável por desenhar o projétil na tela. Essa função receberá a tela, a posição do projétil e um booleano indicando se o projétil está ativo.
def draw_projectile(screen, projectile_x, projectile_y, projectile_active):
if projectile_active:
screen.blit(projectile_image, (projectile_x, projectile_y))Vamos criar uma função move_projectile() que será responsável por atualizar a posição do projétil de acordo com a velocidade do projétil. Se o projétil estiver ativo, ele será movido para cima e, caso saia da tela, será desativado.
Essa função receberá a posição do projétil e um booleano indicando se o projétil está ativo, e retornará a nova posição do projétil e o estado do projétil (ativo ou não).
def move_projectile(projectile_y, projectile_active):
if projectile_active:
projectile_y -= PROJECTILE_PYLADY_SPEED # Move o projétil para cima
if projectile_y < 0:
projectile_active = False # Desativa o projétil quando sair da tela
return projectile_y, projectile_activePara organizar o código do jogo, vamos criar funções que serão responsáveis por lidar com os eventos do teclado e desenhar os elementos do jogo na tela.
Vamos criar uma função handle_events() que será responsável por ler os eventos do teclado e atualizar a posição da PyLady e o estado do projétil.
Essa função precisará receber a posição atual da PyLady, a posição atual e o estado do projétil e retornar a nova posição da PyLady, a nova posição e o estado atualizado do projétil.
def handle_events(pylady_x, pylady_y, projectile_x, projectile_y, projectile_active):
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()
# Verificando eventos do teclado
pressed_keys = pygame.key.get_pressed()
# Movendo a PyLady
if pressed_keys[pygame.K_LEFT]:
move_pylady(pylady_x, pylady_x - PYLADY_SPEED)
if pressed_keys[pygame.K_RIGHT]:
move_pylady(pylady_x, pylady_x + PYLADY_SPEED)
# Criando o projétil
if pressed_keys[pygame.K_SPACE] and not projectile_active:
projectile_active = True
projectile_x = pylady_x + (PYLADY_WIDTH // 2)
projectile_y = pylady_y - PROJECTILE_HEIGHT
return pylady_x, pylady_y, projectile_x, projectile_y, projectile_activeVamos criar uma função draw_elements() que será responsável por desenhar todos os elementos do jogo na tela.
Essa função irá chamar as funções draw_pylady() e draw_projectile() para desenhar a PyLady e o projétil, respectivamente, portanto ela receberá a tela, a posição da PyLady e a posição e estado do projétil como parâmetros.
def draw_elements(screen, pylady_x, pylady_y, projectile_x, projectile_y, projectile_active):
screen.fill(BACKGROUND_COLOR) # Preenche a tela com a cor de fundo
draw_pylady(screen, pylady_x, pylady_y) # Desenha a PyLady
draw_projectile(screen, projectile_x, projectile_y, projectile_active) # Desenha o projétil
pygame.display.flip() # Atualiza a telaVamos criar uma função run() que conterá o loop principal do jogo. Essa função irá gerenciar eventos, atualizar o estado do jogo e desenhar os elementos na tela.
Iremos mover as variáveis de inicialização do jogo (clock e screen) e o estado do jogo (posição da PyLady, posição do projétil e estado do projétil) para dentro da função run(), para que elas sejam gerenciadas dentro do loop principal.
def run():
# Definindo o clock do jogo
clock = pygame.time.Clock()
# Criando a janela do jogo
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
# Definindo a posição inicial da PyLady
pylady_x = (SCREEN_WIDTH - PYLADY_WIDTH) // 2 # Centraliza horizontalmente
pylady_y = SCREEN_HEIGHT - PYLADY_HEIGHT # Posiciona na parte inferior
# Definindo a posição inicial do projétil
projectile_x = pylady_x + (PYLADY_WIDTH // 2) # Posiciona o projétil na mesma posição horizontal da PyLady
projectile_y = pylady_y - PROJECTILE_HEIGHT # Posiciona o projétil logo acima da PyLady
# Variável para controlar se o projétil está ativo
projectile_active = False
while True:
while True:
pylady_x, pylady_y, projectile_x, projectile_y, projectile_active = handle_events(
pylady_x,
pylady_y,
projectile_x,
projectile_y,
projectile_active
) # Lê os eventos do teclado
projectile_y, projectile_active = move_projectile(projectile_y, projectile_active) # Atualiza a posição do projétil, caso ele esteja ativo
draw_elements(screen, pylady_x, pylady_y, projectile_x, projectile_y, projectile_active) # Desenha os elementos do jogo
clock.tick(FPS) # Controla a taxa de quadros do jogoPor fim, vamos chamar a função run() no final do nosso script para iniciar o jogo:
if __name__ == "__main__":
run() # Inicia o jogo
pygame.quit() # Encerra o Pygame ao sair do jogoO código completo até agora deve se parecer com o arquivo parte_2.py:
Como podemos ver, a abordagem de funções nos permite organizar melhor o código, mas ainda assim ele pode se tornar complexo e difícil de manter à medida que o jogo cresce.
Cada objeto do jogo (como a PyLady e o projétil) tem seus próprios atributos e comportamentos, e encapsular essa lógica em funções pode levar a um código confuso e difícil de entender, dado que as funções podem ter muitos parâmetros e a lógica pode se espalhar por várias partes do código.
Considerando que o jogo pode ter muitos elementos, como inimigos, efeitos sonoros, vidas e outros, a abordagem de funções pode se tornar difícil de gerenciar e escalar.
Por isso, a Programação Orientada a Objetos (POO) é uma abordagem mais adequada para organizar o código de jogos, pois permite encapsular a lógica de cada elemento em classes, tornando o código mais modular e fácil de entender.
A Programação Orientada a Objetos (POO) é um paradigma de programação que organiza o código em objetos, que são instâncias de classes. Cada objeto pode ter seus próprios atributos (dados) e métodos (funções).
No Pygame, podemos utilizar a POO para organizar melhor o código do jogo, encapsulando a lógica de cada elemento em classes. Isso nos permitirá criar instâncias desses elementos e gerenciar seu estado de forma mais eficiente.
Criaremos classes para a PyLady e o projétil. Cada classe terá seus próprios atributos e métodos para gerenciar o estado e o comportamento dos elementos do jogo.
Vamos criar uma classe PyLady que representará a PyLady. Essa classe terá atributos para a posição, imagem e velocidade da PyLady, além de métodos para desenhar e mover a PyLady.
class PyLady:
def __init__(self, width=PYLADY_WIDTH, height=PYLADY_HEIGHT, img=PYLADY_IMG):
self.x = (SCREEN_WIDTH - PYLADY_WIDTH) // 2 # Posição horizontal da PyLady
self.y = SCREEN_HEIGHT - PYLADY_HEIGHT # Posição vertical da PyLady
self.width = width # Largura da PyLady
self.height = height # Altura da PyLady
self.image = pygame.image.load(img) # Carrega a imagem da PyLady
def move(self, x):
if x < 0:
self.x = 0 # Não permite que a PyLady saia da tela pela esquerda
elif x >= SCREEN_WIDTH - self.width:
self.x = SCREEN_WIDTH - self.width # Não permite que a PyLady saia da tela pela direita
else:
self.x = x # Atualiza a posição da PyLady
def show(self, screen):
screen.blit(self.image, (self.x, self.y)) # Desenha a PyLady na telaVamos criar uma classe ProjectilePyLady que representará o projétil disparado pela PyLady. Essa classe terá atributos para a posição, imagem e velocidade do projétil, além de métodos para desenhar e mover o projétil.
class ProjectilePyLady:
def __init__(self, x=0, width=PROJECTILE_WIDTH, height=PROJECTILE_HEIGHT, img=PROJECTILE_PYLADY_IMG):
self.x = x # Posição horizontal do projétil
self.y = SCREEN_HEIGHT - PYLADY_HEIGHT - PROJECTILE_HEIGHT # Posição vertical do projétil
self.width = width # Largura do projétil
self.height = height # Altura do projétil
self.image = pygame.image.load(img) # Carrega a imagem do projétil
self.active = True # Estado do projétil (ativo ou não)
def move(self):
if self.active:
self.y -= PROJECTILE_PYLADY_SPEED # Move o projétil para cima
if self.y < 0:
self.active = False # Desativa o projétil quando sair da tela
def show(self, screen):
if self.active:
screen.blit(self.image, (self.x, self.y)) # Desenha o projétil na telaAgora, vamos criar uma classe Game que será responsável por gerenciar o estado do jogo, incluindo a PyLady e o projétil. Essa classe terá métodos para lidar com eventos, atualizar o estado do jogo e desenhar os elementos na tela.
class Game:
def __init__(self):
# Inicializando o Pygame
pygame.init()
# Definindo o clock do jogo
self.clock = pygame.time.Clock()
# Criando a janela do jogo
self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("PyLadies Invaders")
self.pylady = PyLady() # Cria uma instância da PyLady
self.projectile_pylady = None # Será inicializado quando o projétil for disparado
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()
def handle_input(self):
# Lendo os eventos do teclado
pressed_keys = pygame.key.get_pressed()
# Movendo a PyLady
if pressed_keys[pygame.K_LEFT]:
self.pylady.move(self.pylady.x - PYLADY_SPEED)
if pressed_keys[pygame.K_RIGHT]:
self.pylady.move(self.pylady.x + PYLADY_SPEED)
# Criando o projétil
if pressed_keys[pygame.K_SPACE] and not self.projectile_pylady:
self.projectile_pylady = ProjectilePyLady(self.pylady.x + PYLADY_WIDTH // 2) # Cria uma nova instância do projétil
def update(self):
if self.projectile_pylady:
self.projectile_pylady.move() # Atualiza a posição do projétil, se ele estiver ativo
if not self.projectile_pylady.active:
self.projectile_pylady = None # Reseta o projétil se ele não estiver ativo
def draw(self):
self.screen.fill(BACKGROUND_COLOR) # Preenche a tela com a cor de fundo
self.pylady.show(self.screen) # Desenha a PyLady
if self.projectile_pylady:
self.projectile_pylady.show(self.screen) # Desenha o projétil, se ele estiver ativo
pygame.display.flip() # Atualiza a tela
def run(self):
while True:
self.handle_events() # Lê os eventos do teclado
self.handle_input() # Lida com a entrada do usuário
self.update() # Atualiza o estado do jogo
self.draw() # Desenha os elementos do jogo
self.clock.tick(FPS) # Controla a taxa de quadros do jogoPara iniciar o jogo, precisamos criar uma instância da classe Game e chamar o método run().
if __name__ == "__main__":
game = Game() # Cria uma instância do jogo
game.run() # Inicia o jogo
pygame.quit() # Encerra o Pygame ao sair do jogoO código completo até agora deve se parecer com o arquivo parte_3.py.
A Programação Orientada a Objetos (POO) nos permite organizar o código de forma mais modular e reutilizável. Algumas das vantagens incluem:
- Encapsulamento: Cada classe encapsula seus próprios dados e comportamentos, tornando o código mais fácil de entender e manter.
- Reutilização: Podemos criar instâncias de classes e reutilizar seu código em diferentes partes do jogo, evitando duplicação de código.
- Abstração: Podemos abstrair a lógica de cada elemento do jogo em classes, tornando o código mais legível e fácil de entender.
- Escalabilidade: À medida que o jogo cresce, podemos adicionar novas classes e funcionalidades sem complicar o código existente.
- Organização: O código fica mais organizado, com cada classe responsável por uma parte específica do jogo, facilitando a navegação e compreensão do código.
Agora que temos a PyLady e o projétil funcionando, vamos adicionar inimigos ao jogo. Os inimigos serão representados por uma classe Invader, e iremos implementar a lógica de colisão entre o projétil e os invasores.
Antes de criar a classe Invader, vamos analisar as classes que já temos até agora:
Tanto a classe PyLady quanto a classe ProjectilePyLady possuem atributos e métodos semelhantes, como x, y, width, height, image, move() e show(). Podemos criar uma classe base chamada Object que conterá esses atributos e métodos comuns, e as classes PyLady, ProjectilePyLady e Invader herdarão dessa classe base.
class Object:
def __init__(self, img, width, height, x=0, y=0):
self.x = x # Posição horizontal do objeto
self.y = y # Posição vertical do objeto
self.width = width # Largura do objeto
self.height = height # Altura do objeto
self.image = pygame.image.load(img) # Carrega a imagem do objeto
def move(self, x, y):
self.x = x
self.y = y
def show(self, screen):
screen.blit(self.image, (self.x, self.y)) # Desenha o objeto na telaAgora podemos criar a classe Invader que herda da classe Object. A classe Invader implementará métodos específicos para movimentação, dado que os invasores se movem horizontalmente e trocam de direção quando atingem as bordas da tela, além de moverem verticalmente após certo tempo.
Primeiramente, vamos criar um Enum para definir as direções possíveis de movimento dos invasores:
import enum
class Direction(enum.Enum):
LEFT = "left"
RIGHT = "right"Inicialize a direção dos invasores no construtor da classe Game, logo após a inicialização da PyLady:
self.direction = Direction.LEFT # Direção inicial dos invasoresAgora, vamos definir as constantes necessárias para os invasores:
INVADER_WIDTH = 25
INVADER_HEIGHT = 18
INVADER_IMG = "img/invader.png"
PROJECTILE_INVADER_IMG = "img/projectile_invader.png"
PROJECTILE_INVADER_SPEED = 5
INVADER_X_SPEED = 10
INVADER_Y_SPEED = 5
INVADER_TIME_X = 500
INVADER_TIME_Y = 2000
INVADER_TIME_SHOOT = 1000Após isso, podemos definir a classe Invader:
class Invader(Object):
def __init__(self, x, y, img=INVADER_IMG):
super().__init__(img, INVADER_WIDTH, INVADER_HEIGHT, x, y) # Chama o construtor da classe base
def move_y(self):
self.move(self.x, self.y + INVADER_Y_SPEED)
if self.y >= SCREEN_HEIGHT + INVADER_HEIGHT:
self.active = False
def move_x(self, direction):
if direction == Direction.LEFT:
self.move(self.x - INVADER_X_SPEED, self.y)
elif direction == Direction.RIGHT:
self.move(self.x + INVADER_X_SPEED, self.y) Para gerenciar os invasores, vamos adicionar uma lista de invasores na classe Game. Essa lista conterá todas as instâncias de Invader que serão criadas no jogo.
Dentro do construtor da classe Game, vamos inicializar a lista de invasores logo após a inicialização da PyLady e criar alguns invasores para começar o jogo.
self.invaders = [] # Lista para armazenar os invasores
for y in range(5): # Gera 5 linhas de invasores
# Adiciona uma margem de 150 pixels na tela e calcula o número de colunas de 50 pixels que cabem na tela
for x in range((SCREEN_WIDTH - 150) // 50):
# Cria um novo invasor considerando 100 pixels de margem horizontal e 50 pixels de espaçamento vertical
invader = Invader(x * INVADER_WIDTH * 2 + 100, y * 50 + 50)
self.invaders.append(invader)Dentro do método draw() da classe Game, vamos adicionar um loop para desenhar todos os invasores na tela. Para isso, vamos iterar sobre a lista de invasores e chamar o método show() de cada instância.
Adicione o seguinte código dentro do método draw() da classe Game, logo após desenhar a PyLady e o projétil:
for invader in self.invaders: # Desenha todos os invasores
invader.show(self.screen)Como os invasores se movem em um intervalo de tempo, podemos configurar um temporizador para controlar o movimento dos invasores. Dentro do construtor da classe Game, vamos inicializar dois temporizadores para controlar o movimento dos invasores.
Para isso, crie duas constantes no início do arquivo para definir os eventos de movimento dos invasores:
MOVE_INVADERS_X = pygame.USEREVENT + 1
MOVE_INVADERS_Y = pygame.USEREVENT + 2Iremos configurar esses eventos no construtor da classe Game, após definir o clock do jogo:
pygame.time.set_timer(MOVE_INVADERS_X, 500) # Movimenta os invasores horizontalmente a cada meio segundo
pygame.time.set_timer(MOVE_INVADERS_Y, 2000) # Movimenta os invasores verticalmente a cada 2 segundosAgora, dentro do método handle_events() da classe Game, vamos verificar se os eventos de movimento dos invasores foram acionados e chamar os métodos correspondentes para mover os invasores. Adicione o seguinte código após a leitura do evento de fechamento da janela:
if event.type == MOVE_INVADERS_X:
self.move_invaders_x()
if event.type == MOVE_INVADERS_Y:
self.move_invaders_y()Vamos criar o método move_invaders_y() na classe Game, que irá mover todos os invasores verticalmente. Esse método irá iterar sobre a lista de invasores e chamar o método move_y() de cada instância.
def move_invaders_y(self):
for invader in self.invaders:
invader.move_y() # Move o invasor verticalmenteVamos criar o método move_invaders_x() na classe Game, que irá mover todos os invasores horizontalmente. Esse método irá iterar sobre a lista de invasores e chamar o método move_x() de cada instância, passando a direção correta.
def move_invaders_x(self):
for invader in self.invaders:
invader.move_x(self.direction)
if self.invaders:
if self.invaders[-1].x >= SCREEN_WIDTH - INVADER_WIDTH:
self.direction = Direction.LEFT
elif self.invaders[0].x <= 0:
self.direction = Direction.RIGHTPara verificar colisões entre o projétil e os invasores, podemos adicionar um método collision() na classe Object, dado que poderemos ter colisões entre diferentes objetos do jogo, como a PyLady, o projétil e os invasores.
Para verificar se dois objetos colidem, podemos utilizar o método colliderect() do Pygame, que verifica se dois retângulos colidem.
Adicione o seguinte método na classe Object:
def collision(self, other):
return pygame.Rect(self.x, self.y, self.width, self.height).colliderect(
pygame.Rect(other.x, other.y, other.width, other.height)
)Agora, podemos criar um método check_collisions() na classe Game que irá verificar se o projétil colidiu com algum invasor. Se houver uma colisão, o invasor será removido da lista de invasores e o projétil será desativado.
Caso um invasor colida com a PyLady, ele também será removido da lista de invasores, e o jogo poderá ser finalizado ou uma ação específica poderá ser tomada (como perder uma vida).
def check_collision(self):
# Verifica se um invasor colidiu com a PyLady
for invader in self.invaders:
if invader.collision(self.pylady):
self.invaders.remove(invader)
exit() # Encerra o jogo se um invasor colidir com a PyLady
# Verifica se o projétil da PyLady colidiu com algum invasor
if self.projectile_pylady and invader.collision(self.projectile_pylady):
self.invaders.remove(invader)
self.projectile_pylady.active = False
self.projectile_pylady = NoneVamos chamar o método check_collision() dentro do loop do método run() da classe Game, logo após atualizar a posição do projétil. Isso garantirá que as colisões sejam verificadas a cada quadro do jogo.
self.check_collision() # Verifica colisões entre o projétil e os invasoresO código completo até agora deve se parecer com o arquivo parte_4.py.
Agora que temos os invasores funcionando e a lógica de colisão implementada, podemos refatorar o código para utilizar herança e tornar o código mais modular e reutilizável.
Podemos refatorar a classe PyLady para que ela herde da classe Object, assim como fizemos com a classe Invader. Isso nos permitirá reutilizar o código comum entre os objetos do jogo.
Iremos modificar o construtor da classe PyLady para chamar o construtor da classe Object e passar os parâmetros necessários. Podemos remover o método show() da classe PyLady, pois ele já está implementado na classe Object. Manteremos o método move() para atualizar a posição da PyLady.
class PyLady(Object):
def __init__(self, image=PYLADY_IMG):
x = (SCREEN_WIDTH - PYLADY_WIDTH) // 2
y = SCREEN_HEIGHT - PYLADY_HEIGHT
super().__init__(image, PYLADY_WIDTH, PYLADY_HEIGHT, x, y)
def move(self, x):
if x < 0:
self.x = 0 # Não permite que a PyLady saia da tela pela esquerda
elif x >= SCREEN_WIDTH - self.width:
self.x = SCREEN_WIDTH - self.width # Não permite que a PyLady saia da tela pela direita
else:
self.x = x # Atualiza a posição da PyLadyPodemos refatorar a classe ProjectilePyLady para que ela também herde da classe Object. Assim como fizemos com a classe PyLady, iremos modificar o construtor da classe ProjectilePyLady para chamar o construtor da classe Object e passar os parâmetros necessários, além de remover o método show().
class ProjectilePyLady(Object):
def __init__(self, x, image=PROJECTILE_PYLADY_IMG):
y = SCREEN_HEIGHT - PYLADY_HEIGHT - PROJECTILE_HEIGHT
self.active = True
super().__init__(image, PROJECTILE_WIDTH, PROJECTILE_HEIGHT, x, y)
def move(self):
if self.active:
self.y -= PROJECTILE_PYLADY_SPEED
if self.y < 0:
self.active = FalseO código completo até agora deve se parecer com o arquivo parte_4.py.
Agora que temos os invasores funcionando e a lógica de colisão implementada, vamos adicionar o projétil disparado pelos invasores. Esse projétil será semelhante ao projétil disparado pela PyLady, mas terá sua própria classe e lógica de movimento.
Iremos criar uma classe ProjectileInvader que representará o projétil disparado pelos invasores. Essa classe herdará da classe Object, assim como fizemos com as classes PyLady e ProjectilePyLady.
class ProjectileInvader(Object):
def __init__(self, x, y, image=PROJECTILE_INVADER_IMG):
self.active = True
super().__init__(image, PROJECTILE_WIDTH, PROJECTILE_HEIGHT, x, y)
def move(self):
if self.active:
self.y += PROJECTILE_INVADER_SPEED
if self.y > SCREEN_HEIGHT:
self.active = FalseDentro do construtor da classe Game, vamos inicializar uma lista para armazenar os projéteis disparados pelos invasores, logo após a inicialização da lista de invasores:
self.projectiles_invaders = [] # Lista para armazenar os projéteis disparados pelos invasoresIremos sortear um invasor aleatório para disparar o projétil a cada 2 segundos. Para isso, vamos criar um evento de disparo de projétil dos invasores.
Adicione o seguinte código no início do arquivo, logo após a definição dos eventos de movimento dos invasores:
INVADERS_SHOOT = pygame.USEREVENT + 3Agora, dentro do construtor da classe Game, vamos configurar o temporizador para disparar o projétil dos invasores a cada 2 segundos:
pygame.time.set_timer(INVADERS_SHOOT, INVADER_TIME_SHOOT) # Dispara o projétil dos invasores a cada 2 segundosDentro do método handle_events() da classe Game, vamos verificar se o evento de disparo dos invasores foi acionado e chamar o método invader_shoot() para disparar o projétil.
Adicione o seguinte código após a verificação do evento de fechamento da janela:
if event.type == INVADERS_SHOOT:
self.invader_shoot() # Dispara o projétil dos invasoresAgora, vamos implementar o método invader_shoot() na classe Game, que irá sortear um invasor aleatório e disparar o projétil a partir da posição do invasor.
Importe a biblioteca random no início do arquivo, caso ainda não tenha feito isso:
import randomAdicione o método invader_shoot() na classe Game:
def invader_shoot(self):
invader_to_shoot = random.choice(self.invaders) # Seleciona um invasor aleatório da lista de invasores
self.projectiles_invaders.append(
ProjectileInvader(
invader_to_shoot.x + INVADER_WIDTH // 2,
invader_to_shoot.y + INVADER_HEIGHT,
)
)Atualize o método update() da classe Game para mover os projéteis disparados pelos invasores. Adicione o seguinte código logo após atualizar a posição do projétil da PyLady:
for projectile in self.projectiles_invaders:
projectile.move() # Move o projétil dos invasores
if not projectile.active:
self.projectiles_invaders.remove(projectile) # Remove o projétil se não estiver ativoDentro do método draw() da classe Game, vamos adicionar um loop para desenhar todos os projéteis disparados pelos invasores. Para isso, vamos iterar sobre a lista de projéteis dos invasores e chamar o método show() de cada instância.
Adicione o seguinte código dentro do método draw(), logo após desenhar os invasores:
for projectile_invader in self.projectiles_invaders: # Desenha todos os projéteis dos invasores
projectile_invader.show(self.screen)Para verificar colisões entre os projéteis dos invasores e a PyLady, podemos reutilizar o método check_collision() que já implementamos anteriormente. Vamos adicionar a verificação de colisão entre os projéteis dos invasores e a PyLady.
Caso um projétil dos invasores colida com a PyLady, iremos remover o projétil da lista de projéteis dos invasores e, se necessário, implementar alguma lógica para lidar com a colisão (como diminuir a vida da PyLady ou encerrar o jogo).
Adicione o seguinte código dentro do método check_collision() da classe Game, logo após verificar a colisão entre o projétil da PyLady e os invasores:
# Verifica se um projétil dos invasores colidiu com a PyLady
for projectile_invader in self.projectiles_invaders:
if projectile_invader.collision(self.pylady):
self.projectiles_invaders.remove(projectile_invader) # Remove o projétil dos invasores
exit() # Encerra o jogo, por exemploO código completo até agora deve se parecer com o arquivo parte_6.py.
Agora que temos os invasores e os projéteis funcionando, vamos adicionar um sistema de vidas e pontuação ao jogo. Isso tornará o jogo mais interessante e desafiador.
Vamos adicionar uma classe Life que representará as vidas da PyLady. Essa classe terá atributos para a posição, imagem e número de vidas restantes, além de métodos para desenhar as vidas na tela.
Primeiro iremos definir as constantes necessárias para as vidas:
LIFE_WIDTH = 30
LIFE_HEIGHT = 22
LIFE_IMG = "img/life.png"
LIFE_QTD = 3Em seguida, crie a classe Life:
class Life(Object):
def __init__(self, x, y, image=LIFE_IMG):
super().__init__(image, LIFE_WIDTH, LIFE_HEIGHT, x, y)Dentro do construtor da classe Game, vamos inicializar uma lista para armazenar as vidas da PyLady. Essa lista conterá instâncias da classe Life, representando cada vida restante.
# Posição inicial das vidas
base_x = SCREEN_WIDTH - LIFE_WIDTH * LIFE_QTD - 30 # Adiciona uma margem de 30 pixels à direita
self.lives = []
for i in range(LIFE_QTD):
self.lives.append(Life(base_x + i * LIFE_WIDTH, 10)) # Adiciona as vidas na parte superior direita da telaDentro do método draw() da classe Game, vamos adicionar um loop para desenhar todas as vidas na tela. Para isso, vamos iterar sobre a lista de vidas e chamar o método show() de cada instância.
Adicione o seguinte código dentro do método draw(), logo após desenhar a PyLady:
for life in self.lives: # Desenha todas as vidas
life.show(self.screen)Quando um projétil dos invasores colidir com a PyLady, iremos remover uma vida da lista de vidas. Se não houver mais vidas restantes, o jogo será encerrado.
Modifique o método check_collision() da classe Game para remover uma vida quando um projétil dos invasores colidir com a PyLady. Troque o exit() por uma lógica que verifica se ainda há vidas restantes:
if self.lives: # Verifica se ainda há vidas restantes
self.lives.pop() # Remove uma vidaPara verificar se o jogo terminou, podemos adicionar uma condição no final do método run() da classe Game, dentro do loop do jogo. Se não houver mais vidas restantes, o jogo será encerrado.
if not self.lives: # Verifica se não há mais vidas
exit() # Encerra o jogoO código completo até agora deve se parecer com o arquivo parte_7.py.
Agora que temos as vidas e a pontuação funcionando, vamos adicionar sons ao jogo para torná-lo mais envolvente. O Pygame possui suporte a reprodução de sons, o que nos permitirá adicionar efeitos sonoros para ações como disparar projéteis, colisões e outras interações.
Primeiro, precisamos definir as constantes para os arquivos de som que iremos utilizar. Vamos adicionar as seguintes constantes no início do arquivo:
HIT_INVADER_SOUND = "sounds/hit_invader.ogg"
HIT_PYLADY_SOUND = "sounds/hit_pylady.ogg"
EXPLOSION_SOUND = "sounds/explosion.ogg"
VICTORY_SOUND = "sounds/victory.ogg"
GAME_RUN_SOUND = "sounds/game_run.ogg"Vamos criar uma classe Sound que será responsável por carregar e reproduzir os sons do jogo. Essa classe terá métodos para carregar os sons e reproduzi-los quando necessário.
class Sound:
def __init__(self, sound_path, channel_number, volume=0.3):
self.sound = pygame.mixer.Sound(sound_path)
self.channel = pygame.mixer.Channel(channel_number)
self.channel.set_volume(volume)
def play(self, loops=0):
self.channel.play(self.sound, loops=loops)
def stop(self):
self.channel.stop()Dentro do construtor da classe Game, vamos inicializar os sons que iremos utilizar no jogo. Vamos criar instâncias da classe Sound para cada som que definimos anteriormente.
self.sound_hit_invader = Sound(HIT_INVADER_SOUND, 0)
self.sound_hit_pylady = Sound(HIT_PYLADY_SOUND, 1)
self.sound_victory = Sound(VICTORY_SOUND, 3)
self.sound_explosion = Sound(EXPLOSION_SOUND, 2)
self.sound_game_run = Sound(GAME_RUN_SOUND, 6, 0.1)Agora, vamos reproduzir os sons em momentos específicos do jogo. Por exemplo, podemos reproduzir o som de disparo quando a PyLady dispara um projétil, o som de colisão quando um projétil colide com um invasor ou a PyLady, e o som de vitória quando todos os invasores são derrotados.
Dentro do método check_collision() da classe Game, vamos reproduzir o som de colisão quando um projétil da PyLady colide com um invasor e quando um projétil dos invasores colide com a PyLady.
Adicione self.sound_hit_pylady.play() logo após remover o projétil dos invasores e antes de verificar se ainda há vidas restantes:
self.sound_hit_pylady.play() # Reproduz o som de colisão da PyLadyAdicione self.sound_hit_invader.play() logo após desativar o projétil da PyLady.
self.sound_hit_invader.play() # Reproduz o som de colisão do invasorQuando o jogo termina, seja por derrota ou vitória, podemos reproduzir um som específico. Vamos adicionar o som de fim de jogo no final do método run() da classe Game.
if not self.invaders: # Verifica se não há mais invasores
self.sound_victory.play() # Reproduz o som de vitória
pygame.time.delay(2000) # Aguarda 2 segundos antes de encerrar o jogo
exit() # Encerra o jogo
if not self.invaders: # Verifica se não há mais invasores
self.sound_victory.play() # Reproduz o som de vitória
pygame.time.delay(2000) # Aguarda 2 segundos antes de encerrar o jogo
exit() # Encerra o jogo
if not self.lives: # Verifica se não há mais vidas
self.sound_explosion.play() # Reproduz o som de fim de jogo
pygame.time.delay(2000) # Aguarda 2 segundos antes de encerrar o jogo
exit() # Encerra o jogoPara adicionar o som de fundo do jogo, podemos reproduzir o som em loop no início do método run() da classe Game. Isso garantirá que o som de fundo seja reproduzido enquanto o jogo estiver em execução.
Adicione o seguinte código no início do método run() da classe Game, antes do loop principal:
self.sound_game_run.play(-1) # Reproduz o som de fundo do jogo em loopO código completo até agora deve se parecer com o arquivo parte_8.py.
Para adicionar pontuação ao jogo, podemos criar uma variável para armazenar a pontuação e exibi-la na tela. A pontuação será incrementada sempre que um invasor for destruído.
Para tornar o jogo mais interessante, podemos adicionar uma tela de início e uma tela de fim. A tela de início será exibida antes do jogo começar, e a tela de fim será exibida quando o jogo terminar, seja por vitória ou derrota.