Convertendo um jogo escrito em Basic para Python - Parte III
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:
Vídeo:
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.