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

news2024/11/24 17:41:05

文章目录

      • 游戏背景介绍
      • 实现目标
      • 适合人群
      • 所需技术
      • 浅玩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);
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MCK主机加固:智能科技,构筑网络安全的铜墙铁壁

在数字化转型的浪潮中&#xff0c;企业服务器的安全已成为维护业务连续性和保护数据资产的关键。MCK主机加固产品&#xff0c;以其创新技术&#xff0c;为企业提供了一个全面、智能、高效的安全解决方案。 一、智能安全监测 MCK主机加固产品采用深度学习算法&#xff0c;能够…

Pixi.js学习 (四)鼠标跟随、元素组合与图片位控

目录 一、鼠标移动跟随 1.1 获取鼠标坐标 1.2 鼠标跟随 二、锚点、元素组合 2.1 锚点 2.2 元素组合 三、图片图层 四、实战 例题一&#xff1a;完成合金弹头人物交互 例题二&#xff1a;反恐重击瞄准和弹痕 例题一代码&#xff1a; 例题二代码&#xff1a; 总结 前言 为了提高作…

利用flask + pymysql监测数据同步中的数据是否完整

一、背景 最近项目搞重构&#xff0c;将原有的系统拆分成了多个子系统。但是有数据表需要在不同系统中数据&#xff0c;同时为了解决项目性能最了一个很简单的方案&#xff0c;就是公共数据存在每个系统之中。 二、分析 分析这些表&#xff0c;这些表相比源数据表&#xff0c;表…

ArrayList和LinkedList的区别!!!

总结&#xff1a; 1、数据结构的实现 ArrayList&#xff1a;动态数组。 LinkedList&#xff1a;双向链表。 2、时间复杂度不同 ArrayList&#xff1a;O(1) LinkedList: O(n) ①&#xff1a;随机访问---- ArrayList > LinkedList &#xff08;ArrayList采用下标&#xff0…

<<虾皮shopee来赞达lazada>>商品详情API@各区域item~get

数据采集API接口——搭建电商数据采集系统 可采集30多个电商平台数据&#xff0c;采集字段高达40多个&#xff0c;包含标题、价格、图片等&#xff1b; 可采集单个SKU某个时间段的全数据&#xff1b; 采集数据准确率高达98%。 虾皮shopee电商数据&#xff08;来赞达lazada同理&…

一个数据查询导出工具

数据查询导出工具 安装说明 安装完成后在桌面会创建“数据查询导出工具”的查询工具。 程序初始化 配置数据库连接 首次运行&#xff0c;请先配置数据库连接 点击“数据库连接”后&#xff0c;会出现下面的窗体&#xff0c;要求输入维护工程师密码。&#xff08;维护工程师密码…

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

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

7.数据集处理库Hugging Face Datasets

数据集处理库Hugging Face Datasets Datasets 首先解决数据来源问题 使用 Datasets 下载开源数据集 Datasets.load_dataset 实现原理简介 构造 DatasetBuilder 类的主要配置 BuilderConfig 如果您想向数据集添加额外的属性,例如类别标签。有两种方法来填充BuilderConfig类或其…

web刷题记录(5)

[羊城杯 2020]easycon 进来以后就是一个默认测试页面&#xff0c; 在这种默认界面里&#xff0c;我觉得一般不会有什么注入点之类的&#xff0c;所以这里先选择用御剑扫扫目录看看有没有什么存在关键信息的页面 扫了一半发现&#xff0c;很多都是和index.php文件有关&#xff0…

【docker】adoptopenjdk/openjdk8-openj9:alpine-slim了解

adoptopenjdk/openjdk8-openj9:alpine-slim 是一个 Docker 镜像的标签&#xff0c;它指的是一个特定的软件包&#xff0c;用于在容器化环境中运行 Java 应用程序。 镜像相关的网站和资源&#xff1a; AdoptOpenJDK 官方网站 - AdoptOpenJDK 这是 AdoptOpenJDK 项目的官方网站&…

柯桥成人外语培训|职场商务英语剑桥国际英语口语外贸商务英语

01 “puppy love”是什么意思&#xff1f; 大家都知道&#xff0c;puppy有“幼犬&#xff0c;小狗”的意思&#xff0c;love是“爱”&#xff0c;那puppy love是什么意思呢&#xff1f;可不是字面上表达的小狗的爱哦&#xff01; 其实&#xff0c;"puppy love"真正的…

小巧精悍的电脑磁盘分析工具,程序大小仅830KB,支持汉化。

一、简介 1、一款小巧精悍的电脑磁盘分析工具&#xff0c;程序大小仅830KB&#xff0c;支持汉化&#xff1b;当你启动它&#xff0c;指定要分析的系统盘&#xff0c;它会自动将文件和文件夹按照所占用空间大小从小到大可视化显示。各个文件会以大小不同的块展示出来&#xff0c…

VirtFuzz:一款基于VirtIO的Linux内核模糊测试工具

关于VirtFuzz VirtFuzz是一款功能强大的Linux内核模糊测试工具&#xff0c;该工具使用LibAFL构建&#xff0c;可以利用VirtIO向目标设备的内核子系统提供输入测试用例&#xff0c;广大研究人员可以使用该工具测试Linux内核的安全性。 工具要求 1、Rust&#xff1b; 2、修补的Q…

word空白页删除不了怎么办?

上方菜单栏点击“视图”&#xff0c;下方点击“大纲视图”。找到文档分页符的位置。将光标放在要删除的分节符前&#xff0c;按下键盘上的“Delet”键删除分页符。

Linux基础(2)基础命令与vim

文件的复制和移动 cp 拷贝文件和目录 cp file file_copy --> file 是目标文件&#xff0c;file_copy 是拷贝出来的文件 cp file one --> 把 file 文件拷贝到 one 目录下&#xff0c;并且文件名依然为 file cp file one/file_copy --> 把 file 文件拷贝到 one 目录下…

vue input 限制输入,小数点后保留两位 以及 图片垂直居中显示 和 分享 git 小技巧

&#xff08;1&#xff09;input 限制输入&#xff0c;小数点后保留两位 <template><div><el-input v-model"number" input"checkNumber" blur"completeNumber" placeholder"请输入"></el-input></div>…

军事地图管理系统|DW-S403实现军用地图科学管理

军用地图管理系统DW-S403系统通过数据采集、物联网技术&#xff0c;实现数字化智能管控&#xff0c;以提高军用物资的地图的科学化管理&#xff0c;也有助于消除生产过程中的不确定性。 军用地图是反映实际地形的最可靠的资料&#xff0c;是指挥员的"左膀右臂"。在作…

[Algorithm][动态规划][二维费用的背包问题][一和零][盈利计划]详细讲解

目录 0.原理讲解1.一和零1.题目链接2.算法原理详解3.代码实现 2.盈利计划1.题目链接2.算法原理详解3.代码实现 0.原理讲解 本质仍然是背包问题&#xff0c;但是相较于普通的背包问题&#xff0c;只是限制条件多了一个而已 1.一和零 1.题目链接 一和零 2.算法原理详解 思路&…

03-3.5.1~4 特殊矩阵的压缩存储

&#x1f44b; Hi, I’m Beast Cheng&#x1f440; I’m interested in photography, hiking, landscape…&#x1f331; I’m currently learning python, javascript, kotlin…&#x1f4eb; How to reach me --> 458290771qq.com 喜欢《数据结构》部分笔记的小伙伴可以订…

vue elementui 自定义loading显示的icon 文本 背景颜色

<div v-loading"modalLoading"element-loading-text"拼命加载中"element-loading-spinner"el-icon-loading"element-loading-background"rgba(0, 0, 0, 0.8)" class"loading-box" v-if"modalLoading"></…