C语言实现贪吃蛇游戏详解

引言

贪吃蛇是一款经典的游戏,相信很多人在小时候都玩过。本文将详细介绍如何使用C语言实现一个控制台版的贪吃蛇游戏。我们将从游戏的设计思路、关键数据结构和函数实现等方面进行深入分析,并给出完整的代码和运行结果。

目录

引言

正文

一、游戏设计思路

二、关键数据结构

三、核心函数详解

[1. 设置光标位置(SetPos)](#1. 设置光标位置(SetPos))

[2. 欢迎界面(WelcomeToGame)](#2. 欢迎界面(WelcomeToGame))

[3. 创建地图(CreateMap)](#3. 创建地图(CreateMap))

[4. 初始化蛇身(InitSnake)](#4. 初始化蛇身(InitSnake))

[5. 创建食物(CreateFood)](#5. 创建食物(CreateFood))

[6. 游戏初始化(GameStart)](#6. 游戏初始化(GameStart))

[7. 游戏主循环(GameRun)](#7. 游戏主循环(GameRun))

[8. 蛇的移动(SnakeMove)](#8. 蛇的移动(SnakeMove))

[9. 吃食物处理(EatFood)](#9. 吃食物处理(EatFood))

[10. 未吃食物处理(NoFood)](#10. 未吃食物处理(NoFood))

[11. 碰撞检测](#11. 碰撞检测)

[12. 游戏结束处理(GameEnd)](#12. 游戏结束处理(GameEnd))

[13. 测试函数和主函数](#13. 测试函数和主函数)

四、运行结果

总结


正文

一、游戏设计思路

贪吃蛇游戏主要包括以下几个部分:

  1. 游戏初始化:包括设置控制台窗口、隐藏光标、绘制地图、初始化蛇和食物等。

  2. 游戏循环:处理用户输入,更新蛇的状态,判断游戏是否结束。

  3. 游戏结束:释放资源,显示结束信息。

我们使用链表来表示蛇的身体,每个节点代表蛇的一节。食物则是一个单独的节点。蛇的移动通过链表的插入和删除操作来实现。

二、关键数据结构

我们定义了两个重要的结构体:SnakeNode(蛇身节点)和Snake(贪吃蛇)。

snake.h 头文件定义:

复制代码
#pragma once
#include <windows.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#define POS_X 24
#define POS_Y 5

#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'

#define KEY_PRESS(vk)  ((GetAsyncKeyState(vk)&1)?1:0)

//类型的声明
//蛇的方向
enum DIRECTION
{
    UP = 1,
    DOWN,
    LEFT,
    RIGHT
};

//蛇的状态
//正常,撞墙,撞到自己,正常退出
enum GAME_STATUS
{
    OK,//正常
    KILL_BY_WALL,//撞墙
    KILL_BY_SELF,//撞到自己
    END_NORMAL//正常退出
};

//蛇身的节点类型
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 _food_weight;//一个食物的分数
    int _score;//总成绩
    int _sleep_time;//休息时间,时间越短,速度越快,时间越长,速度越慢
}Snake, * pSnake;

//函数的声明
void GameStart(pSnake ps);
void SetPos(short x, short y);
void WelcomeToGame();
void CreateMap();
void InitSnake(pSnake ps);
void CreateFood(pSnake ps);
void GameRun(pSnake ps);
void SnakeMove(pSnake ps);
int NextIsFood(pSnakeNode pn, pSnake ps);
void EatFood(pSnakeNode pn, pSnake ps);
void NoFood(pSnakeNode pn, pSnake ps);
void KillByWall(pSnake ps);
void KillBySelf(pSnake ps);
void GameEnd(pSnake ps);

三、核心函数详解

1. 设置光标位置(SetPos)
复制代码
void SetPos(short x, short y)
{
    //获得标准输出设备的句柄
    HANDLE houtput = NULL;
    houtput = GetStdHandle(STD_OUTPUT_HANDLE);

    //定位光标的位置
    COORD pos = { x, y };
    SetConsoleCursorPosition(houtput, pos);
}

这个函数用于设置控制台光标的位置,以便在指定位置输出字符。

2. 欢迎界面(WelcomeToGame)
复制代码
void WelcomeToGame()
{
    SetPos(40, 14);
    wprintf(L"欢迎来到贪吃蛇游戏\n");
    SetPos(42, 20);
    system("pause");
    system("cls");
    SetPos(25, 14);
    wprintf(L"用 ↑. ↓ . ← . → 来控制蛇的移动,按F3加速,F4减速\n");
    SetPos(25, 15);
    wprintf(L"加速能够得到更高的分数\n");
    SetPos(42, 20);
    system("pause");
    system("cls");
}

显示欢迎信息和操作说明,为玩家提供游戏指引。

3. 创建地图(CreateMap)
复制代码
void CreateMap()
{
    //上
    int i = 0;
    for (i = 0; i < 29; i++)
    {
        wprintf(L"%lc", WALL);
    }

    //下
    SetPos(0, 26);
    for (i = 0; i < 29; i++)
    {
        wprintf(L"%lc", WALL);
    }
    //左
    for (i = 1; i <= 25; i++)
    {
        SetPos(0, i);
        wprintf(L"%lc", WALL);
    }

    //右
    for (i = 1; i <= 25; i++)
    {
        SetPos(56, i);
        wprintf(L"%lc", WALL);
    }
}

绘制游戏地图,由字符方块组成围墙,限定游戏区域。

4. 初始化蛇身(InitSnake)
复制代码
void InitSnake(pSnake ps)
{
    int i = 0;
    pSnakeNode cur = NULL;

    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 + 2 * i;
        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"%lc", BODY);
        cur = cur->next;
    }

    //设置贪吃蛇的属性
    ps->_dir = RIGHT;//默认向右
    ps->_score = 0;
    ps->_food_weight = 10;
    ps->_sleep_time = 200;//单位是毫秒
    ps->_status = OK;
}

初始化蛇身,蛇初始长度为5,使用头插法创建链表,蛇头在链表的头部。初始方向向右,初始分数为0。

5. 创建食物(CreateFood)
复制代码
void CreateFood(pSnake ps)
{
    int x = 0;
    int y = 0;

    //生成x是2的倍数
    //x:2~54
    //y: 1~25
again:
    do
    {
        x = rand() % 53 + 2;
        y = rand() % 25 + 1;
    } while (x % 2 != 0);

    //x和y的坐标不能和蛇的身体坐标冲突
    pSnakeNode cur = ps->_pSnake;
    while (cur)
    {
        if (x == cur->x && y == cur->y)
        {
            goto again;
        }
        cur = cur->next;
    }

    //创建食物的节点
    pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
    if (pFood == NULL)
    {
        perror("CreateFood()::malloc()");
        return;
    }

    pFood->x = x;
    pFood->y = y;
    pFood->next = NULL;

    SetPos(x, y);//定位位置
    wprintf(L"%lc", FOOD);//打印食物

    ps->_pFood = pFood;
}

随机生成食物位置,确保不在蛇身上,且x坐标为偶数(为了对齐,因为蛇身节点之间的x坐标差为2)。

6. 游戏初始化(GameStart)
复制代码
void GameStart(pSnake ps)
{
    //初始化游戏
    //0.先设置窗口的大小,再让光标隐藏
    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);//设置控制台光标状态

    //1.打印环境界面  2.功能介绍
    WelcomeToGame();

    //3.绘制地图
    CreateMap();

    //4.创建蛇
    InitSnake(ps);

    //5.创建食物
    CreateFood(ps);
}

游戏初始化函数,设置窗口、隐藏光标、显示欢迎信息、绘制地图、初始化蛇和食物。

7. 游戏主循环(GameRun)
复制代码
void GameRun(pSnake ps)
{
    //打印帮助信息
    PrintHelpInfo();

    do
    {
        //打印总分数和食物的分值
        SetPos(60, 9);
        printf("总分数:%d\n", ps->_score);
        SetPos(60, 11);
        printf("当前食物的分数:%2d\n", ps->_food_weight);

        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_NORMAL;
        }
        else if (KEY_PRESS(VK_F3))
        {
            //加速
            if (ps->_sleep_time > 80)
            {
                ps->_sleep_time -= 30;
                ps->_food_weight += 2;
            }
        }
        else if (KEY_PRESS(VK_F4))
        {
            //减速
            if (ps->_food_weight > 2)
            {
                ps->_sleep_time += 30;
                ps->_food_weight -= 2;
            }
        }

        SnakeMove(ps);//蛇走一步的过程

        Sleep(ps->_sleep_time);

    } while (ps->_status == OK);
}

游戏主循环,处理用户输入,更新游戏状态,控制游戏速度。

8. 蛇的移动(SnakeMove)
复制代码
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);
        pNextNode = NULL;//(待确定)
    }
    else
    {
        NoFood(pNextNode, ps);
    }

    //检测蛇是否撞墙
    KillByWall(ps);
    //检测蛇是否撞到自己
    KillBySelf(ps);
}

蛇移动的核心函数,根据方向计算下一个位置,判断是否吃到食物,处理移动逻辑。

9. 吃食物处理(EatFood)
复制代码
void EatFood(pSnakeNode pn, pSnake ps)
{
    //头插法
    ps->_pFood->next = ps->_pSnake;
    ps->_pSnake = ps->_pFood;

    //释放下一个位置的节点
    free(pn);
    pn = NULL;

    pSnakeNode cur = ps->_pSnake;
    //打印蛇
    while (cur)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%lc", BODY);
        cur = cur->next;
    }
    ps->_score += ps->_food_weight;

    //重新创建食物
    CreateFood(ps);
}

当蛇吃到食物时,将食物节点插入蛇头,增加分数,重新生成食物。

10. 未吃食物处理(NoFood)
复制代码
void NoFood(pSnakeNode pn, pSnake ps)
{
    //头插法
    pn->next = ps->_pSnake;
    ps->_pSnake = pn;

    pSnakeNode cur = ps->_pSnake;
    while (cur->next->next != NULL)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%lc", BODY);
        cur = cur->next;
    }

    //把最后一个结点打印成空格
    SetPos(cur->next->x, cur->next->y);
    printf("  ");

    //释放最后一个结点
    free(cur->next);

    //把倒数第二个节点的地址置为NULL
    cur->next = NULL;
}

当蛇没有吃到食物时,移动蛇身,移除蛇尾节点。

11. 碰撞检测
复制代码
void 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;
    }
}

void KillBySelf(pSnake ps)
{
    pSnakeNode cur = ps->_pSnake->next;//从蛇头的下一个节点开始检测,如过后面的节点的坐标跟蛇头坐标一样,就撞到自己了
    while (cur)
    {
        if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
        {
            ps->_status = KILL_BY_SELF;
            break;
        }
        cur = cur->next;
    }
}

检测蛇是否撞墙或撞到自己,更新游戏状态。

12. 游戏结束处理(GameEnd)
复制代码
void GameEnd(pSnake ps)
{
    SetPos(24, 12);
    switch (ps->_status)
    {
    case END_NORMAL:
        wprintf(L"您主动结束游戏\n");
        break;
    case KILL_BY_WALL:
        wprintf(L"您撞到墙上,游戏结束\n");
        break;
    case KILL_BY_SELF:
        wprintf(L"您撞到了自己,游戏结束\n");
        break;
    }

    if (ps->_pFood != NULL)
    {
        free(ps->_pFood);
        ps->_pFood = NULL;
    }

    //释放蛇身的链表
    pSnakeNode cur = ps->_pSnake;
    while (cur)
    {
        pSnakeNode del = cur;
        cur = cur->next;
        free(del);
    }

    ps->_pSnake = NULL;  // 添加这行,避免野指针
}

显示游戏结束信息,释放所有动态分配的内存。

13. 测试函数和主函数
复制代码
//完成的是游戏的测试逻辑
void test()
{
    //创建贪吃蛇
    //初始化游戏
    //1.打印环境界面
    //2.功能介绍
    //3.绘制地图
    //4.创建蛇
    //5.创建食物
    //6.设置游戏相关的信息
    char ch = 0;

    do
    {
        //每次开始新游戏前重置蛇结构体
        Snake snake = { 0 };
        
        GameStart(&snake);
        //运行游戏
        GameRun(&snake);
        //结束游戏——善后工作
        GameEnd(&snake);

        SetPos(20, 15);
        printf("再来一局吗?(Y/N):");

        scanf(" %c", &ch);
        
        // 清空输入缓冲区,防止之前游戏过程中的残留输入影响
        while (getchar() != '\n');    

    } while (ch == 'Y' || ch == 'y');
    SetPos(0, 26);
}

int main()
{
    //设置适配本地环境
    setlocale(LC_ALL, "");
    srand((unsigned int)time(NULL));
    test();

    return 0;
}

四、运行结果

运行程序后,首先显示欢迎界面:

复制代码
欢迎来到贪吃蛇游戏

按任意键继续后,显示操作说明:

text

复制代码
用 ↑. ↓ . ← . → 来控制蛇的移动,按F3加速,F4减速
加速能够得到更高的分数

再次按任意键后,进入游戏界面。游戏界面包括:

  • 左侧:游戏区域,有围墙、蛇身(●)和食物(★)

  • 右侧:帮助信息和分数显示

游戏控制:

  • 方向键控制蛇的移动方向

  • F3加速,F4减速

  • 空格键暂停游戏

  • ESC键退出游戏

当蛇撞到墙或自己时,游戏结束,显示相应的结束信息,并询问是否再来一局。

总结

本文详细介绍了如何使用C语言实现贪吃蛇游戏。通过链表来管理蛇身,实现了蛇的移动、吃食物、碰撞检测等功能。代码中注意了内存管理,避免了内存泄漏。游戏具有简单的用户界面和良好的交互体验。

这个项目不仅可以帮助初学者巩固C语言知识,特别是链表、内存管理和控制台编程,还可以作为进一步学习游戏开发的基础。通过这个项目,我们可以学习到如何将复杂的问题分解为多个简单的模块,以及如何设计合理的数据结构来解决问题。

相关推荐
CHANG_THE_WORLD4 小时前
C++ vs Python 参数传递方式对比
java·c++·python
talenteddriver4 小时前
java: 4种API 参数传递方式
java·开发语言
Element_南笙4 小时前
吴恩达新课程:Agentic AI(笔记11)
大数据·人工智能·笔记·算法·机器学习
ULTRA??5 小时前
C++实现右折叠
开发语言·c++
豐儀麟阁贵5 小时前
9.5格式化字符串
java·开发语言·前端·面试
handsomezqh5 小时前
洛谷U611548 助教的比拼
c++·算法
小李小李快乐不已5 小时前
图论理论基础(4)
c++·算法·图论·迭代加深
好易学·数据结构5 小时前
可视化图解算法72:斐波那契数列
数据结构·算法·leetcode·动态规划·力扣·牛客网
崇山峻岭之间5 小时前
C++ Prime Plus 学习笔记025
c++·笔记·学习