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 14 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:

Vídeo:

Para o próximo post:

  1. Sons
  2. 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.