目录

一、游戏效果预览 

二、核心思路拆解 

三,Win32API补充说明 

四、贪吃蛇游戏思路整理

1.地图

1.游戏菜单

2.游戏界面

2.游戏角色   

1.蛇的设计  

2.食物的设计

3.游戏运行核心逻辑  

1.游戏开始  

2.游戏运行  

3.游戏结束   

4.详细代码实现  

1.蛇的初始化  

2.食物的初始化  

3.蛇与食物位置关系(遇到与没遇到)

4.死亡原因(撞墙和撞到自己)

5.蛇的移动(核心)

做有意义的事,过意义的人生!欢迎大家一起讨论!创作不易,小博主求求赞啦!


一、游戏效果预览 

实现的功能 

  1. 方向控制( 上 /  下 /  左 /  右)
  2. 随机生成食物 
  3. 蛇吃到食物变长、得分增加 
  4. 撞墙 / 撞自身游戏结束 
  5. 实时显示分数 
  6. 游戏速度随分数提升变快  

二、核心思路拆解 

  1. 蛇的表示:用结构体存储坐标,用数组存储蛇的每一节身体 
  2. 地图:固定大小的矩形区域,边界为围墙  
  3. 食物:随机生成坐标,不能出现在蛇身上  
  4. 移动逻辑:蛇头向指定方向移动,身体跟随前一节位置  
  5. 碰撞检测:判断蛇头是否撞墙 / 撞自己 
  6. 键盘控制:监听按键,实时改变蛇的移动方向 

三,Win32API补充说明 

1. COORD  
控制台屏幕上的坐标COORD 
COORD 是Windows API中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕上的坐标 
typedef struct _COORD {
 SHORT X;
SHORT Y;
 } COORD, *PCOORD;

2. GetStdHandle

HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。

3. GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息。
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
4. CONSOLE_CURSOR_INFO
这个结构体,包含有关控制台光标的信息。
CursorInfo.bVisible = false; //隐藏控制台光标
5. SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的⼤⼩和可⻅性
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
6. SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调 ⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置。
COORD pos = { 10, 5};
 HANDLE hOutput = NULL;
 //获取标准输出的句柄(⽤来标识不同设备的数值)
 hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
 //设置标准输出上光标的位置为pos
 SetConsoleCursorPosition(hOutput, pos);
7.GetAsyncKeyState
获取按键情况 将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果  
返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态 是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1。
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

四、贪吃蛇游戏思路整理

1.地图

1.游戏菜单

在贪吃蛇游戏地图中,我们要创建两大部分  游戏菜单和游戏界面 ,我们先从游戏菜单说起。在这里我们用到SetPos函数,它的功能是用来定义光标位置,这样我们就能在控制台的不同位置打印我们需要的文字。

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

2.游戏界面

       这⾥不得不讲⼀下控制台窗⼝的⼀些知识,如果想在控制台的窗⼝中指定位置输出信息,我们得知道 该位置的坐标,所以⾸先介绍⼀下控制台窗⼝的坐标知识。
       控制台窗⼝的坐标如下所⽰,横向的是X轴,从左向右依次增⻓,纵向是Y轴,从上到下依次增⻓。 在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使⽤宽字符 ●,打印⻝物使⽤宽字符★ 普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节。
       这⾥再简单的讲⼀下C语⾔的国际化特性相关的知识,过去C语⾔并不适合⾮英语国家(地区)使⽤。 C语⾔最初假定字符都是但⾃⼰的。但是这些假定并不是在世界的任何地⽅都适⽤。   
后来为了使C语⾔适应国际化,C语⾔的标准中不断加⼊了国际化的⽀持。⽐如:加⼊和宽字符的类型 wchar_t 和宽字符的输⼊和输出函数,加⼊和<locale.h>头⽂件,其中提供了允许程序员针对特定 地区(通常是国家或者说某种特定语⾔的地理区域)调整程序⾏为的函数。
setlocale函数
char* setlocale (int category, const char* locale);
1 setlocale(LC_ALL, "C");//切换到正常环境
2 setlocale(LC_ALL, " ");//切换到本地环境
当程序运⾏起来后想改变地区,就只能显⽰调⽤setlocale函数。⽤" "作为第2个参数,调⽤setlocale
函数就可以切换到本地模式,这种模式下程序会适应本地环境。⽐如:切换到我们的本地模式后就⽀
持宽字符(汉字)的输出等。

        了解完宽字符的打印后,看到这个页面时,我们会思考如何打印游戏的边框,可是宽字符和普通字符的区别在哪呢?这里可以通过图解对比。

       我们假设实现⼀个棋盘27⾏,58列的棋盘(⾏和列可以根据⾃⼰的情况修改),再围绕地图画出墙,即可完成基本的游戏界面。
void CreateMap()
{
    int i = 0;
    //上(0,0)-(56, 0)
    SetPos(0, 0);
    for (i = 0; i < 58; i += 2)
    {
        SetPos(i,0);
        wprintf(L"%c", WALL);
    }
    //下(0,26)-(56, 26)
    SetPos(0, 26);
    for (i = 0; i < 58; i += 2)
    {
        SetPos(i, 26);
        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);
    }
}

2.游戏角色   

   蛇和⻝物   

       初始化状态,假设蛇的⻓度是5,蛇⾝的每个节点是●,在固定的⼀个坐标处,⽐如(24, 5)处开始出现 蛇,连续5个节点。注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半出现在墙体中,另外⼀般在墙外的现象,坐标不好对⻬。
       关于⻝物,就是在墙体内随机⽣成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的⾝体重合,然后打印★。
1.蛇的设计  
       在游戏运⾏的过程中,蛇每次吃⼀个⻝物,蛇的⾝体就会变⻓⼀节,如果我们使⽤链表存储蛇的信
息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇⾝节点在地图上的坐标就⾏, 所以蛇节点结构如下:
typedef struct SnakeNode
{
     int x;
     int y;
     struct SnakeNode* next;
 }SnakeNode, * pSnakeNode;
       要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇。
typedef struct Snake
 {
     pSnakeNode _pSnake;//维护整条蛇的指针
     pSnakeNode _pFood;//维护⻝物的指针
     enum DIRECTION _Dir;//蛇头的⽅向默认是向右
     enum GAME_STATUS _Status;//游戏状态
     int _Socre;//当前获得分数
     int _foodWeight;//默认每个⻝物10分
     int _SleepTime;//每⾛⼀步休眠时间
 }Snake, * pSnake;
蛇的⽅向,可以一一 列举,使⽤枚举  
enum DIRECTION
 {
     UP = 1,
     DOWN,
     LEFT,
     RIGHT
 };
蛇的游戏状态,可以一一 列举,使⽤枚举  
enum GAME_STATUS
 {
     OK,//正常运⾏
     KILL_BY_WALL,//撞墙
     KILL_BY_SELF,//咬到⾃⼰
     END_NOMAL//正常结束
 };
2.食物的设计

       对于食物的设计,我们要思考它在游戏中扮演的角色作用,蛇通过吃它,增加身体长度,对于由链表组成的蛇体,食物必然也是单个节点,然后遇到蛇头后,被连接到链表中。对此,我们首先要创建单节点充当食物的角色,然后它会出现在哪呢?是完全随机的吗?我们思考下蛇头出现的位置,对于宽字符,x的坐标必然是偶数,那么食物的x坐标也需要出现在偶数位,才能与蛇头对齐,其次它的位置也不能是位于墙体上,我们要对x和y进行限制。

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

3.游戏运行核心逻辑  

     我们先看下头文件中的函数代码定义进行思路的梳理。

//游戏开始前的初始化    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);
//游戏初始化    void GameStart(pSnake ps);
//游戏运行    void GameRun(pSnake ps);
//游戏结束    void GameEnd(pSnake ps);
1. 蛇的本质  蛇是一串连续的点。
            只有蛇头会主动动,身体都是跟着前面一节走。
2. 移动规则(最核心)
            从尾巴开始,每一节身体,都变成前一节的位置。
            最后只需要移动蛇头,整个蛇就动起来了。
            一句话:身体跟着走,蛇头往前冲。
3. 方向控制  蛇只能上下左右四个方向移动。
            不能直接掉头(比如正在往右走,不能立刻往左),否则会瞬间撞死自己。
4. 食物机制  地图上随机出现一个食物。当蛇头碰到食物:蛇长度加 1+ 分数增加+游戏速度变快+重新生成一个              
5. 死亡判定
            只有两种情况会死:撞墙:蛇头走出地图边界 撞自己:蛇头碰到自己身体任意一节
6. 游戏主循环
整个游戏就是不断重复:看有没有按键,改方向。
蛇移动一步判断是否吃到食物
判断是否死亡
刷新面稍微停顿一下控制速度
回到第一步继续循环

     我们一个一个来梳理,具体的代码实现,我们先不管。

1.游戏开始  

创建地图,蛇,食物。

void GameStart(pSnake ps)
{
	system("mode  con lines=30 cols=100");
	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);
}

2.游戏运行  

     规则提示,计算分数,蛇的移动,游戏的结束与暂停。

void GameRun(pSnake ps)
{
    PrintHelpInfo();
    do
    {
        SetPos(64, 10);
        printf("得分:%d分", ps->_Socre);
        printf("   每个食物得分:%d分", ps->_Add);
        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))
        {
            Sleep(3000);
            while (1)
            {
                Sleep(10);
                if (KEY_PRESS(VK_SPACE))
                {
                    break;
                }
            }
        }
        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->_Add += 2;
            }
        }
        else if (KEY_PRESS(VK_F4))
        {
            if (ps->_SleepTime <350)
            {
                ps->_SleepTime += 30;
                ps->_Add -= 2;
                if (ps->_SleepTime == 350)
                {
                    ps->_Add = 1;
                }
            }
        }
        Sleep(ps->_SleepTime);
        SnakeMove(ps);
    } while (ps->_Status == OK);
}

3.游戏结束   

    归纳失败情况,结束程序。

void GameEnd(pSnake ps)
{
    pSnakeNode cur = ps->_pSnake;
    SetPos(64, 5);
   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);
    }
}

4.详细代码实现  

1.蛇的初始化  

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->_Add = 10;
}

2.食物的初始化  

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

3.蛇与食物位置关系(遇到与没遇到)

int NextIsFood(pSnakeNode psn, pSnake ps)
{
    return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}
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->_Add;

    free(ps->_pFood);
    CreateFood(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;
}

4.死亡原因(撞墙和撞到自己)

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

5.蛇的移动(核心)

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

自取哦!

加油少年 (wxx547803_0) - Gitee.com

做有意义的事,过意义的人生!欢迎大家一起讨论!创作不易,小博主求求赞啦!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐