Introdução
Neste projeto, você aprenderá como criar um jogo da cobrinha simples em C usando a biblioteca ncurses. Este jogo clássico envolve controlar uma cobra para comer comida, crescer, evitando colisões com as paredes e com ela mesma. A funcionalidade do jogo é dividida em vários componentes-chave: inicialização, loop do jogo, movimento da cobra, detecção de colisão, e assim por diante. Ao final deste projeto, você terá um jogo da cobrinha básico que pode ser executado em um terminal.
👀 Pré-visualização

🎯 Tarefas
Neste projeto, você aprenderá:
- Como implementar o loop do jogo para atualizar a posição da cobra e lidar com a entrada do usuário.
- Como criar funções para inicializar o jogo, desenhar a janela do jogo e exibir mensagens de fim de jogo.
- Como implementar a detecção de colisão para verificar colisões com paredes, o próprio corpo da cobra e comida.
- Como desenvolver recursos como aumentar o comprimento da cobra quando ela come comida.
🏆 Conquistas
Após concluir este projeto, você será capaz de:
- Usar a biblioteca ncurses em C para criar um jogo baseado em terminal.
- Implementar a lógica do jogo, incluindo a atualização do estado do jogo e o tratamento da entrada do usuário.
- Criar e manipular estruturas de dados para representar objetos do jogo, como a cobra e a comida.
- Implementar a detecção de colisão para fornecer regras do jogo e determinar quando o jogo deve terminar.
Conhecimentos Básicos
Na era do uso generalizado de máquinas de teletipo, as máquinas de teletipo atuavam como terminais de saída conectados a computadores centrais por meio de cabos. Os usuários precisavam enviar uma série de comandos de controle específicos para o programa do terminal a fim de controlar a saída na tela do terminal. Por exemplo, alterar a posição do cursor na tela, limpar o conteúdo de uma determinada área na tela, rolar a tela, alternar os modos de exibição, sublinhar texto, alterar a aparência, cor, brilho, etc., dos caracteres. Esses controles são implementados por meio de uma string chamada de sequência de escape. As sequências de escape são chamadas assim porque esses bytes contínuos começam com um caractere 0x1B, que é o caractere de escape (o caractere inserido pressionando a tecla ESC). Mesmo agora, podemos simular o efeito de saída de terminais de teletipo daquela época inserindo sequências de escape em programas de emulação de terminal. Se você deseja exibir um trecho de texto no terminal (ou programa de emulação de terminal) com fundo colorido, você pode inserir a seguinte sequência de escape no seu prompt de comando:
echo "^[[0;31;40mIn Color"
Aqui, ^ e [ são os chamados caracteres de escape. (Observação: neste caso, ^[ é um caractere. Ele não é inserido digitando os caracteres ^ e [ sequencialmente. Para imprimir este caractere, você deve primeiro pressionar Ctrl+V e, em seguida, pressionar a tecla ESC.) Após executar o comando acima, você deve ver o fundo de In Color alterado para vermelho. A partir de então, todo o texto exibido será exibido com este efeito. Se você deseja encerrar este efeito e retornar ao formato original, você pode usar o seguinte comando:
echo "^[[0;37;40m"
Agora você sabe o propósito desses caracteres (sequências de escape)? (Tente alterar os parâmetros entre os pontos e vírgula e veja quais resultados você obtém.) Talvez seja diferente do que você imagina? Pode ser porque o ambiente do terminal é diferente, o que depende dos diferentes terminais ou sistemas operacionais. (Você não pode fazer um terminal monocromático exibir caracteres coloridos, certo?) Para evitar esses problemas de compatibilidade e obter uma saída consistente em diferentes terminais, os projetistas do UNIX inventaram um mecanismo chamado termcap. termcap é, na verdade, um arquivo que é lançado junto com as sequências de escape. Este arquivo lista todas as sequências de escape que o terminal atual pode executar corretamente, garantindo que o resultado da execução das sequências de escape de entrada esteja em conformidade com as especificações neste arquivo. No entanto, nos anos seguintes à invenção deste mecanismo, outro mecanismo chamado terminfo gradualmente substituiu termcap. Desde então, os usuários não precisam mais consultar as complexas especificações de sequência de escape em termcap ao programar, e só precisam acessar o banco de dados terminfo para controlar a saída da tela.
Supondo que todos os aplicativos acessem o banco de dados terminfo para controlar a saída (como enviar caracteres de controle, etc.) sob a situação de usar terminfo, em breve essas chamadas de código tornarão todo o programa difícil de controlar e gerenciar. O surgimento desses problemas levou ao nascimento de CURSES. O nome CURSES vem de um trocadilho chamado cursor optimization (otimização do cursor).
A biblioteca CURSES fornece aos usuários uma API (interface de programação de aplicativos) flexível e eficiente, encapsulando os códigos de controle subjacentes (sequências de escape) do terminal, o que permite aos usuários controlar o cursor, criar janelas, alterar as cores de primeiro plano e fundo e lidar com operações do mouse. Isso permite que os usuários ignorem esses mecanismos de baixo nível irritantes ao escrever aplicativos em terminais de caracteres.
NCURSES é um clone de CURSES do System V Release 4.0 (SVr4). É uma biblioteca livremente configurável que é totalmente compatível com versões mais antigas de CURSES. Em suma, é uma biblioteca que permite que os aplicativos controlem diretamente a exibição da tela do terminal. Quando a biblioteca CURSES é mencionada posteriormente, ela também se refere à biblioteca NCURSES.
NCURSES não apenas encapsula as funções de terminal subjacentes, mas também fornece uma estrutura de trabalho bastante estável para gerar interfaces bonitas. Ele inclui funções para criar janelas. E suas bibliotecas irmãs, Menu, Panel e Form, são extensões da biblioteca base CURSES. Essas bibliotecas são geralmente distribuídas junto com CURSES. Podemos construir um aplicativo que contém várias janelas, menus, painéis e formulários. As janelas podem ser gerenciadas independentemente, como rolar ou ocultá-las. Os menus permitem que os usuários criem opções de comando para facilitar a execução de comandos. Os formulários permitem que os usuários criem janelas para entrada e exibição simples de dados. Os painéis são extensões das funções de gerenciamento de janelas do NCURSES e podem sobrepor ou empilhar janelas.
Definir Constantes
Primeiro, abra o terminal e execute o seguinte comando para instalar a biblioteca ncurses:
sudo apt-get update
sudo apt-get install libncurses5-dev
Navegue até o diretório ~/project e crie o arquivo do projeto snake.c:
cd ~/project
touch snake.c
Em seguida, precisamos escrever o código C. A primeira etapa é incluir os arquivos de cabeçalho:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <ncurses.h>
Antes de escrever a função main(), vamos concluir algumas tarefas básicas. Como estamos usando uma interface de caracteres de terminal, os caracteres ASCII são essenciais. Portanto, precisamos definir algumas constantes:
#define TBool int
#define True 1
#define False 0
#define SHAPE_FOOD '@' // Food
#define SHAPE_SNAKE '#' // Snake body
#define GAMEWIN_YLEN 15
#define GAMEWIN_XLEN 60
#define LOGWIN_YLEN 7
#define LOGWIN_XLEN (GAMEWIN_XLEN)
#define LOGBUF_NUM (LOGWIN_YLEN-2)
#define LOGBUF_LEN (GAMEWIN_XLEN-2)
#define MAXLEVEL 12
#define GetSnakeTail(s) ((s)->head->front)
WINDOW *logwin; // Declare a log window
#define INITRUNLOG() logwin = newlogw() // Create a log window by calling the custom function newlogw()
#define RUNLOG(str) runlog(logwin, str) // Run the log window to display game prompts
#define DESTROYRUNLOG() delwin(logwin)
int g_level; // Player level, a global variable
Também adicionamos algumas definições de struct e enum:
enum TDirection {
DIR_UP,
DIR_DOWN,
DIR_LEFT,
DIR_RIGHT
};
struct TFood {
int y;
int x;
};
struct TSnakeNode {
int y;
int x;
struct TSnakeNode *front;
};
struct TSnake {
int length;
struct TSnakeNode *head;
enum TDirection dir;
};
Agora, vamos declarar as funções que vamos criar:
int refreshgamew(WINDOW *win, struct TSnake *psnake);
void movesnake(struct TSnake *psnake);
int checksnake(struct TFood *pfood, struct TSnake *psnake);
void snakegrowup(struct TFood *pfood, struct TSnake *psnake);
void gameover(WINDOW *win, char *str);
struct TSnakeNode *newsnakenode(struct TSnakeNode **ppsnode, int y, int x);
WINDOW* newgamew();
struct TSnake* initsnake();
void destroysnake(struct TSnake *psnake);
void drawsnakew(WINDOW *win, struct TSnake *psnake);
void drawfoodw(WINDOW *win, struct TFood *pfood, struct TSnake *psnake);
TBool checkfood(struct TFood *pfood, struct TSnake *psnake);
WINDOW* newlogw();
void runlog(WINDOW *win, char *str);
void cleanline(WINDOW *win, int y, int x);
Função Principal
int main()
{
initscr(); /* Initialize, enter ncurses mode */
raw(); /* Disable line buffering, see results immediately */
noecho(); /* Do not display control characters on the terminal, such as Ctrl+C */
keypad(stdscr, TRUE); /* Allow user to use keyboard in terminal */
curs_set(0); /* Set cursor visibility, 0 is invisible, 1 is visible, 2 is completely visible */
refresh(); /* Write the contents of the virtual screen to the display and refresh */
g_level = 1;
INITRUNLOG();
RUNLOG(" Press 'q' or 'Q' to quit.");
RUNLOG(" Press 'w/s/a/d' or 'W/S/A/D' to move the snake.");
RUNLOG("Info:");
WINDOW *gwin = newgamew(); /* Create game window, implemented by a custom function called newgamew */
struct TSnake *psnake = initsnake();
drawsnakew(gwin, psnake);
while (refreshgamew(gwin, psnake) >= 0)
;
/* getch() is different from getchar() */
getch();
destroysnake(psnake);
delwin(gwin); /* Clear the game window and free the memory and information of the window data structure */
DESTROYRUNLOG(); /* Clear the information display window */
endwin(); /* Exit ncurses mode */
return 0;
}
Em keypad(stdscr, TRUE), stdscr se refere a uma janela virtual onde todas as nossas operações são primeiro escritas, e então o conteúdo de stdscr é exibido na tela usando a função refresh.
Quando usamos printw, os dados são realmente impressos em uma janela virtual chamada stdscr, que não é diretamente exibida na tela. O objetivo da função printw() é escrever continuamente alguns marcadores de exibição e estruturas de dados relacionadas na tela virtual e escrever esses dados no buffer de stdscr. Portanto, para exibir os dados neste buffer, devemos usar a função refresh() para dizer ao sistema curses para exibir o conteúdo do buffer na tela. Este mecanismo permite que o programador escreva continuamente dados na tela virtual e faça com que pareça que tudo foi feito de uma vez quando refresh() é chamado. Como a função refresh() verifica apenas as partes da janela e dos dados que foram alteradas, este design flexível fornece um mecanismo de feedback eficiente.
Mecanismo da Janela
O mecanismo de janelas é um conceito central do CURSES. Como você viu nos exemplos anteriores, todas as funções operam por padrão em uma "janela" (stdscr). Mesmo que você esteja projetando a interface gráfica do usuário (GUI) mais simples, ainda precisará usar janelas. Uma das principais razões para usar janelas é que você pode dividir a tela em diferentes partes e operar dentro delas simultaneamente. Isso pode melhorar a eficiência. Outra razão é que você sempre deve buscar um design melhor e mais gerenciável em seu programa. Se você estiver projetando uma interface de usuário grande e complexa, pré-projetar essas partes melhorará sua eficiência.
WINDOW* newlogw()
{
/* Parameters are: height, width, starting position (y,x) of the window */
WINDOW *win = newwin(LOGWIN_YLEN, LOGWIN_XLEN, GAMEWIN_YLEN + 2, 3);
/* Parameters are: known window pointer, 0 and 0 are the default row and column starting positions of the characters */
box(win, 0, 0);
mvwprintw(win, 0, 2, " LOG ");
wrefresh(win); // Refresh the specified window
return win;
}
Na função WINDOW* newlogw(), você pode ver que a criação de uma janela começa com newwin(). Embora tenhamos criado uma janela, ela ainda não está visível, o que é semelhante a um elemento <div> em HTML. Se você não adicionar estilos a um elemento <div>, não verá nada na página da web. Então, precisamos usar a função box para adicionar bordas à janela conhecida.
mvwprintw imprime o conteúdo especificado nas coordenadas especificadas (y,x) dentro da janela especificada.
Exibir Informações do Jogo
RUNLOG é uma macro que chama a função personalizada runlog.
void runlog(WINDOW *win, char *str)
{
static char logbuf[LOGBUF_NUM][LOGBUF_LEN] = {0};
static int index = 0;
strcpy(logbuf[index], str);
int i = 0;
/* #define LOGBUF_NUM (LOGWIN_YLEN-2); LOGBUF_NUM=5 */
for (; i < LOGBUF_NUM; ++i) {
/* Custom function, cleanline */
cleanline(win, i+1, 1);
/* Print string on each line */
mvwprintw(win, i+1, 1, logbuf[(index+i) % LOGBUF_NUM]);
wrefresh(win);
}
index = (index + LOGBUF_NUM - 1) % LOGBUF_NUM;
}
runlog recebe uma janela conhecida e as informações a serem exibidas como parâmetros. A str será armazenada em um array bidimensional (logbuf) usando a função strcpy. Em seguida, a função personalizada cleanline é chamada para limpar a linha que será impressa em seguida. A função mvwprintw é usada para imprimir as informações.
/* Clear the coordinates (x,y) of the window win */
void cleanline(WINDOW *win, int y, int x)
{
char EMPTYLINE[LOGBUF_LEN] = {0}; // LOGBUF_LEN=57
/* Set positions 0-56 of the array to empty characters */
memset(EMPTYLINE, ' ', LOGBUF_LEN-1);
/* Move the cursor to position (y,x) in the window win and print the string EMPTYLINE */
mvwprintw(win, y, x, EMPTYLINE);
/* Display content on the specified window */
wrefresh(win);
}
Inicializando a Fila da Cobra
Agora, você definirá a estrutura de dados da cobra e criará funções para inicializá-la e desenhá-la na janela do jogo.
struct TSnake* initsnake()
{
struct TSnake *psnake = (struct TSnake *)malloc(sizeof(struct TSnake));
psnake->dir = DIR_LEFT;
psnake->length = 4; // Initialize the length of the snake to 4
newsnakenode (
&newsnakenode (
&newsnakenode (
&newsnakenode( &psnake->head, 4, 50 )->front, 4, 53
)->front, 4, 52
)->front, 4, 51
)->front = psnake->head;
return psnake;
}
struct TSnakeNode *newsnakenode(struct TSnakeNode **ppsnode, int y, int x)
{
*ppsnode = (struct TSnakeNode *)malloc(sizeof(struct TSnakeNode));
(*ppsnode)->y = y;
(*ppsnode)->x = x;
(*ppsnode)->front = NULL;
return *ppsnode;
}
O objetivo dessas duas funções é inicializar a estrutura de dados da cobra em um jogo da cobra, incluindo definir a direção inicial e o comprimento da cobra, bem como criar uma cobra com um nó de corpo inicial. A função newsnakenode é usada para criar nós individuais, e então esses nós são conectados por meio de chamadas de função aninhadas para formar a cobra inicial.
Exibir a Cobra na Janela do Jogo
void drawsnakew(WINDOW *win, struct TSnake *psnake)
{
static int taily = 0;
static int tailx = 0;
if (taily != 0 && tailx != 0) {
mvwaddch(win, taily, tailx, ' ');
}
/* #define GetSnakeTail(s) ((s)->head->front) */
struct TSnakeNode *psnode = GetSnakeTail(psnake);
int i = 0;
for (; i < psnake->length; ++i) {
mvwaddch(win, psnode->y, psnode->x, SHAPE_SNAKE);
psnode = psnode->front;
}
taily = GetSnakeTail(psnake)->y;
tailx = GetSnakeTail(psnake)->x;
wrefresh(win);
}
Para exibir as coordenadas (y,x) de cada nó, podemos usar a definição de macro GetSnakeTail para recuperar as coordenadas. A etapa restante é usar um loop para exibi-las usando mvwaddch. mvwaddch é usado para mover o cursor para a posição especificada (taily,tailx) na janela especificada (win) e, em seguida, exibir um caractere.
Para realmente exibi-lo na tela, precisamos usar wrefresh(win).
Escrever o Núcleo do Jogo
Observe o ponto e vírgula após while (refreshgamew(gwin, psnake) >= 0);. Isso estabelece um loop para lidar com o movimento e a mudança de comprimento da cobra, bem como a detecção de limites e outros problemas.
O loop while chama a função refreshgamew:
int refreshgamew(WINDOW *win, struct TSnake *psnake)
{
static TBool ffood = False;
struct TFood pfood;
/* When starting the game or when food is eaten, ffood=False, and drawfoodw is executed to redraw the food */
if (!ffood) {
drawfoodw(win, &pfood, psnake);
ffood = True;
}
int key = -1;
fd_set set;
FD_ZERO(&set);
FD_SET(0, &set);
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec= (6 - (int)(g_level/3)) * 100*1000;
// Next, we will use the select kernel API, which will listen for a series of keyboard and mouse inputs. We use `key=getch()` and `switch` to determine the input type. The second `switch` is used to display the level and the current movement speed of the snake.
if (select(1, &set, NULL, NULL, &timeout) < 0)
return -1;
if (FD_ISSET(0, &set)) {
while ((key = getch()) == -1);
switch (key) {
case 'w':
case 'W':
(psnake->dir == DIR_DOWN) ? : (psnake->dir = DIR_UP);
break;
case 's':
case 'S':
(psnake->dir == DIR_UP) ? : (psnake->dir = DIR_DOWN);
break;
case 'a':
case 'A':
(psnake->dir == DIR_RIGHT) ? : (psnake->dir = DIR_LEFT);
break;
case 'd':
case 'D':
(psnake->dir == DIR_LEFT) ? : (psnake->dir = DIR_RIGHT);
break;
case 'q':
case 'Q':
RUNLOG("Quit Game!");
gameover(win, "Quit Game!");
return -1;
default:
break;
}
}
movesnake(psnake);
drawsnakew(win, psnake);
switch (checksnake(&pfood, psnake)) {
case 0:
break;
// Food has been eaten, set ffood to 0 to redraw the food.
case 1:
ffood = False;
if (++g_level > MAXLEVEL) {
RUNLOG("Win!!!");
gameover(win, "Win!!!");
return -1;
}
mvwprintw(win, GAMEWIN_YLEN-1, 2, " Level: %d ", g_level);
mvwprintw(win, GAMEWIN_YLEN-1, 30, " Speed: %d ", (int)(g_level/3));
wrefresh(win);
RUNLOG("Level UP!");
snakegrowup(&pfood, psnake);
break;
default:
RUNLOG("Game over!");
gameover(win, "Game over!");
return -1;
}
return 1;
}
O Movimento da Cobra Gulosa
O movimento da Cobra Gananciosa é implementado usando a função movesnake. Vamos analisar o segmento case DIR_UP, pois o restante é semelhante:
/* A estrutura TSnake é uma lista ligada invertida com a cabeça e a cauda conectadas.
* Exemplo: [a]<-[b]<-[c]<-[d] a é a cabeça
* | ^ Quando a cobra se move, apenas a cabeça aponta para d,
* `--------------' e o (y,x) de d é modificado para a posição para onde a cabeça se move. */
void movesnake(struct TSnake *psnake)
{
int hy = psnake->head->y;
int hx = psnake->head->x;
psnake->head = GetSnakeTail(psnake);
switch (psnake->dir) {
case DIR_UP:
psnake->head->y = hy - 1;
psnake->head->x = hx;
break;
case DIR_DOWN:
psnake->head->y = hy + 1;
psnake->head->x = hx;
break;
case DIR_LEFT:
psnake->head->y = hy;
psnake->head->x = hx - 1;
break;
case DIR_RIGHT:
psnake->head->y = hy;
psnake->head->x = hx + 1;
break;
default:
break;
}
}
Como a origem das coordenadas está localizada no canto superior esquerdo, com os valores de (y,x) aumentando para baixo e para a direita, o sistema de coordenadas é invertido em 180 graus ao longo do eixo x. Portanto, para fazer a cobra se mover para cima, subtraímos 1. Assim, quando o jogador pressiona a tecla w/W, a cobra se moverá para cima.
Aumentando o Corpo da Cobra
Para aumentar o comprimento do corpo da cobra, você pode conseguir isso implementando a função snakegrowup, que é relativamente simples. Basta adicionar um novo nó à fila.
void snakegrowup(struct TFood *pfood, struct TSnake *psnake)
{
// Allocate memory for a new snake node (struct TSnakeNode) and assign the pointer pnode to the new node. This new node will become the new head of the snake.
struct TSnakeNode *pnode = (struct TSnakeNode *)malloc(sizeof(struct TSnakeNode));
switch (psnake->dir) {
case DIR_UP:
pnode->y = psnake->head->y - 1;
pnode->x = psnake->head->x;
break;
case DIR_DOWN:
pnode->y = psnake->head->y + 1;
pnode->x = psnake->head->x;
break;
case DIR_LEFT:
pnode->y = psnake->head->y;
pnode->x = psnake->head->x - 1;
break;
case DIR_RIGHT:
pnode->y = psnake->head->y;
pnode->x = psnake->head->x + 1;
break;
default:
break;
}
// Set the front pointer of the new node (pnode) to the current snake tail in order to establish a connection between the new head and tail.
pnode->front = GetSnakeTail(psnake);
// Set the front pointer of the current snake head to the new node to ensure that the new node becomes the new head.
psnake->head->front = pnode;
psnake->head = pnode;
// Increase the length of the snake to indicate that the snake body has extended by one unit.
++psnake->length;
}
A instrução switch determina as coordenadas do novo nó da cabeça com base na direção atual do movimento da cobra (psnake->dir). Dependendo das diferentes direções, ela atualiza as coordenadas do novo nó para mover uma célula para cima, para baixo, para a esquerda ou para a direita da cabeça atual da cobra (psnake->head).
O objetivo deste código é aumentar o comprimento do corpo da cobra no jogo da Cobrinha. Ele cria um novo nó de cabeça com base na direção atual do movimento da cobra e insere este novo nó de cabeça na frente da cobra, tornando o corpo da cobra mais longo. Esta é a lógica principal para aumentar o comprimento do corpo da cobra depois que ela consome comida no jogo da Cobrinha.
A Localização da Produção de Alimentos
A função drawfoodw é usada para desenhar a comida na janela do jogo (win):
void drawfoodw(WINDOW *win, struct TFood *pfood, struct TSnake *psnake)
{
do {
pfood->y = random() % (GAMEWIN_YLEN - 2) + 1;
pfood->x = random() % (GAMEWIN_XLEN - 2) + 1;
} while (False == checkfood(pfood, psnake));
checkfood(pfood, psnake);
mvwaddch(win, pfood->y, pfood->x, SHAPE_FOOD);
wrefresh(win);
}
- A função
random()é usada para gerar valores de coordenadas aleatórias, o que fará com que a comida apareça em uma posição aleatória dentro da janela do jogo. - Usando um loop
do-while, ele verifica se a posição da comida gerada se sobrepõe a algum nó do corpo da cobra. Se houver uma sobreposição, ele gera uma nova posição de comida até encontrar uma posição que não se sobreponha ao corpo da cobra. - A função
mvwaddché usada para desenhar a forma da comida na posição especificada na janela do jogo.
A função checkfood é usada para verificar se a comida não aparece na cobra:
TBool checkfood(struct TFood *pfood, struct TSnake *psnake)
{
struct TSnakeNode *pnode = GetSnakeTail(psnake);
int i = 0;
for (; i < psnake->length; ++i, pnode = pnode->front)
if (pfood->y == pnode->y && pfood->x == pnode->x)
return False;
return True;
}
- Primeiro, ela obtém o nó da cauda da cobra. Em seguida, usa um loop para iterar por todos os nós do corpo da cobra e verifica se as coordenadas da comida correspondem às coordenadas de algum nó.
- Se as coordenadas da comida corresponderem às coordenadas de algum nó da cobra, significa que a posição da comida se sobrepõe a um nó do corpo da cobra, e a função retorna
False. - Se nenhuma sobreposição for encontrada, a função retorna
True, indicando que a posição da comida é válida.
Detecção de Limites
checksnake é usado para verificar se a cobra colidiu com as bordas do jogo. Isso inclui a detecção de bordas na parte superior, inferior, esquerda e direita do jogo. Colisões com o próprio corpo da cobra também são verificadas, pois ambas as situações resultariam no fim do jogo.
int checksnake(struct TFood *pfood, struct TSnake *psnake)
{
/* Check if the coordinates of the snake's head have collided with the game borders on the top, bottom, left, or right */
if ( psnake->head->y <= 0 || psnake->head->y >= GAMEWIN_YLEN
|| psnake->head->x <= 0 || psnake->head->x >= GAMEWIN_XLEN)
{
return -1;
}
struct TSnakeNode *pnode = GetSnakeTail(psnake);
int i = 0;
/* Check that the snake's head does not collide with any part of its body */
for (; i < psnake->length - 1; ++i, pnode = pnode->front)
if (psnake->head->y == pnode->y && psnake->head->x == pnode->x)
return -1;
/* Of course, colliding with food is allowed */
if (psnake->head->y == pfood->y && psnake->head->x == pfood->x)
return 1;
return 0; // No collision occurred
}
Fim de Jogo
A função gameover é usada para exibir a mensagem de fim de jogo na janela especificada (win) quando o jogo da cobra termina:
void gameover(WINDOW *win, char *str)
{
mvwprintw(win, (int)(GAMEWIN_YLEN/2), (GAMEWIN_XLEN/2 - strlen(str)/2), str);
mvwprintw(win, (int)(GAMEWIN_YLEN/2 + 1), 20, "Press any key to quit...");
wrefresh(win);
}
- Use a função
mvwprintwpara imprimir o texto da mensagem de fim de jogostrno meio da janela. Isso garante que a mensagem de fim de jogo seja exibida centralizada na janela. - Em seguida, imprima "Press any key to quit..." na linha seguinte, no meio da janela, solicitando que o jogador pressione qualquer tecla para sair do jogo.
- Finalmente, use a função
wrefreshpara atualizar a janela e garantir que a mensagem de fim de jogo e o prompt de saída sejam desenhados corretamente na tela.
Para evitar vazamentos de memória, use a função destroysnake para liberar a memória ocupada pelo jogo da cobra, incluindo os nós da cobra e a própria estrutura da cobra. Esta é uma etapa comum de limpeza quando o jogo termina ou reinicia.
void destroysnake(struct TSnake *psnake)
{
struct TSnakeNode *psnode = GetSnakeTail(psnake);
struct TSnakeNode *ptmp = NULL;
int i = 0;
for (; i < psnake->length; ++i) {
ptmp = psnode;
psnode = psnode->front;
free(ptmp);
}
free(psnake);
psnake = NULL;
}
Dentro da função, ela primeiro declara dois ponteiros, psnode e ptmp, para iterar sobre a lista encadeada de nós da cobra.
Ela entra em um loop que itera o número de vezes igual ao comprimento (número de nós) da cobra. Em cada iteração, ela realiza as seguintes operações:
- Define
ptmppara apontar para o nó atualpsnodea fim de liberar a memória para o nó atual mais tarde. - Move
psnodepara o próximo nó (apontado porfront).
Após o término do loop, todos os nós da cobra foram liberados.
Finalmente, a função libera a memória para a própria estrutura da cobra, free(psnake), e define o ponteiro psnake apontando para a estrutura da cobra como NULL para garantir que a memória liberada não seja mais usada.
Compilação e Execução
O comando de compilação é ligeiramente diferente do habitual. Ele requer a adição da opção -l ao gcc para incluir a biblioteca ncurses:
cd ~/project
gcc -o snake snake.c -l ncurses
./snake

Resumo
Você criou com sucesso um jogo da cobra simples em linguagem C usando a biblioteca ncurses. O jogo inclui uma janela de jogo, movimento da cobra, geração de comida e detecção de colisão. Seguindo os passos acima, você pode executar e aproveitar o jogo no terminal. Divirta-se jogando!



