Desenvolvendo o jogo Snake [parte 4]
Neste post vamos introduzir o conceito de colisão, tratando da comida para a cobra.
Criando o alimento para a cobra
No jogo Snake o alimento que a cobra come é o que fará com que ela cresça de tamanho, tornando o jogo, com o tempo, mais desafiador. O algoritmo para criar o alimento para a cobra é muito simples, precisamos apenas sortear valores aleatórios para o x
e y
do alimento a ser posicionado na tela. Ele será sorteado sempre que o jogador realizar a captura do alimento. Vamos tentar criar essa função para o nosso jogo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import random
def nova_comida(self):
comida = Posicao(
random.randrange(self.tamanho_sprite, pyxel.width - self.tamanho_sprite, self.tamanho_sprite),
random.randrange(self.tamanho_sprite, pyxel.height - self.tamanho_sprite, self.tamanho_sprite))
# se a posicao sorteada para comida for uma posicao ocupada por um
# segmento da cobra, tentar novamente ate conseguir um fora da cobra
while comida in self.segmentos:
comida = Posicao(
random.randrange(self.tamanho_sprite, pyxel.width - self.tamanho_sprite, self.tamanho_sprite),
random.randrange(self.tamanho_sprite, pyxel.height - self.tamanho_sprite, self.tamanho_sprite))
return comida
Uma coisa importante para notar é que se não impusermos nenhuma restrição, a função que faz o sorteio da posição da cobra pode sortear uma posição que já é ocupada pela cobra. A estratégia aqui é continuar sorteando até que consigamos uma posição diferente das que a cobra já ocupa. Essa estratégia não é a melhor possível, porque à medida que a cobra for crescendo vai ficar cada vez mais difícil sortear uma posição que não seja ocupada pela cobra, uma vez que com o tempo ela vai ocupar cada vez mais posições na tela. Como esta série de posts é para iniciantes em programação não vamos nos preocupar com isso agora porque a solução ideal é mais complicada que esta que apresento aqui. É até um bom exercício para você tentar resolver esse problema sozinho :wink:
Pra finalizar as modificações relacionadas à criação de alimento, precisamos incluir uma linha nova no método __init__
para criar um novo alimento assim que o jogo for inicializado (a modificação é apenas a inclusão da linha 11):
1
2
3
4
5
6
7
8
9
10
11
12
def __init__(self):
pyxel.init(208, 208)
self.tamanho_sprite = 8
self.segmentos = []
cabeca = Posicao(pyxel.width/2, pyxel.height/2)
self.segmentos.append(cabeca)
self.segmentos.append(Posicao(pyxel.width/2 - self.tamanho_sprite, pyxel.height/2))
self.segmentos.append(Posicao(pyxel.width/2 - self.tamanho_sprite*2, pyxel.height/2))
self.segmentos.append(Posicao(pyxel.width/2 - self.tamanho_sprite*3, pyxel.height/2))
self.direcao = DIREITA
self.comida = self.nova_comida()
pyxel.run(self.update, self.draw)
Perceba que também agora criamos a cobra com apenas 3 segmentos ao invés dos 5 que tínhamos antes (linhas 7-9).
Implementando o método eq
Para que o teste comida in self.segmentos
funcione, precisamos implementar um novo método na nossa classe Posicao
, para que o Python consiga fazer a comparação da posição da comida com a posição de um segmento. Por padrão o operador in
consegue testar, a partir do valor, tipos primitivos (como o int
, por exemplo) e se os objetos são os mesmos. Se tivermos objetos diferentes, mesmo com os mesmos valores internos (no nosso caso x
e y
), o Python não vai dizer que eles são iguais, a não ser que digamos a ele como isso deve ser feito. Para isso, precisamos implementar o método __eq__
, que vai permitir que o Python compare dois objetos diferentes para saber se são iguais semanticamente (baseado em seu conteúdo). Fica assim:
1
2
3
4
5
6
7
class Posicao:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, outro):
return self.x == outro.x and self.y == outro.y
Comendo a comida
Como você deve ter percebido, ainda não detectamos quando a cobre come a comida. Para fazer isso precisamos fazer um teste bem simples. Lembra-se da nossa estratégia para fazer a cobra “parecer” que andava? Sempre criamos uma nova cabeça e removemos um segmento da cauda. Precisamos apenas verificar se essa nova cabeça, ao tentar ser inserida, ocuparia uma posição que já é ocupada pela comida. Para isso podemos usar um laço de repetição e comparar a posição da nova cabeça com a posição da comida. Uma pergunta pra você: onde devemos colocar esse código? Ele é relacionado a desenho de algo na tela? Então não deve ir para a função draw
. Ele é relacionado a inicialização do jogo? Então não deve ir par ao __init__
. Ele é relacionado a atualização de lógica do jogo, pois afinal de contas estamos implementando uma regra do jogo, a cobra deve comer a comida quando passa por cima dela.
Antes de fazer isso, que tal darmos uma refatorada(leia um pouco sobre o que é refatoração de código) no nosso código, pois a função update
já está ficando grande demais, e já está ficando difícil de mexer nela. A primeira coisa que podemos fazer é mover da função update
o código responsável por capturar a interação do usuário. A função fica assim:
1
2
3
4
5
6
7
8
9
def processar_entrada_usuario(self):
if pyxel.btn(pyxel.KEY_UP) and self.direcao != BAIXO:
self.direcao = CIMA
elif pyxel.btn(pyxel.KEY_RIGHT) and self.direcao != ESQUERDA:
self.direcao = DIREITA
elif pyxel.btn(pyxel.KEY_DOWN) and self.direcao != CIMA:
self.direcao = BAIXO
elif pyxel.btn(pyxel.KEY_LEFT) and self.direcao != DIREITA:
self.direcao = ESQUERDA
Agora precisamos apenas invocar esse comportamento dentro da função update
:
1
2
3
4
5
6
7
def update(self):
# mais codigo dentro de update...
# atualiza a direção atual de acordo com entrada do usuario
self.processar_entrada_usuario()
# mais codigo dentro de update...
Sim, é só isso… Esse self.
na frente da chamada da função é importante, pois estamos invocando a função processar_entrada_usuario
de um objeto específico. Se tiver dúvida sobre esse conceito relacionado a orientação a objetos, volte um pouco para os posts iniciais e revisite os links que coloquei por lá.
Outra coisa que podemos fazer é mover o código responsável por criar a nova cabeça para uma outra função, para melhorar ainda mais a função update
, deixa-la um pouco mais enxuta.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def obter_nova_cabeca(self, cabeca):
if self.direcao == CIMA:
if cabeca.y - self.tamanho_sprite < 0: #fazer o wrap
cabeca = Posicao(cabeca.x, pyxel.height - self.tamanho_sprite)
else:
cabeca = Posicao(cabeca.x, cabeca.y - self.tamanho_sprite)
elif self.direcao == DIREITA:
if cabeca.x + self.tamanho_sprite > pyxel.width - self.tamanho_sprite:
cabeca = Posicao(0, cabeca.y)
else:
cabeca = Posicao(cabeca.x + self.tamanho_sprite, cabeca.y)
elif self.direcao == BAIXO:
if cabeca.y + self.tamanho_sprite > pyxel.height - self.tamanho_sprite:
cabeca = Posicao(cabeca.x, 0)
else:
cabeca = Posicao(cabeca.x, cabeca.y + self.tamanho_sprite)
elif self.direcao == ESQUERDA:
if cabeca.x - self.tamanho_sprite < 0:
cabeca = Posicao(pyxel.width - self.tamanho_sprite, cabeca.y)
else:
cabeca = Posicao(cabeca.x - self.tamanho_sprite, cabeca.y)
return cabeca
Você lembra como era nossa função update
? Veja agora como ela ficou e me diga se ficou melhor :smirk: Essa é toda a função update
. Como delegamos alguns pedaços de código bem extensos para funções auxiliares, temos agora um código bem mais legível e enxuto.
1
2
3
4
5
6
7
8
9
10
11
12
13
def update(self):
# atualiza a direção atual de acordo com entrada do usuario
self.processar_entrada_usuario()
if pyxel.frame_count % 3 == 0:
# adicionar uma nova cabeca e remover a cauda eh mais facil
# do que atualizar todos os segmentos
cabeca = self.segmentos[0]
cabeca = self.obter_nova_cabeca(cabeca)
self.segmentos.pop()
self.segmentos.insert(0, cabeca)
A lógica da detecção da colisão
Quando você começar a fazer jogos mais complexos no Pyxel vai se deparar com esse problema clássico em jogos que é a detecção de colisões. Eu digo “quando fizer jogos mais complexos” por que neste aqui a detecção de colisões é muito simples, mas à medida que seu projeto fica mais complexo esse controle pode se tornar mais difícil. E eu digo “no Pyxel” porque como eu falei nos posts iniciais, o Pyxel não vai te dar muitas facilidades para desenvolver o jogo. E advinha, uma das coisas que ele não te dá é a detecção automática de colisões que outros engines vão te dão, como por exemplo a configuração do que é parede (que obvimente o personagem não consegue ultrapassar), das bouding boxes em torno dos personagens (ajudam a configurar de forma quase automática as colisões entre personagem e elementos do jogo) e outros elementos. Mas fique tranquilo, porque isso é bom pra a gente, dá mais trabalho mas assim aprendemos mais.
Então vamos lá… o conceito de colisão é muito simples em um jogo: duas coisas colidem quando durante a execução do jogo elas “querem” ocupar a mesma posição. Nos jogos, algumas coisas se movem sozinhas, outras se movem a partir de interação do usuário, e durante a atualização do estado do jogo, pode acontecer que duas coisas, ao terem suas posições atualizadas de acordo com a mecânica do jogo, estariam ocupando a mesma posição na tela. Ao detectar isso o jogo pode simplesmente não permitir, fazer o jogador sofrer algum dano (perder pontos de vida), ativar algum outro item, tudo vai depender do seu jogo. No nosso caso, queremos que a comida desapareça e que a cobra ganhe um novo segmento.
O código é muito simples, e para isso vamos modificar as duas últimas linhas da nossa função update
. Antes apenas adicionávamos a nova cabeça e removíamos um elemento do fim da lista de segmentos. Agora, nós vamos fazer isso apenas após checarmos se a nova cabeça da cobra está na mesma posição da comida. Após inserir a nova cabeça, fazemos uma checagem pra saber se essa posição é ocupada pela comida. Como já implementamos o __eq__
podemos fazer uma checagem simples usando o ==
para saber tanto a cabeça quanto a comida estão na mesma posição da tela. Se o teste for verdadeiro, tanto a comida e a cabeça estão na mesma posição, e pra a gente a cobra vai aumentar de tamanho. Caso a comida seja consumida, nós criamos uma nova comida e não removemos o segmento da cauda, fazendo assim com que a cobra aumente de tamanho. Se não estiverem na mesma posição removemos o segmento da cauda, fazendo ela continuar andando. Nosso update
agora fica assim:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def update(self):
# atualiza a direção atual de acordo com entrada do usuario
self.processar_entrada_usuario()
if pyxel.frame_count % 3 == 0:
# adicionar uma nova cabeca e remover a cauda eh mais facil
# do que atualizar todos os segmentos
cabeca = self.segmentos[0]
cabeca = self.obter_nova_cabeca(cabeca)
self.segmentos.insert(0, cabeca)
if cabeca == self.comida:
self.comida = self.nova_comida()
else:
self.segmentos.pop()
A cobra não pode passar por cima do próprio corpo
Como você já deve ter percebido também, ainda não implementamos uma mecânica muito importante do jogo. A cobra não pode passar por cima do seu próprio corpo. Agora já sabemos como detectar a colisão no contexto do nosso jogo. Vai ser muito parecido com a colisão com a comida, mas agora vamos precisar verificar a colisão da cabeça com todas as outras partes do corpo.
Faremos essa checagem após a detecção da comida, pois após esse ponto já teremos a nova cabeça no lugar, e também a cauda já terá sido removida. Utilizaremos um laço de repetição simples, pra checar com todos os segmentos. Uma observação importante: fazemos um slice para que não chequemos a colisão da cabeça com a própria cabeça. Se não fizemos isso o jogo não sai do canto :wink:
Também precisaremos de uma nova variável para sinalizar se o jogo deve rodar ou não. Criamos uma variável chamada rodando
dentro do __init__
para guardar essa informação. Modificamos o primeiro if
do update
para além de checar o contador de frames também levar em consideração essa nossa nova variável. Quando detectarmos a colisão da cabeça com o corpo agora o jogo vai parar.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def update(self):
# atualiza a direção atual de acordo com entrada do usuario
self.processar_entrada_usuario()
if pyxel.frame_count % 3 == 0 and self.rodando:
# adicionar uma nova cabeca e remover a cauda eh mais facil
# do que atualizar todos os segmentos
cabeca = self.segmentos[0]
cabeca = self.obter_nova_cabeca(cabeca)
self.segmentos.insert(0, cabeca)
if cabeca == self.comida:
self.comida = self.nova_comida()
else:
self.segmentos.pop()
for segmento in self.segmentos[1:]:
if cabeca == segmento:
self.rodando = False
Por enquanto é isso. O jogo ainda está feio, mas a mecânica principal está praticamente pronta. Nos próximos posts podemos começar a pensar na arte do nosso jogo. Felizmente o Pyxel nos dá algumas ferramentas legais pra ajudar nessa parte.
Leave a comment