大学生作业C/C++语言实现贪吃蛇
·
需求分析与目标设定以及目的
1.应付C语言程序设计作业
2.明确贪吃蛇游戏的基本功能需求:蛇的移动、食物生成、碰撞检测、分数计算、游戏结束判定等。确定技术实现目标,如使用控制台界面或图形库(EasyX)。
开发环境搭建
列出必要的开发工具:编译器MSVC(visual studio自带环境)、visual studio 2022、图形库EasyX。
图形库EasyX的安装
使用官方网站EasyX Graphics Library for C++下载的安装包安装easyx库。
核心数据结构设计
- 蛇节点的表示:使用结构体表示蛇节点。
- 蛇的表示:使用动态数组存储蛇的节点。
- 地图与食物:二维数组表示地图,随机数生成食物坐标,避免与蛇身重叠。
// ------------------- 蛇节点 -------------------
struct SnakeNode { int x, y, dir; };
// ------------------- 全局变量 -------------------
vector<SnakeNode> snake;
vector<vector<int>> canvas;
int score = 0;
const int WIN_SIZE = 600;
const int CELL_NUM = 30;
const int CELL_SIZE = WIN_SIZE / CELL_NUM;
map<string, int> userconfig;
wstring namemp3 = L"res\\food.mp3";
wstring failmp3 = L"res\\fail.mp3";
实现读取config以配置游戏难度
- 获得exe文件所在目录
- 读取config.txt文件
- 使用正则表达式解析
// ------------------- 获取EXE所在目录 -------------------
string getExeDir() {
char buffer[MAX_PATH];
GetModuleFileNameA(NULL, buffer, MAX_PATH);
string path(buffer);
size_t pos = path.find_last_of("\\/");
return path.substr(0, pos); // 返回目录部分
}
// ------------------- 初始化配置 -------------------
void init_data() {
string exeDir = getExeDir();
string configPath = exeDir + "\\config.txt";
// 默认值
userconfig["hard"] = 150;
ifstream f(configPath);
if (!f.is_open()) {
cout << "无法打开配置文件,使用默认值" << endl;
return;
}
string line;
regex config(R"(([a-zA-Z]+)=([0-9]+))");
smatch matchs;
while (getline(f, line)) {
if (regex_search(line, matchs, config)) {
string key = matchs[1];
int value = stoi(matchs[2]);
userconfig[key] = value;
}
}
游戏逻辑实现
- 移动控制:通过键盘输入(如WASD或方向键)改变蛇的方向,定时更新蛇头位置并同步身体。
// ------------------- 移动蛇 -------------------
void moveSnake(int newDir) {
SnakeNode head = snake[0];
if ((newDir == VK_UP && head.dir != VK_DOWN) ||
(newDir == VK_DOWN && head.dir != VK_UP) ||
(newDir == VK_LEFT && head.dir != VK_RIGHT) ||
(newDir == VK_RIGHT && head.dir != VK_LEFT)) {
head.dir = newDir;
}
switch (head.dir) {
case VK_UP: head.y--; break;
case VK_DOWN: head.y++; break;
case VK_LEFT: head.x--; break;
case VK_RIGHT: head.x++; break;
}
snake.insert(snake.begin(), head);
}
- 碰撞检测:检查蛇头是否撞墙、撞自身或吃到食物,触发相应逻辑(游戏结束或增长身体)。
- 分数系统:每吃一个食物增加分数,直接内嵌了没单独写模块。
// ------------------- 碰撞检测 -------------------
int checkCollision() {
SnakeNode head = snake[0];
if (canvas[head.y][head.x] == -1) return 1; // 撞墙
for (size_t i = 1; i < snake.size(); i++)
if (snake[i].x == head.x && snake[i].y == head.y) return 2; // 撞自己
if (canvas[head.y][head.x] == 5) { score += 10; return 3; } // 吃食物
return 0;
}
用户界面设计
- 图形库版本:EasyX_20240601。
void door_init() {
/*创建一个门户界面*/
// 绘图窗口初始化
initgraph(911, 608);
//----------------相对路径-----------------
//-------------暂时没用上------------------
string exeDir = getExeDir();
string imagePath = exeDir + "\\res\\doorpng.png";
// 读取图片至绘图窗口
IMAGE img;
loadimage(&img, _T("res\\doorpng.png"));
//=========================================================================================
// 用 vector 保存三个按钮
std::vector<Button> btns;
const int w = 150; const int h = 300;
btns.emplace_back(100 + w, 100 + h, 120, 40, L"开始");
btns.emplace_back(250 + w, 100 + h, 120, 40, L"占位勿点!");
btns.emplace_back(400 + w, 100 + h, 120, 40, L"退出");
// 给每个按钮绑定一个 lambda 回调
btns[0].setCallback([] {
MessageBox(GetHWnd(), L"游戏开始!", L"提示", MB_OK);
closegraph(); GAMEMAIN(); cout << "exit" << endl; exit(0);
});
btns[1].setCallback([] {
MessageBox(GetHWnd(), L"占位勿点!", L"提示", MB_OK);
});
btns[2].setCallback([] {
closegraph(); cout << "exit未玩游戏" << endl; exit(0); // 关闭窗口
});
// 双缓冲绘图:BeginBatchDraw / FlushBatchDraw
BeginBatchDraw();
ExMessage msg;
while(1) // EasyX 提供的主循环宏
{
// 处理所有鼠标消息
while (peekmessage(&msg, EM_MOUSE))
for (auto& btn : btns) btn.handle(msg);
// 清屏 + 绘制所有按钮
cleardevice();
putimage(0, 0, &img);
for (const auto& btn : btns) btn.draw();
FlushBatchDraw();
}
}
代码模块划分
- 主程序模块:游戏主实现。读取键盘输入比较麻烦,使用读取全局输入的函数,而非单个终端的函数可以在终端最小化后正常运行。
bool gameLoop2(int sleep_time) {
initCanvas();
initSnake();
generateFood();
score = 0;
int currentDir = VK_RIGHT;
BeginBatchDraw();
DWORD lastMove = GetTickCount(); // 记录上次移动时间
while (true) {
// ===== 更频繁检测输入(每帧) =====
if (GetAsyncKeyState(VK_UP) & 0x8000 && currentDir != VK_DOWN) currentDir = VK_UP;
if (GetAsyncKeyState(VK_DOWN) & 0x8000 && currentDir != VK_UP) currentDir = VK_DOWN;
if (GetAsyncKeyState(VK_LEFT) & 0x8000 && currentDir != VK_RIGHT) currentDir = VK_LEFT;
if (GetAsyncKeyState(VK_RIGHT) & 0x8000 && currentDir != VK_LEFT) currentDir = VK_RIGHT;
if (GetAsyncKeyState('W') & 0x8000 && currentDir != VK_DOWN) currentDir = VK_UP;
if (GetAsyncKeyState('S') & 0x8000 && currentDir != VK_UP) currentDir = VK_DOWN;
if (GetAsyncKeyState('A') & 0x8000 && currentDir != VK_RIGHT) currentDir = VK_LEFT;
if (GetAsyncKeyState('D') & 0x8000 && currentDir != VK_LEFT) currentDir = VK_RIGHT;
// ===== 控制蛇的移动节奏 =====
if (GetTickCount() - lastMove >= (DWORD)sleep_time) {
lastMove = GetTickCount();
moveSnake(currentDir);
int collision = checkCollision();
if (collision == 1 || collision == 2) {
PlayMusic2(failmp3);
const WCHAR* msg = (collision == 1) ?
L"撞到边界!游戏结束\n按空格重开,ESC退出" :
L"撞到自己!游戏结束\n按空格重开,ESC退出";
MessageBox(GetHWnd(), msg, L"游戏结束", MB_OK);
EndBatchDraw();
return false;
}
if (collision != 3) { snake.pop_back(); }
else {
generateFood(); PlayMusic2(namemp3);
}
updateCanvas();
drawGame();
}
Sleep(10); // 小睡眠,保持 CPU 占用低
}
EndBatchDraw();
}
- 工具函数模块:按钮类的实现。
#include <graphics.h> // EasyX 图形库
#include <functional> // 用于 std::function 保存回调函数
class Button
{
public:
// 按钮三种视觉状态
enum State { NORMAL, HOVER, DOWN };
/*******************************
* 构造函数
* x,y : 按钮左上角坐标
* w,h : 按钮宽高
* txt : 按钮文字(宽字符)
* normalCol : 正常状态颜色
* hoverCol : 悬停状态颜色
* downCol : 按下状态颜色
*******************************/
Button(int x, int y, int w, int h,
const wchar_t* txt,
COLORREF normalCol = WHITE,
COLORREF hoverCol = 0xEEEEEE,
COLORREF downCol = 0xCCCCCC);
// 设置按钮被点击后的回调函数
void setCallback(std::function<void()> cb) { _callback = cb; }
// 绘制按钮
void draw() const;
// 处理鼠标消息,返回 true 表示“本按钮消费了本次点击”
bool handle(const ExMessage& msg);
private:
int _x, _y; // 按钮左上角坐标
int _w, _h; // 按钮宽高
const wchar_t* _txt; // 按钮文字
COLORREF _col[3]; // 三种状态对应的颜色
State _state = NORMAL; // 当前状态
std::function<void()> _callback; // 点击回调
};
调试与优化
- 常见问题:解决帧刷新率不足,导致输入延迟的问题(采用了更频繁的输入检测)。
- 优化:优化随机食物坐标的生成。
扩展功能建议
- 音效:使用Windows 的API接口简单音频库。
void PlayMusic(const std::wstring& filename) { // 使用宽字符字符串
std::wstring command = L"open \"" + filename + L"\" type mpegvideo alias bgmusic";
mciSendString(command.c_str(), NULL, 0, NULL);
command = L"play bgmusic repeat";
mciSendString(command.c_str(), NULL, 0, NULL);
}
void PauseMusic() {
mciSendString(L"pause bgmusic", NULL, 0, NULL);
}
void ResumeMusic() {
mciSendString(L"resume bgmusic", NULL, 0, NULL);
}
void StopMusic() {
mciSendString(L"stop bgmusic", NULL, 0, NULL);
mciSendString(L"close bgmusic", NULL, 0, NULL);
}
总结与参考资料
- EasyX的参考文档足够让你写出简单的窗口和2D游戏内容,虽然远不如Pygame方便。
- 实际上学校讲的c语言课程不太能够完全支撑从头到尾的开发。
- 该文章不算详细,只是简单的介绍,还有一些源码片段。
- 同时只是一个大学生独立写的程序,肯定有更好的实现思路。
更多推荐


所有评论(0)