Artigos

Introdução ao Desenvolvimento de Games 2D com PyGame

img

"Game creation keeps on expanding, just like the Universe. That is why I keep making games."

— Hideo Kojima

Conteúdo

Introdução

Pygame é um conjunto de módulos Python projetados para criar vídeo-games. O Pygame adiciona funcionalidades à excelente biblioteca SDL, que significa Simple DirectMedia Layer. SDL fornece acesso cross-platform aos componentes de hardware de multimídia de nosso sistema, como som, vídeo, mouse, teclado e joystick. Nos permitindo construir jogos completos e também programas multimídia na linguagem Python.

Pygame é altamente portátil e roda em quase todas as plataformas e sistemas operacionais.

Instalação

Para instalarmos Pygame em nossa máquina, utilizaremos pip, o instalador de pacotes tradicional do Python. Sendo assim, executaremos o seguinte comando em nosso terminal:

pip install pygame

Podemos verificar se a instalação ocorreu corretamente carregando um dos exemplos embutidos com a biblioteca:

python3 -m pygame.examples.aliens
python3 -m pygame.examples.stars
python3 -m pygame.examples.chimp
python3 -m pygame.examples.fonty
python3 -m pygame.examples.eventlist
python3 -m pygame.examples.liquid

Se uma janela de Game for exibida, isso indica que Pygame está instalado corretamente! Se você tiver problemas, o guia de primeiros passos descreve alguns problemas conhecidos e advertências para todas as plataformas.

Observe que cada exemplo apresenta uma funcionalidade que o Pygame nos proporciona. Para conhecer todos os exemplos disponíveis você pode visitar: pygame.examples.

Conceitos Fundamentais

A biblioteca Pygame é composta de vários constructos Python, que incluem vários módulos diferentes. Esses módulos fornecem acesso abstrato ao hardware específico de nosso sistema, bem como métodos uniformes para trabalhar com esse hardware. Por exemplo, o display permite acesso uniforme à tela de vídeo, enquanto o joystick permite o controle abstrato do joystick.

Inicialização e Módulos

Para usar os métodos da biblioteca Pygame, o módulo deve primeiro ser importado da seguinte forma:

import pygame

A instrução import grava a versão do pygame e um link para o site do Pygame no console:

pygame 1.9.6
Hello from the pygame community. https://www.pygame.org/contribute.html

A instrução de importação do Pygame é sempre colocada no início do programa. Ela importa as classes, métodos e atributos do Pygame para o espaço de nomes atual. Agora, esses novos métodos podem ser chamados via pygame.metodo().

Por exemplo, agora podemos inicializar ou sair do pygame com os seguintes comandos:

pygame.init()
pygame.quit()
  • pygame.init(): Inicialize todos os módulos de Pygame importados. Podemos sempre inicializar módulos individuais manualmente, mas pygame.init() inicializa todos os módulos Pygame importados, é uma maneira conveniente de começar tudo.
  • pygame.quit(): Desinicializa todos os módulos do Pygame que foram inicializados anteriormente. Quando o interpretador Python é encerrado, este método é chamado independentemente, portanto, seu programa não deve precisar dele, exceto quando desejar encerrar seus recursos de Pygame.

Se eventualmente precisarmos obter ajuda sobre algum método ou classe do Pygame, podemos utilizar a função help() do Python, por exemplo:

help(pygame)
help(pygame.draw)
help(pygame.event)
help(pygame.image)

Bônus: Para acessar todos os exemplos utilizados neste tutorial e outros adicionais, você pode visitar o repositório do GitHub: PyGameDev.

Displays e Superfícies

Além dos módulos, o Pygame também inclui várias classes Python que encapsulam conceitos não dependentes de hardware. Uma delas é a Surface, que em sua forma mais básica, define uma área retangular na qual podemos desenhar. Objetos Surface são usados em muitos contextos no Pygame.

No Pygame, tudo é visualizado em uma única tela criada pelo usuário, que pode ser uma janela ou tela inteira. O display é criado usando o método set_mode(), que retorna uma Surface representando a parte visível da janela. É essa superfície que passamos para as funções de desenho, como por exemplo pygame.draw.rect(), e o conteúdo dessa superfície é colocado no display quando chamamos pygame.display.flip().

Esta variável será uma das variáveis mais utilizadas. Ele representa a janela que vemos:

screen = pygame.display.set_mode((640, 480))

O argumento do tamanho é uma tupla com um par de números que representam a largura(width) e a altura(height) da tela, que nesse caso chamamos de screen.

Coordenadas

O sistema de coordenadas cartesianas é o sistema ao qual a maioria das pessoas está acostumada ao traçar gráficos. Este é o sistema normalmente ensinado nas escolas. O Pygame usa um sistema de coordenadas semelhante, mas um pouco diferente.

img

Pygame usa um sistema de coordenadas x e y onde a posição (0,0) é definida como o canto superior esquerdo da tela. Mover para baixo significa ter um valor de y mais alto, mover para a direita significa ter um valor de x mais alto.

Imagens e Rects

Podemos desenhar formas(shapes) diretamente na superfície da tela, além disso também podemos trabalhar com imagens no disco. O módulo de imagem permite carregar e salvar imagens em uma variedade de formatos populares. As imagens são carregadas em objetos Surface, que podem ser manipulados e exibidos de várias maneiras.

Os objetos Surface são representados por retângulos, assim como muitos outros objetos no Pygame, como imagens e janelas. Retângulos são tão usados que existe uma classe especial Rect apenas para manipulá-los. Usaremos objetos e imagens Rect em nossos jogos para desenhar personagens e obstáculos e para gerenciar colisões entre eles.

Retângulo

img

Um objeto Rect pode ser criado fornecendo:

  • Os 4 parâmetros left, top, width e height
  • A posição e tamanho
  • Um objeto que tem um atributo rect
Rect(left, top, width, height)
Rect(posicao, tamanho)
Rect(objeto)

A figura a seguir nos ajuda a compreender a criação do retângulo e seu posicionamento:

img

Atributos Virtuais

O objeto Rect tem vários atributos virtuais que podem ser usados para mover e alinhar o Rect. A atribuição a esses atributos apenas move o retângulo sem alterar seu tamanho:

  • x, y
  • top, left, bottom, right
  • topleft, bottomleft, topright, bottomright
  • midtop, midleft, midbottom, midright
  • center, centerx, centery

A atribuição desses 5 atributos a seguir altera o tamanho do retângulo, mantendo sua posição superior esquerda:

  • size, width, height, w, h

Como podemos observar, a classe Rect define 4 pontos de canto, 4 pontos médios e 1 ponto central.

A tabela a seguir apresenta uma lista dos atributos mais importantes que os objetos pygame.Rect fornecem (neste exemplo, a variável onde o objeto Rect é armazenado é chamada de meuRet):

Nome do Atributo Descrição
meuRet.left O valor int da coordenada X do lado esquerdo do retângulo.
meuRet.right O valor int da coordenada X do lado direito do retângulo.
meuRet.top O valor int da coordenada Y do lado superior do retângulo.
meuRet.bottom O valor int da coordenada Y do lado inferior.
meuRet.centerx O valor int da coordenada X do centro do retângulo.
meuRet.centery O valor int da coordenada Y do centro do retângulo.
meuRet.width O valor int da largura do retângulo.
meuRet.height O valor int da altura do retângulo.
meuRet.size Uma tupla de dois ints: (width, height)
meuRet.topleft Uma tupla de dois ints: (left, top)
meuRet.topright Uma tupla de dois ints: (right, top)
meuRet.bottomleft Uma tupla de dois ints: (left, bottom)
meuRet.bottomright Uma tupla de dois ints: (right, bottom)
meuRet.midleft Uma tupla de dois ints: (left, centery)
meuRet.midright Uma tupla de dois ints: (right, centery)
meuRet.midtop Uma tupla de dois ints: (centerx, top)
meuRet.midbottom Uma tupla de dois ints: (centerx, bottom)

Cores

As cores são definidas como tuplas das cores básicas vermelho, verde e azul. Isso é chamado de modelo RGB. Cada cor de base é representada como um número entre 0 (mínimo) e 255 (máximo) que ocupa 1 byte na memória. Uma cor RGB é então representada como um valor de 3 bytes. A mistura de duas ou mais cores resulta em novas cores. Um total aproximado de 16 milhões (255³) de cores diferentes podem ser representadas dessa forma.

img

Definimos então as cores básicas como tuplas dos três valores base. Como as cores são constantes, vamos escrevê-las em maiúsculas. A ausência de todas as cores resulta em preto. O valor máximo para todos os três componentes resulta em branco. Três valores intermediários idênticos resultam em cinza:

PRETO = (0, 0, 0)
CINZA = (128, 128, 128)
BRANCO = (255, 255, 255)

As três cores base são definidas como:

VERMELHO = (255, 0, 0)
VERDE = (0, 255, 0)
AZUL = (0, 0, 255)

Ao misturar duas cores de base, podemos obter mais cores:

AMARELO = (255, 255, 0)
CIANO = (0, 255, 255)
MAGENTA = (255, 0, 255)

O método screen.fill(COR) preenche toda a tela com a cor especificada. Para mostrar qualquer coisa na tela, devemos sempre lembrar de chamar a função pygame.display.update().

É importante lembrarmos que Pygame também conta com cores já definidas, para vermos todas elas podemos usar o seguinte script:

from pprint import pprint
import pygame

pprint(pygame.color.THECOLORS)

Neste caso, podemos por exemplo definir a cor preta da seguinte forma:

BLACK = pygame.Color("black")

Game Loop

Todo Game, de Pong à Diablo, usa um Game Loop para controlar a jogabilidade. O Game Loop possui quatro elementos muito importantes:

  1. Processar o Input do Usuário
  2. Atualizar o estado de todos os objetos do Game
  3. Atualizar o display e o output de áudio
  4. Manter a velocidade do Game

Cada ciclo do Game Loop é chamado de frame e quanto mais rápido fizermos as ações em cada ciclo, mais rápido o jogo será executado. Os frames continuam a ocorrer até que alguma condição para sair do jogo seja satisfeita. Em seu projeto, existem duas condições que podem encerrar o Game Loop:

  1. O jogador colide com um obstáculo.
  2. O jogador fecha a janela do Game.

A primeira coisa que o Game Loop faz é processar o Input do Usuário para permitir que o jogador se mova pela tela. Portanto, precisamos de alguma forma para capturar e processar uma variedade de inputs. Fazemos isso usando o sistema de eventos do Pygame.

O fluxograma a seguir nos apresenta uma ideia geral de como um Game é estruturado e funciona no PyGame:

img

Processando Eventos

A parte essencial de qualquer aplicação interativa é o loop de eventos. Reagir a eventos permite que o usuário interaja com a aplicação. Eventos são ações que podem acontecer em um programa, como:

  • Clique do mouse
  • Movimento do mouse
  • Teclado pressionado
  • Ação do joystick

A seguir temos um exemplo de um loop infinito que imprime todos os eventos no console:

import pygame 
pygame.init()

# Inicializa a tela
screen = pygame.display.set_mode((500,500))
pygame.display.set_caption("Eventos")

# Game Loop ficará ativo até que running seja False
running = True 
while running: 
    # Observa cada evento na fila de eventos
    for event in pygame.event.get():
        # Imprime no console todos os eventos que vierem a ocorrer
        print(event)
        # Fecha o jogo
        if event.type == pygame.QUIT:
           running = False

pygame.quit()

Ao executarmos o script acima veremos uma janela com uma tela preta, ela não irá desaparecer até que cliquemos no botão fechar, todas as ações executadas por nós serão impressas em nosso console.

Template Básico

Uma vez que adquirimos o conhecimento dos conceitos fundamentais do Pygame, vamos definir um template que servirá como um esqueleto para nossos projetos.

from pygame.locals import *
import pygame

# Valores constantes
WIDTH = 500
HEIGHT = 400
FPS = 60

# Cores
BLACK = (13, 13, 13)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)

# Inicializa PyGame, cria a janela e define o relógio
pygame.init()
pygame.mixer.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Título do Game")
clock = pygame.time.Clock()

# Game Loop
running = True
while running:
    # Manter o loop rodando na velocidade correta
    clock.tick(FPS)
    # Processar Inputs (Eventos)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
    # Atualizar
    # Desenhar / Renderizar
    screen.fill(BLACK)
    # Depois de desenhar tudo: flipar o display
    pygame.display.flip()

pygame.quit()

O módulo pygame.locals contém cerca de 280 constantes usadas e definidas por Pygame. Colocar esta declaração no início de seu programa importa todos eles.

Encontramos nele, por exemplo, os modificadores de tecla (alt, ctrl, cmd, etc.):

KMOD_ALT, KMOD_CAPS, KMOD_CTRL, KMOD_LALT,
KMOD_LCTRL, KMOD_LMETA, KMOD_LSHIFT, KMOD_META,
KMOD_MODE, KMOD_NONE, KMOD_NUM, KMOD_RALT, KMOD_RCTRL,
KMOD_RMETA, KMOD_RSHIFT, KMOD_SHIFT,

As teclas numéricas:

K_0, K_1, K_2, K_3, K_4, K_5, K_6, K_7, K_8, K_9,

As teclas de caracteres especiais:

K_AMPERSAND, K_ASTERISK, K_AT, K_BACKQUOTE,
K_BACKSLASH, K_BACKSPACE, K_BREAK,

As teclas das letras do alfabeto:

K_a, K_b, K_c, K_d, K_e, K_f, K_g, K_h, K_i, K_j, K_k, K_l, K_m,
K_n, K_o, K_p, K_q, K_r, K_s, K_t, K_u, K_v, K_w, K_x, K_y, K_z,

O comando pygame.mixer.init() inicialize o módulo do mixer para carregamento e reprodução de som. Os argumentos padrão podem ser substituídos para fornecer mixagem de áudio específica.

O método set_caption() mudará o nome na janela, se o monitor possuir um título de janela.

O comando pygame.time.Clock() cria um objeto relógio que nos ajuda a rastrear o tempo. O relógio também fornece várias funções para ajudar a controlar a taxa de frames de um jogo, neste caso específico estamos setando nosso template para rodar em 60 frames por segundo.

Ao executarmos este template, vamos obter o seguinte resultado:

img

Como podemos observar, é apenas uma tela preenchida com a cor preta, mas que servirá como estrutura básica para nossos projetos futuros.

Desenhando

O módulo pygame.draw permite desenharmos formas simples em uma superfície. Pode ser a superfície da tela ou qualquer objeto Surface, como uma imagem ou desenho.

Podemos desenhar formas como:

  • Retângulo
  • Polígono
  • Círculo
  • Elipse

As funções têm em comum que:

  • Recebem um objeto Surface como primeiro argumento
  • Recebem uma cor como segundo argumento
  • Recebem um parâmetro de largura como último argumento
  • Retornam um objeto Rect que delimita a área alterada

Seguimos então o seguinte formato:

rect(Surface, color, Rect, width) -> Rect
polygon(Surface, color, pointlist, width) -> Rect
circle(Surface, color, center, radius, width) -> Rect

A maioria das funções tem um argumento de largura. Se a largura for 0, a forma será preenchida com a devida cor.

O seguinte código preenche a cor de fundo com branco e, em seguida, adiciona três retângulos sólidos sobrepostos e, ao lado, três retângulos sobrepostos contornados com largura de linha crescente.

Vamos executá-lo em nosso console:

>>> import pygame
>>> screen = pygame.display.set_mode((625, 220))
>>> screen.fill(BRANCO)

>>> pygame.draw.rect(screen, VERMELHO, (50, 20, 120, 100))
>>> pygame.draw.rect(screen, VERDE, (100, 60, 120, 100))
>>> pygame.draw.rect(screen, AZUL, (150, 100, 120, 100))

>>> pygame.draw.rect(screen, VERMELHO, (350, 20, 120, 100), 1)
>>> pygame.draw.rect(screen, VERDE, (400, 60, 120, 100), 4)
>>> pygame.draw.rect(screen, AZUL, (450, 100, 120, 100), 8)

Perceba que o segundo comando que executamos irá abrir a tela e os comandos seguintes não apresentam nenhum resultado na tela, isso porque devemos atualizá-la:

pygame.display.flip()

O resultado será este:

img

Para fechar a janela podemos utilizar o método quit():

pygame.quit()

O código a seguir preenche a cor de fundo com branco e, em seguida, adiciona três elipses sólidas sobrepostas e, ao lado, três elipses sobrepostas contornadas com largura de linha crescente.

Novamente, vamos executá-lo em nosso console:

>>> import pygame
>>> screen = pygame.display.set_mode((660, 220))
>>> screen.fill(BRANCO)

>>> pygame.draw.ellipse(screen, VERMELHO, (50, 20, 160, 100))
>>> pygame.draw.ellipse(screen, VERDE, (100, 60, 160, 100))
>>> pygame.draw.ellipse(screen, AZUL, (150, 100, 160, 100))

>>> pygame.draw.ellipse(screen, VERMELHO, (350, 20, 160, 100), 1)
>>> pygame.draw.ellipse(screen, VERDE, (400, 60, 160, 100), 4)
>>> pygame.draw.ellipse(screen, AZUL, (450, 100, 160, 100), 8)

>>> pygame.display.update()

Que nos trará o seguinte output:

img

Importante: display.update() nos permite atualizar uma parte da tela, em vez de toda a área da tela. Sem passar argumentos, atualizará toda a tela.

Para compreender a diferença entre update() e flip() você pode visitar este Link.

O script a seguir nos apresenta um exemplo de todas as formas possíveis que podemos desenhar:

from math import pi
import pygame as pg
pg.init()

# Cores
BLACK = (27, 27, 27)
WHITE = (255, 255, 255)
GREEN = (42, 130, 72)
BLUE = (92, 127, 184)
YELLOW = (199, 177, 36)
RED = (179, 41, 7)

# Define dimensões do display
width = 600
height = 450
screen = pg.display.set_mode([width, height])

# Define três retângulos
retangulos = [
    pg.Rect(20, 20, 100, 50), 
    pg.Rect(20, 90, 50, 50),
    pg.Rect(500, 30, 80, 60)
]

done = True
while done:
    screen.fill(BLACK)
    for event in pg.event.get():
        if event.type == pg.QUIT:
            done = False
    
    # Desenha três retângulos azuis
    for retangulo in retangulos:
    	pg.draw.rect(screen, BLUE, retangulo)

    # Desenha um retângulo verde
    pg.draw.rect(screen, GREEN, [115, 280, 70, 40])
    # Desenha um retângulo vermelho (borda)
    pg.draw.rect(screen, RED, [115, 280, 71, 41], 2)
    # Desenha um círculo amarelo
    pg.draw.circle(screen, YELLOW, (325,70), 30)
    # Desenha um círculo azul
    pg.draw.circle(screen, BLUE, [250, 250], 25, True)
    # Desenha uma elipse branca
    pg.draw.ellipse(screen, WHITE, (250, 300, 100, 100))
    # Desenha um arco vermelho
    pg.draw.arc(screen, RED, [430, 150, 150, 125], pi/100, 1.13*pi, 2)
    # Desenha uma linha azul
    pg.draw.line(screen, BLUE, (0, height-100), (width, height-100), 5)
    # Desenha uma linha verde
    pg.draw.aaline(screen, GREEN, (0, height-200), (width, height-200))
    # Desenha linhas brancas
    pg.draw.lines(screen, WHITE, False, [[400, 400], [400, 20], [200, 20]], 2)
    # Desenha um polígono amarelo
    pg.draw.polygon(screen, YELLOW, [[140, 120], [100, 200], [300, 200]])
    # Desenha um polígono verde (borda)
    pg.draw.polygon(screen, GREEN, [[140, 120], [100, 200], [300, 200]], 3)
    # Atualiza a tela
    pg.display.update()
    
pg.quit()

Observe que neste exemplo específico estamos importando pygame como pg, uma forma conveniente que Python nos fornece de abreviarmos a escrita dos módulos.

Ao executarmos este script, obteremos como resultado diversos desenhos em nossa tela:

img

Trabalhando com Imagens

O módulo de imagem contém funções para carregar e salvar imagens, bem como transferir Superfícies para formatos utilizáveis por outros pacotes.

Observe que não há classe Image; uma imagem é carregada como um objeto Surface. A classe Surface permite a manipulação (desenhar linhas, definir pixels, capturar regiões, etc).

Quando construída com suporte total de imagem, a função pygame.image.load() pode suportar os formatos a seguir:

  • JPG
  • PNG
  • GIF (não-animado)
  • BMP
  • TGA (não-comprimido)
  • TIF
  • LBM (e PBM)
  • PBM (e PGM, PPM)
  • XPM

Salvar imagens suporta apenas um conjunto limitado de formatos. Podemos salvar nos seguintes formatos:

  • BMP
  • TGA
  • PNG
  • JPEG

O método load() carrega uma imagem do sistema de arquivos e retorna um objeto Surface. O método convert() otimiza o formato da imagem e torna o desenho mais rápido. Por exemplo:

imagem = pygame.image.load('personagem.png')
imagem.convert()

O método get_rect() retorna um objeto Rect de uma imagem, nos permitindo assim trabalhar com colisões.

O módulo pygame.transform fornece métodos para dimensionar, girar e inverter imagens.

No exemplo a seguir iremos carregar a imagem player.png, redimensioná-la e desenhá-la na tela.

from dataclasses import dataclass
import pygame

# Define o relógio
clock = pygame.time.Clock()

# Inicializa pygame
pygame.init()

# Define a cor de fundo
BACKGROUND_COLOR = (70,86,94)

# Define o nome da janela
pygame.display.set_caption('PyGame')

# Define o número de quadros por segundo
FPS = 60

# Define o tamanho da tela
WIDTH, HEIGHT = 450, 200
WINDOW_SIZE = (WIDTH, HEIGHT)

# Inicia a tela
screen = pygame.display.set_mode(WINDOW_SIZE, True, 32)

# Carrega e altera a imagem do personagem
player_image = pygame.image.load('player.png').convert_alpha()
player = pygame.transform.scale(player_image, (50,75))

moving_right = False 
moving_left = False

@dataclass
class PlayerLocation:
    x: int 
    y: int

player_location = PlayerLocation(x=155, y=125)
velocity = 3.5

# Game Loop
running = True
while running:   
    # Preenche a tela com cinza              
    screen.fill(BACKGROUND_COLOR) 
    # Desenha o player
    screen.blit(player, (player_location.x, player_location.y))

    if moving_right:
        player_location.x += velocity
    if moving_left: 
        player_location.x -= velocity
        
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                moving_right = True 
            if event.key == pygame.K_LEFT:
                moving_left = True 
        if event.type == pygame.KEYUP:
            if event.key == pygame.K_RIGHT:
                moving_right = False 
            if event.key == pygame.K_LEFT:
                moving_left = False
    
    if player_location.x < 0:
        player_location.x = 0
    if player_location.x + player.get_width() > WIDTH:
        player_location.x = WIDTH - player.get_width()

    pygame.display.update() 
    clock.tick(FPS) 

pygame.quit()

A função blit() é muito importante, o termo blit significa Block Transfer e é como copiamos o conteúdo de um Surface para outra. O desenho ou imagem pode ser posicionado com o argumento dest. Dest pode ser um par de coordenadas que representam o canto superior esquerdo da superfície de origem. Um Rect também pode ser passado como o destino e o canto superior esquerdo do retângulo será usado como a posição para o blit. O tamanho do retângulo de destino não afeta o blit.

Sendo assim, blit() recebe dois importantes argumentos:

  1. A superfície para desenhar (neste caso estamos usando uma imagem)
  2. O local onde desenhá-lo na superfície de origem

Perceba também que definimos um objeto chamada de player_location que representa as coordenadas da posição do player na tela. A variável velocity representa a velocidade de deslocamento do player. Para mover o player usamos as Arrow Keys do teclado (<- & ->), ao pressionarmos elas, iremos acionar as respectivas variáveis moving_right e moving_left como True fazendo assim o player se movimentar. Por fim definimos os limites da tela, para que o player não desapareça de nossa visão e atualizamos a tela com o comando pygame.display.update().

Para transparência alfa, como em imagens .png, usamos o método convert_alpha() após o carregamento para que a imagem tenha transparência por pixel.

Este exemplo nos trará o seguinte resultado:

img

Trabalhando com Textos

No Pygame, o texto não pode ser escrito diretamente na tela, o módulo pygame.font nos permite “desenhar” textos em nossa tela. Para isso precisamos seguir alguns passos.

  1. A primeira etapa é criar um objeto Font com um determinado tamanho de fonte.
  2. A segunda etapa é transformar o texto em uma imagem com uma determinada cor.
  3. A terceira etapa é enviar a imagem para a tela.

Por exemplo:

font = pygame.font.SysFont(None, 24)
imagem = font.render('Texto', True, VERDE)
screen.blit(imagem, (20, 20))

Pygame vem com uma fonte padrão embutida. Isso sempre pode ser acessado passando None como o nome da fonte como argumento para o método SysFont(), o segundo argumento representa o tamanho da Font.

Uma vez que a fonte é criada, seu tamanho não pode ser alterado. Um objeto Font é usado para criar um objeto Surface a partir de uma string. O Pygame não fornece uma maneira direta de escrever texto em um objeto Surface. O método render() deve ser usado para criar um objeto Surface a partir do texto, que então pode ser enviado para a tela. O método render() só pode renderizar linhas simples. Um caractere de nova linha não é renderizado.

A função get_fonts() retorna uma lista de todas as fontes instaladas e disponíveis. O código a seguir verifica quais fontes estão em seu sistema e quantas existem, e as imprime no console:

from pprint import pprint 
import pygame

fonts = pygame.font.get_fonts()

print(f'Existem {len(fonts)} fonts disponíveis')
pprint(fonts)

No exemplo a seguir vamos exibir o texto “Hello PyGame” no centro de nossa tela:

import pygame
pygame.init()

def main():
    # Inicializa a Tela
    screen = pygame.display.set_mode((250, 100))
    pygame.display.set_caption('PyGame Text')

    # Define e Preenche o Background
    background = pygame.Surface(screen.get_size())
    background = background.convert()
    background.fill((15, 15, 15))

    # Define, Posiciona e Apresenta o Texto no Background
    font = pygame.font.SysFont('dyuthi', 36)
    text = font.render("Hello PyGame", 1, (195, 195, 195))
    textpos = text.get_rect()
    textpos.centerx = background.get_rect().centerx
    textpos.centery = background.get_rect().centery
    background.blit(text, textpos)

    # Loop de Eventos
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return

        # Desenha o Background
        screen.blit(background, (0, 0))
        pygame.display.flip()

if __name__ == '__main__': 
    main()

Observe que escolhemos a fonta dyuthi. Também utilizamos os atributos centerx e centery para nos auxiliar a centralizar o texto. O resultado será este:

img

Detectando Colisões

Verificar colisões é uma técnica fundamental de programação de Games e geralmente requer um pouco de matemática para determinar se dois sprites estão se sobrepondo.

É neste momento que uma biblioteca como o Pygame se torna muito útil! Escrever código de detecção de colisão é tedioso, por isso Pygame possui diversos métodos de detecção de colisão disponíveis para usarmos, sem precisarmos “reinventar a roda”.

No exemplo a seguir usaremos o método colliderect, que retornará True se qualquer parte do retângulo se sobrepor (exceto as bordas top + bottom ou left + right).

from dataclasses import dataclass
import pygame
pygame.init()

# Define cores
WHITE = (255, 255, 255)
BLACK = (17, 17, 17)
 
# Define o comprimento e altura (WIDTH e HEIGHT) da tela (screen)
WIDTH = 500
HEIGHT = 400
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Collisions")
 
# Game Loop ficará ativo até que playing seja False
playing = True
# Define relógio e quadros por segundo
clock = pygame.time.Clock()
FPS = 60

# Carrega o fundo
background = pygame.image.load('sprites/background.png').convert_alpha()

# Carrega o sprite, transforma sua dimensão, obtém retângulo e define posição x e y
player_image = pygame.image.load('sprites/player.png').convert_alpha()
player_transformed = pygame.transform.scale(player_image, (50,75))
player_rect = player_transformed.get_rect()
player_rect.x = 10
player_rect.y = 10
# Velocidade x e y do player
dx = 3.5
dy = 3.5

portal_image = pygame.image.load('sprites/portal.png').convert_alpha()
portal_transformed = pygame.transform.scale(portal_image, (65,65))
portal_rect = portal_transformed.get_rect()
portal_rect.x = 195
portal_rect.y = 95

trunk_image = pygame.image.load('sprites/trunk.png')
trunk_image.set_colorkey(WHITE)
trunk_transformed = pygame.transform.scale(trunk_image, (65,65))
trunk_rect = trunk_transformed.get_rect()
trunk_rect.x = 355
trunk_rect.y = 205

box_image = pygame.image.load('sprites/box.png').convert_alpha()
box_transformed = pygame.transform.scale(box_image, (65,65))
box_rect = box_transformed.get_rect()
box_rect.x = 100
box_rect.y = 255

skull_image = pygame.image.load('sprites/skull.png').convert_alpha()
skull_transformed = pygame.transform.scale(skull_image, (60,70))
skull_rect = skull_transformed.get_rect()
skull_rect.x = 430
skull_rect.y = 10

# Direção do personagem
moving_right = False 
moving_left = False
moving_top = False 
moving_down = False

# Movimento e colisões
@dataclass
class Movement:
    x: int 
    y: int

def collision_test(player, obstacles):
    collisions = []
    for obstacle in obstacles:
        if player.colliderect(obstacle):
            collisions.append(obstacle)
    return collisions

def move_and_collide(player, movement, obstacles):
    player.x += movement.x
    collisions_x = collision_test(player, obstacles)
    for obstacle in collisions_x:
        if movement.x > 0:
            player.right = obstacle.left
        if movement.x < 0:
            player.left = obstacle.right
        if obstacle.x == skull_rect.x:
            screen.fill(BLACK)
    player.y += movement.y
    collisions_y = collision_test(player, obstacles)
    for obstacle in collisions_y:
        if movement.y > 0:
            player.bottom = obstacle.top
        if movement.y < 0:
            player.top = obstacle.bottom
        if obstacle.y == skull_rect.y:
            screen.fill(BLACK)

# Início do Game Loop
while playing:
    screen.blit(background, (0,0))

    movement = Movement(x=0, y=0)
    if moving_right:
        movement.x += dx
    if moving_left: 
        movement.x -= dx
    if moving_top:
        movement.y -= dy
    if moving_down: 
        movement.y += dy  

    if player_rect.x < 0:
        player_rect.x = 0
    elif player_rect.x + player_transformed.get_width() > WIDTH:
        player_rect.x = WIDTH - player_transformed.get_width()
    if player_rect.y < 0:
        player_rect.y = 0
    elif player_rect.y + player_transformed.get_height() > HEIGHT:
        player_rect.y = HEIGHT - player_transformed.get_height()

    if player_rect.colliderect(portal_rect):
        player_rect.x = 430
        player_rect.y = 310

    obstacles = [trunk_rect, box_rect, skull_rect]
    move_and_collide(player_rect, movement, obstacles)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            playing = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                moving_right = True 
            if event.key == pygame.K_LEFT:
                moving_left = True 
            if event.key == pygame.K_UP:
                moving_top = True
            if event.key == pygame.K_DOWN:
                moving_down = True
        if event.type == pygame.KEYUP:
            if event.key == pygame.K_RIGHT:
                moving_right = False 
            if event.key == pygame.K_LEFT:
                moving_left = False
            if event.key == pygame.K_UP:
                moving_top = False
            if event.key == pygame.K_DOWN:
                moving_down = False

    screen.blit(player_transformed, [player_rect.x, player_rect.y])
    screen.blit(portal_transformed, [portal_rect.x, portal_rect.y])
    screen.blit(trunk_transformed, [trunk_rect.x, trunk_rect.y])
    screen.blit(box_transformed, [box_rect.x, box_rect.y])
    screen.blit(skull_transformed, [skull_rect.x, skull_rect.y])

    pygame.display.update()
    clock.tick(FPS)

pygame.quit()

Observe que estamos carregando cinco imagens, redimensionando-as e usando o método get_rect() para obter um retângulo delas (necessário para testarmos as colisões).

As imagens, representam, respectivamente:

Um detalhe que devemos citar é que o fundo do tronco é branco, então estamos usando uma técnica chamada colorkey que torna uma cor totalmente transparente. A função é bastante simples, é chamada set_colorkey(COR).

Atenção: Se uma imagem tiver um valor alfa definido, o colorkey não funcionará! Um truque simples para fazer o colorkey funcionar é: image.set_alpha(None) para desabilitá-lo e então você poderá usar set_colorkey(COR).

O jogador poderá se mover livremente para as quatro direções (norte, sul, leste e oeste) usando as Arrow Keys:

img

E testaremos se ele irá colidir com o portal, o tronco, o baú ou o crânio, este último que apagará a luz. Se houver uma colisão com o portal, iremos mover o jogador para uma posição específica da tela, caso haja uma colisão com o tronco ou o baú, não permitiremos que ocorra sobreposição entre os retângulos.

Executando este script, teremos a seguinte tela como output:

img

Sprites

Em computação gráfica, um sprite é um bitmap bidimensional integrado em uma cena maior, na maioria das vezes usado no contexto de um videogame 2D. O termo foi usado pela primeira vez por Danny Hillis na Texas Instruments no final dos anos 1970.

Pygame fornece uma classe Sprite que é projetada para conter uma ou várias representações gráficas de qualquer objeto do Game que você deseja exibir na tela. Para usá-la, criamos uma nova classe que estende Sprite. Isso permite usarmos todos os seus métodos embutidos.

Existe a classe Sprite principal e várias classes de Grupo que contêm Sprites. O uso dessas classes é totalmente opcional ao usar Pygame. As classes são bastante leves e fornecem apenas um ponto de partida para o código comum à maioria dos Games.

A classe Sprite tem como intenção ser usada como uma classe base para os diferentes tipos de objetos do Game. Existe também uma classe base Group que simplesmente armazena sprites. Um Game pode criar novos tipos de classes de Grupo que operam em instâncias de Sprite especialmente personalizadas.

A classe Sprite básica pode desenhar os Sprites que ela contém em uma Surface. O método Group.draw() requer que cada Sprite tenha um atributo Surface.image e um Surface.rect. O método Group.clear() requer esses mesmos atributos e pode ser usado para apagar todos os Sprites com background. Existem também grupos mais avançados: pygame.sprite.RenderUpdates() e pygame.sprite.OrderedUpdates().

Finalmente, este módulo Sprite contém várias funções de colisão. Isso ajuda a encontrar sprites dentro de vários grupos que possuem retângulos delimitadores que se cruzam. Para encontrar as colisões, os Sprites precisam ter um atributo Surface.rect atribuído.

Os grupos são projetados para alta eficiência na remoção e adição de Sprites a eles. Eles também permitem testes de baixo custo computacional para ver se um Sprite já existe em um Grupo. Um determinado Sprite pode existir em qualquer número de grupos. Um Game pode usar alguns grupos para controlar a renderização de objetos e um conjunto completamente separado de grupos para controlar a interação ou o movimento do jogador. Em vez de adicionar atributos de tipo ou bools a uma classe Sprite derivada, considere manter os Sprites dentro de Grupos organizados. Isso permitirá uma pesquisa mais fácil posteriormente no Game.

Sprites e grupos gerenciam seus relacionamentos com os métodos add() e remove(). Esses métodos podem aceitar um único ou vários destinos para associação. Os inicializadores padrão para essas classes também usam um único ou uma lista de destinos para a associação inicial. É seguro adicionar e remover repetidamente o mesmo Sprite de um Grupo.

A classe base para objetos visíveis do Game é pygame.sprite.Sprite. As classes derivadas necessitarão substituir Sprite.update() e atribuir atributos Sprite.image e Sprite.rect. O inicializador pode aceitar qualquer número de instâncias de Grupo a serem adicionadas.

Ao criar uma subclasse do Sprite, certifique-se de chamar o inicializador base antes de adicionar o Sprite aos grupos. Por exemplo:

class Block(pygame.sprite.Sprite):
    # Construtor. Recebe a cor do bloco e sua posição x e y como argumento
    def __init__(self, color, width, height):
       # Chama o construtor da classe pai (Sprite)
       pygame.sprite.Sprite.__init__(self)
       # Cria uma imagem do bloco e preenche com uma cor respectiva
       # Também pode ser uma imagem carregada do disco
       self.image = pygame.Surface([width, height])
       self.image.fill(color)
       # Busca o objeto retângulo que possui as dimensões da imagem
       # Atualiza a posição deste objeto setando os valores de rect.x e rect.y
       self.rect = self.image.get_rect()

Observe que neste exemplo estamos apenas definindo um simples Bloco.

O método update() é utilizado para controlar o comportamento de um Sprite. A implementação padrão deste método não faz nada, é apenas um “gancho” conveniente que você pode sobrescrever. Este método é chamado por Group.update() com quaisquer argumentos que você fornecer a ele.

Vejamos agora um exemplo com mais detalhes:

import pygame 
import os

WIDTH = 800
HEIGHT = 600
FPS = 30

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)

game_folder = os.path.dirname(__file__)
img_folder = os.path.join(game_folder, 'img')

class Player(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join(img_folder, 'guy.png')).convert()
        self.image.set_colorkey(BLACK)
        self.rect = self.image.get_rect()
        self.rect.center = (WIDTH / 2, HEIGHT / 2)
        self.y_speed = 10

    def update(self):
        self.rect.x += 4
        self.rect.y += self.y_speed
        if self.rect.bottom > HEIGHT - 100:
            self.y_speed = -5
        if self.rect.top < 100:
            self.y_speed = 5
        if self.rect.left > WIDTH:
            self.rect.right = 0

pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption('Sprite OOP')
clock = pygame.time.Clock()

all_sprites = pygame.sprite.Group()
player = Player()
all_sprites.add(player)

running = True 
while running:
    clock.tick(FPS)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
    
    screen.fill(BLACK)
    all_sprites.update()
    all_sprites.draw(screen)
    pygame.display.flip()

pygame.quit()

Neste exemplo definimos um Sprite chamado de Player, que é uma imagem do personagem Guy Fawkes que carregamos de nosso disco, localizada em um diretório que denominei img, também estamos definindo algumas propriedades básicas para este Sprite, como o retângulo e sua velocidade no eixo y. Também estamos sobrescrevendo o método update(), fazendo o Sprite se deslocar para a direita e alternando os valores do eixo y quando o Sprite atinge uma determinada posição na tela, nos dando assim a impressão de um movimento diagonal.

Instanciamos o objeto Group em uma variável chamada de all_sprites, lembre que Group é uma classe de contêiner para armazenar e gerenciar vários objetos Sprite.

Em seguida instanciamos o sprite Player e guardamos ele na variável player, que por sua vez é adicionada ao grupo all_sprites.

Em nosso Game Loop estamos atualizando todos os Sprites do grupo all_sprites e também desenhando eles (neste exemplo é apenas um Sprite). Preenchemos o fundo com a cor preta, que eventualmente nos fornece o seguinte resultado:

img

Efeitos Sonoros

O módulo pygame.mixer permite reproduzir arquivos OGG compactados ou WAV descompactados.

O exemplo a seguir verifica os parâmetros de inicialização e imprime o número de canais disponíveis. Ele inicializa um objeto som e imprime o tempo do arquivo em segundos, ao pressionarmos a tecla [Enter] o som de um Corvo Americano será tocado.

import pygame
pygame.mixer.init()

print(f'init = {pygame.mixer.get_init()}')
print(f'channels = {pygame.mixer.get_num_channels()}')
som = pygame.mixer.Sound('american_crow_spring.ogg')
print(f'length = {som.get_length()}')

while True:
    input('Aperte Enter para tocar o Som')
    som.play()
    print('Tocando o som... CTRL+Z para cancelar')

Para cancelarmos a execução do script podemos usar os comandos CTRL + Z ou CTRL + D.

No exemplo a seguir vamos tocar a Piano Sonata No. 14 de Ludwig van Beethoven, popularmente conhecida como Moonlight Sonata.

from pygame.locals import *
import pygame

WIDTH = 500
HEIGHT = 150
FPS = 60

BLACK = (13, 13, 13)
WHITE = (255, 255, 255)

pygame.init()
pygame.mixer.init()
logo = pygame.image.load("icon.png")
pygame.display.set_icon(logo)
pygame.display.set_caption("Moonlight Sonata - Beethoven")
screen = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()
sound = pygame.mixer.Sound('beethoven.ogg') 
sound.set_volume(0.7)
myriad_pro_font = pygame.font.SysFont("Myriad Pro", 48)
text = myriad_pro_font.render("p = play | s = stop", 1, WHITE)

running = True
while running:
    clock.tick(FPS)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_p:
                sound.play()
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_s:
                sound.stop()

    screen.fill(BLACK)
    screen.blit(text, (100, 50))
    pygame.display.flip()

pygame.quit()

Este exemplo irá nos apresentar a seguinte tela:

img

Usamos a tecla p para dar Play na música e a tecla s para dar Stop.

Perceba também que estamos carregando um ícone de música para customizar nossa janela.

Debugging

Debugging é o processo de detecção e remoção de erros existentes e potenciais (também chamados de ‘bugs’) em um código de software, que podem fazer com que ele se comporte inesperadamente ou falhe. Para evitar a operação incorreta de um software ou sistema, debugging é usada para localizar e resolver bugs ou defeitos.

Sabemos que no Pygame temos um Game Loop e que quando ele está executando é interessante que possamos inspecionar o valor de certas variáveis que estão sofrendo alterações, para isso, poderíamos utilizar o comando print(), porém seria inconveniente devido ao fato de que o valor será impresso muitas vezes na tela e será difícil de rastreá-lo.

Para solucionar este problema com o comando print(), podemos utilizar as próprias funcionalidades da biblioteca Pygame, utilizando o módulo pygame.font para carregar e renderizar fontes.

No exemplo a seguir vamos definir uma função chamada debug que receberá como argumento um valor a ser inspecionado e também as coordenadas em que ele será apresentado na tela:

import pygame 
pygame.init()

WIDTH = 835
HEIGHT = 450
FPS = 30
WHITE = (255, 255, 255)
font = pygame.font.Font(None, 33)

def debug(info, x=10, y=10):
    display_surface = pygame.display.get_surface()
    debug_surface = font.render(str(info), True, WHITE)
    debug_rect = debug_surface.get_rect(topleft=(x,y))
    display_surface.blit(debug_surface, debug_rect)

class Bug(pygame.sprite.Sprite):
    def __init__(self, x, y, scale):
        pygame.sprite.Sprite.__init__(self)
        self.images_right = []
        self.images_left = []
        self.index = 0
        self.counter = 0
        for num in range(1,5):
            img_left = pygame.image.load(f'images/{num}.png').convert_alpha()
            new_dimension = (int(img_left.get_width() * scale), int(img_left.get_height() * scale))
            img_left = pygame.transform.scale(img_left, new_dimension)
            img_right = pygame.transform.flip(img_left, True, False)
            self.images_right.append(img_right)
            self.images_left.append(img_left)   
        self.image = self.images_right[self.index]    
        self.rect = self.image.get_rect()
        self.rect.center = (x, y)
        self.rect.x = x 
        self.rect.y = y
        self.direction = 0

    def update(self):
        dx = 0 
        dy = 0
        walk_cooldown = 4
        key = pygame.key.get_pressed()

        if key[pygame.K_LEFT]:
            dx -= 6
            self.counter += 1
            self.direction = -1
            if self.rect.left <= 0:
                self.rect.left = 0
        if key[pygame.K_RIGHT]:
            dx += 6
            self.counter += 1
            self.direction = 1
            if self.rect.right >= WIDTH:
                self.rect.right = WIDTH
        if key[pygame.K_LEFT] == False and key[pygame.K_RIGHT] == False:
            self.counter = 0
            self.index = 0
            if self.direction == 1:
                self.image = self.images_right[self.index]
            if self.direction == -1:
                self.image = self.images_left[self.index]

        if self.counter > walk_cooldown:
            self.counter = 0
            self.index += 1 
            if self.index >= len(self.images_right):
                self.index = 0
            if self.direction == 1:
                self.image = self.images_right[self.index]
            if self.direction == -1:
                self.image = self.images_left[self.index]

        self.rect.x += dx 
        self.rect.y += dy

pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption('Debugging')
clock = pygame.time.Clock()

background = pygame.image.load('images/bg.png').convert_alpha()
all_sprites = pygame.sprite.Group()
player = Bug(50, 165, 0.3)
all_sprites.add(player)

running = True 
while running:
    screen.blit(background,(0,0))
    clock.tick(FPS)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
    
    all_sprites.update()
    all_sprites.draw(screen)
    debug((player.rect.x, player.rect.y))
    debug(pygame.mouse.get_pressed(), 380)
    debug(pygame.mouse.get_pos(), 725)
    pygame.display.flip()

pygame.quit()

Este exemplo nos trará o seguinte resultado:

img

Observe que na primeira tupla estamos apresentando os valores x e y que representam a posição do jogador, na tupla do meio estamos mostrando o estado de cada botão do mouse (esquerda, centro e direita), sendo 0 indicando que ele não está pressionado e 1 indicando que está, e por fim, na última tupla temos os valores x e y da posição do cursor.

Perceba também que é possível mover o personagem para a esquerda e direita (utilizando as setas do teclado), além disso, estamos animando o personagem ao carregar diversas imagens e realizar um loop circular por elas ao sempre resetar o seu índice.

Com essa técnica de debugging, podemos agora inspecionar as variáveis internas de nossos Games com mais facilidade, fazendo que consigamos solucionar os problemas com mais agilidade e eficiência.

Construindo um Platform Game

Uma vez que adquirimos o conhecimento dos fundamentos essenciais da biblioteca Pygame, vamos agora construir um Platform Game básico que servirá como estrutura para criarmos nossos Games, nos permitindo adicionar mais features a ele quando desejarmos.

Os jogos de plataforma (muitas vezes simplificados como platformers ou jump ‘n’ run) são um gênero de vídeo-game e um subgênero de jogos de ação. Esses Games são caracterizados pelo uso intenso de saltos e escaladas para navegar pelo ambiente do jogador e alcançar seu objetivo. Os níveis e ambientes tendem a apresentar terrenos irregulares e plataformas suspensas de alturas variáveis que exigem o uso das habilidades do personagem do jogador para atravessar.

Games como Super Mario World, Super Castlevania IV e Sonic the Hedgehog são exemplos de platform games.

Neste exemplo, vamos emular um simples Mario 8-bit.

Vamos usar apenas duas imagens:

  • O personagem Mario
  • O tijolo tradicional do Game Mario

O Game contará com apenas 4 Classes e uma função main():

  • CameraLayeredUpdates: Representará a câmera que irá seguir o nosso personagem. Nela também trataremos as colisões do personagem com os blocos.
  • Entity: Representará um objeto genérico que outras classes poderão herdar suas propriedades.
  • Player: Representará nosso personagem.
  • Platform: Representará uma plataforma no qual o personagem poderá caminhar sob e irá colidir.

Na função main() vamos inicializar nosso mapa, os Sprites e desenharemos tudo na tela no Game Loop.

Vejamos então o código para compreendermos melhor:

import pygame

SCREEN_SIZE = pygame.Rect((0, 0, 800, 640))
INITIAL_POS = (35, 700)
BACKGROUND_BLUE = (104, 136, 247)
GRAVITY = pygame.Vector2((0, 0.29))
TILE_SIZE = 32
FPS = 60

class CameraLayeredUpdates(pygame.sprite.LayeredUpdates):
    def __init__(self, target, world_size):
        super().__init__()
        self.target = target
        self.cam = pygame.Vector2(0, 0)
        self.world_size = world_size
        if self.target:
            self.add(target)

    def update(self, *args):
        super().update(*args)
        if self.target:
            x = -self.target.rect.centerx + SCREEN_SIZE.width/2
            y = -self.target.rect.centery + SCREEN_SIZE.height/2
            self.cam += (pygame.Vector2((x, y)) - self.cam) * 0.05
            self.cam.x = max(-(self.world_size.width-SCREEN_SIZE.width), min(0, self.cam.x))
            self.cam.y = max(-(self.world_size.height-SCREEN_SIZE.height), min(0, self.cam.y))
        if self.target.moving_left:
            self.target.image = self.target.flipped
        elif self.target.moving_right:
            self.target.image = self.target.original_image

    def draw(self, surface):
        spritedict = self.spritedict
        surface_blit = surface.blit
        dirty = self.lostsprites
        self.lostsprites = []
        dirty_append = dirty.append
        init_rect = self._init_rect
        for sprite in self.sprites():
            rec = spritedict[sprite]
            newrect = surface_blit(sprite.image, sprite.rect.move(self.cam))
            if rec is init_rect:
                dirty_append(newrect)
            else:
                if newrect.colliderect(rec):
                    dirty_append(newrect.union(rec))
                else:
                    dirty_append(newrect)
                    dirty_append(rec)
            spritedict[sprite] = newrect
        return dirty            

class Entity(pygame.sprite.Sprite):
    def __init__(self, pos, *groups):
        super().__init__(*groups)
        self.image = pygame.Surface((TILE_SIZE, TILE_SIZE))
        self.rect = self.image.get_rect(topleft=pos)

class Player(Entity):
    def __init__(self, platforms, pos, *groups):
        super().__init__(pos)
        self.image_load = pygame.image.load('mario.png').convert_alpha()
        self.original_image = pygame.transform.scale(self.image_load, (55,65))
        self.image = pygame.transform.scale(self.image_load, (55,65))
        self.flipped = pygame.transform.flip(self.image, True, False)
        self.rect = self.image.get_rect(topleft=pos)
        self.vel = pygame.Vector2((0, 0))
        self.on_ground = False
        self.moving_right = False
        self.moving_left = False
        self.platforms = platforms
        self.speed = 6
        self.jump_strength = 9

    def update(self):
        pressed = pygame.key.get_pressed()
        up = pressed[pygame.K_UP]
        left = pressed[pygame.K_LEFT]
        right = pressed[pygame.K_RIGHT]
        running = pressed[pygame.K_SPACE]

        if up:
            # pular apenas se estiver no chão
            if self.on_ground: 
                self.vel.y = -self.jump_strength
        if left:
            self.vel.x = -self.speed
            self.moving_left = True
            self.moving_right = False
        if right:
            self.vel.x = self.speed
            self.moving_right = True
            self.moving_left = False
        if running:
            self.vel.x *= 1.3
        if not self.on_ground:
            # só acelere com a gravidade se estiver no ar
            self.vel += GRAVITY
            # velocidade máxima de queda
            if self.vel.y > 100: 
                self.vel.y = 100
        if not(left or right):
            self.vel.x = 0
        # incrementar na direção x
        self.rect.left += self.vel.x
        # executar colisão no eixo-x
        self.collide(self.vel.x, 0, self.platforms)
        # incrementar na direção y
        self.rect.top += self.vel.y
        # assumindo que estamos no ar
        self.on_ground = False
        # executar a colisão no eixo-y
        self.collide(0, self.vel.y, self.platforms)

    def collide(self, xvel, yvel, platforms):
        for p in platforms:
            if pygame.sprite.collide_rect(self, p):
                if xvel > 0:
                    self.rect.right = p.rect.left
                if xvel < 0:
                    self.rect.left = p.rect.right
                if yvel > 0:
                    self.rect.bottom = p.rect.top
                    self.on_ground = True
                    self.yvel = 0
                if yvel < 0:
                    self.rect.top = p.rect.bottom

class Platform(Entity):
    def __init__(self, pos, *groups):
        super().__init__(pos, *groups)
        self.image_load = pygame.image.load('brick.png').convert_alpha()
        self.image = pygame.transform.scale(self.image_load, (32,32))

def main():
    pygame.init()
    screen = pygame.display.set_mode(SCREEN_SIZE.size)
    pygame.display.set_caption("Mario")
    timer = pygame.time.Clock()

    level = [
        "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP",
        "P                                                       P",
        "P                                                       P",
        "P                                                       P",
        "P                    PPPPPPPPPPP                        P",
        "P                                                       P",
        "P                                            PPPPP      P",
        "P                                                       P",
        "P    PPPPPPPP                                           P",
        "P                                                     PPP",
        "P                          PPPPPPP                      P",
        "P                 PPPPPP                                P",
        "P                                                       P",
        "P         PPPPPPP                                PP     P",
        "P                                               P       P",
        "P                     PPPPPP          PPPPPPPPPP        P",
        "P                                                       P",
        "P   PPPPPPPPPPP                                         P",
        "P                                                       P",
        "P                 PPPPPPPPPPP      PPP        PPPPPPPPPPP",
        "P                                                       P",
        "P                                                       P",
        "P                                                       P",
        "P                                                       P",
        "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP",]

    platforms = pygame.sprite.Group()
    player = Player(platforms, INITIAL_POS)
    level_width  = len(level[0]) * TILE_SIZE
    level_height = len(level) * TILE_SIZE
    entities = CameraLayeredUpdates(player, pygame.Rect(0, 0, level_width, level_height))

    # construir o level do game
    x = y = 0
    for row in level:
        for col in row:
            if col == "P":
                Platform((x, y), platforms, entities)
            x += TILE_SIZE
        y += TILE_SIZE
        x = 0

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT: 
                return
            if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
                return

        entities.update()
        screen.fill(BACKGROUND_BLUE)
        entities.draw(screen)
        pygame.display.update()
        timer.tick(FPS)

if __name__ == "__main__":
    main()

Irei salvar o código como mario.py e executá-lo com o comando python mario.py, que irá me trazer o seguint output:

img

Usamos as Arrow Keys para movimentar o personagem pela tela. Para encerrar o Game podemos clicar no botão fechar ou pressionar a tecla [ESC].

Devemos agora considerar alguns detalhes importantes sobre nosso código.

Vamos usar a classe CameraLayeredUpdates para construir o objeto entities, que receberá o objeto player como argumento e um retângulo que representará nosso mapa. Nela temos dois métodos:

  • update(): Responsável por atualizar a posição do personagem na tela e fixar a câmera no personagem fazendo com que ela siga ele.
  • draw(): Responsável por atualizar apenas certas áreas, essas áreas são chamadas de “dirty rects” porque precisam de um redesenho e normalmente têm uma forma retangular. Para compreender melhor este conceito você poder ler este excelente artigo escrito pelo dr0id.

A classe player será utilizada para construir o objeto que representará nosso personagem, ela recebe como argumento as plataformas e a posição inicial do personagem e conta com dois métodos:

  • update(): Responsável por atualizar a posição do personagem na tela de acordo com a tecla acionada pelo jogador.
  • collide(): Responsável por tratar as colisões com os retângulos.

Um detalhe importante que devemos lembrar é que estamos utilizando o conceito de Vetores para manipular as coordenadas x e y, para encontrar mais detalhes sobre eles, podemos visitar a documentação do módulo math do Pygame.

Na função main() de nosso Game, estamos definindo o mapa de nosso Jogo na variável level, onde todos os P’s serão renderizados como tijolos, perceba que esta variável é uma list de strings.

Finalmente construímos o nosso “level” através da classe Platform, damos início ao Game Loop, atualizamos todas as entities, preenchemos o background com a cor definida na variável BACKGROUND_BLUE, desenhamos as entities na tela e atualizamos o display. Nosso clock é setado para operar em 60 FPS.

Conclusão

Através deste tutorial fomos capazes de aprender diversos conceitos importantes no campo de Desenvolvimento de Games 2D, porém exploramos somente a superfície deste grandioso universo.

Com este conhecimento obtido, podemos agora aprofundar nossa sabedoria, para isso, fiz esta lista de materiais importantes que poderá nos ajudar bastante e que também serviu como inspiração e referência quando escrevi este material.

Você também poderá explorar todos os códigos utilizados neste tutorial no repositório do GitHub: PyGameDev. Espero que você possa aperfeiçoá-los e criar games ultra divertidos.

Boa diversão e bons estudos.