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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import pyglet | |
from pyglet.window import key | |
from pyglet.gl import * | |
import random | |
DEBUG = False | |
def rgb_to_f(r, g, b): | |
return(r / 255.0, g / 255.0, b / 255.0) | |
# Fonte: https://github.com/AppleWin/AppleWin/issues/254 | |
CORES = [(0.0, 0.0, 0.0), # Preto 0 | |
rgb_to_f(20, 245, 60), # Verde 1 | |
rgb_to_f(255, 68, 253), # Magenta 2 | |
rgb_to_f(255, 255, 255), # Branco 3 | |
rgb_to_f(255, 106, 60), # Laranja 5 | |
rgb_to_f(20, 207, 253), # Azul médio 6 | |
rgb_to_f(255, 255, 255), # Branco 7 | |
rgb_to_f(255, 0, 0), # Vermelho 8 | |
rgb_to_f(255, 255, 0), # Amarelo 9 | |
] | |
FRAMES_ORIGINAL = 8 | |
FRAMES_NOVO = 60 | |
RATIO = FRAMES_ORIGINAL / FRAMES_NOVO | |
FUNDO = 0 | |
AVIAO = 1 | |
TIRO = 2 | |
def set_color(color): | |
glColor3f(*CORES[color]) | |
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 | |
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))) | |
def atualiza(self): | |
"""Calcula nova posição, atualiza valores a cada frame""" | |
pass | |
def colidiu(self, outro): | |
"""O que fazer em caso de colisao? Recebe o outro objeto""" | |
pass | |
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) | |
def desativa(self): | |
self.ativo = False | |
self.jogo.desativa(self) | |
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() | |
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() | |
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() | |
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, | |
))) | |
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') | |
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 | |
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() | |
def atualiza(self): | |
self.jogue() | |
if self.estado != "jogando": | |
if self.pressionado: | |
pyglet.app.event_loop.exit() | |
self.mostra_mensagem() | |
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 | |
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) | |
def perdeu(self): | |
self.imprima("MISSED") | |
self.muda_estado("fimdejogo") | |
def acertou(self): | |
self.imprima("HIT!!!") | |
self.muda_estado("fimdejogo") | |
def muda_estado(self, estado): | |
print("Mudança de Estado: {} --> {}".format(self.estado, estado)) | |
self.estado = estado | |
def imprima(self, mensagem): | |
self.label = self.window.crialabel(mensagem, x=280 * 2, y=-192 * 2, tamanho=100) | |
class Missile(pyglet.window.Window): | |
def __init__(self): | |
self.scale = 4 # Escala os gráficos 4x | |
self.frames = FRAMES_NOVO # Frames por segundo. | |
# Define quantas vezes vamos atualizar a tela por segundo | |
super(Missile, self).__init__(280 * self.scale, | |
192 * self.scale, | |
caption="Missiles") | |
self.set_mouse_visible(False) | |
self.game = Game(self) | |
self.schedule = pyglet.clock.schedule_interval( | |
func=self.update, interval=1.0 / self.frames) | |
def update(self, interval): | |
pass | |
def on_draw(self): | |
window.clear() | |
self.game.atualiza() | |
def on_resize(self, width, height): | |
# Inicializa a view | |
glViewport(0, 0, width, height) | |
glMatrixMode(gl.GL_PROJECTION) | |
glLoadIdentity() | |
# Inverte as coordenadas do eixo Y | |
glOrtho(0, width, height, 0, -1, 1) | |
# Aplica a escala | |
glScalef(self.scale, self.scale, 1.0) | |
glMatrixMode(gl.GL_MODELVIEW) | |
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 | |
def crialabel(self, mensagem, x=None, y=None, | |
fonte='Times New Roman', tamanho=36): | |
"""Prepara uma mensagem de texto para ser exibida""" | |
x = x or self.width // 2 | |
y = y or self.height // 2 | |
return pyglet.text.Label( | |
mensagem, font_name=fonte, font_size=tamanho, | |
x=x, y=y, anchor_x='center', anchor_y='center') | |
window = Missile() | |
pyglet.app.run() |
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