• Jogos
  • 08. Classes

Classes no Pygame

Classes dos componentes visuais

Vamos começar com um exemplo de botões, semelhante ao que fizemos no handout sobre colisões. Suponha que queremos ter tanto botões circulares quanto retangulares. Vamos listar algumas características de qualquer botão:

  • Deve ser desenhado na tela;
  • Quando ocorre um clique dentro de sua área, ele reage de alguma forma.

Para simplificar, vamos supor que quando o botão é clicado ele muda de cor.

Exercício 1

Resolva o exercício Cria botões.

Acessar exercício

Note que adotar classes muda muito o código do nosso jogo. Uma das vantagens é tirar código das funções principais do jogo e agrupar tudo relativo a um tipo de elemento do jogo em uma classe. Por exemplo, ao invés de termos vários dicionários representando botões no dicionário state agora podemos ter somente uma lista de objetos do tipo Botao. Depois disso, no código principal, não precisamos mais nos preocupar com a implementação específica da colisão do mouse com a forma (círculo ou retângulo). Basta chamar a função verifica_clique.

Mais um exemplo#

Vamos introduzir um recurso disponível no Pygame através de um exemplo. O código que gera o "jogo" abaixo já está pronto no exercício Movimentos aleatórios. Nós vamos modificá-lo ao longo deste handout.

Movimentos aleatórios

Exercício 2

Abra o exercício Movimentos aleatórios e volte para este handout.

Acessar exercício

Execute o jogo.py para garantir que o jogo está funcionando inicialmente.

Vamos começar entendendo o código que já foi fornecido.

Exercício 3

Abra o arquivo monstro.py e leia o código do construtor (__init__) da classe. Escreva em poucas frases o que acontece nesse código.

Resposta

O construtor guarda em atributos as dimensões da janela e a lista de monstros recebidas como argumentos. Além disso, ela carrega a imagem do monstro e guarda em atributos tanto a imagem quanto o seu retângulo (que indica a sua posição). O construtor também sorteia velocidades e posições aleatórias. Caso a posição já esteja ocupada por outro monstro, continua sorteando outras posições.

Exercício 4

Ainda na classe Monstro, leia o código do método colide_com_outros_monstros. Escreva em poucas frases o que acontece nesse código.

Resposta

O método percorre a lista de monstros e verifica se o retângulo de algum deles colide com o próprio retângulo. Esse método verifica se o monstro que está sendo testado é diferente dele mesmo, pois ele também estará na lista de monstros.

Exercício 5

Finalmente, escreva abaixo o que o método atualiza faz.

Resposta

O método tenta movimentar o monstro na horizontal. Se ele sair da janela ou colidir com outro monstro, inverte a direção da velocidade. Posteriormente, o método repete esse processo para o movimento vertical.

Sprites#

Como o Pygame é uma biblioteca de desenvolvimento de jogos, é esperado que seja muito comum termos classes similares à nossa classe Monstro: guardam uma imagem e sua posição e podem ser atualizadas para se mover no mapa ou realizar outras ações. Para facilitar o nosso trabalho, o Pygame já disponibiliza a classe pygame.sprite.Sprite.

Exercício 6

Leia a documentação da classe pygame.sprite.Sprite. Quais atributos nós devemos criar para que ela funcione corretamente?

Resposta

Devemos criar os atributos image e rect para definir a imagem a ser mostrada e a sua posição.

Agora precisamos introduzir um outro conceito de classes que ainda não havíamos mencionado: a herança. Uma classe pode estender as funcionalidades de outra classe através do mecanismo de herança. No exemplo da documentação, a classe Block estende a classe pygame.sprite.Sprite com a declaração da primeira linha:

class Block(pygame.sprite.Sprite):

Isso quer dizer que o Block é um pygame.sprite.Sprite, ou seja, o Block tem e faz tudo o que o pygame.sprite.Sprite faz e mais algumas coisas.

No construtor da classe Block, existe também a seguinte chamada:

pygame.sprite.Sprite.__init__(self)

Essa linha chama a inicialização do pygame.sprite.Sprite. Como o Block é um pygame.sprite.Sprite, precisamos inicializar a parte pygame.sprite.Sprite dele.

Exercício 7

Agora vamos refatorar a nossa classe Monstro. Faça a classe estender/herdar a classe pygame.sprite.Sprite. Lembre-se de chamar o construtor do pygame.sprite.Sprite dentro do seu construtor (deve ser a primeira coisa a ser feita no seu construtor).

Além disso, será necessário renomear os atributos imagem e retangulo para image e rect, como vimos no exercício acima.

Ainda para nos prepararmos para os próximos passos, renomeie o método atualiza para update. Ao fazer isso, forçaremos o Pygame a usar o nosso método de atualização ao invés do método padrão do pygame.sprite.Sprite.

Faça esses ajustes e corrija os erros no resto do código. Ao final desta etapa o jogo deve continuar funcionando.

Sprite Groups#

A classe pygame.sprite.Sprite pode ser usada em conjunto com a classe pygame.sprite.Group. O pygame.sprite.Group é similar a uma lista, mas com algumas funcionalidades extras. Ele já possui métodos que:

  1. Desenham todos os seus sprites;
  2. Chamam o método update de todos os seus sprites (por isso renomeamos o atualiza para update no exercício anterior).

Além disso, existem funções que verificam colisões entre sprites e grupos ou entre grupos de sprites.

Exercício 8

Abra o arquivo funcoes_do_jogo.py e modifique as linhas 15 a 17 para:

monstros = pygame.sprite.Group()
for i in range(10):
    monstros.add(Monstro(largura_janela, altura_janela, monstros))

Essa linha cria um grupo de sprites ao invés de uma lista, como tínhamos anteriormente.

Substitua também as linhas 28 a 31 por:

state['monstros'].draw(window)

Modifique também as linhas 38 e 39:

state['monstros'].update()

Exercício 9

Agora que temos grupos, também poderemos usar outras funções que já verificam a colisão para nós. Leia a documentação da função pygame.sprite.spritecollide.

Descreva cada um dos 3 primeiros argumentos (por enquanto não vamos nos preocupar com o último argumento). Além disso, explique o que é retornado pela função.

Resposta

Os argumentos são:

  1. sprite: um objeto do tipo pygame.sprite.Sprite que será o alvo da colisão com todos os sprites do grupo;
  2. group: um grupo de sprites do tipo pygame.sprite.Group. A função verifica quais sprites deste grupo colidiram com o primeiro argumento;
  3. dokill: remove todos os sprites do grupo que colidirem com o sprite do primeiro argumento.

O valor retornado pela função é uma lista com todos os sprites do grupo que colidiram com o sprite principal (primeiro argumento).

Exercício 10

Abra o arquivo monstro.py e modifique o método colide_com_outros_monstros para usar a função pygame.sprite.spritecollide. Lembre-se que o monstro também está no grupo, portanto você deve verificar se existe algum outro sprite do grupo que colidiu com ele. Dica: você pode usar a função len na lista retornada para verificar quantos sprites colidiram.

Melhorando a colisão#

Talvez você tenha reparado que as colisões são um pouco estranhas. Os monstros "colidem" muito antes de realmente encostarem. Isso acontece porque estamos comparando os retângulos de suas imagens. Essas imagens são transparentes, então o desenho do monstro em si ocupa apenas uma parte central da imagem.

Vamos aproveitar que os monstros são relativamente circulares e vamos trocar a colisão por algo uma um pouco mais precisa. Vamos utilizar a colisão de círculos, que o Pygame também já tem implementada. Na verdade, é para isso que serve o último argumento da função pygame.sprite.spritecollide.

Exercício 11

Leia a documentação da função pygame.sprite.collide_circle. A documentação diz que existe um atributo opcional nos sprites que pode ser usado por ela. Qual é esse atributo?

Resposta

Caso o sprite tenha o atributo radius, ele será utilizado para a colisão entre sprites, quando a função de colisão de círculos for utilizada.

Exercício 12

Inicialize no construtor da classe Monstro um atributo radius com o valor de ⅕ da altura da imagem (estimamos esse valor olhando a própria imagem e verificando qual é aproximadamente o raio do monstro).

Adicione pygame.sprite.collide_circle como o 4o argumento da função pygame.sprite.spritecollide no seu método colide_com_outros_monstros. Importante: você não deve chamar a função pygame.sprite.collide_circle (ou seja, não deve colocar os parênteses). A linha ficará mais ou menos assim:

colisoes = pygame.sprite.spritecollide(self, self.monstros, False, pygame.sprite.collide_circle)

Teste seu jogo. Agora as colisões devem ocorrer apenas quando os monstros estão mais próximos!

Outros métodos de sprites#

O módulo pygame.sprite possui diversas outras classes e métodos que podem ser úteis em algum momento. É sempre bom dar uma olhada geral na documentação para saber o que existe, caso você venha a precisar. Você não precisa (e nem deve) se preocupar em conhecer toda a documentação e decorar todos os métodos e classes. Isso vem com o uso, mas é importante ter dado uma olhada geral alguma vez, para saber que existe algo pronto que já resolve o seu problema, quando ele surgir.

Em particular, as funções a seguir podem ser úteis em algum momento do seu projeto:

Agora que vimos algumas classes do Pygame, vamos também refletir sobre o uso de classes em outros lugares do nosso jogo.