一、前言
贪吃蛇是一款经典的小游戏,玩家通过键盘控制蛇的移动,吃掉随机出现的食物,同时避免撞到墙壁或自己的身体。本文将从零开始,使用C语言在Windows控制台下实现一个贪吃蛇游戏。我们将使用Windows API进行光标定位和按键检测,并利用宽字符显示图形界面,使游戏更加生动。
二、开发环境
PixPin_2026-02-18_18-40-22
- 操作系统:Windows
- 编译器:Visual Studio(或其他支持Windows API的C编译器)
- 字符集:Unicode(用于显示宽字符)
- 知识点:链表、Windows API、控制台编程
三、整体设计
1. 数据结构
蛇由多个节点组成,每个节点包含坐标和指向下一个节点的指针。我们使用单向链表存储蛇身。
c
typedef struct SnakeNode {
int x; // 控制台列坐标(x为偶数)
int y; // 控制台行坐标
struct SnakeNode* next;
} SnakeNode, *pSnakeNode;
游戏全局信息用一个结构体保存:
c
typedef struct Snake {
pSnakeNode phead; // 蛇头指针
pSnakeNode pfood; // 食物指针
enum State state; // 游戏状态
enum Direction dir; // 当前移动方向
int food_wight; // 当前食物分值(吃一个得几分)
int score; // 总分
int sleep_time; // 移动速度(毫秒)
} Snake, *pSnake;
- 状态枚举:
Ok(正常)、Kill_self(撞自身)、Kill_Wall(撞墙)、End_Nor(正常退出) - 方向枚举:
Up、Down、Left、Right
2. 界面绘制
控制台以字符单元格为单位,我们使用宽字符(如■、●、★、□)来绘制蛇身、食物和墙壁。由于宽字符在控制台中通常占用两个普通字符的宽度,因此蛇的x坐标必须为偶数,以保证对齐。我们通过GotoXY函数将光标移动到指定位置,然后输出相应符号。
3. 游戏逻辑
- 蛇的移动:在蛇头前方创建一个新节点,若该位置是食物,则将食物节点插入蛇头(蛇变长);否则将新节点插入蛇头,并删除尾节点(保持长度不变)。
- 碰撞检测:移动后检查蛇头是否越界(撞墙),或与蛇身其他节点重合(撞自身)。
- 食物生成:随机生成一个位置(x为偶数,且在墙内),确保不与当前蛇身重合。
- 键盘控制 :通过
GetAsyncKeyState检测按键,实现方向控制、加速/减速、暂停和退出。
四、代码实现详解
1. 头文件(snake.h)
首先定义所需的常量、枚举、结构体和函数声明。
c
#pragma once
#include <locale.h>
#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <stdbool.h>
// 宽字符符号
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define HEAD L'■'
// 按键检测宏(检测按键的上升沿)
#define KeyPress(VK) ((GetAsyncKeyState(VK) & 1) ? 1 : 0)
// 游戏状态
enum State {
Ok, // 正常进行
Kill_self, // 撞到自己
Kill_Wall, // 撞到墙
End_Nor // 正常退出(按ESC)
};
// 移动方向
enum Direction {
Up,
Down,
Left,
Right
};
// 蛇节点
typedef struct SnakeNode {
int x;
int y;
struct SnakeNode* next;
} SnakeNode, *pSnakeNode;
// 游戏主体
typedef struct Snake {
pSnakeNode phead;
pSnakeNode pfood;
enum State state;
enum Direction dir;
int food_wight; // 食物分值
int score; // 总分
int sleep_time; // 移动间隔(ms)
} Snake, *pSnake;
// 函数声明
void GotoXY(short x, short y);
void HideCursor(void);
void WelcomeUI(void);
void InitGame(pSnake ps);
void RunGame(pSnake ps);
void Check(pSnake ps);
void GreatFood(pSnake ps);
void EndGame(pSnake ps);
2. 工具函数(snake.c)
隐藏光标
c
void HideCursor() {
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO tmp = { 0 };
GetConsoleCursorInfo(handle, &tmp);
tmp.bVisible = false;
SetConsoleCursorInfo(handle, &tmp);
}
光标定位
c
void GotoXY(short x, short y) {
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
COORD rd = { x, y };
SetConsoleCursorPosition(handle, rd);
}
欢迎界面(可选)
c
void WelcomeUI() {
GotoXY(50, 15);
printf("欢迎来到贪吃蛇游戏");
GotoXY(50, 17);
system("pause");
system("cls");
GotoXY(42, 15);
printf("按↑、↓、←、→控制方向,按空格暂停,按ESC退出");
GotoXY(50, 17);
system("pause");
system("cls");
}
3. 初始化游戏(InitGame)
初始化游戏参数,绘制墙壁,创建蛇身,并生成第一个食物。
c
void InitGame(pSnake ps) {
// 设置初始参数
ps->dir = Right;
ps->food_wight = 2; // 初始食物分值
ps->score = 0;
ps->sleep_time = 300; // 初始速度
ps->state = Ok;
// 绘制墙壁(矩形框)
// 上边界
GotoXY(10, 3);
for (int i = 0; i < 25; i++) {
wprintf(L"%lc", WALL);
}
// 左边界
for (int i = 1; i <= 20; i++) {
GotoXY(10, 3 + i);
wprintf(L"%lc", WALL);
}
// 下边界
GotoXY(10, 24);
for (int i = 0; i < 25; i++) {
wprintf(L"%lc", WALL);
}
// 右边界
for (int i = 1; i <= 20; i++) {
GotoXY(58, 3 + i);
wprintf(L"%lc", WALL);
}
// 创建蛇身(5个节点,初始位置水平排列)
ps->phead = NULL;
for (int i = 0; i < 5; i++) {
pSnakeNode newnode = (pSnakeNode)malloc(sizeof(SnakeNode));
newnode->next = NULL;
newnode->x = 16 + i * 2; // x坐标递增2,保证连续
newnode->y = 6;
// 头插法建立链表(蛇头在最后一个插入的节点)
if (ps->phead == NULL) {
ps->phead = newnode;
} else {
newnode->next = ps->phead;
ps->phead = newnode;
}
}
// 绘制蛇
pSnakeNode pur = ps->phead;
while (pur) {
GotoXY(pur->x, pur->y);
if (pur == ps->phead) {
wprintf(L"%lc", HEAD); // 蛇头
} else {
wprintf(L"%lc", BODY); // 蛇身
}
pur = pur->next;
}
// 生成第一个食物
GreatFood(ps);
}
4. 生成食物(GreatFood)
随机生成食物位置,要求:
- x坐标在[12, 46]之间(墙壁内)
- y坐标在[4, 22]之间(墙壁内)
- x为偶数(与蛇身对齐)
- 不能与蛇身重合
c
void GreatFood(pSnake ps) {
int x = 0;
int y = 0;
again:
do {
x = rand() % 35 + 12; // 12 ~ 46
y = rand() % 19 + 4; // 4 ~ 22
} while (x % 2 != 0); // 保证x为偶数
// 分配食物节点
ps->pfood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (ps->pfood == NULL) {
perror("malloc");
return;
}
ps->pfood->x = x;
ps->pfood->y = y;
ps->pfood->next = NULL;
// 检查是否与蛇身重合
pSnakeNode pur = ps->phead;
while (pur) {
if (x == pur->x && y == pur->y) {
free(ps->pfood); // 释放冲突节点
goto again; // 重新生成
}
pur = pur->next;
}
// 绘制食物
GotoXY(ps->pfood->x, ps->pfood->y);
wprintf(L"%lc", FOOD);
}
5. 游戏主循环(RunGame)
这是游戏的核心,循环执行以下步骤:
- 显示当前分数和提示信息
- 检测按键(Check)
- 创建新节点(根据方向)
- 判断是否吃到食物
- 更新蛇的位置
- 碰撞检测
c
void RunGame(pSnake ps) {
while (1) {
// 显示游戏信息
GotoXY(78, 8);
wprintf(L"贪吃蛇小游戏");
GotoXY(72, 10);
wprintf(L"当前食物分数为%2d, 总分为%2d", ps->food_wight, ps->score);
GotoXY(72, 12);
wprintf(L"按↑、↓、←、→控制方向");
GotoXY(72, 14);
wprintf(L"按空格暂停, 按ESC退出");
// 检测按键
Check(ps);
// 创建新节点(即将成为新蛇头)
pSnakeNode Newnode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (Newnode == NULL) {
perror("malloc");
return;
}
Newnode->next = NULL;
// 根据方向计算新节点坐标
switch (ps->dir) {
case Down:
Newnode->x = ps->phead->x;
Newnode->y = ps->phead->y + 1;
break;
case Up:
Newnode->x = ps->phead->x;
Newnode->y = ps->phead->y - 1;
break;
case Left:
Newnode->x = ps->phead->x - 2;
Newnode->y = ps->phead->y;
break;
case Right:
Newnode->x = ps->phead->x + 2;
Newnode->y = ps->phead->y;
break;
}
// 判断是否吃到食物
if (Newnode->x == ps->pfood->x && Newnode->y == ps->pfood->y) {
// 吃到食物:将食物节点插入蛇头,蛇变长
ps->pfood->next = ps->phead;
ps->phead = ps->pfood;
// 重新绘制整个蛇(注意:原代码此处有bug,全部画成了BODY)
// 修正:根据节点身份分别画HEAD和BODY
pSnakeNode pur = ps->phead;
while (pur) {
GotoXY(pur->x, pur->y);
if (pur == ps->phead) {
wprintf(L"%lc", HEAD);
} else {
wprintf(L"%lc", BODY);
}
pur = pur->next;
}
// 生成新食物
GreatFood(ps);
// 增加分数
ps->score += ps->food_wight;
} else {
// 没吃到食物:移动(插入新头,删除尾)
Newnode->next = ps->phead;
ps->phead = Newnode;
// 重新绘制蛇(清除尾节点)
pSnakeNode pur = ps->phead;
while (pur) {
GotoXY(pur->x, pur->y);
if (pur->next == NULL) {
// 尾节点:清除原位置(打印两个空格)
printf(" ");
} else {
if (pur == ps->phead) {
wprintf(L"%lc", HEAD);
} else {
wprintf(L"%lc", BODY);
}
}
pur = pur->next;
}
// 找到倒数第二个节点,删除尾节点
pur = ps->phead;
while (pur->next->next) {
pur = pur->next;
}
free(pur->next);
pur->next = NULL;
}
// 控制速度
Sleep(ps->sleep_time);
// 碰撞检测
// 撞墙
if (ps->phead->x <= 10 || ps->phead->x >= 58 || ps->phead->y <= 3 || ps->phead->y >= 23) {
ps->state = Kill_Wall;
}
// 撞自身(排除蛇头)
pSnakeNode tmp1 = ps->phead->next;
while (tmp1) {
if (tmp1->x == ps->phead->x && tmp1->y == ps->phead->y) {
ps->state = Kill_self;
break;
}
tmp1 = tmp1->next;
}
if (ps->state != Ok) {
break; // 游戏结束,退出循环
}
}
}
注意:原代码在吃到食物后的绘制部分将所有节点都画成了BODY,导致蛇头也变为身体符号。这里已修正为根据是否为头节点分别绘制HEAD和BODY。
6. 按键检测(Check)
检测按键并做出相应处理。使用GetAsyncKeyState的返回值的第0位(上升沿)来判断按键是否被按下一次。
c
void Check(pSnake ps) {
if (KeyPress(VK_F3)) {
// 加速:减小sleep_time,增加食物分值
if (ps->sleep_time > 120) {
ps->sleep_time -= 20;
ps->food_wight += 2;
}
} else if (KeyPress(VK_F4)) {
// 减速:增大sleep_time,减少食物分值
if (ps->food_wight >= 2) {
ps->sleep_time += 20;
ps->food_wight -= 2;
}
} else if (KeyPress(VK_ESCAPE)) {
// 退出游戏
ps->state = End_Nor;
} else if (KeyPress(VK_UP) && ps->dir != Down) {
ps->dir = Up;
} else if (KeyPress(VK_DOWN) && ps->dir != Up) {
ps->dir = Down;
} else if (KeyPress(VK_LEFT) && ps->dir != Right) {
ps->dir = Left;
} else if (KeyPress(VK_RIGHT) && ps->dir != Left) {
ps->dir = Right;
} else if (KeyPress(VK_SPACE)) {
// 空格暂停,再次按空格继续
while (1) {
Sleep(300);
if (KeyPress(VK_SPACE)) {
break;
}
}
}
}
7. 游戏结束释放内存(EndGame)
释放蛇的所有节点以及可能未被吃掉的食物节点。
c
void EndGame(pSnake ps) {
// 释放蛇身节点
pSnakeNode pur = ps->phead;
while (pur) {
pSnakeNode tmp = pur->next;
free(pur);
pur = tmp;
}
// 如果食物节点还在(未被吃),也要释放
if (ps->pfood != NULL) {
free(ps->pfood);
ps->pfood = NULL;
}
}
五、主函数(test.c)
主函数负责设置控制台环境、循环开始游戏以及询问是否继续。
c
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
int main() {
char ch = 0;
do {
ch = 0;
system("cls"); // 清屏
srand((unsigned int)time(NULL)); // 初始化随机种子
setlocale(LC_ALL, ""); // 启用本地化(支持宽字符)
system("mode con cols=120 lines=33"); // 设置控制台窗口大小
system("title 贪吃蛇"); // 设置标题
HideCursor(); // 隐藏光标
Snake snake;
// WelcomeUI(); // 可选的欢迎界面
InitGame(&snake);
RunGame(&snake);
EndGame(&snake);
GotoXY(24, 10);
printf("还要再来一局吗?请输入(Y/N):");
ch = getchar();
while (getchar() != '\n'); // 清空输入缓冲区
} while (ch == 'Y' || ch == 'y');
GotoXY(0, 28);
return 0;
}
六、完整代码
以下为三个文件的完整代码(已修正上述bug):
snake.h
c
#pragma once
#include <locale.h>
#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <stdbool.h>
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define HEAD L'■'
#define KeyPress(VK) ((GetAsyncKeyState(VK) & 1) ? 1 : 0)
enum State {
Ok,
Kill_self,
Kill_Wall,
End_Nor
};
enum Direction {
Up,
Down,
Left,
Right
};
typedef struct SnakeNode {
int x;
int y;
struct SnakeNode* next;
} SnakeNode, *pSnakeNode;
typedef struct Snake {
pSnakeNode phead;
pSnakeNode pfood;
enum State state;
enum Direction dir;
int food_wight;
int score;
int sleep_time;
} Snake, *pSnake;
void GotoXY(short x, short y);
void HideCursor();
void WelcomeUI();
void InitGame(pSnake ps);
void RunGame(pSnake ps);
void Check(pSnake ps);
void GreatFood(pSnake ps);
void EndGame(pSnake ps);
snake.c
c
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
void HideCursor() {
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO tmp = { 0 };
GetConsoleCursorInfo(handle, &tmp);
tmp.bVisible = false;
SetConsoleCursorInfo(handle, &tmp);
}
void GotoXY(short x, short y) {
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
COORD rd = { x, y };
SetConsoleCursorPosition(handle, rd);
}
void WelcomeUI() {
GotoXY(50, 15);
printf("欢迎来到贪吃蛇游戏");
GotoXY(50, 17);
system("pause");
system("cls");
GotoXY(42, 15);
printf("按↑、↓、←、→控制方向,按空格暂停,按ESC退出");
GotoXY(50, 17);
system("pause");
system("cls");
}
void InitGame(pSnake ps) {
ps->dir = Right;
ps->food_wight = 2;
ps->score = 0;
ps->sleep_time = 300;
ps->state = Ok;
// 画墙
GotoXY(10, 3);
for (int i = 0; i < 25; i++) {
wprintf(L"%lc", WALL);
}
for (int i = 1; i <= 20; i++) {
GotoXY(10, 3 + i);
wprintf(L"%lc", WALL);
}
GotoXY(10, 24);
for (int i = 0; i < 25; i++) {
wprintf(L"%lc", WALL);
}
for (int i = 1; i <= 20; i++) {
GotoXY(58, 3 + i);
wprintf(L"%lc", WALL);
}
// 创建蛇
ps->phead = NULL;
for (int i = 0; i < 5; i++) {
pSnakeNode newnode = (pSnakeNode)malloc(sizeof(SnakeNode));
newnode->next = NULL;
newnode->x = 16 + i * 2;
newnode->y = 6;
if (ps->phead == NULL) {
ps->phead = newnode;
} else {
newnode->next = ps->phead;
ps->phead = newnode;
}
}
// 画蛇
pSnakeNode pur = ps->phead;
while (pur) {
GotoXY(pur->x, pur->y);
if (pur == ps->phead) {
wprintf(L"%lc", HEAD);
} else {
wprintf(L"%lc", BODY);
}
pur = pur->next;
}
// 生成食物
GreatFood(ps);
}
void GreatFood(pSnake ps) {
int x = 0, y = 0;
again:
do {
x = rand() % 35 + 12;
y = rand() % 19 + 4;
} while (x % 2 != 0);
ps->pfood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (ps->pfood == NULL) {
perror("malloc");
return;
}
ps->pfood->x = x;
ps->pfood->y = y;
ps->pfood->next = NULL;
pSnakeNode pur = ps->phead;
while (pur) {
if (x == pur->x && y == pur->y) {
free(ps->pfood);
goto again;
}
pur = pur->next;
}
GotoXY(ps->pfood->x, ps->pfood->y);
wprintf(L"%lc", FOOD);
}
void Check(pSnake ps) {
if (KeyPress(VK_F3)) {
if (ps->sleep_time > 120) {
ps->sleep_time -= 20;
ps->food_wight += 2;
}
} else if (KeyPress(VK_F4)) {
if (ps->food_wight >= 2) {
ps->sleep_time += 20;
ps->food_wight -= 2;
}
} else if (KeyPress(VK_ESCAPE)) {
ps->state = End_Nor;
} else if (KeyPress(VK_UP) && ps->dir != Down) {
ps->dir = Up;
} else if (KeyPress(VK_DOWN) && ps->dir != Up) {
ps->dir = Down;
} else if (KeyPress(VK_LEFT) && ps->dir != Right) {
ps->dir = Left;
} else if (KeyPress(VK_RIGHT) && ps->dir != Left) {
ps->dir = Right;
} else if (KeyPress(VK_SPACE)) {
while (1) {
Sleep(300);
if (KeyPress(VK_SPACE)) {
break;
}
}
}
}
void RunGame(pSnake ps) {
while (1) {
GotoXY(78, 8);
wprintf(L"贪吃蛇小游戏");
GotoXY(72, 10);
wprintf(L"当前食物分数为%2d, 总分为%2d", ps->food_wight, ps->score);
GotoXY(72, 12);
wprintf(L"按↑、↓、←、→控制方向");
GotoXY(72, 14);
wprintf(L"按空格暂停, 按ESC退出");
Check(ps);
pSnakeNode Newnode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (Newnode == NULL) {
perror("malloc");
return;
}
Newnode->next = NULL;
switch (ps->dir) {
case Down:
Newnode->x = ps->phead->x;
Newnode->y = ps->phead->y + 1;
break;
case Up:
Newnode->x = ps->phead->x;
Newnode->y = ps->phead->y - 1;
break;
case Left:
Newnode->x = ps->phead->x - 2;
Newnode->y = ps->phead->y;
break;
case Right:
Newnode->x = ps->phead->x + 2;
Newnode->y = ps->phead->y;
break;
}
if (Newnode->x == ps->pfood->x && Newnode->y == ps->pfood->y) {
// 吃到食物
ps->pfood->next = ps->phead;
ps->phead = ps->pfood;
pSnakeNode pur = ps->phead;
while (pur) {
GotoXY(pur->x, pur->y);
if (pur == ps->phead) {
wprintf(L"%lc", HEAD);
} else {
wprintf(L"%lc", BODY);
}
pur = pur->next;
}
GreatFood(ps);
ps->score += ps->food_wight;
} else {
// 没吃到食物,移动
Newnode->next = ps->phead;
ps->phead = Newnode;
pSnakeNode pur = ps->phead;
while (pur) {
GotoXY(pur->x, pur->y);
if (pur->next == NULL) {
printf(" ");
} else {
if (pur == ps->phead) {
wprintf(L"%lc", HEAD);
} else {
wprintf(L"%lc", BODY);
}
}
pur = pur->next;
}
pur = ps->phead;
while (pur->next->next) {
pur = pur->next;
}
free(pur->next);
pur->next = NULL;
}
Sleep(ps->sleep_time);
// 撞墙检测
if (ps->phead->x <= 10 || ps->phead->x >= 58 || ps->phead->y <= 3 || ps->phead->y >= 23) {
ps->state = Kill_Wall;
}
// 撞自身检测
pSnakeNode tmp1 = ps->phead->next;
while (tmp1) {
if (tmp1->x == ps->phead->x && tmp1->y == ps->phead->y) {
ps->state = Kill_self;
break;
}
tmp1 = tmp1->next;
}
if (ps->state != Ok) {
break;
}
}
}
void EndGame(pSnake ps) {
pSnakeNode pur = ps->phead;
while (pur) {
pSnakeNode tmp = pur->next;
free(pur);
pur = tmp;
}
if (ps->pfood != NULL) {
free(ps->pfood);
ps->pfood = NULL;
}
}
test.c
c
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
int main() {
char ch = 0;
do {
ch = 0;
system("cls");
srand((unsigned int)time(NULL));
setlocale(LC_ALL, "");
system("mode con cols=120 lines=33");
system("title 贪吃蛇");
HideCursor();
Snake snake;
// WelcomeUI(); // 若需要欢迎界面,取消注释
InitGame(&snake);
RunGame(&snake);
EndGame(&snake);
GotoXY(24, 10);
printf("还要再来一局吗?请输入(Y/N):");
ch = getchar();
while (getchar() != '\n');
} while (ch == 'Y' || ch == 'y');
GotoXY(0, 28);
return 0;
}
七、总结
本文从零开始,详细讲解了如何使用C语言在Windows控制台下实现贪吃蛇游戏。我们学习了:
- 使用Windows API进行光标定位和按键检测
- 利用链表动态管理蛇身
- 控制台宽字符输出实现图形界面
- 游戏循环和碰撞检测的设计
代码中还存在一些可以优化的地方,例如:
- 增加难度选择
- 优化绘制效率(避免全屏重绘)
- 增加分数排行榜等
读者可以在此基础上继续扩展,打造更丰富的游戏体验。希望本文对初学者理解C语言和Windows编程有所帮助。