需求分析与目标设定以及目的

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语言课程不太能够完全支撑从头到尾的开发。
    • 该文章不算详细,只是简单的介绍,还有一些源码片段。
    • 同时只是一个大学生独立写的程序,肯定有更好的实现思路。

    Logo

    Agent 垂直技术社区,欢迎活跃、内容共建。

    更多推荐