贪吃蛇小游戏简单制作-C语言

文章目录

      • 游戏背景介绍
      • 实现目标
      • 适合人群
      • 所需技术
      • 浅玩Window API
        • 什么是API
        • 控制台程序
          • 窗口大小,名称设置
        • Handle(句柄)
          • 获取句柄
        • 坐标结构体
          • 设置光标位置
        • 光标属性
          • 获取光标属性
          • 设置光标属性
        • 按键信息获取
      • 贪吃蛇游戏设计
        • 游戏前的初始化
          • 设置窗口的大小和名称
          • 本地化设置
        • 宽字符
          • Waht is 宽字符
          • 宽字符的打印
          • 光标的设置
          • 欢迎界面打印
          • 地图绘制
          • 帮助信息
          • 蛇身的管理
          • 贪吃蛇游戏的管理(插播,重要)
          • 蛇的初始化
          • 食物的管理
        • 蛇的移动
      • 游戏结束
      • 拓展建议
      • 寄语
      • 源码

游戏背景介绍

贪吃蛇是一个非常经典的小游戏,笔者曾今在古早的案件手机,mp4上面玩过这款游戏,今天就让我们使用C语言一起复刻这个简单的小游戏吧~,好玩简单-

实现目标

在这个游戏中,我们需要控制一条可以上下左右移动的小蛇,在指定的墙体内进行移动,吃到食物后,蛇的身体会变长,当蛇撞墙或者撞到自己的身体的时候,游戏结束,我们需要实现的功能有:

  • 蛇的移动
  • 食物的生成
  • 蛇的身体的增长
  • 游戏结束的判断
  • 游戏结束后的处理

适合人群

这是我学习完C语言语法和顺序表,链表之后的一个小项目,也同样适合像我一样刚学完C语言的同学,通过这个项目,巩固一下C语言的基础知识,也可以学习一下C语言的一些高级知识,比如Windows API的使用,宽字符的打印等等

所需技术

  1. C语言基础
  2. 链表
  3. 简单的Windows API
  4. 动态内存分配

浅玩Window API

什么是API

API全称Application Programming Interface,翻译过来就是应用程序接口,是一组预先定义的函数,类,协议的集合,这些函数,类,协议可以被其他程序调用,用来实现一些功能,比如Windows API就是用来实现Windows系统的一些功能的

上面说的有些复杂,我们可以把API想象成一个工厂,我们只需要知道向工厂输入什么,工厂会输出什么,而不需要知道工厂内部是怎么实现的

比如你向一个面包工厂输入了一些小麦,工厂会给你输出一些面包,而工厂内部可能是用了一些机器,工人等等来实现的,但是你不需要知道这些,你只需要知道你给他小麦,他给你面包就行了

小麦
面包工厂加工
面包
控制台程序

控制台程序其实就是我们平时编译完文件之后运行打开的那个黑框框,我们可以在这个黑框框里面输入一些命令,然后程序会给我们输出一些结果,这个黑框框就是一个控制台,我们可以通过控制台程序来和用户进行交互

窗口大小,名称设置

既然是贪吃蛇小游戏,那么我们需要一个固定的窗口和一个有趣的名字,我们可以怎么做呢?
可以使用cmd命令在控制台程序中设置窗口的大小,名称

mode con cols=100 lines=30
title 贪吃蛇
  • mode con cols=100 lines=30 设置窗口的大小为100列,30行
  • title 贪吃蛇 设置窗口的名称为贪吃蛇

但是,我们只希望贪吃蛇程序运行后自动设置窗口的大小和名称,而不是让用户手动输入这些命令,因此我们可以使用system函数来调用这些命令,让程序自动设置窗口的大小和名称

#include <stdlib.h>
int main()
{
    system("mode con cols=100 lines=30");
    system("title 贪吃蛇");
    return 0;
}
Handle(句柄)

如果我们想要对控制台的一些属性进行设置,比如设置光标的位置,设置控制台的颜色等等,我们就需要使用句柄来进行操作,我们可以把句柄想象成是控制台大哥的身份标识,只有知道大哥的身份标识才能找到大哥并更改他的一些属性

获取句柄

我们可以使用GetStdHandle函数来获取控制台的句柄,这个函数的原型是

HANDLE GetStdHandle(DWORD nStdHandle);

我们可以使用一个HANDLE类型变量来接受返回值

坐标结构体

控制台上面的每一个字符都有一个坐标,
坐标是从左上角开始计算的,左上角的坐标是(0,0),向右是x轴正方向,向下是y轴正方向
我们可以使用一个结构体来表示这个坐标
COORD是Windows API中的一个结构体,定义如下

typedef struct _COORD {
  SHORT X;
  SHORT Y;
} COORD, *PCOORD;
设置光标位置

我们平时在使用printf打印字符的时候,每打印一个字符,光标都会自动向后移动一个位置,我们可以使用SetConsoleCursorPosition函数来手动设置光标的位置

BOOL SetConsoleCursorPosition(
  HANDLE hConsoleOutput,
  COORD  dwCursorPosition
);

eg:

#include <windows.h>

int main()
{
    HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); // 获取控制台句柄
    COORD pos = {10, 10};                          // 设置坐标
    SetConsoleCursorPosition(hOut, pos);           // 设置光标位置
    printf("hello world");
    return 0;
}
光标属性

我们平时运行控制台程序的时候,会发现光标是一个闪烁的小方块,那我们有没有什么办法可以让小方块变大变小或是直接隐藏呢?
答案肯定是有哒

我们先来介绍一下光标的两个属性

  • bVisible : 是否可见
  • dwSize : 光标的大小 当这个值是100的时候,光标是一个小方块█,这个值也就是显示这个方块的百分比,比如50就是显示一半的方块
获取光标属性

我们可以使用GetConsoleCursorInfo函数来获取光标的属性

BOOL GetConsoleCursorInfo(
  HANDLE               hConsoleOutput,
  PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

eg:

#include <windows.h>

int main()
{
    HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); // 获取控制台句柄
    CONSOLE_CURSOR_INFO cursorInfo;
    GetConsoleCursorInfo(hOut, &cursorInfo;);
    printf("bVisible:%d, dwSize:%d\n", cursorInfo.bVisible, cursorInfo.dwSize);
    return 0;
}

大家可以自行运行试一试

设置光标属性

在贪吃蛇游戏中,我们肯定希望没有光标的出现,因此我们可以使用SetConsoleCursorInfo函数来设置光标的属性

BOOL SetConsoleCursorInfo(
  HANDLE               hConsoleOutput,
  const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);

eg:

#include <windows.h>

int main()
{
    HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); // 获取控制台句柄
    CONSOLE_CURSOR_INFO cursorInfo;
    cursorInfo.bVisible = 0; // 设置光标不可见 也可以写flase
    cursorInfo.dwSize = 100;  // 设置光标大小
    SetConsoleCursorInfo(hOut, &cursorInfo);
    return 0;
}
按键信息获取

我们需要使用↑↓←→来控制蛇的移动,因此我们可以使用GetAsyncKeyState函数来获取按键信息

SHORT GetAsyncKeyState(
  int vKey
);

在返回的值中,如果最高位是1,表示这个键正在被按下,如果最低位是1,表示这个键被按过
我们可以使用一个预定义的宏来判断这个键是否被按下

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

这个大家肯定看的懂,如果不懂的话可以去学习一下预编译和位运算

贪吃蛇游戏设计

我们可以把贪吃蛇游戏分为三个部分:

  1. 游戏前的初始化,包括窗口的设置,光标的设置,欢迎界面打印,地图的初始化,蛇的初始化,食物的初始化等等
  2. 游戏中的循环,包括蛇的移动,食物的生成,蛇的身体的增长,游戏结束的判断等等
  3. 游戏结束后的处理,包括释放资源,打印游戏结束的信息等等

补充:当程序需要实现按任意键继续的时候,我们使用system(“pause”)函数即可

游戏前的初始化
设置窗口的大小和名称
void CmdInit(void)
{
    // 设置控制台窗口大小 100列 30行
    system("mode con cols=100 lines=30");
    // 设置控制台名称
    system("title snake");
}
本地化设置

正常在C语言中,我们使用的是ASCII字符集,他只使用了一个字节来表示一个字符,而在不同的国家和地区,字符集是不一样的,因此我们可以使用setlocale函数来设置字符集

char *setlocale(int category, const char *locale);

在C标准库中,我们可以更改的地区设置有以下这些:

  • LC_ALL:所有的地区设置
  • LC_COLLATE:字符串比较
  • LC_CTYPE:字符分类和转换
  • LC_MONETARY:货币格式
  • LC_NUMERIC:非货币数字格式
  • LC_TIME:时间格式

我么可以使用setlocale函数来设置地区,比如把地区设置为当前地区

#include <locale.h>
int main()
{
    setlocale(LC_ALL, "");
    return 0;
}
宽字符
Waht is 宽字符

宽字符是指一个字符占用两个字节的字符,中文以及一些特殊字符都是宽字符,
而且宽字符在控制台中是占用两个x坐标

graph LR
A[宽字符] --> B[占用两个坐标位置] --> D[普通字符:█]
B --> E[宽字符:██]
A --> C[占用两个字节]
宽字符的打印

在C语言中,我们可以使用wprintf函数来打印宽字符,在打印宽字符之前,我们需要进行本地化设置

int wprintf(const wchar_t *format, ...);

eg:

#include <stdio.h>
int main()
{
    setlocale(LC_ALL, "");
    wchar_t str[] = L"你好,世界\n";
    wprintf(L"%s", str);
    return 0;
}
光标的设置
  1. 定义一个变量来接受控制台的句柄
  2. 定义一个变量来接受光标的属性
  3. 改变光标的属性
  4. 通过SetConsoleCursorInfo函数,输入句柄和光标属性,设置光标的属性
欢迎界面打印

这里我们需要设置光标位置并打印所需信息,因此我们可以封装一个函数来快捷地设置光标位置

void SetPos(int x, int y)
{
    HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
    COORD pos = {x, y};
    SetConsoleCursorPosition(hOut, pos);
}

然后我们就可以打印欢迎信息和游戏规则了

// 打印欢迎信息
void WelcomeToGame(void)
{
    SetPos(45, 10);
    wprintf(L"欢迎来到贪吃蛇游戏");
    // 暂停
    SetPos(45, 20);
    system("pause");
}

// 游戏介绍
void GameIntroduction(void)
{
    // 清屏
    system("cls");
    SetPos(45, 10);
    wprintf(L"游戏介绍:");
    SetPos(45, 12);
    wprintf(L"1. 使用↑,↓,←,→控制蛇的移动");
    SetPos(45, 14);
    wprintf(L"2. 吃到食物蛇的长度加1");
    SetPos(45, 16);
    wprintf(L"2. F3加速, F4减速");
    SetPos(45, 18);
    wprintf(L"3. 空格暂停");
    SetPos(45, 20);
    wprintf(L"4. Esc退出游戏");
    // 暂停
    SetPos(45, 22);
    system("pause");
}

在这里插入图片描述

在这里插入图片描述

地图绘制

我们这里使用□来表示墙体,然后运用宽字符的打印知识来绘制一个27 * 27的地图

// 地图绘制 
// 上(0,0) - (56,0)
// 下(0,26) - (56,26)
// 左(0,0) - (0,26)
// 右(56,0) - (56,26)
// 注意打印的是宽字符 占两个x坐标 因此左右打印的时候要每打印一个x坐标加2
void MapDraw(void)
{   
    // 清屏
    system("cls");
    // 上墙
    for (int i = 0; i < 57; i+=2)
    {
        SetPos(i, 0);
        wprintf(L"%c", WALL);
    }
    // 下墙
    for (int i = 0; i < 57; i+=2)
    {
        SetPos(i, 26);
        wprintf(L"%c", WALL);
    }
    // 最上面和最下面已经打印过了
    // 左墙
    for (int i = 1; i < 26; i++)
    {
        SetPos(0, i);
        wprintf(L"%c", WALL);
    }
    // 右墙
    for (int i = 1; i < 26; i++)
    {
        SetPos(56, i);
        wprintf(L"%c", WALL);
    } 
}
帮助信息

我们在地图的右侧打印一些帮助信息,比如当前的分数,按键操作等等

// 打印静态帮助信息
// 单个食物分数,总分数的名称
// 操作说明
void PrintStaticHelp(void)
{
    SetPos(70, 5);
    wprintf(L"单个食物分数: 10");
    SetPos(70, 7);
    wprintf(L"总分数: 0");
    SetPos(70, 9);
    wprintf(L"操作说明:");
    SetPos(70, 11);
    wprintf(L"↑ : 上移");
    SetPos(70, 13);
    wprintf(L"↓ : 下移");
    SetPos(70, 15);
    wprintf(L"← : 左移");
    SetPos(70, 17);
    wprintf(L"→ : 右移");
    SetPos(70, 19);
    wprintf(L"F3: 加速");
    SetPos(70, 21);
    wprintf(L"F4: 减速");
    SetPos(70, 23);
    wprintf(L"空格: 暂停");
    SetPos(70, 25);
    wprintf(L"Esc: 退出");
}

在这里插入图片描述

蛇身的管理

我们可以将蛇看作是一个一个节点相互连接组成,因此我们可以使用链表来管理蛇的身体,我们先创建一个结构体来管理蛇身的节点

// 蛇节点
typedef struct SnakeNode
{
    int x;
    int y;
    struct SnakeNode *next;
} SnakeNode, * pSnakeNode;
贪吃蛇游戏的管理(插播,重要)

使用一个结构体来管理整个贪吃蛇游戏,包括:

  • 蛇身
  • 食物
  • 单个食物分数
  • 总分数
  • 睡眠时间
  • 方向
  • 游戏状态
// 方向
typedef enum Direction
{
    UP = 1,     // 上
    DOWN,       // 下
    LEFT,       // 左
    RIGHT       // 右

} Direction;

// 游戏状态
typedef enum GameStatus
{
    OK = 0,         //运行中
    KILL_BY_WALL,   //撞墙
    KILL_BY_SELF,   //撞自己
    PAUSE,          //暂停
    ESC             //退出
} GameStatus;

// 贪吃蛇游戏
typedef struct Snake
{
    pSnakeNode _pSnake;     // 蛇头
    pSnakeNode _pFood;      // 食物 可以共用蛇节点比较方便,下面会提
    int _foodScore;         // 单个食物分数
    int _totalScore;        // 总分数
    int _sleepTime;         // 睡眠时间 -- 控制速度
    Direction _dir;         // 方向
    GameStatus _status;     // 游戏状态
} Snake, * pSnake;

在游戏开始前对这个结构体进行初始化

pSnake ps = (pSnake)malloc(sizeof(Snake));
蛇的初始化

我们可以使用一个链表来存储蛇的身体,这里我们创建一个有5个节点的蛇,并且初始化蛇的位置


#define BODY L'●'
#define POS_X 6
#define POS_Y 6

// 蛇初始化
// 在地图的(6,6)位置初始化蛇
// 蛇的长度为5
// 使用头插法
void InitSnake(pSnake ps)
{
    pSnakeNode cur = NULL;
    int i = 0;
    // 创建蛇身节点 并初始化坐标    
    for (i = 0; i < 5; i++)
    {
        pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));
        if (node == NULL)
        {
            perror("pSnakeNode malloc failed");
            exit(1);
        }
        node->x = POS_X + i * 2;
        node->y = POS_Y;
        node->next = NULL;
        // 头插法
        if (ps->_pSnake == NULL)
        {
            ps->_pSnake = node;
        }
        else
        {
            node->next = ps->_pSnake;
            ps->_pSnake = node;
        }
    }
    // 打印蛇
    cur = ps->_pSnake;
    while (cur != NULL)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%c", BODY);
        cur = cur->next;
    }
}

在这里插入图片描述

食物的管理

我们可以把食物也看作是一个蛇节点,只不过还没有加入到蛇身体中,因此我们可以使用蛇身结构体来存储食物
我们在游戏开始和蛇吃到食物的时候,生成一个食物节点,并且打印食物到屏幕上

// 创建食物节点
void CreatFood(pSnake ps)
{
    // 申请节点空间
    pSnakeNode foodnode = (pSnakeNode)malloc(sizeof(SnakeNode));
    if (foodnode == NULL)
    {
        perror("foodnode malloc failed");
        exit(1);
    }
    foodnode->next = NULL;
    // 随机生成食物坐标 在墙范围内但是不能在蛇身上
    while (1)
    {
        int x = rand() % 54 + 2;
        int y = rand() % 24 + 2;
        pSnakeNode cur = ps->_pSnake;
        while (cur != NULL)
        {
            if (cur->x == x && cur->y == y)
            {
                break;
            }
            cur = cur->next;
        }
        if (cur == NULL)
        {
            foodnode->x = x;
            foodnode->y = y;
            break;
        }
    }
    // 打印食物
    SetPos(foodnode->x, foodnode->y);
    wprintf(L"%c", FOOD);
}

这里的随机数并不是真正的随机数,而是伪随机数,因此我们可以在mian里面使用srand函数来设置随机数的种子

int main()
{
    srand(time(NULL));
    return 0;
}

在这里插入图片描述

蛇的移动
  1. 按键检测, 改变方向
  2. 判断是否吃到食物
    • 吃到食物,头插食物节点,创建新的食物节点
    • 没吃到食物,头插一个新的节点,新的节点是蛇头移动到的下一个坐标,尾删一个节点
  3. 蛇移动后进行打印,判断是否撞到自己或者墙壁,是则游戏结束

这里封装了一个宏来读取按键状态,如果最低位是1,则返回1,反之返回0
也就是一个按键按下过返回1,否则返回0

#define KEY_PRESS(VK) (GetAsyncKeyState(VK) & 0x01 ? 1 : 0)
// 蛇的移动 -- 主游戏程序 在此循环
// 吃到食物 创建新的食物节点
// 没有吃到食物头插新节点删除尾节点并释放空间
// 减速睡眠时间-30 睡眠时间最少减4次 单个食物分数加2
// 加速睡眠时间+30 睡眠时间最多加4次 单个食物分数减2
void SnakeMove(pSnake ps)
{
    int x = 0;
    int y = 0;
    again:
    while (ps->_status == OK)
    {
        // 按键检测
        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))
        {
            ps->_status = PAUSE;
        }
        else if (KEY_PRESS(VK_F3) && ps->_foodScore < 20)
        {
            ps->_sleepTime -= 30;
            ps->_foodScore += 2;
            // 更改单个食物分数
            SetPos(84, 5);
            printf("%2d", ps->_foodScore);
        }
        else if (KEY_PRESS(VK_F4) && ps->_foodScore > 2)
        {
            ps->_sleepTime += 30;
            ps->_foodScore -= 2;
            // 更改单个食物分数
            SetPos(84, 5);
            printf("%2d", ps->_foodScore);
        }
        else if (KEY_PRESS(VK_ESCAPE))
        {
            ps->_status = ESC;
        }

        // 设置下一个节点的x,y坐标
        switch (ps->_dir)
        {
            case UP:
                x = ps->_pSnake->x;
                y = ps->_pSnake->y - 1;
                break;
            case DOWN:
                x = ps->_pSnake->x;
                y = ps->_pSnake->y + 1;
                break;
            case LEFT:
                x = ps->_pSnake->x - 2;
                y = ps->_pSnake->y;
                break;
            case RIGHT:
                x = ps->_pSnake->x + 2;
                y = ps->_pSnake->y;
                break;
        }
        // 判断是否吃到食物
        if (ps->_pFood->x == x && ps->_pFood->y == y)
        {
            EatFood(ps);
        }
        else 
        {
            NotEatFood(ps, x, y);
        }
        // 撞墙检测
        if (IsKillByWall(ps))
        {
            ps->_status = KILL_BY_WALL;
        }
        // 撞自己检测
        if (IsKillBySelf(ps))
        {
            ps->_status = KILL_BY_SELF;
        }
        Sleep(ps->_sleepTime);
    }
    while (ps->_status == PAUSE)
    {
        if (KEY_PRESS(VK_SPACE))
        {
            ps->_status = OK;
        }
        goto again; //使用goto 返回到前面
    }
    // 撞墙打印信息
    if (IsKillByWall(ps))
    {
        SetPos(45, 10);
        wprintf(L"墙墙被你撞西了,110把你抓走了");
    }
    // 撞自己打印信息
    if (IsKillBySelf(ps))
    {
        SetPos(45, 10);
        wprintf(L"自己撞自己了,120把你带走了");
    }
    return;
}

EatFood函数

void EatFood(pSnake ps)
{
    // 加分
    ps->_totalScore += ps->_foodScore;
    // 打印总分数
    SetPos(82, 7);
    printf("%4d", ps->_totalScore);
    // 是 头插食物节点 创建新节点
    ps->_pFood->next = ps->_pSnake;
    ps->_pSnake = ps->_pFood;
    // 打印新蛇头
    SetPos(ps->_pSnake->x, ps->_pSnake->y);
    wprintf(L"%c", BODY);
    // 创建新食物
    CreatFood(ps);
}

NotEatFood函数

// 没有迟到食物的处理
void NotEatFood(pSnake ps, int x, int y)
{
    // 创建新节点并头插
    pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));
    if (node == NULL)
    {
        perror("node malloc failed");
        exit(1);
    }
    node->x = x;
    node->y = y;
    node->next = ps->_pSnake;
    ps->_pSnake = node;
    // 打印新蛇 顺便删除尾节点 释放空间 打印空格
    pSnakeNode cur = ps->_pSnake;
    while (cur->next->next != NULL)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%c", BODY);
        cur = cur->next;
    }
    SetPos(cur->x, cur->y);
    wprintf(L"%c", BODY);
    SetPos(cur->next->x, cur->next->y);
    wprintf(L"  ");
    free(cur->next);
    cur->next = NULL;
}

IsKillByWall函数

// 撞墙检测
// 撞墙返回1 否则返回0
int IsKillByWall(pSnake ps)
{
    if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26)
    {
        return 1;
    }
    return 0;
}

在这里插入图片描述

IsKillBySelf函数

// 撞自己检测
// 撞自己返回1 否则返回0
int IsKillBySelf(pSnake ps)
{
    pSnakeNode cur = ps->_pSnake->next;
    // 从第二个节点开始遍历 并判断是否和蛇头坐标相同
    while (cur != NULL)
    {
        if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
        {
            return 1;
        }
        cur = cur->next;
    }
    return 0;
}

在这里插入图片描述

除了提到的以外,我们还要对加速减速的按键进行处理,还有总分的统计,失败的打印信息等等,这里就由大家自由发挥啦,不行的话也可以看我的源码

游戏结束

恭喜你,你已经得到了一个属于你的小小贪吃蛇游戏了,童年的游戏已经被你复刻,但是你现在还不可以拍拍屁股走人哦,我们还需要做好善后工作啦

  1. 释放资源
// 善后 释放蛇节点 以及食物节点
void GameEnd(pSnake ps)
{
    // 释放蛇节点
    pSnakeNode cur = NULL;
    while (ps->_pSnake != NULL)
    {
        cur = ps->_pSnake;
        ps->_pSnake = ps->_pSnake->next;
        free(cur);
    }
    // 释放食物节点
    free(ps->_pFood);
}

拓展建议

本文会有一些不足之处,但不影响整体的学习,如果大家有问题可以用自己的方法解决或者发在评论区
拓展建议:

  1. 可以优化画面,多加一些符号,可以fancy一点,比如这样
// 使用--,|,/ 绘制英文SNAKE
/* 
           _____                  _                 _____
           |    \   |\_    |     / \      |   _/   |
           |_____   |  \   |    /   \     |__/     |_____
                |   |   \_ |   /_____\    |  \_    |
           \____|   |     \|  /       \   |    \   |_____
 */
void PrintSnake(void)
{
    SetPos(32, 5);
    printf(" _____                  _                 _____");
    SetPos(32, 6);
    printf(" |    \\   |\\_    |     / \\      |   _/   |");
    SetPos(32, 7);
    printf(" |_____   |  \\   |    /   \\     |__/     |_____");
    SetPos(32, 8);
    printf("      |   |   \\_ |   /_____\\    |  \\_    |");
    SetPos(32, 9);
    printf(" \\____|   |     \\|  /       \\   |    \\   |_____");
}

在这里插入图片描述

  1. 可以配合EasyX图形库,做一个图形化的贪吃蛇游戏
  2. 可以加入再来一局的功能
  3. 可以加入排行榜,计分等,配合写入读取文件
  4. 可以加入音效,配合Windows API的Beep函数,不过会有阻塞
  5. 加入双人模式,可以使用WSAD控制第二条蛇
  6. 等等等等还有好多啦,大家可以自行探索

寄语

感谢每一位看到这里的自己🌹🌹🌹
分享一句话:
我将玫瑰藏于身后,风起花落,从此鲜花赠自己,纵马踏花向自由

源码

test.c

#include "snake.h"

int main()
{
    // 使用当前的时间作为种子值
    srand(time(NULL));
    pSnake ps = (pSnake)malloc(sizeof(Snake));
    SnakeInit(ps);
    GameStart(ps); // 游戏初始化
    GameRun(ps); // 游戏运行
    GameEnd(ps); // 游戏结束
    return 0;
}

snake.h

#pragma once

#include <Windows.h>
#include <locale.h>
#include <wchar.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>


// 方向
typedef enum Direction
{
    UP = 1,     // 上
    DOWN,       // 下
    LEFT,       // 左
    RIGHT       // 右

} Direction;

// 游戏状态
typedef enum GameStatus
{
    OK = 0,         //运行中
    KILL_BY_WALL,   //撞墙
    KILL_BY_SELF,   //撞自己
    PAUSE,          //暂停
    ESC             //退出
} GameStatus;

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

// 贪吃蛇游戏
typedef struct Snake
{
    pSnakeNode _pSnake;     // 蛇头
    pSnakeNode _pFood;      // 食物
    int _foodScore;         // 单个食物分数
    int _totalScore;        // 总分数
    int _sleepTime;         // 睡眠时间 -- 控制速度
    Direction _dir;         // 方向
    GameStatus _status;     // 游戏状态
} Snake, * pSnake;


void GameStart(pSnake ps);
void SnakeInit(pSnake ps);
void GameRun(pSnake ps);
void GameEnd(pSnake ps);

snake.c

#include "snake.h"
#include <stdio.h>
#include <stdlib.h>


#define KEY_PRESS(VK) (GetAsyncKeyState(VK) & 0x01 ? 1 : 0)
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define POS_X 6
#define POS_Y 6

// ↑↓←→●□★ 

// 贪吃蛇游戏结构体初始化
void SnakeInit(pSnake ps)
{
    ps->_pSnake = NULL;
    ps->_pFood = NULL;
    ps->_foodScore = 10;
    ps->_totalScore = 0;
    ps->_status = OK;
    ps->_dir = RIGHT;
    ps->_sleepTime = 200;
}

// 设置控制台窗口大小和名称
void CmdInit(void)
{
    // 设置控制台窗口大小 100列 30行
    system("mode con cols=100 lines=30");
    // 设置控制台名称
    system("title snake");
}

// 光标隐藏
void CursorHide(void)
{
    // 获取句柄
    HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
    // 光标信息
    CONSOLE_CURSOR_INFO cursor_info = {0};
    // 获取光标信息
    GetConsoleCursorInfo(handle, &cursor_info);
    // 设置光标属性
    cursor_info.dwSize = 10;
    cursor_info.bVisible = 0;
    SetConsoleCursorInfo(handle, &cursor_info);
}

// 设置光标位置
void SetPos(int x, int y)
{
    // 获取句柄
    HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
    // 设置光标位置
    COORD pos = {x, y};
    SetConsoleCursorPosition(handle, pos);
}


void PrintSnake(void)
{
    SetPos(45, 10);
    wprintf(L"欢迎来到贪吃蛇游戏");
}

// 打印欢迎信息
void WelcomeToGame(void)
{
    PrintSnake();
    // 暂停
    SetPos(45, 20);
    system("pause");
}

// 游戏介绍
void GameIntroduction(void)
{
    // 清屏
    system("cls");
    SetPos(45, 10);
    wprintf(L"游戏介绍:");
    SetPos(45, 12);
    wprintf(L"1. 使用↑,↓,←,→控制蛇的移动");
    SetPos(45, 14);
    wprintf(L"2. 吃到食物蛇的长度加1");
    SetPos(45, 16);
    wprintf(L"2. F3加速, F4减速");
    SetPos(45, 18);
    wprintf(L"3. 空格暂停");
    SetPos(45, 20);
    wprintf(L"4. Esc退出游戏");
    // 暂停
    SetPos(45, 22);
    system("pause");
}

// 地图绘制 
// 上(0,0) - (56,0)
// 下(0,26) - (56,26)
// 左(0,0) - (0,26)
// 右(56,0) - (56,26)
// 注意打印的是宽字符 占两个x坐标 因此左右打印的时候要每打印一个x坐标加2
void MapDraw(void)
{   
    // 清屏
    system("cls");
    // 上墙
    for (int i = 0; i < 57; i+=2)
    {
        SetPos(i, 0);
        wprintf(L"%c", WALL);
    }
    // 下墙
    for (int i = 0; i < 57; i+=2)
    {
        SetPos(i, 26);
        wprintf(L"%c", WALL);
    }
    // 最上面和最下面已经打印过了
    // 左墙
    for (int i = 1; i < 26; i++)
    {
        SetPos(0, i);
        wprintf(L"%c", WALL);
    }
    // 右墙
    for (int i = 1; i < 26; i++)
    {
        SetPos(56, i);
        wprintf(L"%c", WALL);
    } 
}

// 打印静态帮助信息
// 单个食物分数,总分数的名称
// 操作说明
void PrintStaticHelp(void)
{
    SetPos(70, 5);
    wprintf(L"单个食物分数: 10");
    SetPos(70, 7);
    wprintf(L"总分数:        0");
    SetPos(70, 9);
    wprintf(L"操作说明:");
    SetPos(70, 11);
    wprintf(L"↑ : 上移");
    SetPos(70, 13);
    wprintf(L"↓ : 下移");
    SetPos(70, 15);
    wprintf(L"← : 左移");
    SetPos(70, 17);
    wprintf(L"→ : 右移");
    SetPos(70, 19);
    wprintf(L"F3: 加速");
    SetPos(70, 21);
    wprintf(L"F4: 减速");
    SetPos(70, 23);
    wprintf(L"空格: 暂停");
    SetPos(70, 25);
    wprintf(L"Esc: 退出");
}

// 蛇初始化
// 在地图的(6,6)位置初始化蛇
// 蛇的长度为5
// 使用头插法
void InitSnake(pSnake ps)
{
    pSnakeNode cur = NULL;
    int i = 0;
    // 创建蛇身节点 并初始化坐标    
    for (i = 0; i < 5; i++)
    {
        pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));
        if (node == NULL)
        {
            perror("pSnakeNode malloc failed");
            exit(1);
        }
        node->x = POS_X + i * 2;
        node->y = POS_Y;
        node->next = NULL;
        // 头插法
        if (ps->_pSnake == NULL)
        {
            ps->_pSnake = node;
        }
        else
        {
            node->next = ps->_pSnake;
            ps->_pSnake = node;
        }
    }
    // 打印蛇
    cur = ps->_pSnake;
    while (cur != NULL)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%c", BODY);
        cur = cur->next;
    }
}


// 创建食物节点
void CreatFood(pSnake ps)
{
    // 申请节点空间
    pSnakeNode foodnode = (pSnakeNode)malloc(sizeof(SnakeNode));
    if (foodnode == NULL)
    {
        perror("foodnode malloc failed");
        exit(1);
    }
    foodnode->next = NULL;
    // 随机生成食物坐标 在墙范围内但是不能在蛇身上 而且x坐标是偶数
    while (1)
    {
        int x = rand() % 54 + 2;
        int y = rand() % 24 + 2;
        pSnakeNode cur = ps->_pSnake;
        while (cur != NULL)
        {
            if (cur->x == x && cur->y == y || x % 2 != 0)
            {
                break;
            }
            cur = cur->next;
        }
        if (cur == NULL)
        {
            foodnode->x = x;
            foodnode->y = y;
            break;
        }
    }
    // 打印食物
    SetPos(foodnode->x, foodnode->y);
    wprintf(L"%c", FOOD);
    ps->_pFood = foodnode;
}

// 撞墙检测
// 撞墙返回1 否则返回0
int IsKillByWall(pSnake ps)
{
    if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26)
    {
        return 1;
    }
    return 0;
}

// 撞自己检测
// 撞自己返回1 否则返回0
int IsKillBySelf(pSnake ps)
{
    pSnakeNode cur = ps->_pSnake->next;
    // 从第二个节点开始遍历 并判断是否和蛇头坐标相同
    while (cur != NULL)
    {
        if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
        {
            return 1;
        }
        cur = cur->next;
    }
    return 0;
}

// 吃掉食物后的处理
void EatFood(pSnake ps)
{
    // 加分
    ps->_totalScore += ps->_foodScore;
    // 打印总分数
    SetPos(82, 7);
    printf("%4d", ps->_totalScore);
    // 是 头插食物节点 创建新节点
    ps->_pFood->next = ps->_pSnake;
    ps->_pSnake = ps->_pFood;
    // 打印新蛇头
    SetPos(ps->_pSnake->x, ps->_pSnake->y);
    wprintf(L"%c", BODY);
    // 创建新食物
    CreatFood(ps);
}


// 没有迟到食物的处理
void NotEatFood(pSnake ps, int x, int y)
{
    // 创建新节点并头插
    pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));
    if (node == NULL)
    {
        perror("node malloc failed");
        exit(1);
    }
    node->x = x;
    node->y = y;
    node->next = ps->_pSnake;
    ps->_pSnake = node;
    // 打印新蛇 顺便删除尾节点 释放空间 打印空格
    pSnakeNode cur = ps->_pSnake;
    while (cur->next->next != NULL)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%c", BODY);
        cur = cur->next;
    }
    SetPos(cur->x, cur->y);
    wprintf(L"%c", BODY);
    SetPos(cur->next->x, cur->next->y);
    wprintf(L"  ");
    free(cur->next);
    cur->next = NULL;
}


// 蛇的移动 -- 主游戏程序 在此循环
// 吃到食物 创建新的食物节点
// 没有吃到食物头插新节点删除尾节点并释放空间
// 减速睡眠时间-30 睡眠时间最少减4次 单个食物分数加2
// 加速睡眠时间+30 睡眠时间最多加4次 单个食物分数减2
void SnakeMove(pSnake ps)
{
    int x = 0;
    int y = 0;
    again:
    while (ps->_status == OK)
    {
        // 按键检测
        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))
        {
            ps->_status = PAUSE;
        }
        else if (KEY_PRESS(VK_F3) && ps->_foodScore < 20)
        {
            ps->_sleepTime -= 30;
            ps->_foodScore += 2;
            // 更改单个食物分数
            SetPos(84, 5);
            printf("%2d", ps->_foodScore);
        }
        else if (KEY_PRESS(VK_F4) && ps->_foodScore > 2)
        {
            ps->_sleepTime += 30;
            ps->_foodScore -= 2;
            // 更改单个食物分数
            SetPos(84, 5);
            printf("%2d", ps->_foodScore);
        }
        else if (KEY_PRESS(VK_ESCAPE))
        {
            ps->_status = ESC;
        }

        // 设置下一个节点的x,y坐标
        switch (ps->_dir)
        {
            case UP:
                x = ps->_pSnake->x;
                y = ps->_pSnake->y - 1;
                break;
            case DOWN:
                x = ps->_pSnake->x;
                y = ps->_pSnake->y + 1;
                break;
            case LEFT:
                x = ps->_pSnake->x - 2;
                y = ps->_pSnake->y;
                break;
            case RIGHT:
                x = ps->_pSnake->x + 2;
                y = ps->_pSnake->y;
                break;
        }
        // 判断是否吃到食物
        if (ps->_pFood->x == x && ps->_pFood->y == y)
        {
            EatFood(ps);
        }
        else 
        {
            NotEatFood(ps, x, y);
        }
        // 撞墙检测
        if (IsKillByWall(ps))
        {
            ps->_status = KILL_BY_WALL;
        }
        // 撞自己检测
        if (IsKillBySelf(ps))
        {
            ps->_status = KILL_BY_SELF;
        }
        Sleep(ps->_sleepTime);
    }
    while (ps->_status == PAUSE)
    {
        if (KEY_PRESS(VK_SPACE))
        {
            ps->_status = OK;
        }
        goto again;
    }
    // 撞墙打印信息
    if (IsKillByWall(ps))
    {
        SetPos(45, 10);
        wprintf(L"墙墙被你撞西了,110把你抓走了");
    }
    // 撞自己打印信息
    if (IsKillBySelf(ps))
    {
        SetPos(45, 10);
        wprintf(L"自己撞自己了,120把你带走了");
    }
    return;
}

// 游戏前的初始化
void GameStart(pSnake ps)
{
    // 初始化控制台
    CmdInit();
    CursorHide();
    // 本地化配置
    setlocale(LC_ALL, "");
    WelcomeToGame();
    GameIntroduction();
    MapDraw();
    InitSnake(ps);
    CreatFood(ps);
}

// 游戏运行
void GameRun(pSnake ps)
{
    PrintStaticHelp();
    SnakeMove(ps);
    SetPos(45, 28);
    system("pause");
}

// 善后 释放蛇节点 以及食物节点
void GameEnd(pSnake ps)
{
    // 释放蛇节点
    pSnakeNode cur = NULL;
    while (ps->_pSnake != NULL)
    {
        cur = ps->_pSnake;
        ps->_pSnake = ps->_pSnake->next;
        free(cur);
    }
    // 释放食物节点
    free(ps->_pFood);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/700709.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

采用PHP开发的一套(项目源码)医疗安全(不良)事件报告系统源码:统计分析,持续整改,完成闭环管理

采用PHP开发的一套&#xff08;项目源码&#xff09;医疗安全&#xff08;不良&#xff09;事件报告系统源码&#xff1a;统计分析&#xff0c;持续整改&#xff0c;完成闭环管理 医疗安全确实是医疗领域中不容忽视的重要问题。医院不良安全事件&#xff0c;即医疗质量安全不良…

宋街宣传活动-循环利用,绿色生活

善于善行回收团队是一支致力于推动环保事业&#xff0c;积极倡导和实践绿色生活的志愿者队伍。我们的宗旨是通过回收再利用&#xff0c;减少资源浪费&#xff0c;降低环境污染&#xff0c;同时提高公众的环保意识&#xff0c;共同构建美丽和谐的家园。 善于善行志愿团队于2024年…

免费、无广告、界面简洁、简单好用的轻量级思维导图软件

一、简介 1、一款免费、无广告、界面简洁、简单好用的轻量级思维导图软件。它目前支持 Windows、MacOS 平台。其中 Windows 版大小在 104MB 左右&#xff08;UWP 应用&#xff09;&#xff0c;Mac 版大小在 167MB 左右。 二、下载 1、下载地址&#xff1a; MindAtom官网&#…

【保姆级讲解下QT6.3】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

用户和权限

Linux的root用户 无论是Windows、MacOS、Linux均采用多用户的管理模式进行权限管理 超级管理员: root用户拥有最大的系统操作权限(不建议长期使用root用户&#xff0c;避免带来系统损坏)普通用户的权限: 一般在其HOME目录内是不受限的,在HOME目录外仅有只读和执行权限&#x…

go-zero整合Excelize并实现Excel导入导出

go-zero整合Excelize并实现Excel导入导出 本教程基于go-zero微服务入门教程&#xff0c;项目工程结构同上一个教程。 本教程主要实现go-zero框架整合Excelize&#xff0c;并暴露接口实现Excel模板下载、Excel导入、Excel导出。 go-zero微服务入门教程&#xff1a;https://blo…

【深度学习】AI换脸,EasyPhoto: Your Personal AI Photo Generator【一】

论文&#xff1a;https://arxiv.org/abs/2310.04672 文章目录 摘要IntroductionTraining Process3 推理过程3.1 面部预处理3.3 第二扩散阶段3.4 多用户ID 4 任意ID5 实验6 结论 下篇文章进行实战。 摘要 稳定扩散Web UI&#xff08;Stable Diffusion Web UI&#xff0c;简称…

MYSQL八、MYSQL的SQL优化

一、SQL优化 sql优化是指&#xff1a;通过对sql语句和数据库结构的调整&#xff0c;来提高数据库查询、插入、更新和删除等操作的性能和效率。 1、插入数据优化 要一次性往数据库表中插入多条记录&#xff1a; insert into tb_test values(1,tom); insert into tb_tes…

CyberDAO:引领Web3时代的DAO社区文化

致力于Web3研究和孵化 CyberDAO自成立以来&#xff0c;致力于推动Web3研究和孵化&#xff0c;吸引了来自技术、资本、商业、应用与流量等领域的上千名热忱成员。我们为社区提供多元的Web3产品和商业机会&#xff0c;触达行业核心&#xff0c;助力成员捕获Web3.0时代的红利。 目…

远程链接服务 ssh

① 指定用户身份登录 ssh root10.36.105.100 ssh jim10.36.105.100 ② 不登陆远程执行命令 ssh root10.36.105.100 ls /opt ③ 远程拷贝 scp -r // 拷贝目录 -p // 指定端口 将本地文件拷贝给远程主机 scp -r /opt/test1 10.36.105.100:/tmp/// 将本…

Windows电脑清理C盘内存空间

ps&#xff1a;过程截图放在篇末 一、%tmp%文件 win R键呼出运行窗口&#xff0c;输入 %tmp% 自动进入tmp文件夹&#xff0c;ctrl A全选删除 遇到权限不足&#xff0c;正在运行&#xff0c;丢失的文件直接跳过即可 二、AppData文件夹 1、pipcache 在下列路径下面&…

小目标检测篇 | YOLOv8改进之空间上下文感知模块SCAM + 超轻量高效动态上采样DySample

前言:Hello大家好,我是小哥谈。小目标检测是计算机视觉领域中的一个研究方向,旨在从图像或视频中准确地检测和定位尺寸较小的目标物体。相比于常规目标检测任务,小目标检测更具挑战性,因为小目标通常具有低分辨率、低对比度和模糊等特点,容易被背景干扰或遮挡。本篇文章就…

Unity 实现WebSocket 简单通信——客户端

创建连接 ClientWebSocket socket new ClientWebSocket(); string url $"ws://{ip}:{port}"; bool createUri Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out Uri uri); if (createUri) {var task socket.ConnectAsync(uri, CancellationToken.None);task…

django学习入门系列之第二点《浏览器能识别的标签1》

文章目录 文件的编码(head)网站表头信息(head)标题&#xff08;body&#xff09;div和span往期回顾 文件的编码(head) <!--浏览器会以"UTF-8"这种编码来读取文件--> <meta charset"UTF-8">网站表头信息(head) <title>Title</title&…

Android帧绘制流程深度解析 (一)

Android帧绘制技术有很多基础的知识&#xff0c;比如多buffer、vsync信号作用等基础知识点很多笔记讲的已经很详细了&#xff0c;我也不必再去总结&#xff0c;所以此处不再过多赘述安卓帧绘制技术&#xff0c;基础知识这篇文章总结的很好&#xff0c;一文读懂"系列&#…

VBA即用型代码手册:删除空列Delete Empty Columns

我给VBA下的定义&#xff1a;VBA是个人小型自动化处理的有效工具。可以大大提高自己的劳动效率&#xff0c;而且可以提高数据的准确性。我这里专注VBA,将我多年的经验汇集在VBA系列九套教程中。 作为我的学员要利用我的积木编程思想&#xff0c;积木编程最重要的是积木如何搭建…

细说ARM MCU的串口接收数据的实现过程

目录 一、硬件及工程 1、硬件 2、软件目的 3、创建.ioc工程 二、 代码修改 1、串口初始化函数MX_USART2_UART_Init() &#xff08;1&#xff09;MX_USART2_UART_Init()串口参数初始化函数 &#xff08;2&#xff09;HAL_UART_MspInit()串口功能模块初始化函数 2、串口…

爱奇艺视频怎么转换成mp4格式,爱奇艺qsv转换mp4最简单方法

在数字化时代&#xff0c;视频格式的转换成为了我们日常生活中常见的需求。特别是对于那些经常从各大视频平台下载视频的朋友来说&#xff0c;将特定格式的视频转换为更通用的格式&#xff0c;如MP4&#xff0c;变得尤为重要。其中&#xff0c;qsv格式的视频转换就是一项常见的…

C++|哈希结构封装unordered_set和unordered_map

上一篇章&#xff0c;学习了unordered系列容器的使用&#xff0c;以及哈希结构&#xff0c;那么这一篇章将通过哈希结构来封装unordered系列容器&#xff0c;来进一步的学习他们的使用以及理解为何是如此使用。其实&#xff0c;哈希表的封装方式和红黑树的封装方式形式上是差不…

极坐标下的牛拉法潮流计算9节点MATLAB程序

微❤关注“电气仔推送”获得资料&#xff08;专享优惠&#xff09; 潮流计算&#xff1a; 潮流计算是根据给定的电网结构、参数和发电机、负荷等元件的运行条件&#xff0c;确定电力系统各部分稳态运行状态参数的计算。通常给定的运行条件有系统中各电源和负荷点的功率、枢纽…