Building the Classic Snake Game in C
Welcome back to our C Language Series! In this installment, we're taking a nostalgic trip to build one of the most iconic retro games: Snake. This project is a fantastic way to solidify your understanding of C fundamentals, including console input/output, array manipulation, conditional logic, and game loop design.
By the end of this tutorial, you'll have a fully functional Snake game running in your console, and a deeper appreciation for how classic games are structured.
Prerequisites
Before we dive in, ensure you have:
- A basic understanding of C programming (variables, loops, functions, arrays).
- A C compiler (GCC, MinGW, Visual Studio, etc.).
- For Windows users, familiarity with
windows.hfunctions for console manipulation (likeSetConsoleCursorPosition,Sleep) orconio.hforgetch(). For cross-platform, you might look into libraries likencurses, but for simplicity, our primary examples will lean towards a Windows-compatible console approach for screen clearing and cursor control.
Game Mechanics: The Heart of Snake
The Snake game, despite its simplicity, involves several core mechanics:
- Game Board: A defined area where the snake moves.
- Snake: A series of segments (characters) that move in unison. The head leads, and the body follows.
- Food: A single item that appears randomly on the board. When eaten, the snake grows, and a new food item appears.
- Movement: The snake continuously moves in a chosen direction (up, down, left, right).
- Input: Player input changes the snake's direction.
- Collision Detection:
- Wall Collision: If the snake's head hits the boundaries of the game board, the game ends.
- Self Collision: If the snake's head hits any part of its own body, the game ends.
- Score: Increases each time the snake eats food.
- Game Over: Triggered by collisions.
Setting Up Our C Project
We'll start by including necessary headers and defining global variables to manage our game state. Using global variables simplifies access across different functions in a small console application like this.
Headers and Global Variables
We'll need stdio.h for standard I/O, stdlib.h for rand() and srand(), conio.h (for _kbhit() and _getch() on Windows) or windows.h for console-specific functions like SetConsoleCursorPosition and Sleep. We'll include both and choose one set for demonstration.
#include <stdio.h>
#include <stdlib.h>
#include <conio.h> // For _kbhit() and _getch() on Windows
#include <windows.h> // For Sleep() and SetConsoleCursorPosition()
// Global variables for game state
int width = 20;
int height = 20;
int score = 0;
int gameOver = 0;
int headX, headY; // Snake head coordinates
int foodX, foodY; // Food coordinates
// Snake body segments
int tailX[100], tailY[100]; // Max 100 segments
int nTail = 0; // Current number of tail segments
// Enum for snake direction
enum eDirection { STOP = 0, LEFT, RIGHT, UP, DOWN };
enum eDirection dir;
// Function to set cursor position (Windows specific)
void gotoxy(int x, int y) {
COORD coord;
coord.X = x;
coord.Y = y;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord);
}
The gotoxy function is a utility to move the console cursor, which is crucial for redrawing parts of the screen efficiently without flickering.
1. The Setup() Function
This function initializes all game variables when a new game starts or restarts.
void Setup() {
gameOver = 0;
dir = STOP; // Snake is initially stopped
headX = width / 2; // Start snake head in the middle
headY = height / 2;
// Generate random food position
srand(time(NULL)); // Seed for random number generator
foodX = rand() % width;
foodY = rand() % height;
score = 0;
nTail = 0; // No tail segments initially
}
2. The Draw() Function
This function is responsible for rendering the game board, snake, food, and score on the console. We use system("cls") (Windows) or printf("\033[H\033[J") (ANSI escape codes) to clear the screen or gotoxy(0,0) to move the cursor to the top-left for redraw.
void Draw() {
// system("cls"); // Clear the console screen (Windows specific)
gotoxy(0,0); // Move cursor to top-left for redraw
// Top wall
for (int i = 0; i < width + 2; i++)
printf("#");
printf("\n");
for (int i = 0; i < height; i++) {
for (int j = 0; j < width + 2; j++) {
if (j == 0 || j == width + 1) // Side walls
printf("#");
else if (i == headY && j == headX + 1) // Snake head
printf("O");
else if (i == foodY && j == foodX + 1) // Food
printf("F");
else {
int printTail = 0;
for (int k = 0; k < nTail; k++) {
if (tailX[k] == j - 1 && tailY[k] == i) {
printf("o"); // Snake body
printTail = 1;
}
}
if (!printTail)
printf(" "); // Empty space
}
}
printf("\n");
}
// Bottom wall
for (int i = 0; i < width + 2; i++)
printf("#");
printf("\n");
printf("Score: %d\n", score);
}
Note: The j == headX + 1 and j == foodX + 1 are used because we are printing the walls, effectively shifting the internal game coordinates by 1 for display.
3. The Input() Function
This function checks for keyboard input and updates the snake's direction accordingly. We use _kbhit() to check if a key has been pressed without waiting, and _getch() to read the character (both from conio.h for Windows).
void Input() {
if (_kbhit()) { // Check if a key is pressed
switch (_getch()) { // Read the key
case 'a':
if (dir != RIGHT) dir = LEFT;
break;
case 'd':
if (dir != LEFT) dir = RIGHT;
break;
case 'w':
if (dir != DOWN) dir = UP;
break;
case 's':
if (dir != UP) dir = DOWN;
break;
case 'x':
gameOver = 1; // Exit game
break;
}
}
}
4. The Logic() Function
This is the core of the game. It updates the snake's position, checks for collisions, handles food consumption, and updates the score.
void Logic() {
int prevX = tailX[0];
int prevY = tailY[0];
int prev2X, prev2Y;
tailX[0] = headX; // First tail segment takes head's previous position
tailY[0] = headY;
// Shift tail segments: each segment takes the position of the one before it
for (int i = 1; i < nTail; i++) {
prev2X = tailX[i];
prev2Y = tailY[i];
tailX[i] = prevX;
tailY[i] = prevY;
prevX = prev2X;
prevY = prev2Y;
}
// Move the snake's head based on direction
switch (dir) {
case LEFT: headX--; break;
case RIGHT: headX++; break;
case UP: headY--; break;
case DOWN: headY++; break;
default: break;
}
// Check for wall collision
if (headX < 0 || headX >= width || headY < 0 || headY >= height) {
gameOver = 1;
}
// Check for self-collision
for (int i = 0; i < nTail; i++) {
if (tailX[i] == headX && tailY[i] == headY) {
gameOver = 1;
}
}
// Check for food consumption
if (headX == foodX && headY == foodY) {
score += 10;
// Generate new food at a random position
foodX = rand() % width;
foodY = rand() % height;
nTail++; // Grow the snake
}
}
5. The main() Game Loop
The main function orchestrates the game by calling Setup() once, and then repeatedly calling Draw(), Input(), and Logic() within a while loop until gameOver is true. We also add a Sleep() call to control the game speed.
int main() {
Setup();
while (!gameOver) {
Draw();
Input();
Logic();
Sleep(100); // Control game speed (100 milliseconds)
}
// Game over screen
gotoxy(width/2 - 5, height/2);
printf("GAME OVER!");
gotoxy(width/2 - 8, height/2 + 1);
printf("Final Score: %d", score);
getch(); // Wait for a key press before exiting
return 0;
}
Compiling and Running
To compile this code, save it as a .c file (e.g., snake.c) and use your C compiler. For GCC on Windows, you might use:
gcc snake.c -o snake.exe -lwinmm
The -lwinmm flag might be needed if you use certain multimedia functions, though not strictly required for this exact code (Sleep is in windows.h directly). Simply gcc snake.c -o snake.exe should suffice.
Then run it from your command prompt:
./snake.exe
Enhancements and Next Steps
This basic Snake game provides a solid foundation. Here are some ideas for how you can enhance it:
- Speed Control: Make the snake move faster as the score increases by decreasing the
Sleep()duration. - Difficulty Levels: Implement different starting speeds or board sizes.
- Border Types: Allow the snake to pass through walls and reappear on the opposite side.
- Improved UI: Use different characters for the snake's head and body, or even colored output (using ANSI escape codes or Windows console API).
- Pause Feature: Add a 'P' key to pause and unpause the game.
- High Score: Save the highest score to a file.
- Cross-Platform Compatibility: Replace
windows.handconio.hspecific functions with a library like ncurses for Linux/macOS.
Conclusion
Building the Snake game is an excellent exercise in C programming, demonstrating how simple logic and console manipulation can create an engaging interactive experience. You've learned about game loop architecture, input handling, rendering game states, and collision detection—fundamental concepts applicable to many types of software development.
Feel free to experiment with the code, add new features, and make it your own. Happy coding!