Aula 04 – Criando games em python – Movimento e Colisões

Aula 04 – Criando games em python – Movimento e Colisões

Criando games em python - Movimento e Colisões

Criando jogos em python – Movimento e Colisões

Voltar para página principal do blog

Todas as aulas desse curso

Aula 03            Aula 05

Se gostarem do conteúdo dêem um joinha 👍 na página do Código Fluente no
Facebook

Esse é o link do código fluente no Pinterest

Meus links de afiliados:

Hostinger

Digital Ocean

One.com

Para baixar o código acesse o link abaixo:

https://github.com/toticavalcanti/curso_python_games/aula_04/

Link da documentação oficial do Tkinter:

https://tkdocs.com/

Movimento e Colisões

Agora que colocamos todos os nossos objetos dentro da tela do jogo, o rebatedor, os tijolos, a bola, o texto que informa a quantidade de vidas restante, podemos definir então, os métodos que serão executados no loop do jogo.

Esse loop é executado indefinidamente até o final do jogo, e a cada iteração atualiza a posição da bola e verifica a colisão que ocorre.

Com o widget Canvas, podemos calcular quais itens se sobrepõem à determinadas coordenadas, então, por enquanto, implementaremos os métodos responsáveis por mover a bola e por mudar a direção.

Obs. O código completo dessa aula está no meu github.

Dicas de livros relacionados:

                                   

Colisão com as bordas da tela

Vamos começar com o movimento da bola e as condições para criar o efeito de quicar ela quando atingir as bordas da tela.

A canvas.create_oval(x0, y0, x1, y1, options) é usada para criar um círculo ou uma elipse nas coordenadas fornecidas para representar a bola.

São necessários dois pares de coordenadas os cantos superior esquerdo e inferior direito do retângulo delimitador da bola.

Coordenadas da bola

Coordenadas da bola


def update(self):
    coords = self.get_position()
    width = self.canvas.winfo_width()
    #verifica se a bola atingiu a borda esquerda ou direita da tela
    if coords[0] <= 0 or coords[2] >= width:
        #inverte a direção do sentido da bola
        self.direction[0] *= -1
    #Verifica se a bola atingiu o topo da tela (y <= 0)
    if coords[1] <= 0:
        self.direction[1] *= -1
    x = self.direction[0] * self.speed
    y = self.direction[1] * self.speed
    self.move(x, y)

O método de update() faz o seguinte:

    • Obtém a posição atual do objeto e a largura da tela. Armazena respectivamente os valores em coords e na variável local width.
    • Depois, no if, é verificado se a posição colide com a borda esquerda ou direita da tela (coords[0] <= 0 or coords[2] >= width), se isso acontece, o componente horizontal do vetor de direção muda de sinal (self.direction[0] *= -1),
    • Se a posição colide com a borda superior da tela(if coords[1] <= 0), o componente vertical do vetor de direção muda de sinal(self.direction[1] *= -1).
    • Escalamos o vetor de direção pela velocidade da bola.
    • O self.move(x, y) move a bola.

Então por exemplo, se a bola bate na borda esquerda, a condição de coords[0] <= 0 é avaliada como true, portanto, o componente do eixo x da direção altera seu sinal, conforme mostrado na imagem a seguir:

Direção da bola de baixo para cima quicando a esquerda

Direção da bola de baixo para cima quicando a esquerda

Se a bola atingir o canto superior direito, ambas as coords[2] >= width or coords[1] <= 0 for avaliado como true, o sinal de ambos os componentes do vetor de direção será alterado dessa forma

colisão com um tijolo

A lógica da colisão com um tijolo é um pouco mais complexa, pois a direção do rebote depende do lado onde a colisão ocorre.

Vamos calcular o componente do eixo x do centro da bola e verificar se está entre as coordenadas mais baixa e mais alta do eixo x do tijolo em colisão.

Para traduzir isso em uma implementação rápida, o seguinte snippet de código mostra as possíveis mudanças no vetor de direção de acordo com as coordenadas da bola e do tijolo:


coords = self.get_position()
x = (coords[0] + coords[2]) * 0.5
brick_coords = brick.get_position()
if x > brick_coords[2]:
    self.direction[0] = 1
elif x < brick_coords[0]:
    self.direction[0] = -1
else:
    self.direction[1] *= -1

Por exemplo, essa colisão causa um rebote horizontal, uma vez que o tijolo está sendo atingido na parte de cima, como mostrado figura logo abaixo:

Rebote horizontal da bola

Rebote horizontal da bola

Por outro lado, uma colisão do lado direito do tijolo seria a seguinte:

 Rebote do lado direito do tijolo

Rebote do lado direito do tijolo

Isso é válido quando a bola bate no rebatedor ou em um único tijolo.

No entanto, a bola pode bater em dois tijolos ao mesmo tempo.

Nesta situação, não podemos executar as instruções anteriores para cada tijolo.

Se a direção do eixo y for multiplicada por -1 duas vezes, o valor na próxima iteração do loop do jogo será o mesmo.

Poderíamos verificar se a colisão ocorreu de cima ou de trás, mas, o problema com vários tijolos é que a bola pode se sobrepor à lateral de um dos tijolos e portanto, alterar também a direção do eixo x.

Isso acontece por causa da velocidade da bola e a taxa na qual sua posição é atualizada.

Simplificaremos isso assumindo que uma colisão com vários tijolos ao mesmo tempo ocorre apenas de cima ou de baixo.

Isso significa que ele altera a direção do componente no eixo y  sem calcular a posição dos tijolos que colidem.

O código abaixo verifica se a bola colidiu com mais de um tijolo.

Se sim, inverte o direction na posição 1.


if len(game_objects) > 1:
    self.direction[1] *= -1

Com essas duas condições, podemos definir o método de colisão.

Como veremos mais a frente, outro método será responsável por determinar a lista de tijolos colididos, portanto, o método lida apenas com o resultado de uma colisão com um ou mais tijolos:


def collide(self, game_objects):
    coords = self.get_position()
    x = (coords[0] + coords[2]) * 0.5
    if len(game_objects) > 1:
        self.direction[1] *= -1
    elif len(game_objects) == 1:
        game_object = game_objects[0]
        coords = game_object.get_position()
        if x > coords[2]:
            self.direction[0] = 1
        elif x < coords[0]:
            self.direction[0] = -1
        else:
            self.direction[1] *= -1
    for game_object in game_objects:
        if isinstance(game_object, Brick):
            game_object.hit()

Veja que esse método trata todas as ocorrências de colisão que estão ocorrendo com a bola, portanto, o contador de hits, isto é, o contador de vezes que o tijolo foi atingido pela bola, vai sendo decrementado e os tijolos são removidos quando atingem zero.

Por fim, criamos a funcionalidade necessária para executar o ciclo do jogo, a lógica necessária para atualizar a posição da bola de acordo com os rebotes e reiniciar o jogo se o jogador perdeu uma vida.

Concluindo o desenvolvimento do nosso jogo

Agora podemos adicionar os seguintes métodos à nossa classe Game


def start_game(self):
    self.canvas.unbind('<space>')
    self.canvas.delete(self.text)
    self.paddle.ball = None
    self.game_loop()
def game_loop(self):
    self.check_collisions()
    num_bricks = len(self.canvas.find_withtag('brick'))
    if num_bricks == 0:
        self.ball.speed = None
        self.draw_text(300, 200, 'You win, congratulations!')
    elif self.ball.get_position()[3] >= self.height:
        self.ball.speed = None
        self.lives -= 1
        if self.lives < 0:
            self.draw_text(300, 200, 'Game Over')
        else:
            self.after(1000, self.setup_game)
    else:
        self.ball.update()
        self.after(50, self.game_loop)

O método start_game(), que deixamos de implementar nas aulas anteriores, é responsável por desvincular a tecla de barra de espaço para que o jogador não possa iniciar o jogo duas vezes, retirando a bola do rebatedor e iniciando o loop do jogo.

Passo a passo, o método game_loop() faz o seguinte:

    • Ele chama self.check_collisions() para processar as colisões da bola. Vamos ver a sua implementação no próximo snippet de código.
    • Se o número de tijolos restantes for zero, significa que o jogador venceu e um texto de parabéns(“You win, congratulations!“) é exibido.
    • Suponha que a bola tenha atingido a parte inferior da tela:
      • Então, o jogador perde uma vida. Se o número de vidas restantes for zero, significa que o jogador perdeu e o texto “Game Over” é exibido. Caso contrário, o jogo segue.
    • Caso contrário, é isso que acontece:
      • A posição da bola é atualizada de acordo com sua velocidade e direção, e o loop do jogo é chamado novamente. O método .after(delay, callback) no widget Tkinter, define um tempo limite para chamar uma função após um atraso em milissegundos, como esta declaração será executada quando o jogo ainda não acabou, isso cria o loop necessário para executar essa lógica continuamente:

def check_collisions(self):
    ball_coords = self.ball.get_position()
    items = self.canvas.find_overlapping(*ball_coords)
    objects = [self.items[x] for x in items \
        if x in self.items]
    self.ball.collide(objects)

O método check_collisions() vincula o loop do jogo ao método de colisão da bola.

Como ball.collide() recebe uma lista de objetos do jogo e canvas.find_overlapping() retorna todos os itens que se sobrepõem ao retângulo definido por X1, Y1, X2, Y2.

Ele retorna uma lista de itens em colisão com uma determinada posição, usamos o dicionário de itens para transformar cada item de tela em seu objeto de jogo correspondente.

Lembre-se de que o atributo de itens da classe Game contém apenas os itens de tela que podem colidir com a bola.

Portanto, precisamos passar apenas os itens contidos neste dicionário.

Depois de filtrarmos os itens da tela que não podem colidir com a bola, como o texto exibido no canto superior esquerdo, recuperamos cada objeto do jogo por sua chave.

list comprehensions

Obs. Para saber mais sobre list comprehensions, acesse a aula 18 do curso de python aqui do código fluente.

Com list comprehensions, podemos criar a lista necessária em uma declaração simples:


objects = [self.items[x] for x in items if x in self.items]

A sintaxe básica de list comprehensions é a seguinte:


new_list = [expr(elem) for elem in collection]

Isso significa que a variável new_list será uma lista cujos elementos são o resultado da aplicação da função expr() a cada elem na lista.

Podemos filtrar os elementos aos quais a expressão será aplicada adicionando uma cláusula if:


new_list = [expr(elem) for elem in collection if elem is not None]Visualizar (abrir em uma nova aba)

Essa sintaxe é equivalente ao seguinte loop:


new_list = []
for elem in collection:
    if elem is not None:
        new_list.append(elem)

No nosso caso, a lista inicial é a lista de itens em colisão.

A cláusula if filtra os itens que não estão contidos no dicionário e a expressão aplicada a cada elemento recupera o objeto do jogo associado ao item de tela.

O método de colisão collide() é chamado com essa lista como parâmetro e a lógica do loop do jogo é concluída.

Baixe o código completo em:

https://github.com/toticavalcanti/curso_python_games/aula_04/

Jogando Breakout

Abra o script game_version_03_complete_game.py para ver a versão final do jogo e execute-o com:


python game_version_03_complete.py

Quando você pressiona a barra de espaço, o jogo inicia e o jogador controla o rebatedor com as teclas de seta para direita e esquerda.

Cada vez que o jogador erra a bola, o contador de vidas diminuirá e o jogo terminará se a bola ricochetear novamente e não houver vidas restantes:

Tela do Jogo Breakout

Em nosso primeiro jogo, todas as classes foram definidas em um único script.

No entanto, como o número de linhas de código ficou muito grande, é melhor definir scripts separados para cada parte.

Nas próximas aulas, veremos como é possível organizar nosso código por módulos.

Resumo do que aprendemos fazendo o jogo Breakout

Criamos nosso primeiro jogo com Python.

Abordamos o básico do fluxo de controle e sintaxe de classe.

Usamos widgets Tkinter, especialmente o widget Canvas e seus métodos, para obter a funcionalidade necessária para desenvolver um jogo baseado em colisões e detecção de entrada simples.

Nosso jogo Breakout pode ser personalizado como queremos.

Sinta-se à vontade para alterar os padrões de cores, a velocidade da bola ou o número de linhas de tijolos.

Entretanto, as bibliotecas da GUI são muito limitadas e estruturas mais complexas são necessárias para alcançar uma gama mais ampla de recursos.

Nas próximas aulas, apresentaremos o Cocos2d, um estrutura de jogo que nos ajudará com o desenvolvimento do nosso próximo jogo.

Vlw \o/ 😉

Ficamos por aqui e até a próxima.

Aula 03            Aula 05

Todas as aulas desse curso

Voltar para página principal do blog

Para baixar o código acesse o link abaixo:

https://github.com/toticavalcanti/curso_python_games/aula_04/

Se gostarem do conteúdo dêem um joinha 👍 na página do Código Fluente no
Facebook

Link do código fluente no Pinterest

Novamente deixo meus link de afiliados:

Hostinger

Digital Ocean

One.com

Obrigado, até a próxima e bons estudos. 😉

 

About The Author
-

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>