Introdução à Programação Orientada a Objetos em Python com Pygame(parte 2)
Tags: python, oop, gamedev, pygame
Published: 2023-10-04 02:19:09

## Resumo

Este guia tem como objetivo simplificar o ensino da programação orientada a objetos de forma lúdica e acessível, usando Python como linguagem de base e o Pygame como ferramenta de apoio. A programação orientada a objetos é uma abordagem crucial para desenvolvedores, pois oferece um meio organizado e eficiente de criar sistemas complexos e reutilizáveis. Está é a segunda parte do guia, e nela abordaremos conceitos de herança e suas aplicações.

## Requisitos para compreensão desta publicação:

- Noção de programação estruturada
- Compreensão da linguagem Python
- Biblioteca pygame devidamente instalada no ambiente de desenvolvimento
- Ter já ter executado as instruções da [parte-1](https://dev.to/mts-lucas/introducao-a-programacao-orientada-a-objetos-em-python-com-pygameparte-1-22fe) desse guia

## Herança

Herança é um conceito fundamental na programação orientada a objetos, que permite que uma classe herde atributos e métodos de outra classe, criando uma relação de hierarquia entre elas. Isso segue o princípio de reaproveitamento de código, onde se eu preciso de duas classes diferentes porem que tenham bases similares, essas classes vão ter suas distinções porem com uma mesma origem. É um conceito parecido com o que vimos até agora, porém ficara mais claro em código.

### Herança em Python

Em python para se fazer uma classe herdar de outra, basta apenas na criação da classe, entre parênteses inserir a classe que se está herdando.

```python
class Animal:
    def __init__(self, nome):
        self.nome = nome

    def comer(self):
        print(f"{self.nome} está comendo.")


class Cachorro(Animal):
    def latir(self):
        print(f"{self.nome} está latindo.")


class Gato(Animal):
    def miar(self):
        print(f"{self.nome} está miando.")


# Criando objetos
rex = Cachorro("Rex")
mingal = Gato("Mingal")

# Usando métodos da classe base (Animal)
rex.comer()
mingal.comer()

# Usando métodos das subclasses
rex.latir()
mingal.miar()

```
Perceba que quando as subclasses(ou classes filhas) herdam todos os métodos e atributos de sua classe mãe, porém ainda podem possuir seus próprios métodos e atributos únicos, sem interferir na classe mãe ou nas outra subclasses. 

## Polimorfismo

Mas e se quisermos que algum método da classe mãe se comporte de maneira diferente na minha subclasse? No mundo da orientação a objetos isso se chama Polimorfismo, O termo polimorfismo é originário do grego e significa "muitas formas". Em termos simples o polimorfismo é definido quando subclasses usam um método da sua superclasse, que apesar de ter a mesma assinatura (nome) ele irá se comportar de maneira distinta em cada subclasse.

### Reescrever

Quando queremos que algum método da subclasse se comporte de maneira diferente de um método da superclasse, podemos **reescrever** esse método. Para fazer isso em python basta apenas literalmente reescrever o método da superclasse com o mesmo nome na subclasse, só que dessa vez adicionando sua própria lógica.

```python
class Animal:
    def fazer_som(self):
        print("Som genérico de um animal.")

class Cachorro(Animal):
    def fazer_som(self):
        print("Latir!")

```

### Estender

Como o próprio nome diz, estamos **estendendo** uma funcionalidade, ou seja, estamos **adicionando** algo a mais. Quando estendemos um método estamos fazendo com que o novo método que estamos criando, execute o método que herdamos da superclasse e logo após execute nossa funcionalidade, e para isso usamos o operador `super().metodo_da_superclassse()`.

```python
class Veiculo:
    def mover(self):
        print("O veículo está se movendo.")

class Carro(Veiculo):
    def mover(self):
        super().mover()  # Chama o método da superclasse
        print("O carro está se movendo a 60 km/h.")

```

Ao criar um novo objeto da classe Carro e chamarmos o método mover(), ele executará o método vindo da classe Veiculo e logo após o que inserimos, então em seu terminal estará assim:

```python
O veículo está se movendo.
O carro está se movendo a 60 km/h.
```

# De volta ao jogo

Em ultimo momento de nosso projeto, criamos nossa tela mas ainda não possuíamos nada nela, então agora iremos mudar isso e adicionar novos elementos e lógica ao nosso jogo.

## Plano cartesiano no pygame

Antes de prosseguirmos é importante salientar que o pygame segue uma dinâmica de plano cartesiano um pouco diferente, onde o eixo **Y** é invertido, logo se quisermos que algo caia pelo cenário temos de incrementar valores a sua posição Y e não decrementar.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iiiez1gdz3z5mx4rblpr.jpg)

## Novas constantes e sprites

Em seu arquivo de constantes `consts.py` adicione essa linhas, elas serão importantes para o funcionamento das nossas novas classes.

```python
# conf plane
GRAVITY = 0.25
FLAP_STRENGTH = 7

# conf pipe
PIPE_GAP = 150
PIPE_SPEED = 4

# color
BROWN = (163, 128, 104)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
```

Agora no diretório do seu projeto crie um novo subdiretório chamado  `sprites` e adicione as imagens contidas [aqui](https://github.com/mts-lucas/poo-pygame/tree/main/sprites). Com isso feito, no diretório principal crie 3 arquivos com esses nomes: `ground.py`, `pipe.py` e `plane.py`. Logo seu diretório principal deve estar nessa configuração:



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ybfp8amdmbhe6iheru38.png)




## Criando classes

### Plane

Em seu arquivo `plane.py` adicione o seguinte código:

```python
import pygame

import consts


class Plane(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        # importando a imagem
        self.image = pygame.image.load("sprites/plane.png")
        # criando caixa em torno da imagem
        self.rect = self.image.get_rect()
        self.rect.center = (100, consts.SCREEN_HEIGHT // 2)
        self.velocity = 0

    def update(self):
        self.velocity += consts.GRAVITY
        self.rect.y += self.velocity

    def flap(self):
        self.velocity = -consts.FLAP_STRENGTH
```

Estamos herdando de uma classe predefinida do pygame `Sprites`, e **estendendo** seu **método construtor** para moldarmos a nossa necessidade. Estamos reescrevendo o método `update()` que vem da superclasse, que é responsável por atualizar o objeto na tela com o passar dos frames, adicionando nossa lógica de queda. Por ultimo estamos criando nosso método `flap()` que será responsável pelo "salto" do avião. Todas as próximas  classes vão seguir o mesmo esquema de construção.

### Pipe

Em seu arquivo `pipe.py` adicione o seguinte código:

```python
import pygame

import consts


class Pipe(pygame.sprite.Sprite):
    def __init__(self, x, y, path):
        super().__init__()
        self.image = pygame.image.load(path)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.speed = consts.PIPE_SPEED

    def update(self):
        # a cada momento atualizada a posicao
        self.rect.x -= self.speed
        if self.rect.right < 0:
            self.kill()
```

### Ground

Em seu arquivo `ground.py` adicione o seguinte código:

```python
import pygame

import consts


class Ground(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.Surface((consts.SCREEN_WIDTH, 20))
        self.image.fill(consts.BROWN)
        self.rect = self.image.get_rect()
        self.rect.x = 0
        self.rect.y = consts.SCREEN_HEIGHT - 20
```


## Criando objeto dentro da tela

Com todos os passos anteriores feitos, no nosso arquivo `game.py` devemos fazer as seguintes alterações:

```python
import pygame

import consts
from plane import Plane


class Game:
    def __init__(self):
        # iniciando o pygame
        pygame.init()
        # criando tela
        self.screen = pygame.display.set_mode(
            (consts.SCREEN_WIDTH, consts.SCREEN_HEIGHT))
        pygame.display.set_caption(consts.TITLE)
        self.clock = pygame.time.Clock()
        self.running = True

        # Inicialização de grupos de sprites
        self.all_sprites = pygame.sprite.Group()
        self.plane = Plane()
        self.all_sprites.add(self.plane)

    def game_events(self):

        # retorna todos os objetos do tipo event
        for event in pygame.event.get():
            # acessa o atributo type do objeto event
            if event.type == pygame.QUIT:
                self.running = False

            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE:
                    self.plane.flap()

    def run(self):
        while self.running:
            # o metodo tick define os FPS do jogo
            self.clock.tick(consts.FPS)
            self.game_events()
            # desenha as imagens na tela
            self.all_sprites.draw(self.screen)
            # chama o metodo update das classes
            self.all_sprites.update()
            pygame.display.flip()
```

Agora adicionamos novos atributos que vão tomar conta das sprites na tela, e criamos um novo objeto da classe `Plane`. Nos eventos adicionamos um novo evento que verifica as teclas pressionadas, e ao apertar a tecla `SPACE` iremos chamar o método `flap()` do nosso objeto plane. Por ultimo no loop do nosso jogo, chamamos os _métodos_ que desenham os sprites na tela, e atualizam a tela de acordo com o **clock** que definimos**(60 FPS)**. Volte ao arquivo `main.py` e o execute agora.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ran2gok6jpjtnixxbdlu.png)

Parece errado? E está mesmo! Nosso objeto está sendo desenhado na tela porem a ultima posição em que ele estava ainda fica desenhada tambem, gerando essa situação feia. Resolveremos isso de forma simples, entre um frame e outro nossa tela vai ser **limpa**, fazendo com que tudo na tela seja apagado, porem no frame seguinte quando o loop iterar, a imagem irá se redesenhar, dando a ilusão de movimento. Vamos fazer isso em código, modifique seu método `run()` com o trecho a seguir e verá tudo funcionando como devia.

```python
    def run(self):
        while self.running:
            # o metodo tick define os FPS do jogo
            self.clock.tick(consts.FPS)
            self.game_events()
            # desenha as imagens na tela
            self.all_sprites.draw(self.screen)
            # chama o metodo update das classes
            self.all_sprites.update()
            pygame.display.flip()
            # sem isso a posicao anterior dos desenhos continua na tela
            self.screen.fill(consts.BLACK)
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ch6bow5objerpv9ozbzu.png)

## Adicionando os canos e nossas telas

Agora que nossa mecânica de voo está totalmente funcional, precisamos adicionar obstáculos a nossa tela. Vamos adicionar o chão e os canos a tela, modifique seu arquivo game.py dessa maneira:

```python
import random

import pygame

import consts
from ground import Ground
from pipe import Pipe
from plane import Plane


class Game:
    def __init__(self):
        # iniciando o pygame
        pygame.init()
        # criando tela
        self.screen = pygame.display.set_mode(
            (consts.SCREEN_WIDTH, consts.SCREEN_HEIGHT))
        pygame.display.set_caption(consts.TITLE)
        self.clock = pygame.time.Clock()
        self.running = True

        # Inicialização de grupos de sprites
        self.all_sprites = pygame.sprite.Group()
        self.plane = Plane()
        self.ground = Ground()
        self.all_sprites.add(self.plane, self.ground)
        self.pipes = pygame.sprite.Group()

    def game_events(self):

        # retorna todos os objetos do tipo event
        for event in pygame.event.get():
            # acessa o atributo type do objeto event
            if event.type == pygame.QUIT:
                self.running = False

            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE:
                    self.plane.flap()

    def run(self):
        while self.running:
            # o metodo tick define os FPS do jogo
            self.clock.tick(consts.FPS)
            self.game_events()
            # desenha as imagens na tela
            self.all_sprites.draw(self.screen)

            if len(self.pipes) < 2:
                self.spawn_pipe()
            self.remove_pipes()

            # chama o metodo update das classes
            self.all_sprites.update()
            pygame.display.flip()
            # sem isso a posicao anterior dos desenhos continua na tela
            self.screen.fill(consts.BLACK)

    def randomize_height(self):
        # Define a altura aleatória dos canos
        self.y = random.randint(100, 500)

    def spawn_pipe(self):
        self.randomize_height()
        self.pipe_bot = Pipe(300, self.y, "sprites/pipe.png")
        self.pipe_top = Pipe(
            300,
            (self.y - consts.PIPE_GAP - 500),
            "sprites/pipe_top.png")
        self.pipes.add(self.pipe_bot, self.pipe_top)
        self.all_sprites.add(self.pipe_bot, self.pipe_top)

    def remove_pipes(self):
        for pipe in self.pipes:
            if pipe.rect.x < 0:
                self.pipes.remove(pipe)
                self.all_sprites.remove(pipe)
```
Adicionamos muitos novos métodos e atributos, porém não é nada que já não tenhamos trabalhado antes. Agora temos o nosso chão de forma fixa, e os canos que tem a altura gerada aleatoriamente pelo método  `randomize_height()`. Quando um cano chega o final da tela, um novo cano é gerado e o anterior é descartado do nosso grupo de sprites a serem exibidas. Com tudo pronto, execute o arquivo `main.py`.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j1aeghmvf6jxht2bukyf.png)

Com o código rodando em sua tela deve aparecer algo do gênero. Todas classes estão operantes e funcionais no nosso programa, porem ainda falta um ultimo **tchan** para finalizarmos. Precisamos reconhecer quando nosso avião se choca contra o solo ou contra um dos canos, então vamos modificar mais uma vez nosso código, e aproveitar o embalo e inserir alguns adicionais gráficos. Em `game.py` cole o código a seguir:

```python
import random
import sys

import pygame

import consts
from ground import Ground
from pipe import Pipe
from plane import Plane


class Game:
    def __init__(self):
        # iniciando o pygame
        pygame.init()
        # criando tela
        self.screen = pygame.display.set_mode(
            (consts.SCREEN_WIDTH, consts.SCREEN_HEIGHT))
        pygame.display.set_caption(consts.TITLE)
        self.clock = pygame.time.Clock()
        self.running = True

        # Inicialização de grupos de sprites
        self.all_sprites = pygame.sprite.Group()
        self.plane = Plane()
        self.ground = Ground()
        self.all_sprites.add(self.plane, self.ground)
        self.pipes = pygame.sprite.Group()

        # score
        self.score = 0

    def game_events(self):

        # retorna todos os objetos do tipo event
        for event in pygame.event.get():
            # acessa o atributo type do objeto event
            if event.type == pygame.QUIT:
                self.running = False

            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE:
                    self.plane.flap()

    def run(self):
        while self.running:
            # o metodo tick define os FPS do jogo
            self.clock.tick(consts.FPS)
            self.game_events()
            # desenha as imagens na tela
            self.all_sprites.draw(self.screen)

            if len(self.pipes) < 2:
                self.spawn_pipe()
            self.remove_pipes()

            # Verifica colisões com o solo
            hits = pygame.sprite.spritecollide(
                self.plane, [self.ground], False)
            if hits:
                self.running = False

            # Verifica colisões com os canos
            hits = pygame.sprite.spritecollide(self.plane, self.pipes, False)
            if hits:
                self.running = False

            # Mostra a pontuação na tela
            self.font = pygame.font.Font(None, 36)
            self.score_text = self.font.render(
                f"Score: {self.score}", True, consts.WHITE)
            self.screen.blit(self.score_text, (10, 10))

            # chama o metodo update das classes
            self.all_sprites.update()
            pygame.display.flip()
            # sem isso a posicao anterior dos desenhos continua na tela
            self.screen.fill(consts.BLACK)

        pygame.quit()
        sys.exit()

    def randomize_height(self):
        # Define a altura aleatória dos canos
        self.y = random.randint(100, 500)

    def spawn_pipe(self):
        self.randomize_height()
        self.pipe_bot = Pipe(300, self.y, "sprites/pipe.png")
        self.pipe_top = Pipe(
            300,
            (self.y - consts.PIPE_GAP - 500),
            "sprites/pipe_top.png")
        self.pipes.add(self.pipe_bot, self.pipe_top)
        self.all_sprites.add(self.pipe_bot, self.pipe_top)

    def remove_pipes(self):
        for pipe in self.pipes:
            if pipe.rect.x < -80:
                print('entrou')
                self.pipes.remove(pipe)
                self.all_sprites.remove(pipe)
                self.score += 1
```
Com essas mudanças feitas, agora temos verificações se as nosso objetos na tela estão colidindo contra nosso avião, caso ocorra o jogo se encerrará. Com isso nosso joguinho agora está totalmente funcional, e tudo isso foi feito usando os conceitos de OO que aprendemos até aqui.

## Considerações finais

O nosso jogo está pronto mas ainda há muito a aprender, o mundo da orientação a objetos ainda tem muito conteúdo porem com o que foi visto durante esse guia, já é mais do que o suficiente para entender os demais conceitos. Estarei deixando uma lista de tópicos relevantes para se estudar a respeito:

- Encapsulamento
- Sobrecarga
- Herança Múltipla
- Classes abstratas

E caso tenha gostado da nossa brincadeira **pygame** você pode dar uma olhada na [documentação](https://www.pygame.org/docs/) da biblioteca e começar a fazer seus próprios projetos. E se quiser visualizar esse projeto completo, ele está disponível na minha página do [github](https://github.com/mts-lucas/poo-pygame/tree/main).

Agradeço demais por me acompanhar até aqui com essa ideia. Planejar esse guia foi muito prazeroso, pois foi literalmente com essa biblioteca, fazendo pequenos jogos, que eu pude fixar e ver na prática o que estava aprendendo com OO pela primeira vez, então fiz tudo isso com muito carinho. Espero poder estar trazendo novos conteúdos logo mais, dito isso...

**OBRIGADO PELA ATENÇÃO!**