[13] Construindo um jogo Snake usando a biblioteca Gloss (parte 2)

9 minute read

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

Updated: