c语言贪吃蛇游戏开发
C 语言实战:Win32 API 打造经典贪吃蛇游戏(附完整代码解析)
大家好!今天我们来拆解一个经典编程练手项目 ——C 语言 + Win32 API 实现贪吃蛇。作为入门级游戏开发案例,贪吃蛇不仅能巩固 C 语言语法,还能让你掌握「数据结构(链表)」「控制台交互(Win32 API)」「游戏逻辑设计」等核心技能。本文会从项目结构、核心函数到游戏逻辑,一步步带你看懂每一行代码,即使是编程新手也能轻松跟上。
一、前置知识与环境准备
在开始前,先确认你具备这些基础,以及准备好开发环境:
1. 前置知识
-
C 语言基础:结构体、枚举、动态内存管理(malloc/free)、宏定义
-
数据结构:链表的基本操作(头插法、遍历、节点删除)
-
简单 Win32 API 概念:控制台窗口控制、光标操作、按键检测
2. 开发环境
推荐使用 Visual Studio 2022/2019(支持 Win32 API,无需额外配置),创建「控制台应用」项目即可。
3. 项目结构
我们将代码拆分为 3 个文件,职责清晰,便于维护:
文件名 | 作用 |
---|---|
snake.h | 头文件:声明枚举(方向 / 游戏状态)、结构体(蛇节点 / 蛇管理)、函数原型 |
snake.cpp | 源文件:实现所有游戏逻辑函数(地图创建、蛇移动、碰撞检测等) |
test.cpp | 入口文件:主函数(初始化环境、调用游戏启动 / 运行 / 结束函数) |
二、核心头文件解析(snake.h)
头文件是项目的「骨架」,定义了游戏的核心数据结构和接口。我们逐段解析关键内容:
1. 引入依赖与宏定义
#pragma once
#include <windows.h> // Win32 API头文件(控制台控制、按键检测等)
#include <time.h> // 随机数种子(食物随机生成)
#include <stdio.h> // 输入输出(printf/wprintf)// 按键检测宏:判断某个键是否被按下(基于GetAsyncKeyState)
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)// 宽字符定义(地图、蛇身、食物的显示符号)
#define WALL L'□' // 墙体符号(宽字符,占2个字节)
#define BODY L'●' // 蛇身符号
#define FOOD L'★' // 食物符号// 蛇的初始位置(X必须是2的倍数,避免宽字符显示错位)
#define POS_X 24 // 初始X坐标
#define POS_Y 5 // 初始Y坐标
-
宽字符说明:普通字符(如
'a'
)占 1 字节,中文 / 特殊符号(如□
)需用宽字符wchar_t
,前缀加L
(如L'□'
),否则会显示乱码。 -
KEY_PRESS 宏:
GetAsyncKeyState(VK)
获取按键状态,返回值最低位为 1 表示按键被按下,通过&0x1
提取该位,简化按键判断逻辑。
2. 枚举定义(清晰表示状态)
用枚举替代魔法数字,让代码更易读:
// 蛇的移动方向
enum DIRECTION {UP = 1, // 上DOWN, // 下(默认比前一个值+1,即2)LEFT, // 左(3)RIGHT // 右(4,初始方向)
};// 游戏状态
enum GAME_STATUS {OK, // 正常运行KILL_BY_WALL, // 撞墙死亡KILL_BY_SELF, // 撞自身死亡END_NOMAL // 主动退出(ESC键)
};
3. 结构体定义(数据模型)
贪吃蛇的核心是「蛇身」和「食物」,用链表管理蛇身(动态变长),用结构体封装蛇的整体状态:
// 蛇身节点(链表的每个节点,存储单个蛇节的坐标)
typedef struct SnakeNode {int x; // 节点X坐标int y; // 节点Y坐标struct SnakeNode* next; // 指向下一个节点的指针(链表核心)
} SnakeNode, *pSnakeNode;// 蛇的整体管理结构体(封装蛇的所有状态)
typedef struct Snake {pSnakeNode _pSnake; // 指向蛇头的指针(管理整条蛇)pSnakeNode _pFood; // 指向食物的指针enum DIRECTION _Dir; // 当前移动方向(默认RIGHT)enum GAME_STATUS _Status; // 当前游戏状态(默认OK)int _Socre; // 当前得分int _Add; // 每个食物的加分(默认10,加速时增加)int _SleepTime; // 每步休眠时间(控制速度,默认200ms)
} Snake, *pSnake;
- 链表选择原因:蛇身长度会动态变化(吃食物变长),链表的「头插法」能高效添加新节点,删除尾节点也方便,比数组更灵活。
4. 函数声明(接口约定)
// 游戏流程函数
void GameStart(pSnake ps); // 游戏初始化(窗口、地图、蛇、食物)
void GameRun(pSnake ps); // 游戏主循环(按键检测、蛇移动)
void GameEnd(pSnake ps); // 游戏结束(释放内存、提示原因)// 辅助函数
void SetPos(short x, short y); // 设置光标位置(控制台定位)
void WelcomeToGame(); // 欢迎界面
void PrintHelpInfo(); // 打印操作提示(右侧帮助栏)
void CreateMap(); // 创建游戏地图(墙体)
void InitSnake(pSnake ps); // 初始化蛇身(5个节点)
void CreateFood(pSnake ps); // 随机生成食物(避免与蛇身重叠)
void pause(); // 暂停游戏(空格触发)
int NextIsFood(pSnakeNode psn, pSnake ps); // 判断下一个节点是否是食物
void EatFood(pSnakeNode psn, pSnake ps); // 吃食物逻辑(蛇身变长)
void NoFood(pSnakeNode psn, pSnake ps); // 不吃食物逻辑(删尾巴)
int KillByWall(pSnake ps); // 撞墙检测
int KillBySelf(pSnake ps); // 撞自身检测
void SnakeMove(pSnake ps); // 蛇移动核心逻辑
三、核心源文件解析(snake.cpp)
这部分是游戏的「肌肉」,实现了所有交互逻辑。我们挑关键函数拆解:
1. 辅助函数:控制台定位与界面搭建
(1)SetPos:设置光标位置
要在控制台指定位置显示内容(如蛇身、食物),必须先移动光标,否则会出现乱跳:
void SetPos(short x, short y) {COORD pos = {x, y}; // COORD是Win32定义的坐标结构体(X横向,Y纵向)HANDLE hOutput = NULL;// 获取标准输出设备句柄(控制台窗口)hOutput = GetStdHandle(STD_OUTPUT_HANDLE);// 设置光标位置到指定坐标SetConsoleCursorPosition(hOutput, pos);
}
-
控制台坐标规则:左上角为
(0,0)
,X 轴从左到右递增,Y 轴从上到下递增(和数学坐标系不同)。 -
宽字符坐标注意:由于
WALL
/BODY
是宽字符(占 2 个 X 坐标),蛇的 X 坐标必须是 2 的倍数(如 24、26),否则会显示错位。
(2)WelcomeToGame:欢迎界面
游戏启动前的引导界面,提升用户体验:
void WelcomeToGame() {SetPos(40, 15); // 光标定位到中间位置printf("欢迎来到贪吃蛇小游戏");SetPos(40, 25); // 定位到下方system("pause"); // 按任意键继续system("cls"); // 清屏(准备显示操作提示)// 显示操作说明SetPos(25, 12);printf("用 ↑ ↓ ← → 分别控制蛇的移动,F3为加速,F4为减速\n");SetPos(25, 13);printf("加速将能得到更高的分数!\n");SetPos(40, 25);system("pause");system("cls"); // 清屏(准备显示地图)
}
(3)CreateMap:创建游戏地图
地图由「上下左右四堵墙」组成,用宽字符WALL
绘制:
void CreateMap() {int i = 0;// 1. 上墙:Y=0,X从0到56(步长2,宽字符)SetPos(0, 0);for (i = 0; i < 58; i += 2) {wprintf(L"%c", WALL); // 宽字符打印用wprintf}// 2. 下墙:Y=26,X从0到56SetPos(0, 26);for (i = 0; i < 58; i += 2) {wprintf(L"%c", WALL);}// 3. 左墙:X=0,Y从1到25for (i = 1; i < 26; i++) {SetPos(0, i);wprintf(L"%c", WALL);}// 4. 右墙:X=56,Y从1到25for (i = 1; i < 26; i++) {SetPos(56, i);wprintf(L"%c", WALL);}
}
- 地图大小:横向
X=0~56
(共 29 个宽字符位置),纵向Y=0~26
(共 27 行),中间区域为游戏可移动范围。
2. 核心逻辑:蛇与食物的初始化
(1)InitSnake:初始化蛇身(5 个节点)
用「头插法」创建 5 个链表节点,初始方向为右,X 坐标依次递增 2(宽字符对齐):
void InitSnake(pSnake ps) {pSnakeNode cur = NULL;int i = 0;// 1. 头插法创建5个蛇节点for (i = 0; i < 5; i++) {// 动态分配节点内存cur = (pSnakeNode)malloc(sizeof(SnakeNode));if (cur == NULL) {perror("InitSnake()::malloc()"); // 内存分配失败提示return;}// 设置节点坐标:X=24+2*i(右移),Y=5(固定行)cur->x = POS_X + i * 2;cur->y = POS_Y;cur->next = NULL;// 头插法:新节点作为新蛇头if (ps->_pSnake == NULL) {ps->_pSnake = cur; // 第一个节点直接作为蛇头} else {cur->next = ps->_pSnake; // 新节点指向旧蛇头ps->_pSnake = cur; // 更新蛇头为新节点}}// 2. 打印蛇身(遍历链表)cur = ps->_pSnake;while (cur) {SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}// 3. 初始化蛇的状态ps->_SleepTime = 200; // 初始速度(200ms一步)ps->_Socre = 0; // 初始得分0ps->_Status = OK; // 游戏状态正常ps->_Dir = RIGHT; // 初始方向右ps->_Add = 10; // 每个食物加10分
}
- 头插法优势:蛇移动时,新节点(蛇头)需要添加在最前面,头插法时间复杂度为 O (1),效率高。
(2)CreateFood:随机生成食物
食物需满足两个条件:① X 是 2 的倍数(宽字符对齐);② 不与蛇身重叠:
void CreateFood(pSnake ps) {int x = 0, y = 0;
again: // 标签:若食物与蛇身重叠,重新生成// 1. 随机生成坐标:X∈[2,54](步长2),Y∈[1,25]do {x = rand() % 53 + 2; // X范围:2~54(53=56-2-1,避免超出右墙)y = rand() % 25 + 1; // Y范围:1~25(避免超出上下墙)} while (x % 2 != 0); // 确保X是2的倍数// 2. 检查食物是否与蛇身重叠pSnakeNode cur = ps->_pSnake;while (cur) {if (cur->x == x && cur->y == y) {goto again; // 重叠则重新生成}cur = cur->next;}// 3. 创建食物节点并打印pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));if (pFood == NULL) {perror("CreateFood::malloc()");return;}pFood->x = x;pFood->y = y;SetPos(x, y);wprintf(L"%c", FOOD);ps->_pFood = pFood; // 关联食物指针
}
- 随机数种子:在
test.cpp
中用srand((unsigned int)time(NULL))
初始化,确保每次运行食物位置不同。
3. 游戏主循环与移动逻辑
(1)GameRun:游戏主循环
游戏的「心脏」,持续检测按键、更新蛇的状态,直到游戏结束:
void GameRun(pSnake ps) {PrintHelpInfo(); // 打印右侧操作提示// 主循环:只要游戏状态为OK,就持续运行do {// 1. 显示当前得分和加分SetPos(64, 10);printf("得分:%d ", ps->_Socre);printf("每个食物得分:%d分", ps->_Add);// 2. 按键检测与处理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_NOMAL; // ESC主动退出break;} else if (KEY_PRESS(VK_F3) && ps->_SleepTime >= 50) {// F3加速:减少休眠时间,增加加分ps->_SleepTime -= 30;ps->_Add += 2;} else if (KEY_PRESS(VK_F4) && ps->_SleepTime < 350) {// F4减速:增加休眠时间,减少加分ps->_SleepTime += 30;ps->_Add -= 2;if (ps->_SleepTime == 350) {ps->_Add = 1; // 最低加分1}}// 3. 控制蛇的移动速度(休眠时间越短,速度越快)Sleep(ps->_SleepTime);// 4. 蛇移动(核心逻辑)SnakeMove(ps);} while (ps->_Status == OK);
}
-
方向限制:蛇不能直接反向移动(如向右时不能直接向左),避免「瞬间自杀」。
-
速度控制:
Sleep(ps->_SleepTime)
控制每步间隔,_SleepTime
越小,蛇移动越快。
(2)SnakeMove:蛇移动核心逻辑
蛇移动的本质是「添加新蛇头,删除旧蛇尾」(吃食物时不删尾):
void SnakeMove(pSnake ps) {// 1. 创建新蛇头(下一个位置的节点)pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));if (pNextNode == NULL) {perror("SnakeMove()::malloc()");return;}// 2. 根据当前方向计算新蛇头坐标switch (ps->_Dir) {case UP:pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y - 1; // Y减1(上移)break;case DOWN:pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y + 1; // Y加1(下移)break;case LEFT:pNextNode->x = ps->_pSnake->x - 2; // X减2(左移,宽字符)pNextNode->y = ps->_pSnake->y;break;case RIGHT:pNextNode->x = ps->_pSnake->x + 2; // X加2(右移)pNextNode->y = ps->_pSnake->y;break;}// 3. 判断新蛇头是否是食物if (NextIsFood(pNextNode, ps)) {EatFood(pNextNode, ps); // 吃食物(蛇身变长)} else {NoFood(pNextNode, ps); // 不吃食物(删尾巴)// 4. 碰撞检测(不吃食物时才检测,避免吃食物后误判)KillByWall(ps);KillBySelf(ps);}
}
4. 碰撞检测与游戏结束
(1)KillByWall:撞墙检测
判断蛇头坐标是否超出墙体范围:
int 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; // 标记为撞墙死亡return 1;}return 0;
}
- 墙体边界:X=0(左墙)、X=56(右墙)、Y=0(上墙)、Y=26(下墙)。
(2)KillBySelf:撞自身检测
遍历蛇身节点,判断蛇头坐标是否与任意蛇身节点重叠:
int KillBySelf(pSnake ps) {pSnakeNode cur = ps->_pSnake->next; // 从蛇头的下一个节点开始遍历while (cur) {if (ps->_pSnake->x == cur->x && ps->_pSnake->y == cur->y) {ps->_Status = KILL_BY_SELF; // 标记为撞自身死亡return 1;}cur = cur->next;}return 0;
}
(3)GameEnd:游戏结束处理
释放链表内存(避免内存泄漏),并提示游戏结束原因:
void GameEnd(pSnake ps) {pSnakeNode cur = ps->_pSnake;// 1. 提示结束原因SetPos(24, 12);switch (ps->_Status) {case END_NOMAL:printf("您主动退出游戏!\n");break;case KILL_BY_SELF:printf("您撞上自己了,游戏结束!\n");break;case KILL_BY_WALL:printf("您撞墙了,游戏结束!\n");break;}// 2. 释放蛇身节点内存(遍历链表)while (cur) {pSnakeNode del = cur; // 暂存当前节点cur = cur->next; // 移动到下一个节点free(del); // 释放当前节点}// 3. 释放食物节点内存if (ps->_pFood != NULL) {free(ps->_pFood);ps->_pFood = NULL;}
}
四、入口文件解析(test.cpp)
主函数负责初始化环境,调用游戏的「启动 - 运行 - 结束」流程:
#include "Snake.h"
#include <locale.h> // 宽字符本地化支持// 游戏测试函数
void test() {int ch = 0;// 初始化随机数种子(确保每次运行食物位置不同)srand((unsigned int)time(NULL));do {Snake snake = {0}; // 初始化蛇结构体(所有成员为0)GameStart(&snake); // 游戏启动(窗口、地图、蛇、食物)GameRun(&snake); // 游戏运行(主循环)GameEnd(&snake); // 游戏结束(释放内存、提示原因)// 询问是否再来一局SetPos(20, 15);printf("再来一局吗?(Y/N):");ch = getchar();getchar(); // 清理输入缓冲区的换行符(避免下次循环误判)system("cls"); // 清屏(准备新游戏)} while (ch == 'Y' || ch == 'y'); // 输入Y/y则重新开始SetPos(0, 27); // 光标定位到窗口底部
}int main() {// 设置本地化环境(支持中文宽字符显示,避免乱码)setlocale(LC_ALL, "");test(); // 调用游戏测试函数return 0;
}
- setlocale(LC_ALL, “”):关键!设置当前系统的本地化环境,确保宽字符(中文、□、●等)正常显示,否则会出现乱码。
五、运行效果与优化方向
1. 运行效果
-
启动后显示欢迎界面,按任意键进入操作提示;
-
进入游戏界面:左侧为地图(□为墙,●为蛇,★为食物),右侧为操作提示;
-
用方向键控制蛇移动,F3 加速(加分增加),F4 减速(加分减少),空格暂停,ESC 退出;
-
撞墙 / 撞自身后提示结束原因,询问是否再来一局。
2. 优化方向(进阶练习)
-
增加难度梯度:随着得分增加,自动加速;
-
加入排行榜:用文件存储最高得分,每次运行时读取并更新;
-
自定义皮肤:允许用户选择蛇身、食物、墙体的符号;
-
音效支持:用 Win32 API 添加吃食物、撞墙的音效;
-
图形界面:用 EasyX 或 SDL 替代控制台,实现更精美的界面。
六、总结
通过这个贪吃蛇项目,我们不仅巩固了 C 语言基础,还掌握了:
-
数据结构:链表在动态数据(蛇身)管理中的实际应用;
-
Win32 API:控制台窗口控制、光标定位、按键检测等底层交互;
-
游戏逻辑:主循环设计、状态管理、碰撞检测等核心思想。
建议大家亲手敲一遍代码,尝试修改参数(如地图大小、初始速度、加分规则),甚至实现上面的优化方向,这样才能真正掌握!