首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从零开始,用C语言实现经典贪吃蛇游戏(附完整代码)

从零开始,用C语言实现经典贪吃蛇游戏(附完整代码)

作者头像
用户11987584
发布2026-01-12 20:17:27
发布2026-01-12 20:17:27
30
举报

今天想和大家分享一个特别适合C语言初学者的项目——贪吃蛇游戏的实现。相信很多同学在学习C语言时都遇到过这样的困惑:学了语法、学了结构,但不知道该怎么把它们用起来。别担心,这个项目就是为你量身定制的!它能帮助你巩固C语言知识,还能让你体验到编程的乐趣。下面,就让我手把手带你完成这个经典小游戏吧 为什么选择贪吃蛇 贪吃蛇和俄罗斯方块、扫雷一样,都是经典游戏中的"元老级"角色。它看似简单,却包含了丰富的编程知识点,特别适合作为C语言学习后的第一个实战项目。通过实现贪吃蛇,你可以: • 巩固C语言基础:函数、结构体、枚举、指针等知识都会用到 • 理解数据结构:链表的实际应用场景 • 接触系统API:学习如何与操作系统交互 • 培养编程思维:从设计到实现的完整流程 最重要的是,当你看到自己写的代码变成一个可玩的游戏时,那种成就感会让你爱上编程!记得我第一次做出贪吃蛇时,兴奋得一晚上没睡着呢!😄

一、项目目标与准备 我们要实现什么?

我们的目标是:使用C语言在Windows控制台中实现一个完整的贪吃蛇游戏,包含以下功能: • 🐍 蛇的移动控制(上下左右方向键) • 🍎 蛇吃食物后身体变长 • 🧱 蛇撞墙或撞到自己时游戏结束 • 📊 实时显示得分 • ⚡ 蛇身加速、减速功能 • ⏸️ 暂停游戏功能

技术栈准备

实现这个项目,我们需要掌握: • C语言基础:函数、结构体、枚举、指针、动态内存管理 • Win32 API:控制台操作相关API • 链表:用于表示蛇的身体 别担心,我会详细解释每个知识点,即使你对某些概念还不太熟悉也没关系!

二、控制台编程基础

在开始写游戏之前,我们先要了解如何在控制台中进行精细的屏幕操作。这部分是很多初学者的"拦路虎",但其实掌握了就很简单。

1. 控制台窗口设置

首先,我们需要设置控制台窗口的大小和标题,让游戏界面看起来更专业:

代码语言:javascript
复制
// 设置控制台窗口大小:100列,30行
system("mode con cols=100 lines=30");
// 设置窗口标题
system("title 贪吃蛇");

这里用到了system()函数,它可以执行DOS命令。就像你打开命令提示符(cmd)输入命令一样,system()让我们的程序也能执行这些命令。

小贴士:在实际开发中,尽量少用system(),因为它效率较低且有安全风险。但在这个小项目中,它是最快捷的方式。

2. 控制台坐标系统

控制台窗口有自己的一套坐标系统,理解这个对游戏开发至关重要:

控制台坐标系统示意图

  • X轴:横向,从左到右递增
  • Y轴:纵向,从上到下递增

注意:控制台中的一个普通字符占1个位置,但宽字符(如中文、特殊符号)占2个位置。这就是为什么在贪吃蛇中,我们的地图坐标要特别注意对齐。

3. 光标控制

默认情况下,控制台会显示一个闪烁的光标,这对游戏体验很不友好。我们需要隐藏它:

代码语言:javascript
复制
// 获取标准输出的句柄
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
// 获取光标信息
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);
// 隐藏光标
CursorInfo.bVisible = false;
// 设置光标状态
SetConsoleCursorInfo(hOutput, &CursorInfo);

我们来演示对比一下:

使用这段代码后:

这段代码可能看起来有点复杂,我就简单解释一下:

想象控制台是一个画布,HANDLE hOutput就是我们拿在手里的画笔。CONSOLE_CURSOR_INFO则是画笔的属性设置本,我们通过GetConsoleCursorInfo查看当前设置,然后修改bVisible(是否可见)属性,最后用SetConsoleCursorInfo应用新设置。

不理解也没有关系,直接使用即可

4. 设置光标位置

为了让蛇和食物出现在正确的位置,我们需要能够精确控制输出位置:

代码语言:javascript
复制
void SetPos(short x, short y) {
    COORD pos = {x, y};
    HANDLE hOutput = NULL;
    hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    SetConsoleCursorPosition(hOutput, pos);
}

COORD是一个结构体,表示坐标点。SetConsoleCursorPosition就是我们的"定位器",告诉系统下次输出应该从哪个位置开始。

演示:

同样的会使用即可

5. 宽字符处理

贪吃蛇中我们会用到一些特殊符号(如蛇身●、食物★、墙体□),这些是宽字符,需要特别处理:

代码语言:javascript
复制
#include <locale.h>
// 设置本地化,支持宽字符
setlocale(LC_ALL, "");
// 定义宽字符
wchar_t ch = L'●';
// 使用wprintf打印宽字符
wprintf(L"%c", ch);

这里有个关键点:C语言默认使用ASCII编码,只支持单字节字符。当我们需要显示中文或特殊符号时,就需要使用宽字符(wchar_t)和宽字符函数(如wprintf)。

专业解释:ASCII编码只能表示128个字符,对于中文等需要更多符号的语言就不够用了。宽字符使用Unicode编码,可以表示更多字符。setlocale(LC_ALL, "")的作用是让程序适应本地环境,支持宽字符显示。

简单的理解这样操作以后就可以打印特殊符号了

6. 按键检测

游戏需要实时响应用户按键,这里我们使用GetAsyncKeyState:

代码语言:javascript
复制
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)

// 检测是否按下了上箭头键
if(KEY_PRESS(VK_UP)) {
    // 处理向上移动
}

GetAsyncKeyState会返回一个16位的值,最高位表示按键状态(按下/抬起),最低位表示是否被按过。我们的宏KEY_PRESS就是提取最低位来判断按键是否被触发。

三、游戏设计与数据结构

现在,我们进入游戏设计的核心部分。一个好的数据结构设计能让代码清晰、易于维护。

1. 地图设计

我们的地图是一个27行×58列的区域(可根据需要调整),四周是墙体:

创建地图的代码:

代码语言:javascript
复制
void CreateMap() {
    int i = 0;
    // 上边墙 (0,0)-(56,0)
    SetPos(0, 0);
    for(i = 0; i < 58; i += 2) {
        wprintf(L"%c", ‘■’);
    }
    // 下边墙 (0,26)-(56,26)
    SetPos(0, 26);
    for(i = 0; i < 58; i += 2) {
        wprintf(L"%c", ‘■’);
    }
    // 左边墙
    for(i = 1; i < 26; i++) {
        SetPos(0, i);
        wprintf(L"%c", ‘■’);
    }
    // 右边墙
    for(i = 1; i < 26; i++) {
        SetPos(56, i);
        wprintf(L"%c", ‘■’);
    }
}

注意:因为宽字符占2个位置,所以墙体坐标要以2为步长(i += 2),否则会出现错位。

2. 蛇身表示:为什么用链表?

这是很多初学者的疑问:为什么蛇身要用链表而不是数组?

这里简单说明一下,之后会在《数据结构杂谈》系列的文章中详细介绍,敬请期待哦!

想象一下,贪吃蛇吃食物后身体会变长,这个长度是动态变化的。如果用数组,我们需要预先分配足够大的空间,而且插入新节点(吃食物后)需要移动大量元素,效率很低。

链表天生适合这种长度动态变化的场景:

  • 添加节点(吃食物)只需在头部插入
  • 移动时只需调整头尾
  • 内存使用更高效

蛇身节点的结构定义:

代码语言:javascript
复制
typedef struct SnakeNode {
    int x;          // 蛇身节点的x坐标
    int y;          // 蛇身节点的y坐标
    struct SnakeNode* next;  // 指向下一个节点的指针
} SnakeNode, *pSnakeNode;

3. 游戏状态管理

我们需要跟踪游戏的各种状态,这里使用枚举类型非常合适:

代码语言:javascript
复制
// 蛇的移动方向
enum DIRECTION {
    UP = 1,
    DOWN,
    LEFT,
    RIGHT
};

// 游戏状态
enum GAME_STATUS {
    OK,             // 正常运行
    KILL_BY_WALL,   // 撞墙
    KILL_BY_SELF,   // 撞到自己
    END_NOMAL       // 正常结束(主动退出)
};

枚举让代码更具可读性。想象一下,如果用数字0、1、2表示方向,以后回头看代码时可能都忘了哪个数字对应哪个方向。而用UPDOWN这样的名字,一目了然!

4. 完整游戏数据结构

最后,我们定义一个结构体来管理整个游戏状态:

代码语言:javascript
复制
typedef struct Snake {
    pSnakeNode _pSnake;     // 维护整条蛇的指针
    pSnakeNode _pFood;      // 维护食物的指针
    enum DIRECTION _Dir;    // 蛇头的方向
    enum GAME_STATUS _Status;// 游戏状态
    int _Socre;             // 当前得分
    int _foodWeight;        // 每个食物的分数
    int _SleepTime;         // 每走一步的休眠时间(控制速度)
} Snake, *pSnake;

这个结构体就像游戏的"大脑",保存了游戏运行所需的所有关键信息。

四、核心功能实现详解

1. 游戏初始化

游戏开始时,我们需要完成一系列准备工作:

代码语言:javascript
复制
void GameStart(pSnake ps) {
    // 设置控制台窗口
    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);
}

其中,InitSnake函数 初始化蛇的初始状态(长度为5,向右移动):

代码语言:javascript
复制
void InitSnake(pSnake ps) {
    pSnakeNode cur = NULL;
    int i = 0;
    
    // 创建5个蛇身节点(头插法)
    for(i = 0; i < 5; i++) {
        cur = (pSnakeNode)malloc(sizeof(SnakeNode));
        cur->next = NULL;
        cur->x = POS_X + i * 2;  // x坐标以2为步长(宽字符对齐)
        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"%c", BODY);
        cur = cur->next;
    }
    
    // 初始化游戏数据
    ps->_SleepTime = 200;   // 初始速度
    ps->_Socre = 0;         // 初始分数
    ps->_Status = OK;       // 游戏状态
    ps->_Dir = RIGHT;       // 初始方向
    ps->_foodWeight = 10;   // 食物分值
}

学长经验:这里使用头插法构建链表,是因为贪吃蛇的移动逻辑中,新节点总是添加在头部(蛇头前方)。头插法让这个操作非常高效。

2. 蛇的移动逻辑

这是游戏的核心!蛇的移动看似简单,实际上包含多个关键步骤:

代码语言:javascript
复制
void SnakeMove(pSnake ps) {
    // 1. 创建下一个节点
    pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
    
    // 2. 根据当前方向计算下一个位置
    switch(ps->_Dir) {
        case UP:
            pNextNode->x = ps->_pSnake->x;
            pNextNode->y = ps->_pSnake->y - 1;
            break;
        // 其他方向类似...
    }
    
    // 3. 检查下一个位置是否是食物
    if(NextIsFood(pNextNode, ps)) {
        EatFood(pNextNode, ps);  // 是食物,吃掉
    } else {
        NoFood(pNextNode, ps);   // 不是食物,正常移动
    }
    
    // 4. 检测碰撞
    KillByWall(ps);
    KillBySelf(ps);
}
吃食物的处理

当蛇头到达食物位置时,我们需要:

  1. 将新节点插入蛇头前方(不需要删除尾部节点)
  2. 增加分数
  3. 生成新的食物
代码语言:javascript
复制
void EatFood(pSnakeNode psn, pSnake ps) {
    // 头插法:新节点成为新的蛇头
    psn->next = ps->_pSnake;
    ps->_pSnake = psn;
    
    // 重新绘制整条蛇
    pSnakeNode cur = ps->_pSnake;
    while(cur) {
        SetPos(cur->x, cur->y);
        wprintf(L"%c", BODY);
        cur = cur->next;
    }
    
    // 增加分数
    ps->_Socre += ps->_foodWeight;
    
    // 释放旧食物,创建新食物
    free(ps->_pFood);
    CreateFood(ps);
}
普通移动的处理

当蛇没有吃到食物时,移动逻辑是:

  1. 将新节点插入蛇头前方
  2. 删除蛇尾节点(保持长度不变)
代码语言:javascript
复制
void NoFood(pSnakeNode psn, pSnake ps) {
    // 头插法:新节点成为新的蛇头
    psn->next = ps->_pSnake;
    ps->_pSnake = psn;
    
    // 绘制除尾部外的所有节点
    pSnakeNode cur = ps->_pSnake;
    while(cur->next->next) {
        SetPos(cur->x, cur->y);
        wprintf(L"%c", BODY);
        cur = cur->next;
    }
    
    // 清除尾部并释放内存
    SetPos(cur->next->x, cur->next->y);
    printf(" ");
    free(cur->next);
    cur->next = NULL;
}

关键点:这里我们只绘制到倒数第二个节点,然后清除最后一个节点并释放内存。这样就实现了"移动"效果——蛇头前进一格,蛇尾跟着前移一格。

碰撞检测

最后,我们需要检测两种碰撞:

  1. 撞墙检测:蛇头坐标是否与墙体坐标重合
  2. 自撞检测:蛇头坐标是否与身体其他部分重合
代码语言:javascript
复制
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;
}

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. 游戏主循环

游戏的主循环负责处理用户输入、更新游戏状态和渲染画面:

代码语言:javascript
复制
void GameRun(pSnake ps) {
    PrintHelpInfo();  // 打印右侧帮助信息
    
    do {
        // 显示当前得分
        SetPos(64, 10);
        printf("得分:%d", ps->_Socre);
        printf(" 每个食物得分:%d分", ps->_foodWeight);
        
        // 检测按键并更新方向
        if(KEY_PRESS(VK_UP) && ps->_Dir != DOWN) {
            ps->_Dir = UP;
        }
        // 其他方向类似...
        
        // 暂停功能
        else if(KEY_PRESS(VK_SPACE)) {
            pause();
        }
        
        // 退出游戏
        else if(KEY_PRESS(VK_ESCAPE)) {
            ps->_Status = END_NOMAL;
            break;
        }
        
        // 加速功能
        else if(KEY_PRESS(VK_F3)) {
            if(ps->_SleepTime >= 50) {
                ps->_SleepTime -= 30;
                ps->_foodWeight += 2;
            }
        }
        
        // 减速功能
        else if(KEY_PRESS(VK_F4)) {
            if(ps->_SleepTime < 350) {
                ps->_SleepTime += 30;
                ps->_foodWeight -= 2;
                if(ps->_SleepTime == 350) {
                    ps->_foodWeight = 1;
                }
            }
        }
        
        // 控制移动速度
        Sleep(ps->_SleepTime);
        // 执行移动逻辑
        SnakeMove(ps);
        
    } while(ps->_Status == OK);  // 游戏继续条件
}

学长小技巧Sleep(ps->_SleepTime)控制蛇的移动速度。值越小,速度越快。通过F3/F4键调整这个值,就能实现加速/减速功能。

五、常见问题与解决方案

在实现过程中,你可能会遇到一些问题,这里分享几个常见问题的解决方案:

1. 字符显示错位

问题:墙体或蛇身显示不整齐,出现"半截"现象。

原因:宽字符占2个位置,但坐标计算时没有考虑这一点。

解决方案

  • 蛇身和食物的x坐标必须是2的倍数
  • 绘制墙体时,循环步长应为2(i += 2

2. 蛇移动过快或过慢

问题:蛇移动速度不合适,游戏体验差。

解决方案

  • 调整_SleepTime的初始值(200毫秒是个不错的起点)
  • 实现加速/减速功能,让玩家可以自定义速度

3. 内存泄漏

问题:长时间游戏后程序崩溃。

原因:没有正确释放动态分配的内存。

解决方案

  • 吃食物时释放旧食物节点:free(ps->_pFood)
  • 游戏结束时释放整条蛇:遍历链表并释放每个节点

4. 按键响应不灵敏

问题:快速按键时,蛇的反应跟不上。

原因GetAsyncKeyState的使用方式不当。

解决方案

  • 确保在每次循环中都检测按键状态
  • 使用KEY_PRESS宏正确提取按键信息

六、完整可运行的代码分享给大家

为什么需要完整代码?

很多同学在学习过程中会遇到这样的问题:

  • 理解了原理,但不知道如何组织代码
  • 遇到编译错误不知道如何解决
  • 想快速看到效果,验证自己的理解

所以,今天我特意整理了经过测试的完整代码,你可以直接复制粘贴,编译运行。如果在实现过程中遇到问题,也可以对照完整代码进行排查。

完整代码结构

我们的贪吃蛇项目采用模块化设计,分为三个文件:

  1. test.cpp - 主程序文件,包含main函数和游戏流程控制
  2. snake.h - 头文件,包含数据结构定义和函数声明
  3. snake.cpp - 实现文件,包含所有功能函数的具体实现

这种组织方式让代码结构清晰,便于理解和维护,也是实际项目开发中常用的方式。

1. test.cpp(主程序文件)
代码语言:javascript
复制
#include "Snake.h"
#include <locale.h>

void test() {
    int ch = 0;
    srand((unsigned int)time(NULL));
    
    do {
        Snake snake = { 0 };
        GameStart(&snake);
        GameRun(&snake);
        GameEnd(&snake);
        SetPos(20, 15);
        printf("再来一局吗?(Y/N):");
        ch = getchar();
        getchar(); // 清理\n
    } while (ch == 'Y');
    SetPos(0, 27);
}

int main() {
    // 修改当前地区为本地模式,为了支持中文宽字符的打印
    setlocale(LC_ALL, "");
    // 测试逻辑
    test();
    return 0;
}
2. snake.h(头文件)
代码语言:javascript
复制
#pragma once
#include <windows.h>
#include <time.h>
#include <stdio.h>

#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)

// 方向
enum DIRECTION {
    UP = 1,
    DOWN,
    LEFT,
    RIGHT
};

// 游戏状态
enum GAME_STATUS {
    OK,             // 正常运行
    KILL_BY_WALL,   // 撞墙
    KILL_BY_SELF,   // 咬到自己
    END_NOMAL       // 正常结束
};

#define WALL L'□'
#define BODY L'●'   // ★○●◇◆□■
#define FOOD L'★'   // ★○●◇◆□■

// 蛇的初始位置
#define POS_X 24
#define POS_Y 5

// 蛇身节点
typedef struct SnakeNode {
    int x;
    int y;
    struct SnakeNode* next;
} SnakeNode, *pSnakeNode;

typedef struct Snake {
    pSnakeNode _pSnake;     // 维护整条蛇的指针
    pSnakeNode _pFood;      // 维护食物的指针
    enum DIRECTION _Dir;    // 蛇头的方向默认是向右
    enum GAME_STATUS _Status;// 游戏状态
    int _Socre;             // 当前获得分数
    int _foodWeight;        // 每个食物10分
    int _SleepTime;         // 每走一步休眠时间
} Snake, *pSnake;

// 游戏开始前的初始化
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);

// 创建食物
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);
3. snake.cpp(实现文件)
代码语言:javascript
复制
#include "Snake.h"

// 设置光标的坐标
void SetPos(short x, short y) {
    COORD pos = { x, y };
    HANDLE hOutput = NULL;
    // 获取标准输出的句柄(用来标识不同设备的数值)
    hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    // 设置标准输出上光标的位置为pos
    SetConsoleCursorPosition(hOutput, pos);
}

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");
}

void CreateMap() {
    int i = 0;
    // 上(0,0)-(56, 0)
    SetPos(0, 0);
    for (i = 0; i < 58; i += 2) {
        wprintf(L"%c", WALL);
    }
    // 下(0,26)-(56, 26)
    SetPos(0, 26);
    for (i = 0; i < 58; i += 2) {
        wprintf(L"%c", WALL);
    }
    // 左 //x是0,y从1开始增长
    for (i = 1; i < 26; i++) {
        SetPos(0, i);
        wprintf(L"%c", WALL);
    }
    // x是56,y从1开始增长
    for (i = 1; i < 26; i++) {
        SetPos(56, i);
        wprintf(L"%c", WALL);
    }
}

void InitSnake(pSnake ps) {
    pSnakeNode cur = NULL;
    int i = 0;
    
    // 创建蛇身节点,并初始化坐标 //头插法
    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 + i * 2;
        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"%c", BODY);
        cur = cur->next;
    }
    
    // 初始化贪吃蛇数据
    ps->_SleepTime = 200;
    ps->_Socre = 0;
    ps->_Status = OK;
    ps->_Dir = RIGHT;
    ps->_foodWeight = 10;
}

void CreateFood(pSnake ps) {
    int x = 0;
    int y = 0;
    
again:
    // 产生的x坐标应该是2的倍数,这样才可能和蛇头坐标对齐。
    do {
        x = rand() % 53 + 2;
        y = rand() % 25 + 1;
    } while (x % 2 != 0);
    
    pSnakeNode cur = ps->_pSnake; // 获取指向蛇头的指针
    // 食物不能和蛇身冲突
    while (cur) {
        if (cur->x == x && cur->y == y) {
            goto again;
        }
        cur = cur->next;
    }
    
    pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); // 创建食物
    if (pFood == NULL) {
        perror("CreateFood::malloc()");
        return;
    }
    else {
        pFood->x = x;
        pFood->y = y;
        SetPos(pFood->x, pFood->y);
        wprintf(L"%c", FOOD);
        ps->_pFood = pFood;
    }
}

void PrintHelpInfo() {
    // 打印提示信息
    SetPos(64, 15);
    printf("不能穿墙,不能咬到自己\n");
    SetPos(64, 16);
    printf("用↑.↓.←.→分别控制蛇的移动.");
    SetPos(64, 17);
    printf("F3为加速,F4为减速\n");
    SetPos(64, 18);
    printf("ESC :退出游戏.space:暂停游戏.");
    
}

void pause() // 暂停
{
    while (1) {
        Sleep(300);
        if (KEY_PRESS(VK_SPACE)) {
            break;
        }
    }
}

// pSnakeNode psn是下一个节点的地址
// pSnake ps维护蛇的指针
int NextIsFood(pSnakeNode psn, pSnake ps) {
    return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}

// pSnakeNode psn是下一个节点的地址
// pSnake ps维护蛇的指针
void EatFood(pSnakeNode psn, pSnake ps) {
    // 头插法
    psn->next = ps->_pSnake;
    ps->_pSnake = psn;
    pSnakeNode cur = ps->_pSnake;
    
    // 打印蛇
    while (cur) {
        SetPos(cur->x, cur->y);
        wprintf(L"%c", BODY);
        cur = cur->next;
    }
    
    ps->_Socre += ps->_foodWeight;
    
    free(ps->_pFood);
    CreateFood(ps);
}

// pSnakeNode psn是下一个节点的地址
// pSnake ps维护蛇的指针
void NoFood(pSnakeNode psn, pSnake ps) {
    // 头插法
    psn->next = ps->_pSnake;
    ps->_pSnake = psn;
    pSnakeNode cur = ps->_pSnake;
    
    // 打印蛇
    while (cur->next->next) {
        SetPos(cur->x, cur->y);
        wprintf(L"%c", BODY);
        cur = cur->next;
    }
    
    // 最后一个位置打印空格,然后释放节点
    SetPos(cur->next->x, cur->next->y);
    printf(" ");
    free(cur->next);
    cur->next = NULL;
}

// pSnake ps维护蛇的指针
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;
}

// pSnake ps维护蛇的指针
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;
}

void SnakeMove(pSnake ps) {
    // 创建下一个节点
    pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
    if (pNextNode == NULL) {
        perror("SnakeMove()::malloc()");
        return;
    }
    
    // 确定下一个节点的坐标,下一个节点的坐标根据,蛇头的坐标和方向确定
    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;
            pNextNode->y = ps->_pSnake->y;
        }
        break;
    case RIGHT:
        {
            pNextNode->x = ps->_pSnake->x + 2;
            pNextNode->y = ps->_pSnake->y;
        }
        break;
    }
    
    // 如果下一个位置就是食物
    if (NextIsFood(pNextNode, ps)) {
        EatFood(pNextNode, ps);
    }
    else // 如果没有食物
    {
        NoFood(pNextNode, ps);
    }
    
    KillByWall(ps);
    KillBySelf(ps);
}

void GameStart(pSnake ps) {
    // 设置控制台窗口的大小,30行,100列 //mode为DOS命令
    system("mode con cols=100 lines=30");
    // 设置cmd窗口名称
    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 GameRun(pSnake ps) {
    // 打印右侧帮助信息
    PrintHelpInfo();
    
    do {
        SetPos(64, 10);
        printf("得分:%d", ps->_Socre);
        printf(" 每个食物得分:%d分", ps->_foodWeight);
        
        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;
            break;
        }
        else if (KEY_PRESS(VK_F3)) {
            if (ps->_SleepTime >= 50) {
                ps->_SleepTime -= 30;
                ps->_foodWeight += 2;
            }
        }
        else if (KEY_PRESS(VK_F4)) {
            if (ps->_SleepTime < 350) {
                ps->_SleepTime += 30;
                ps->_foodWeight -= 2;
                if (ps->_SleepTime == 350) {
                    ps->_foodWeight = 1;
                }
            }
        }
        
        // 蛇每次一定之间要休眠的时间,时间短,蛇移动速度就快
        Sleep(ps->_SleepTime);
        SnakeMove(ps);
        
    } while (ps->_Status == OK);
}

void GameEnd(pSnake ps) {
    pSnakeNode cur = ps->_pSnake;
    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;
    }
    
    // 释放蛇身的节点
    while (cur) {
        pSnakeNode del = cur;
        cur = cur->next;
        free(del);
    }
}

扩展建议

当你成功运行基础版本后,可以尝试以下扩展:

  1. 添加音效:使用Windows的PlaySound API添加吃食物、游戏结束等音效
  2. 保存最高分:使用文件操作保存和读取最高分记录
  3. 多种食物类型:实现不同分数、不同效果的食物

等等

最后的话

看到这里,相信你已经掌握了用C语言实现贪吃蛇的完整过程。编程最重要的不是记住代码,而是理解思路和解决问题的方法。这个项目虽然不大,但它涵盖了C语言的许多核心概念:

  • 结构体与链表:管理复杂数据
  • 枚举类型:提高代码可读性
  • 动态内存管理:合理使用和释放内存
  • 系统API调用:与操作系统交互
  • 游戏循环设计:状态管理和逻辑控制

学长寄语:当你看到自己写的代码变成一个可玩的游戏时,那种成就感会让你明白:所有的努力都是值得的!编程就像搭积木,开始时你可能只认识几块基础积木(语法),但随着经验积累,你会学会如何将它们组合成各种有趣的形状(项目)。贪吃蛇只是你编程之旅的第一站,前方还有更多精彩的风景等着你!

最后,动手去做吧! 不要害怕犯错,调试的过程本身就是最好的学习。期待看到你实现的贪吃蛇,以及你对它的各种创意扩展!

互动时间:你成功实现贪吃蛇了吗?遇到了什么有趣的问题?欢迎在评论区分享你的经验和心得!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-10-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、项目目标与准备 我们要实现什么?
    • 技术栈准备
  • 二、控制台编程基础
    • 1. 控制台窗口设置
    • 2. 控制台坐标系统
    • 3. 光标控制
    • 4. 设置光标位置
    • 5. 宽字符处理
    • 6. 按键检测
  • 三、游戏设计与数据结构
    • 1. 地图设计
    • 2. 蛇身表示:为什么用链表?
    • 3. 游戏状态管理
    • 4. 完整游戏数据结构
  • 四、核心功能实现详解
    • 1. 游戏初始化
    • 2. 蛇的移动逻辑
      • 吃食物的处理
      • 普通移动的处理
      • 碰撞检测
    • 3. 游戏主循环
  • 五、常见问题与解决方案
    • 1. 字符显示错位
    • 2. 蛇移动过快或过慢
    • 3. 内存泄漏
    • 4. 按键响应不灵敏
  • 六、完整可运行的代码分享给大家
    • 为什么需要完整代码?
    • 完整代码结构
      • 1. test.cpp(主程序文件)
      • 2. snake.h(头文件)
      • 3. snake.cpp(实现文件)
    • 扩展建议
  • 最后的话
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档