[13] Construindo um jogo Snake usando a biblioteca Gloss (parte 2)
Agora que temos o esqueleto básico do jogo, podemos começar a evoluí-lo. Neste post modificaremos o jogo para incluir de fato a representação da serpente e ter seu movimento. A versão base do código sobre o qual vamos trabalhar hoje pode ser encontrada aqui: https://github.com/emanoelbarreiros/hsnake/tree/parte1.
Representando a serpente
Como você pode imaginar, uma serpente não pode ser representada apenas por um segmento, ela é longa, composta por vários segmentos. No momento, usamos o tipo Pos
para representar a posição de um segmento. A primeira modificação que precisamos fazer ao código é representar a serpente como uma lista de segmentos:
1
2
3
4
5
6
7
8
9
10
11
12
13
data Mundo = Estado {
cobra :: [Pos] -- mudança aqui
, direcao :: Direcao
, contFrames :: Int
} deriving Show
mundoInicial :: Mundo
mundoInicial = Estado {
cobra = [(0,0), (0,-1), (0,-2)] -- mudança aqui
, direcao = Parado
, contFrames = 0
}
Adaptando o restante do código para esta modificação
Essas modificações introduzem erros de compilação em outros trechos do código. Vamos endereçá-los agora. Primeiro vamos mexer na função que desenha o mundo. Como nossa serpente inteira era apenas um segmento, precisamos agora fazer com que ela seja capaz de desenhar toda a cobra. A função desenhaSegmento
não precisa ser modificada, vamos continuar usando-a, e vamos modificar a função desenhaMundo
para conseguir desenhar todos os segmentos da serpente:
1
2
desenhaMundo :: Mundo -> Picture
desenhaMundo m = pictures $ map desenhaSegmento (cobra m)
Utilizamos agora a função map
para aplicar a função desenhaSegmento
a cada elemento da lista. Após a aplicação do map
, teremos uma lista de Picture
, mas a função deve devolver apenas uma Picture
. Para resolver isso usamos a função pictures
, que transforma uma lista de Picture
em apenas uma Picture
.
Outra modificação necessária é na função tratarEvento
, pois ela só era capaz de movimentar um segmento da cobra, e agora precisamos mover todos. Ainda não é a melhor forma de movimentar a cobra, mas vamos falar sobre isso mais tarde…
1
2
3
4
5
6
tratarEvento :: Event -> Mundo -> Mundo
tratarEvento (EventKey (SpecialKey KeyUp) Down _ _) est@(Estado ss _ _) = est {cobra = map (\(x,y) -> (x,y+1)) ss}
tratarEvento (EventKey (SpecialKey KeyDown) Down _ _) est@(Estado ss _ _) = est {cobra = map (\(x,y) -> (x,y-1)) ss}
tratarEvento (EventKey (SpecialKey KeyLeft) Down _ _) est@(Estado ss _ _) = est {cobra = map (\(x,y) -> (x-1,y)) ss}
tratarEvento (EventKey (SpecialKey KeyRight) Down _ _) est@(Estado ss _ _) = est {cobra = map (\(x,y) -> (x+1,y)) ss}
tratarEvento _ est = est
Como pode ser visto, precisamos agora mover todos os segmentos da cobra, então usamos novamente o map
, aplicando a semântica do movimento a todos os segmentos. Ao testar, você verá que esse tipo de movimento não é apropriado, pois ao mover para a direita, por exemplo, todos os segmentos da cobra se movem para a direita. Vamos então consertar isso agora.
Corrigindo o movimento da serpente (mais ou menos…)
Lembre-se do tipo Direcao
que possuímos, ele será útil agora. Atualmente temos um campo chamado direcao
do tipo Direcao
, que assume o valor Parado
no mundo inicial. Agora, ao receber um evento, precisamos modificar essa direção de acordo com a tecla pressionada pelo usuário, e usar a direção para movimentar a serpente. Vamos lá mudar de novo a função tratarEvento
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
tratarEvento :: Event -> Mundo -> Mundo
tratarEvento (EventKey (SpecialKey KeyUp) Down _ _) m = m {direcao = novaDirecao (direcao m) Norte }
tratarEvento (EventKey (SpecialKey KeyDown) Down _ _) m = m {direcao = novaDirecao (direcao m) Sul }
tratarEvento (EventKey (SpecialKey KeyLeft) Down _ _) m = m {direcao = novaDirecao (direcao m) Oeste }
tratarEvento (EventKey (SpecialKey KeyRight) Down _ _) m = m {direcao = novaDirecao (direcao m) Leste }
tratarEvento _ m = m
novaDirecao :: Direcao -> Direcao -> Direcao
novaDirecao Parado i = i
novaDirecao atual inten
| atual == inten || atual == oposto inten = atual
| otherwise = inten
oposto :: Direcao -> Direcao
oposto Norte = Sul
oposto Sul = Norte
oposto Leste = Oeste
oposto Oeste = Leste
oposto Parado = Parado
Infelizmente não podemos apenas modificar o valor de direcao
, pois nem todas as direções são permitidas a todo momento. Se a serpente está a se mover para o Norte, ela não pode passar a se mover para o Sul, caso contrário ela vai passar por cima do próprio corpo. Então nós delegamos essa decisão para a função novaDirecao
, que recebe a direção atual, a intenção da nova direção, e devolve a nova direção. Caso a intenção seja diferente da intenção atual, ela vai checar se a intenção é permitida. Se não for, a direção retornada é a mesma da direção atual.
Agora finalmente vamos mexer no código da função. E vamos mexer um bocado!
Fazendo a serpente se movimentar de acordo com a direção
Como já foi comentado anteriormente, a função atualizaMundo
é responsável por atualizar o mundo de acordo com a passagem do tempo. Neste programa, entretanto, eu acredito que seja melhor contabilizar a passagem de frames em vez do tempo, pois nossa serpente se movimenta em um grid, sua movimentação não é pixel a pixel. Desta forma, uma coisa importante que devemos fazer na função atualizaMundo
é a contagem dos frames.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
atualizaMundo :: Float -> Mundo -> Mundo
atualizaMundo _ est@(Estado cob dir fra )
| fra `mod` acaoFrames == 0 = Estado novaCobra dir (fra + 1)
| otherwise = est {contFrames = fra + 1}
where
novaCobra = movimentaCobra cob dir
movimentaCobra :: [Pos] -> Direcao -> [Pos]
movimentaCobra cob Parado = cob
movimentaCobra cob d = novaPosicao : init cob
where
novaPosicao = head cob +> d
(+>) :: Pos -> Direcao -> Pos
(x,y) +> Norte = (x, y + 1)
(x,y) +> Sul = (x, y - 1)
(x,y) +> Leste = (x + 1, y)
(x,y) +> Oeste = (x - 1, y)
(x,y) +> Parado = (x,y)
Vamos entender de baixo pra cima. Definimos agora um operador novo, que chamamos de +>
. Esse operador faz apenas a aplicação de uma direção a uma posição. Em outras palavras, ele dá a você a célula vizinha, correspondente à direção informada, da posição passada como argumento.
A função movimentaCobra
faz de fato a movimentação da cobra. Observe, entretanto, que ela não muda nada de posição, apenas adiciona uma nova cabeça e remove a cauda.
Por fim, vamos à função atualizaMundo
. Ela recebe o tempo passado desde a última invocação da função (nós não o utilizamos, então jogamos fora usando o _
), o mundo atual, e devolve um novo mundo. Caso a quantidade de frames sejam múltiplo de acaoFrames
, retornamos um novo estado, que é composto pela nova cobra, a direção e incrementamos a quantidade de frames. Caso a quantidade de frames não seja múltiplo de acaoFrames
, atualizamos apenas a contagem de frames. A novaCobra
é calculada pela função movimentaCobra
, sobre a qual já conversamos.
Agora, a cobra é capaz de movimentar-se livremente de acordo com a direção passada pelo usuário a partir da sua interação com o jogo através do teclado. Você pode perceber, entretanto, que ao sair do campo, a cobra não retorna do outro lado, como é de se esperar neste joguinho. Vamos corrigir isso.
Fazendo o wrap nas bordas
Essa parte vai ser fácil. O que precisamos garantir é que sempre que criarmos uma nova cabeça (movimentaCobra
), ela precisa estar dentro do campo do jogo. Vamos introduzir uma nova função para resolver esse problema e modificar movimentaCobra
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
movimentaCobra :: [Pos] -> Direcao -> [Pos]
movimentaCobra cob Parado = cob
movimentaCobra cob d = novaPosicao : init cob
where
novaCabeca = head cob +> d
novaPosicao = reposicionaCabeca novaCabeca
reposicionaCabeca :: Pos -> Pos
reposicionaCabeca (x,y)
| x > limite = (-limite, y)
| x < -limite = (limite, y)
| y < -limite = (x, limite)
| y > limite = (x, -limite)
| otherwise = (x,y)
A função reposicionaCabeca
checa se a Pos
passada como argumento está dentro dos limites da janela. Caso ela tenha extrapolado um dos limites, ela reposiciona a Pos
para o lado oposto. Caso a posição esteja dentro dos limites, ela retorna a própria Pos
, sem alteração. Em movimentaCobra
, agora passamos a novaCabeca
para a função reposicionaCabeca
, que fará o reposicionamento quando necessário. E voilá, temos o movimento básico da serpente funcionando! Nas próximas postagens incluiremos a comida. Fica por aqui, vai ser legal.
O resultado desse post pode ser encontrado no Github: https://github.com/emanoelbarreiros/hsnake/tree/parte2