Desenvolvimento de um Jogo de Console em C: Gerenciamento de Tabuleiro e Interação do Jogador

Este projeto, desenvolvido para a disciplina de Programação em C no outono de 2022, foca na criação de um jogo de console interativo. O jogo utiliza o padrão de linguagem C99 e é compilado para um executável Windows (.exe) utilizando GCC 11.2.0. As funcionalidades foram projetadas para rodar em um ambiente de console Windows.

A base de código utiliza predominantemente inglês para evitar problemas de codificação, com poucas exceções em comentários. Para uma correta visualização, o código-fonte deve ser aberto com a codificação UTF-8. Os exemplos de código apresentados neste documento são simplificados para clareza e podem diferir ligeiramente do código-fonte real disponível na pasta 'src'.

Funcionalidades

  1. Geração Aleatória do Tabuleiro

    O jogo opera em um tabuleiro de 10x10. Inicialmente, cada célula tem uma probabilidade de 50% de conter um item (neste caso, um "lata").

  2. Impressão do Tabuleiro

    O estado atual do tabuleiro é exibido na saída padrão, que em sistemas Windows corresponde à janela do console.

  3. Processamento de Comandos do Usuário

    Os comandos são processados de forma insensível a maiúsculas e minúsculas, sendo convertidos para minúsculas. Comandos inválidos após a conversão recebem uma notificação.

    • Comandos Predefinidos:
      • exit ou Ctrl+Z: Encerra o jogo e exibe a pontuação final.
      • Um único dígito de 0 a 6: Controla as ações do jogador.
        1. Movimento Aleatório: Executa uma das ações de movimento (2 a 7) aleatoriamente.
        2. Mover para Cima: O jogador se move para cima. Se a célula superior for uma parede, a posição do jogador permanece inalterada, contando como uma colisão com a parede.
        3. Mover para Baixo: Similar ao movimento para cima, mas na direção para baixo.
        4. Mover para a Esquerda: Similar ao movimento para cima, mas na direção para a esquerda.
        5. Mover para a Dirieta: Similar ao movimento para cima, mas na direção para a direita.
        6. Não Mover: O jogador permanece na posição atual.
        7. Coletar Lata: Se houver uma lata na posição atual do jogador, ela é coletada, e a célula fica vazia.
    • Comandos Adicionais:
      • help: Exibe a documentação de ajuda.
      • wasd: Ativa o modo rápido. Neste modo, os comandos de movimento (WASD para direcional, E para coletar, Q para movimento aleatório) são processados sem a necessidade de pressionar Enter. As teclas I, J, K, L, O, U também executam as mesmas funções. Pressionar Enter sai deste modo.
  4. Atualização em Tempo Real do Painel

    Após cada comando, o nome do comando é exibido em um painel lateral direito, e a pontuação atualizada é mostrada no painel esuqerdo.

Implementação do Código

Visão Geral

A abordagem de desenvolvimento seguiu um processo de "top-down", refinando gradualmente os detalhes. O código apresentado aqui é uma representação simplificada, omitindo alguns detalhes e com formatação de indentação alterada para facilitar a leitura. O código fonte real na pasta 'src' é a versão definitiva.

main.c

A função principal, main, orquestra a lógica geral do programa. Ela inicializa o jogo, gerencia o loop principle de entrada do usuário e processa os comandos até que o jogo seja encerrado.


#include "main.h" // Inclui definições e protótipos

// Função para exibir mensagens de ajuda
void display_help() {
   // Implementação da exibição de ajuda...
   printf("Comandos disponiveis: exit, help, wasd, 0-6\n");
}

// Função para processar um comando de movimento
bool process_move(char command_char) {
   int move_code = command_char - '0'; // Converte char para int
   // Lógica de movimento aqui...
   // Retorna true se o jogo deve continuar, false caso contrário
   return true;
}

// Função para lidar com comandos inválidos
void handle_invalid_command() {
   printf("Comando invalido. Digite 'help' para mais informacoes.\n");
}

int main(int argc, char **argv) {
   initialize_game(); // Função de inicialização (definida em init.c)

   char user_input[512];
   // Loop principal do jogo
   while (1) {
       printf("Digite um comando: ");
       if (scanf("%s", user_input) == EOF) {
           break; // Sai se for fim de arquivo
       }

       // Converte para minúsculas para comparação insensível a maiúsculas/minúsculas
       for(int i = 0; user_input[i]; i++){
           user_input[i] = tolower(user_input[i]);
       }

       if (strcmp(user_input, "exit") == 0) {
           break; // Sai do loop se o comando for 'exit'
       } else if (strcmp(user_input, "help") == 0) {
           display_help();
       } else if (isdigit(user_input[0]) && user_input[0] >= '0' && user_input[0] <= '6') {
           if (!process_move(user_input[0])) {
               break; // Sai se process_move retornar false (ex: jogo terminou)
           }
       } else {
           handle_invalid_command();
       }
       update_dashboard(); // Atualiza os painéis de status
   }

   printf("Pontuacao Final:\n");
   printf("O jogo terminou! Sua pontuacao final e: %d\n", get_current_score());
   // system("pause"); // Pausa em sistemas Windows
   return 0;
}
   

Funções como display_help(), process_move() e handle_invalid_command() podem ser definidas em arquivos separados para melhor organização.

main.h

Este arquivo de cabeçalho declara os tipos de dados personalizados, variáveis globais e funções essenciais utilizadas em todos os arquivos fonte. Para otimização de desempenho, algumas funções podem ser implementadas como macros.


#ifndef PROJECT_MAIN_HEADER
#define PROJECT_MAIN_HEADER

#include <stdlib.h>
#include <string.h>
#include <windows.h> // Necessário para funções do console Windows
#include <ctype.h>   // Para tolower, isdigit

// Estrutura para representar a posição (x, y)
typedef struct {
   unsigned int x_coord : 4;
   unsigned int y_coord : 4;
} PlayerPosition;

// Constante para o espaçamento horizontal no tabuleiro
const int HORIZONTAL_SPACING = 3; // Espaçamento de (HORIZONTAL_SPACING - 1) colunas

// Variáveis globais
extern int current_score;
extern int item_count;
extern char game_board[11][11];      // Matriz para o tabuleiro do jogo
extern PlayerPosition player_pos;    // Instância da posição do jogador

// Macro para mover o cursor do console.
// Adaptada para corresponder ao estilo de nomenclatura da API do Windows.
// Reduz a sobrecarga de chamadas de função.
#define MOVE_CURSOR_TO(row, col) \
   SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), (COORD){(short)(row), (short)(col)})

// Protótipo da função principal
int main(int argc, char *argv[]);

// Outros protótipos de funções globais (definidos em outros .c)
void initialize_game();
void update_dashboard();
int get_current_score();

#endif // PROJECT_MAIN_HEADER
   

A macro MOVE_CURSOR_TO é usada extensivamente para posicionar o cursor e atualizar o tabuleiro com precisão.

init.c

Este arquivo contém a lógica para inicializar o tabuleiro, desenhá-lo no console e atualizar elementos específicos na tela.


#include "init.h" // Inclui definições e protótipos de inicialização

// Função para desenhar o tabuleiro e a posição do jogador
void draw_board() {
   // Desenha a borda superior
   for (int j = 0; j <= HORIZONTAL_SPACING * 10; ++j) {
       putchar(j % HORIZONTAL_SPACING ? ' ' : '#');
   }
   putchar('\n');

   // Desenha o corpo do tabuleiro
   for (int i = 1; i <= 10; ++i) { // Linhas
       putchar('#'); // Borda esquerda
       for (int j = 1; j <= HORIZONTAL_SPACING * 10; ++j) { // Colunas
           if (j % HORIZONTAL_SPACING == 0) {
               putchar(game_board[j / HORIZONTAL_SPACING][i]);
           } else {
               putchar(' '); // Preenche com espaços
           }
       }
       puts("#"); // Borda direita
   }

   // Desenha a borda inferior
   for (int j = 0; j <= HORIZONTAL_SPACING * 10; ++j) {
       putchar(j % HORIZONTAL_SPACING ? ' ' : '#');
   }
   putchar('\n'); // Nova linha

   // Linha divisória
   for (int j = 0; j <= HORIZONTAL_SPACING * 10; ++j) {
       putchar('_');
   }
   puts("\n"); // Nova linha e espaço

   // Desenha o jogador ('!') na sua posição atual
   MOVE_CURSOR_TO(player_pos.x_coord * HORIZONTAL_SPACING, player_pos.y_coord);
   putchar('!');
}

// Função para criar o tabuleiro com itens aleatórios
inline void generate_items() {
   // Inicializa o gerador de números aleatórios
   srand(time(0));
   // Inicializa todas as células como vazias
   memset(game_board, ' ', sizeof(game_board));

   // Preenche o tabuleiro com itens aleatórios
   for (int i = 1; i <= 10; ++i) {
       for (int j = 1; j <= 10; ++j) {
           // 50% de chance de colocar um item ('@')
           if (rand() % 2 == 0) {
               game_board[j][i] = '@';
               item_count++; // Conta o número de itens
           }
       }
   }
}

// Outras funções auxiliares
inline void initialize_prompts() { /* Implementação do painel */ }
void display_help_documentation() { /* Implementação da ajuda */ }
   

Funções como initialize_prompts() e display_help_documentation() realizam tarefas mais simples e não são detalhadas aqui.

move.c

Este arquivo gerencia a lógica de movimento do jogador e a interação com os itens no tabuleiro.


#include "move.h" // Inclui definições e protótipos de movimento

// Enumeração para os códigos de ação (para clareza)
typedef enum {
   ACTION_RANDOM = 1,
   ACTION_UP,
   ACTION_DOWN,
   ACTION_LEFT,
   ACTION_RIGHT,
   ACTION_STAY,
   ACTION_COLLECT
} ActionCode;

// Função principal de movimento do jogador
// Retorna true se todos os itens foram coletados, false caso contrário.
bool perform_player_action(int action_code) {
   // Se o código for 0, seleciona uma ação aleatória válida
   if (action_code == 0) {
       action_code = (rand() % 6) + 1; // Gera número entre 1 e 6
   }

   switch (action_code) {
       case ACTION_UP:
       case ACTION_DOWN:
       case ACTION_LEFT:
       case ACTION_RIGHT:
           // Lógica de movimento direcional
           // Verificar colisão com paredes e atualizar player_pos se o movimento for válido
           // Se colidir com parede, subtrair pontos: current_score -= 5;
           printf("Movimento executado.\n");
           break;
       case ACTION_COLLECT:
           printf("Tentando coletar...\n");
           // Verifica se há um item na posição atual
           if (game_board[player_pos.x_coord][player_pos.y_coord] == '@') {
               game_board[player_pos.x_coord][player_pos.y_coord] = ' '; // Remove o item
               current_score += 10; // Adiciona pontos
               if (--item_count == 0) {
                   return true; // Todos os itens foram coletados
               }
           } else {
               current_score -= 2; // Penalidade por tentar coletar onde não há item
           }
           break;
       default: // ACTION_STAY ou ação não definida
           printf("Nenhuma acao realizada.\n");
           break;
   }

   update_score_display(); // Atualiza a exibição da pontuação
   return false; // Continua o jogo
}

// Função para movimento rápido (reutiliza perform_player_action)
void handle_fast_move(char key) {
   // Mapeia teclas para action_code e chama perform_player_action
   // Ex: 'w' -> ACTION_UP, 'e' -> ACTION_COLLECT, 'q' -> ACTION_RANDOM
   int code = -1;
   switch(tolower(key)) {
       case 'w': case 'i': code = ACTION_UP; break;
       case 's': case 'k': code = ACTION_DOWN; break;
       case 'a': case 'j': code = ACTION_LEFT; break;
       case 'd': case 'l': code = ACTION_RIGHT; break;
       case 'e': case 'o': code = ACTION_COLLECT; break;
       case 'q': case 'u': code = ACTION_RANDOM; break;
   }
   if (code != -1) {
       perform_player_action(code);
   }
}
   

Uma função handle_fast_move() foi implementada para reutilizar a lógica de perform_player_action(), evitando duplicação de código. Detalhes completos estão no código fonte.

Desafios e Soluções

Abordagens Iniciais

  1. Leitura de Caractere Único: Em plataformas não-Windows, a leitura de um único caractere sem esperar Enter pode ser feita usando bibliotecas como termios.h. O código a seguir ilustra essa técnica: ```

    #include <termios.h> #include <unistd.h> // Para STDIN_FILENO

    char get_single_char() { struct termios old_tio, new_tio; char input_char;

     // Obtém os atributos atuais do terminal
     tcgetattr(STDIN_FILENO, &old_tio);
     new_tio = old_tio;
    
     // Desabilita o modo canônico (buffer de linha) e o eco
     new_tio.c_lflag &= ~(ICANON | ECHO);
    
     // Aplica os novos atributos imediatamente
     tcsetattr(STDIN_FILENO, TCSANOW, &new_tio);
    
     // Lê um caractere
     input_char = getchar();
    
     // Restaura os atributos originais do terminal
     tcsetattr(STDIN_FILENO, TCSANOW, &old_tio);
    
     return input_char;
    

    }

  2. Manipulação do Cursor: Para tabuleiros pequenos, o cursor poderia ser movido usando sequências de escape como \\b (backspace) ou recriando a linha inteira com \\r (carriage return). No entanto, essas abordagens podem ser ineficientes.

Solução Adotada

Após pesquisa na documentação do Windows, a função SetConsoleCursorPosition foi utilizada. Ela permite mover o cursor para coordenadas específicas (nRows, nCols) no console, possibilitando a atualização precisa do tabuleiro.


void move_cursor_to(short nRows, short nCols) {
   COORD position = {nRows, nCols};
   SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), position);
}
   

Esta função, implementada como a macro MOVE_CURSOR_TO no cabeçalho, é crucial para a atualização eficiente da interface do console.

Depuração

Um bug comum encontrado foi a inversão das coordenadas (x, y) em relação às variáveis de loop do console, que geralmente correspondem a j (coluna) e i (linha). A correção envolveu garantir a correspondência correta entre as coordenadas do jogo e os índices da matriz.

Referências

As principais referências para este projeto foram:

  • Documentação oficial do Windows Console (Microsoft Learn), especificamente sobre a função SetConsoleCursorPosition.

Sugestões de Melhoria

  • Interface Gráfica (GUI): A interface de console preta e branca pode ser substituída por uma GUI para uma experiência de usuário mais amigável. Alterar cores no console, como a cor do jogador, oferece melhorias limitadas.
  • Documentação para Console: Para projetos que utilizam interfaces de console, fornecer guias claros sobre como interagir com essa UI pode ser benéfico, pois é um padrão comum em muitos executáveis.
  • Comandos Intuitivos: Comandos como "0123456" são menos intuitivos. A adoção de esquemas de controle comuns em jogos, como "WASD" para movimento e "Q", "E", "Espaço" para interações, melhoraria a usabilidade. Essas teclas são de caractere único e fáceis de integrar em estruturas switch.

Tags: C Console Windows API Jogo de Console Desenvolvimento de Jogos

Publicado em 7-3 20:43