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到56
    SetPos(0, 26);
    for (i = 0; i < 58; i += 2) {
        wprintf(L"%c", WALL);
    }
    // 3. 左墙:X=0,Y从1到25
    for (i = 1; i < 26; i++) {
        SetPos(0, i);
        wprintf(L"%c", WALL);
    }
    // 4. 右墙:X=56,Y从1到25
    for (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;        // 初始得分0
    ps->_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. 运行效果

  1. 启动后显示欢迎界面,按任意键进入操作提示;

  2. 进入游戏界面:左侧为地图(□为墙,●为蛇,★为食物),右侧为操作提示;

  3. 用方向键控制蛇移动,F3 加速(加分增加),F4 减速(加分减少),空格暂停,ESC 退出;

  4. 撞墙 / 撞自身后提示结束原因,询问是否再来一局。

2. 优化方向(进阶练习)

  • 增加难度梯度:随着得分增加,自动加速;

  • 加入排行榜:用文件存储最高得分,每次运行时读取并更新;

  • 自定义皮肤:允许用户选择蛇身、食物、墙体的符号;

  • 音效支持:用 Win32 API 添加吃食物、撞墙的音效;

  • 图形界面:用 EasyX 或 SDL 替代控制台,实现更精美的界面。

六、总结

通过这个贪吃蛇项目,我们不仅巩固了 C 语言基础,还掌握了:

  1. 数据结构:链表在动态数据(蛇身)管理中的实际应用;

  2. Win32 API:控制台窗口控制、光标定位、按键检测等底层交互;

  3. 游戏逻辑:主循环设计、状态管理、碰撞检测等核心思想。

建议大家亲手敲一遍代码,尝试修改参数(如地图大小、初始速度、加分规则),甚至实现上面的优化方向,这样才能真正掌握!

相关推荐
韩立学长3 小时前
【开题答辩实录分享】以《C#大型超市商品上架调配管理系统的设计与实现》为例进行答辩实录分享
开发语言·c#
十重幻想3 小时前
PTA6-4 使用函数统计指定数字的个数(C)
c语言·c++·算法
夜月yeyue3 小时前
ART 加速器、流水线与指令预测的关系详解
linux·服务器·c语言·单片机·嵌入式硬件·性能优化·嵌入式高阶技巧
迎風吹頭髮3 小时前
UNIX下C语言编程与实践36-UNIX 时钟:系统时间、高分辨率时间与日历时间的转换与使用
服务器·c语言·unix
Yupureki3 小时前
从零开始的C++学习生活 5:内存管理和模板初阶
c语言·c++·学习·visual studio
tao3556673 小时前
【Python刷力扣hot100】49. Group Anagrams
开发语言·python·leetcode
龙腾AI白云3 小时前
大模型-扩散模型(Diffusion Model)原理讲解(4)
开发语言
爱吃小胖橘4 小时前
Lua语法(2)
开发语言·unity·lua
_Power_Y4 小时前
SSM面试题学习
java·开发语言·学习