Aula 04 – Criando games em python – Movimento e Colisões
Aula 04 – Criando games 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.
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:
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:
Por outro lado, uma colisão do lado direito do tijolo seria a seguinte:
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:
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. 😉