贪吃蛇是一款经典的游戏,通过C语言实现它可以帮助我们掌握结构体、链表、动态内存管理、Win32 API等核心概念。本文将详细介绍贪吃蛇游戏的完整实现,包含所有源代码,不省略任何关键部分。
目录
[头文件 snake.h](#头文件 snake.h)
[源文件 snake.c](#源文件 snake.c)
[测试文件 test.c](#测试文件 test.c)
[1. 输入缓冲区管理](#1. 输入缓冲区管理)
[2. 链表管理蛇身](#2. 链表管理蛇身)
[3. 坐标系统设计](#3. 坐标系统设计)
[4. 游戏状态机](#4. 游戏状态机)
游戏设计概述
核心数据结构设计
游戏使用链表来表示蛇身,每个节点代表蛇的一节。同时使用控制结构来管理整个游戏的状态。
模块划分
-
游戏初始化:设置控制台窗口,隐藏光标,打印欢迎界面,绘制地图,初始化蛇和食物
-
游戏运行:处理用户输入,更新蛇的位置,检测碰撞,更新分数
-
游戏结束:释放资源,显示结束信息
完整代码实现
头文件 snake.h
头文件定义了游戏所需的所有数据类型、常量和函数声明。
cpp
#pragma once
#include <windows.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#define POS_X 24
#define POS_Y 5
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)
// 蛇的方向枚举
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
// 游戏状态枚举
enum GAME_STATUS
{
OK, // 正常
KILL_BY_WALL, // 撞墙
KILL_BY_SELF, // 撞到自己
END_NORMAL // 正常退出
};
// 蛇身节点结构
typedef struct SnakeNode
{
int x; // 坐标x
int y; // 坐标y
struct SnakeNode* next; // 指向下一个节点
}SnakeNode, * pSnakeNode;
// 贪吃蛇控制结构
typedef struct Snake
{
pSnakeNode _pSnake; // 指向蛇头的指针
pSnakeNode _pFood; // 指向食物节点的指针
enum DIRECTION _dir; // 蛇的方向
enum GAME_STATUS _status; // 游戏状态
int _food_weight; // 一个食物的分数
int _score; // 总成绩
int _sleep_time; // 休息时间,控制蛇的移动速度
}Snake, * pSnake;
// 函数声明
void GameStart(pSnake ps);
void SetPos(short x, short y);
void WelcomeToGame();
void CreateMap();
void InitSnake(pSnake ps);
void CreateFood(pSnake ps);
void GameRun(pSnake ps);
void SnakeMove(pSnake ps);
int NextIsFood(pSnakeNode pn, pSnake ps);
void EatFood(pSnakeNode pn, pSnake ps);
void NoFood(pSnakeNode pn, pSnake ps);
void KillByWall(pSnake ps);
void KillBySelf(pSnake ps);
void GameEnd(pSnake ps);
void ClearInputBuffer();
源文件 snake.c
源文件包含了所有游戏功能的实现。
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
#include <conio.h> // 用于 _kbhit 和 _getch 函数
// 设置光标位置
void SetPos(short x, short y)
{
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x, y };
SetConsoleCursorPosition(houtput, pos);
}
// 清空输入缓冲区
void ClearInputBuffer()
{
fflush(stdin);
while (_kbhit()) {
_getch();
}
}
// 欢迎界面
void WelcomeToGame()
{
SetPos(40, 14);
wprintf(L"欢迎来到贪吃蛇游戏\n");
SetPos(42, 20);
system("pause");
system("cls");
SetPos(25, 14);
wprintf(L"用 ↑. ↓ . ← . → 来控制蛇的移动,按F3加速,F4减速\n");
SetPos(25, 15);
wprintf(L"加速能够得到更高的分数\n");
SetPos(42, 20);
system("pause");
system("cls");
}
// 创建地图
void CreateMap()
{
int i = 0;
// 上墙
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);
}
// 下墙
SetPos(0, 26);
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);
}
// 左墙
for (i = 1; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
// 右墙
for (i = 1; i <= 25; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
}
// 初始化蛇身
void InitSnake(pSnake ps)
{
int i = 0;
pSnakeNode cur = NULL;
// 创建5个初始蛇身节点
for (i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnake()::malloc()");
return;
}
cur->next = NULL;
cur->x = POS_X + 2 * i; // x坐标必须是偶数
cur->y = POS_Y;
// 头插法插入链表
if (ps->_pSnake == NULL)
{
ps->_pSnake = cur;
}
else
{
cur->next = ps->_pSnake;
ps->_pSnake = cur;
}
}
// 打印初始蛇身
cur = ps->_pSnake;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
// 设置贪吃蛇的初始属性
ps->_dir = RIGHT; // 默认向右移动
ps->_score = 0; // 初始分数为0
ps->_food_weight = 10; // 每个食物10分
ps->_sleep_time = 200; // 移动速度200毫秒
ps->_status = OK; // 游戏状态正常
}
// 创建食物
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
again:
// 生成随机坐标,x必须是2的倍数
do
{
x = rand() % 53 + 2; // 2-54范围内
y = rand() % 25 + 1; // 1-25范围内
} while (x % 2 != 0);
// 检查食物是否与蛇身重叠
pSnakeNode cur = ps->_pSnake;
while (cur)
{
if (x == cur->x && y == cur->y)
{
goto again; // 重叠则重新生成
}
cur = cur->next;
}
// 创建食物节点
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
perror("CreateFood()::malloc()");
return;
}
pFood->x = x;
pFood->y = y;
pFood->next = NULL;
// 打印食物
SetPos(x, y);
wprintf(L"%lc", FOOD);
ps->_pFood = pFood;
}
// 游戏初始化
void GameStart(pSnake ps)
{
// 清空输入缓冲区,防止残留输入影响
ClearInputBuffer();
// 设置控制台窗口
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
// 隐藏光标
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(houtput, &CursorInfo);
// 打印欢迎界面
WelcomeToGame();
// 绘制地图
CreateMap();
// 初始化蛇
InitSnake(ps);
// 创建第一个食物
CreateFood(ps);
}
// 打印帮助信息
void PrintHelpInfo()
{
SetPos(60, 13);
wprintf(L"%ls", L"游戏规则:");
SetPos(60, 15);
wprintf(L"%ls", L"1.不能穿墙,不能咬到自己");
SetPos(60, 17);
wprintf(L"%ls", L"2.用 ↑. ↓ . ← . → 来控制蛇的移动");
SetPos(60, 19);
wprintf(L"%ls", L"3.按F3加速,F4减速");
SetPos(60, 21);
wprintf(L"%ls", L"4.按ESC退出游戏,按空格暂停游戏");
}
// 暂停游戏
void Pause()
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
// 判断下一个位置是否是食物
int NextIsFood(pSnakeNode pn, pSnake ps)
{
return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
}
// 吃食物处理
void EatFood(pSnakeNode pn, pSnake ps)
{
// 将食物节点作为新的蛇头
ps->_pFood->next = ps->_pSnake;
ps->_pSnake = ps->_pFood;
// 释放临时节点
free(pn);
pn = NULL;
// 重新打印蛇身
pSnakeNode cur = ps->_pSnake;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
// 增加分数
ps->_score += ps->_food_weight;
// 重新创建食物
CreateFood(ps);
}
// 普通移动处理(不吃食物)
void NoFood(pSnakeNode pn, pSnake ps)
{
// 将新节点作为蛇头
pn->next = ps->_pSnake;
ps->_pSnake = pn;
// 打印蛇身(除了尾部)
pSnakeNode cur = ps->_pSnake;
while (cur->next->next != NULL)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
// 清除蛇尾
SetPos(cur->next->x, cur->next->y);
printf(" ");
// 释放尾部节点
free(cur->next);
cur->next = NULL;
}
// 撞墙检测
void KillByWall(pSnake ps)
{
if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 ||
ps->_pSnake->y == 0 || ps->_pSnake->y == 26)
{
ps->_status = KILL_BY_WALL;
}
}
// 撞自身检测
void KillBySelf(pSnake ps)
{
pSnakeNode cur = ps->_pSnake->next;
while (cur)
{
if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
{
ps->_status = KILL_BY_SELF;
break;
}
cur = cur->next;
}
}
// 蛇移动逻辑
void SnakeMove(pSnake ps)
{
// 创建下一个位置的节点
pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNextNode == NULL)
{
perror("SnakeMove()::malloc()");
return;
}
pNextNode->next = NULL;
// 根据方向计算下一个位置
switch (ps->_dir)
{
case UP:
pNextNode->x = ps->_pSnake->x;
pNextNode->y = ps->_pSnake->y - 1;
break;
case DOWN:
pNextNode->x = ps->_pSnake->x;
pNextNode->y = ps->_pSnake->y + 1;
break;
case LEFT:
pNextNode->x = ps->_pSnake->x - 2; // 注意:宽字符占2个位置
pNextNode->y = ps->_pSnake->y;
break;
case RIGHT:
pNextNode->x = ps->_pSnake->x + 2; // 注意:宽字符占2个位置
pNextNode->y = ps->_pSnake->y;
break;
}
// 检测下一个位置是否是食物
if (NextIsFood(pNextNode, ps))
{
EatFood(pNextNode, ps);
}
else
{
NoFood(pNextNode, ps);
}
// 碰撞检测
KillByWall(ps);
KillBySelf(ps);
}
// 游戏运行主循环
void GameRun(pSnake ps)
{
// 打印帮助信息
PrintHelpInfo();
do
{
// 每帧开始前清空可能的残留输入
ClearInputBuffer();
// 显示分数信息
SetPos(60, 9);
printf("总分数:%d\n", ps->_score);
SetPos(60, 11);
printf("当前食物的分数:%2d\n", ps->_food_weight);
// 处理按键输入
if (KEY_PRESS(VK_UP) && ps->_dir != DOWN)
{
ps->_dir = UP;
}
else if (KEY_PRESS(VK_DOWN) && ps->_dir != UP)
{
ps->_dir = DOWN;
}
else if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT)
{
ps->_dir = LEFT;
}
else if (KEY_PRESS(VK_RIGHT) && ps->_dir != LEFT)
{
ps->_dir = RIGHT;
}
else if (KEY_PRESS(VK_SPACE))
{
Pause();
}
else if (KEY_PRESS(VK_ESCAPE))
{
ps->_status = END_NORMAL;
}
else if (KEY_PRESS(VK_F3))
{
// 加速:减少休眠时间,增加食物分数
if (ps->_sleep_time > 80)
{
ps->_sleep_time -= 30;
ps->_food_weight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
// 减速:增加休眠时间,减少食物分数
if (ps->_food_weight > 2)
{
ps->_sleep_time += 30;
ps->_food_weight -= 2;
}
}
// 蛇移动一步
SnakeMove(ps);
// 控制移动速度
Sleep(ps->_sleep_time);
} while (ps->_status == OK);
// 游戏结束时清空缓冲区
ClearInputBuffer();
}
// 游戏结束处理
void GameEnd(pSnake ps)
{
// 显示游戏结束信息
SetPos(24, 12);
switch (ps->_status)
{
case END_NORMAL:
wprintf(L"您主动结束游戏\n");
break;
case KILL_BY_WALL:
wprintf(L"您撞到墙上,游戏结束\n");
break;
case KILL_BY_SELF:
wprintf(L"您撞到了自己,游戏结束\n");
break;
}
// 释放食物节点
if (ps->_pFood != NULL)
{
free(ps->_pFood);
ps->_pFood = NULL;
}
// 释放蛇身链表
pSnakeNode cur = ps->_pSnake;
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
ps->_pSnake = NULL; // 避免野指针
}
测试文件 test.c
测试文件包含游戏的主循环和用户交互逻辑。
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <locale.h>
#include "snake.h"
// 游戏测试逻辑
void test()
{
char ch = 0;
do
{
// 每次开始新游戏前重置蛇结构体
Snake snake = { 0 };
// 游戏流程:开始→运行→结束
GameStart(&snake);
GameRun(&snake);
GameEnd(&snake);
// 询问是否再来一局
SetPos(20, 15);
printf("再来一局吗?(Y/N):");
fflush(stdout); // 确保提示信息立即显示
// 安全的输入处理
ClearInputBuffer(); // 先清空缓冲区
// 读取第一个非空白字符
int input;
do {
input = getchar();
} while (input == ' ' || input == '\t' || input == '\n');
ch = (char)input;
// 清空该行剩余的所有字符(包括回车)
while (getchar() != '\n');
// 再次清空缓冲区确保干净
ClearInputBuffer();
} while (ch == 'Y' || ch == 'y');
SetPos(0, 26);
}
// 主函数
int main()
{
// 设置本地化环境,支持中文宽字符
setlocale(LC_ALL, "");
// 初始化随机数种子
srand((unsigned int)time(NULL));
// 运行游戏测试
test();
return 0;
}
关键技术解析
1. 输入缓冲区管理
游戏中最重要的技术点之一是输入缓冲区管理。我们通过以下方式彻底解决了输入残留问题:
-
ClearInputBuffer函数 :结合fflush(stdin)和_kbhit()/_getch()彻底清空缓冲区 -
多位置调用:在游戏开始、每帧开始、游戏结束、用户输入前后都清空缓冲区
-
安全输入:使用跳过空白字符的方式确保读取到有效输入
2. 链表管理蛇身
使用链表来管理蛇身具有以下优势:
-
动态增长:蛇吃食物时可以方便地增加节点
-
高效移动:通过头插法和尾删法实现蛇的移动
-
内存管理:游戏结束时需要仔细释放所有节点
3. 坐标系统设计
游戏中的坐标系统需要特别注意:
-
宽字符处理:中文字符占2个普通字符宽度,因此x坐标必须是偶数
-
边界检测:墙体的坐标范围是固定的(0-56, 0-26)
-
移动计算:左右移动每次±2,上下移动每次±1
4. 游戏状态机
游戏使用状态枚举来管理不同的游戏状态:
-
OK:正常运行
-
KILL_BY_WALL:撞墙结束
-
KILL_BY_SELF:撞自己结束
-
END_NORMAL:正常退出
编译和运行
编译命令
在Windows环境下使用支持Win32 API的编译器编译:
cpp
gcc test.c snake.c -o snake.exe
游戏操作说明
-
方向控制:↑ ↓ ← → 控制蛇的移动方向
-
加速减速:F3加速,F4减速
-
暂停游戏:空格键
-
退出游戏:ESC键
总结
通过这个贪吃蛇项目的完整实现,我们深入学习了:
-
链表数据结构的应用和内存管理
-
Win32 API在控制台程序中的使用
-
输入缓冲区管理的重要性和解决方案
-
游戏循环和状态管理机制
-
模块化编程的设计思想
这个项目不仅是一个完整的游戏实现,更是C语言综合应用的优秀示例。读者可以通过扩展功能(如关卡设计、障碍物、存档等)来进一步加深对C语言的理解。
所有代码都已完整呈现,没有省略任何关键部分,确保读者可以完全理解并复现整个项目。