Game Architecture and State Management
Developing a console-based Snake game in C requires careful management of game state, including the snake's position, length, and direction. Unlike simpler programs, this game relies on a continuous loop that handles rendering, input processing, and logic updates. To encapsulate the game's properties, a structured approach using a GameState structure clarifies data management compared to loose global variables. The core logic involves tracking the snake's head coordinates and an array of coordinates representing the tail segments.
Rendering the Game Board
The visual representation is achieved by manipulating the console cursor to redraw the scene at a fixed rate. The rendering function clears the screen and iterates through a grid defined by specific width and height constants. During this iteration, the logic checks each coordinate: if it matches the wall boundary, a wall character is drawn; if it matches the snake's head or body segments, the respective symbols are printed. This process must occur efficiently to prevent flickering, although standard console output often incurs some performance overhead.
Input Handling and Movement Logic
Capturing user input without halting the program execution necessitates non-blocking input functions. On Windows environments, _kbhit() checks if a keyboard key has been pressed, allowing the game loop to proceed smoothly if no input is detected. The logic updates the snake's velocity vector based on key presses (W, A, S, D). Crucially, the movement logic must prevent the snake from reversing directly into itself—for instance, disallowing a 'Left' move while currently moving 'Right'.
The tail movement is simulated by shifting the coordinate values of each segment. The position of the last segment is discarded unless the snake consumes food. Upon consuming food, the snake's length increases, the score is updated, and a new food coordinate is generated randomly within the grid boundaries.
Complete Source Code
The following implementation demonstrates these concepts using the windows.h library for sleep functionality and conio.h for keyboard input.
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <windows.h>
#define MAP_WIDTH 20
#define MAP_HEIGHT 20
#define MAX_LENGTH 100
typedef struct {
int posX, posY;
} Coordinate;
typedef struct {
Coordinate head;
Coordinate body[MAX_LENGTH];
int length;
Coordinate food;
int score;
int isRunning;
int velocityX, velocityY; // Direction vectors: -1, 0, 1
} GameContext;
GameContext game;
void InitializeGame() {
game.isRunning = 1;
game.head.posX = MAP_WIDTH / 2;
game.head.posY = MAP_HEIGHT / 2;
game.length = 0;
game.score = 0;
game.velocityX = 0;
game.velocityY = 0;
game.food.posX = rand() % MAP_WIDTH;
game.food.posY = rand() % MAP_HEIGHT;
}
void DrawScene() {
system("cls");
// Draw Top Border
for (int i = 0; i < MAP_WIDTH + 2; i++) printf("#");
printf("\n");
for (int i = 0; i < MAP_HEIGHT; i++) {
printf("#"); // Left Border
for (int j = 0; j < MAP_WIDTH; j++) {
int printed = 0;
// Draw Head
if (j == game.head.posX && i == game.head.posY) {
printf("O");
printed = 1;
}
// Draw Food
if (!printed && j == game.food.posX && i == game.food.posY) {
printf("F");
printed = 1;
}
// Draw Tail
if (!printed) {
for (int k = 0; k < game.length; k++) {
if (game.body[k].posX == j && game.body[k].posY == i) {
printf("o");
printed = 1;
break;
}
}
}
if (!printed) printf(" ");
}
printf("#\n"); // Right Border
}
// Draw Bottom Border
for (int i = 0; i < MAP_WIDTH + 2; i++) printf("#");
printf("\n");
printf("Score: %d\n", game.score);
}
void ProcessInput() {
if (_kbhit()) {
switch (_getch()) {
case 'w':
if (game.velocityY != 1) { game.velocityX = 0; game.velocityY = -1; }
break;
case 's':
if (game.velocityY != -1) { game.velocityX = 0; game.velocityY = 1; }
break;
case 'a':
if (game.velocityX != 1) { game.velocityX = -1; game.velocityY = 0; }
break;
case 'd':
if (game.velocityX != -1) { game.velocityX = 1; game.velocityY = 0; }
break;
case 'x':
game.isRunning = 0;
break;
}
}
}
void UpdateLogic() {
// Shift tail segments
Coordinate prevSeg = game.body[0];
Coordinate currentSeg;
// Update the first tail segment to old head position
if (game.length > 0) {
game.body[0] = game.head;
}
for (int i = 1; i < game.length; i++) {
currentSeg = game.body[i];
game.body[i] = prevSeg;
prevSeg = currentSeg;
}
// Update Head Position
game.head.posX += game.velocityX;
game.head.posY += game.velocityY;
// Boundary Wrapping
if (game.head.posX >= MAP_WIDTH) game.head.posX = 0;
else if (game.head.posX < 0) game.head.posX = MAP_WIDTH - 1;
if (game.head.posY >= MAP_HEIGHT) game.head.posY = 0;
else if (game.head.posY < 0) game.head.posY = MAP_HEIGHT - 1;
// Self Collision Detection
for (int i = 0; i < game.length; i++) {
if (game.body[i].posX == game.head.posX && game.body[i].posY == game.head.posY) {
game.isRunning = 0;
}
}
// Food Consumption
if (game.head.posX == game.food.posX && game.head.posY == game.food.posY) {
game.score += 10;
game.food.posX = rand() % MAP_WIDTH;
game.food.posY = rand() % MAP_HEIGHT;
game.length++;
}
}
int main() {
InitializeGame();
while (game.isRunning) {
DrawScene();
ProcessInput();
UpdateLogic();
Sleep(120); // Control game speed
}
return 0;
}