Nesta terceira parte, temos os seguintes objetivos:
- Limpar as classes
- Vários aviões
- Múltiplos tiros.
- Generalizar os objetos do jogo em uma super classe
- Exibir um placar
- Atribuir teclas para atirar e jogar de novo ou sair
Na versão da Parte II, as classes tem muito código repetido.
Analisando cada uma delas, podemos chegar a conclusão de um comportamento comum quanto
a forma de desenhar e atualizar os objetos. Um método para retirar o objeto do jogo também é utilizado.
Todo objeto do jogo precisa ter acesso ao jogo em sim.
Embora quase não utilizemos cores, eu adicionei a cor como parâmetro, uma vez que pode ser usada para representar múltiplos jogadores ou diferentes tipos de objetos.
Uma das coisas que não ficaram claras na versão anterior é o controle de colisão. Na realidade, o controle de colisão que utilizamos é bem básico e difícil de generalizar. Para permitir o controle da colisão entre objetos, cada objeto terá uma posição x e y na tela, com altura e largura.
Desta forma, dois objetos colidem se o retângulo formado pelos pontos:
(x, y), (x + largura, y), (x + largura, y + altura), (x, y + altura)
De dois objetos tiverem pontos em comum.
Como diferentes tipos de objetos colidem entre si, a propriedade colisão guarda o tipo de objeto.
Desta forma, podemos separar os tiros do avião em si.
Uma variável de DEBUG também foi adicionada, para mostrar o retângulo de colisão caso precisemos fazer ajustes.
class ObjetoDoJogo: def __init__(self, jogo, cor): self.jogo = jogo self.cor = cor self.ativo = True self.colisao = FUNDO self.largura = 0 self.altura = 0 self.x = 0 self.y = 0 self.debug = DEBUG
Se debug estiver ativo, vamos desenhar uma caixa em volta do objeto.
Desta forma, fica mais fácil detectar erros nos limites e no tamanho do objeto.
def desenha(self): if self.debug: set_color(2) pyglet.graphics.draw(4, pyglet.gl.GL_LINE_LOOP, ('v2f', (self.x, self.y, self.x + self.largura, self.y, self.x + self.largura, self.y + self.altura, self.x, self.y + self.altura)))
A atualização das propriedades, embora presente em todos os objetos, não tem um comportamento padrão.
Vamos utilizar apenas pass para marcar o lugar.
def atualiza(self): """Calcula nova posição, atualiza valores a cada frame""" pass
Aqui um método para dar uma oportunidade ao objeto de tratar a colisão com outro objeto.
Este método será chamado pelo jogo, sempre que dois objetos colidirem.
def colidiu(self, outro): """O que fazer em caso de colisao? Recebe o outro objeto""" pass
E este aqui é o teste de colisão em si, bem simples. Ele só funciona com larguras e alturas positivas.
def colide(self, outro): if self.x < outro.x + outro.largura and self.x + self.largura > outro.x and \ self.y < outro.y + outro.altura and self.altura + self.y > outro.y: self.colidiu(outro) outro.colidiu(self)
Quando a colisão é detectada, o método colidiu dos dois objetos é chamado.
E finalmente o método para desativar e retirar o objeto do jogo:
def desativa(self): self.ativo = False self.jogo.desativa(self)
Vejamos como ficou a classe do Avião:
class Aviao(ObjetoDoJogo): def __init__(self, jogo, cor=3): super(Aviao, self).__init__(jogo, cor) self.velocidade = (random.randint(0, 6) + 4) * RATIO self.y = 159 - (random.randint(0, 135) + 11) self.x = self.velocidade self.altura = 9 self.largura = 15 self.colisao = AVIAO self.escapou = False def desenha(self): super().desenha() y = self.y + self.altura x = self.x set_color(self.cor) pyglet.graphics.draw(6, pyglet.gl.GL_LINE_LOOP, ('v2f', (x, y, x, y - 8, x + 3, y - 2, x + 12, y - 2, x + 14, y, x, y))) def atualiza(self): self.x += self.velocidade if self.x > 265: self.escapou = True self.desativa() def colidiu(self, outro): if outro.colisao == TIRO: self.jogo.objetos.append(Explosao(self.jogo, self.x, self.y)) self.desativa()
Nesta versão, o nome das propriedades já foi ajustado. Não utilizo mais altura_do_aviao que se tornou apenas a coordenada y. Na classe Avião, adicionei a propriedade escapou para marcar os aviões que saem da tela sem terem sido destruídos, dos que são atingidos por mísseis. Mais tarde, esta informação será utilizada para contar pontos.
Quando o avião colide com um Tiro, um novo tipo de objeto, Explosao é adicionado ao jogo. Desta forma, teremos um efeito simples quando o avião for atingido.
Uma das vantagens de se trabalhar com objetos é a facilidade de criá-los. No caso, a explosão é apenas outro objeto adicionado ao jogo.
class Explosao(ObjetoDoJogo): def __init__(self, jogo, x, y): super(Explosao, self).__init__(jogo, cor=0) self.estagio = 8 self.x = x self.y = y def desenha(self): dx = 4 * self.estagio #random.randint(1, 4) dy = random.randint(1, 4) * self.estagio dx2 = random.randint(1, 4) * self.estagio dy2 = random.randint(1, 4) * self.estagio y = self.y set_color(7) pyglet.graphics.draw(3, pyglet.gl.GL_TRIANGLES, ('v2f', (self.x, y, self.x - dx, y + dy, self.x + dx2, y + dy2))) set_color(8) pyglet.graphics.draw(3, pyglet.gl.GL_TRIANGLES, ('v2f', (self.x, y, self.x -dx/self.estagio , y + dx, self.x + dx2 * self.estagio, y + dy2))) def atualiza(self): self.estagio -= 0.4 if(self.estagio < 0): self.desativa()
A ideia da explosão é que triângulos amarelos e vermelhos, de tamanhos aleatórios apareçam na tela.
Como a explosão não se move, a propriedade estagio foi adicionada para controlar seu tempo de vida.
No caso, a cada frame, subtrairemos 0.4 de estagio e quando este for negativo, o objeto será retirado do jogo.
A classe Tiro foi também melhorada:
class Tiro(ObjetoDoJogo): def __init__(self, jogo, posicao): super(Tiro, self).__init__(jogo, cor=3) self.y = 170 self.x = (posicao + 1) * 70 self.velocidade = 5 * RATIO self.colisao = 2 self.altura = 5 self.largura = 2 self.errou = False def desenha(self): super().desenha() set_color(self.cor) y = self.y + self.altura pyglet.graphics.draw(2, pyglet.gl.GL_LINES, ('v2f', (self.x, y, self.x, y - 4))) v = 4 a = random.randint(3, 4) set_color(7) pyglet.graphics.draw(3, pyglet.gl.GL_TRIANGLES, ('v2f', (self.x, y, self.x - 1, y + v, self.x + 1, y + v))) set_color(8) pyglet.graphics.draw(3, pyglet.gl.GL_TRIANGLES, ('v2f', (self.x, y, self.x - 0.5, y + a, self.x + 0.5, y + a))) def atualiza(self): self.y -= self.velocidade if self.y < 0: self.errou = True self.desativa() def colidiu(self, outro): if outro.colisao == AVIAO: self.desativa()
O Tiro agora tem um foguete, o efeito é interessante, embora simples.
Quando o tiro sai da tela sem atingir um avião, este é considerado um erro e o jogador é penalizado.
As bases são também desenhadas como objetos do jogo. Nesta versão eu mudei o desenho.
class Base(ObjetoDoJogo):
def __init__(self, jogo, posicao):
super(Base, self).__init__(jogo, cor=3)
self.y = 175
self.x = (posicao + 1) * 70
def desenha(self):
super().desenha()
set_color(self.cor)
pyglet.graphics.draw(5, pyglet.gl.GL_LINE_STRIP,
('v2f', (self.x - 5, self.y + 5,
self.x - 3, self.y + 2,
self.x, self.y,
self.x + 3, self.y + 2,
self.x + 5, self.y + 5,
)))
A classe do jogo em si foi simplificada:
class Game: def __init__(self, parent): self.cor = CORES self.window = parent self.tiros_disparados = 0 # tiros já disparados self.estado = "jogando" self.pressionado = False self.label = None self.objetos = [Aviao(self)] self.objetos.extend([Base(self, base) for base in range(0, 3)]) self.pontos = 0 self.placar = pyglet.text.Label("Pontos: 0", font_name="Times New Roman", font_size=16, x=80, y=-185 * 4, anchor_x='center', anchor_y='center')
Agora temos um contador de pontos e um placar. Um objeto da classe Avião e as três bases são adicionadas ao jogo inicial.
def desativa(self, objeto): self.objetos.remove(objeto) if objeto.colisao == AVIAO: if objeto.escapou: self.pontos -= 3 else: self.pontos += 5 self.objetos.append(Aviao(self)) elif objeto.colisao == TIRO: if objeto.errou: self.pontos -= 1
A desativação de um objeto é a retirada deste da lista de objetos do jogo.
Quando é um avião, verifica se o avião escapou da tela sem ter sido atingido.
Se escapou, o jogador perde 3 pontos. Se foi destruído, o jogador ganha 5 pontos.
A cada tiro disparado que sai da tela sem atingir um avião, o jogador perde 1 ponto.
Agora que temos o placar a mostrar, precisamos modificar o método mostra_mensagem.
Este deve exibir o placar sempre:
def mostra_mensagem(self): glPushMatrix() glScalef(0.25, -0.25, 0.25) if self.label: self.label.draw() self.placar.text = "Pontos: {}".format(self.pontos) self.placar.draw() glPopMatrix()
O glPushMatrix e glPopMatrix foram utilizados para corrigir o desenho do texto.
Como estamos utilizando uma escala 4 com inversão das coordenadas do eixo Y, o glScalef
foi utilizado com 0.25 (que na multiplicação é o mesmo que 1/4 ou que dividir por 4) e
-0.25 no eixo Y, que divide por 4 e inverte o sinal.
O glPushMatrix é utilizado para salvar a matriz atual na pilha, desta forma o glScalef
será utilizado apenas até o glPopMatrix que restaura os parâmetros anteriores.
O método atualiza foi simplificado:
def atualiza(self): self.jogue() if self.estado != "jogando": if self.pressionado: pyglet.app.event_loop.exit() self.mostra_mensagem()
O método jogue sempre é chamado. Desta forma podemos ter o desenho do jogo, mesmo quando exibimos uma mensagem de fim por exemplo. Nesta versão, não há mudança de estado, pois eu modifiquei o jogo para gerar outro avião sempre que um for destruído ou sumir, não tendo fim.
E o método jogue também simplificado:
def jogue(self): for objeto in self.objetos: objeto.desenha() self.processa_tiros() if self.estado == "jogando": for objeto in self.objetos: objeto.atualiza() if self.pressionado: self.objetos.append(Tiro(self, self.tiros_disparados % 3)) self.tiros_disparados += 1 self.pressionado = False
Ele apenas desenha todos os objetos da lista e chama processa_tiros que testa colisões.
Se ainda estivermos jogando, chama a atualização de todos os objetos da lista e se algo foi pressionado, gera um tiro. Para termos vários tiros, a base que o dispara é calculada pelo módulo do número do tiro e o número de bases.
def processa_tiros(self): tiros = [objeto for objeto in self.objetos if objeto.colisao == TIRO] avioes = [objeto for objeto in self.objetos if objeto.colisao == AVIAO] for aviao in avioes: for tiro in tiros: aviao.colide(tiro)
Aqui uma simplificação, onde abuso um pouco do computador :-D
Utilizando duas "list comprehensions", eu filtro a lista de objetos, procurando aviões e tiros.
Desta forma, a verificação de colisão se torna mais simples, pois basta verificar se o avião colidiu com um dos tiros.
O jogo principal pouco mudou, salvo o método on_key_press:
def on_key_press(self, symbol, modifiers): if symbol == key.S: print("Placar: {}".format(self.game.pontos)) pyglet.app.event_loop.exit() elif symbol == key.R: self.game = Game(self) else: self.game.pressionado = True
No caso, o jogo termina quando pressionamos a tecla S e reinicia se pressionarmos a tecla R.
O jogo completo ficou assim:
Video:
Para o próximo post:
- Sons
- Imagens
A pyglet não é atualizada há bastante tempo. Eu devo procurar outra biblioteca gráfica para outros posts, mas vou terminar as melhorias deste jogo ainda com pyglet e abusando da OpenGL.
Nenhum comentário:
Postar um comentário