一篇文章教会你写一个贪吃蛇小游戏(纯C语言)

这篇具有很好参考价值的文章主要介绍了一篇文章教会你写一个贪吃蛇小游戏(纯C语言)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1、游戏展示

记事本写一个贪吃蛇游戏,C语言进阶,数据结构,c语言

2、游戏功能

实现基本的功能

• 贪吃蛇地图绘制
• 蛇吃⻝物的功能(上、下、左、右⽅向键控制蛇的动作)
• 蛇撞墙死亡
• 蛇撞⾃⾝死亡
• 计算得分
• 蛇⾝加速、减速
• 暂停游戏

3、Win32 API

Win32 API是一套由Microsoft提供的应用程序编程接口,用于开发Windows平台上的应用程序。它包括了丰富的函数、数据结构和消息机制,允许开发者与操作系统进行交互。这些接口覆盖了各个方面,如图形用户界面(GUI)、文件和输入输出、多媒体、网络通信等。通过调用这些API,开发者可以实现窗口创建、消息处理、事件响应、内存管理等功能,从而构建功能完善的Windows应用程序。Win32 API是基于C语言的,但也可以通过其他编程语言进行调用。

实现贪吃蛇会使⽤到的⼀些Win32API知识 ,下面我们来看看

3.1 控制台程序

平常我们运行起来的cmd命令框程序其实就是控制台程序

比如我们可以使用cmd命令来设置控制台窗口的大小

mode con cols=100 lines=30

也可以通过命令设置控制台窗口的名字

title 爱学习的鱼佬
记事本写一个贪吃蛇游戏,C语言进阶,数据结构,c语言

这些能在控制台窗口执行的命令,也可以调用C语言函数system来执行。例如:

#include<stdio.h>
int main(){
	system("mode con cols=100 lines=30");
	system("title 爱学习的鱼佬");

	return 0;
}

3.2 控制台屏幕上的坐标COORD

COORD是Windows API中定义的⼀个结构体,表示⼀个字符在控制台屏幕上的坐标

typedef struct _COORD {
    SHORT X;
    SHORT Y;
} COORD, *PCOORD;

给坐标赋值:

COORD pos={10,15};

3.3 GetStdHandle函数

GetStdHandle 函数是用于获取标准输入、标准输出和标准错误输出的句柄(handle)的 Windows API 函数。它通常用于控制台应用程序,允许你访问这些标准流以进行输入和输出操作。

以下是 GetStdHandle 函数的基本用法:

#include <windows.h>

int main() {
    HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
    HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
    HANDLE hStderr = GetStdHandle(STD_ERROR_HANDLE);

    // 使用 hStdout, hStdin, 和 hStderr 进行输入、输出和报错操作

    return 0;
}

这个示例演示如何获取标准输出、标准输入和标准错误输出的句柄,并将它们存储在 HANDLE 变量中。你可以使用这些句柄来执行与控制台输入和输出相关的操作,例如写入到控制台或从控制台读取数据。

请注意,GetStdHandle 函数需要包含 <windows.h> 头文件,并且通常与其他 Windows 控制台函数一起使用,如 WriteFileReadFile 用于实际的输入和输出操作。

3.4 GetConsoleCursorInfo函数

GetConsoleCursorInfo 函数是一个用于获取控制台光标信息的 Windows API 函数。它允许你检索控制台光标的可见性和闪烁属性以及光标的大小。

以下是 GetConsoleCursorInfo 函数的基本用法:

#include <windows.h>

int main() {
    HANDLE hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    CONSOLE_CURSOR_INFO cursorInfo;

    if (GetConsoleCursorInfo(hConsoleOutput, &cursorInfo)) {
        // cursorInfo.dwSize 表示光标的大小
        // cursorInfo.bVisible 表示光标是否可见
        // cursorInfo.dwSize 和 cursorInfo.bVisible 可以用于读取光标的属性
    } else {
        // 处理获取光标信息失败的情况
    }

    return 0;
}

在上述示例中,我们首先获取标准输出句柄,并然后使用 GetConsoleCursorInfo 函数来检索控制台光标的信息,包括光标的大小 (dwSize) 和可见性 (bVisible)。获取成功时,你可以读取这些属性,以了解当前光标的状态。

这个函数通常在控制台应用程序中用于获取和修改光标的属性,例如改变光标的可见性或大小。你还可以使用其他控制台函数来设置新的光标属性,如 SetConsoleCursorInfo 来修改光标的属性。

3.4.1 CONSOLE_CURSOR_INFO结构体

CONSOLE_CURSOR_INFO 是一个结构体,用于控制和设置控制台光标的属性。这个结构体通常用于控制光标的大小和可见性。

在 Windows API 中,CONSOLE_CURSOR_INFO 结构体的定义如下:

typedef struct _CONSOLE_CURSOR_INFO {
    DWORD dwSize;    // 光标的百分比高度 (1 到 100)
    BOOL bVisible;   // 光标是否可见
} CONSOLE_CURSOR_INFO;
  • dwSize 表示光标的大小,以百分比高度的形式表示,1 到 100 之间的值。光标的高度由控制台的字符单元高度和 dwSize 决定。
  • bVisible 表示光标的可见性,TRUE 表示光标可见,FALSE 表示光标不可见。

这个结构体通常与 GetConsoleCursorInfoSetConsoleCursorInfo 函数一起使用,用于获取和设置控制台光标的属性。通过设置 CONSOLE_CURSOR_INFO 结构体的属性,你可以控制控制台中光标的外观和行为。

例如,可以使用这个结构体来设置光标的大小和可见性,然后将其传递给 SetConsoleCursorInfo 函数,从而更改控制台光标的属性

3.5 SetConsoleCursorInfo函数

SetConsoleCursorInfo 函数是一个用于设置控制台光标信息的 Windows API 函数。它允许你更改控制台光标的可见性、闪烁属性以及光标的大小。

以下是 SetConsoleCursorInfo 函数的基本用法:

#include <windows.h>

int main() {
    HANDLE hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    CONSOLE_CURSOR_INFO cursorInfo;

    cursorInfo.dwSize = 25; // 设置光标的大小,单位是百分之一
    cursorInfo.bVisible = TRUE; // 设置光标可见

    if (SetConsoleCursorInfo(hConsoleOutput, &cursorInfo)) {
        // 光标属性设置成功
    } else {
        // 处理设置光标属性失败的情况
    }

    return 0;
}

在上述示例中,我们首先获取标准输出句柄,并然后使用 SetConsoleCursorInfo 函数来设置控制台光标的信息,包括光标的大小 (dwSize) 和可见性 (bVisible)。你可以根据需要将这些属性设置为所需的值。

这个函数通常在控制台应用程序中用于自定义控制台光标的属性,例如更改光标的大小或可见性。通过调用 SetConsoleCursorInfo 函数,你可以实时更改控制台光标的外观。如果需要获取光标信息,可以使用 GetConsoleCursorInfo 函数。

3.6 SetConsoleCursorPosition函数

SetConsoleCursorPosition 函数是一个 Windows API 函数,用于设置控制台窗口的光标位置。通过调用这个函数,你可以将光标移动到指定的控制台窗口坐标位置。

以下是 SetConsoleCursorPosition 函数的基本用法:

#include <windows.h>

int main() {
    HANDLE hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    COORD cursorPosition;

    cursorPosition.X = 10; // 设置光标的水平位置
    cursorPosition.Y = 5;  // 设置光标的垂直位置

    if (SetConsoleCursorPosition(hConsoleOutput, cursorPosition)) {
        // 光标位置设置成功
    } else {
        // 处理设置光标位置失败的情况
    }

    return 0;
}

在上述示例中,我们首先获取标准输出句柄,然后创建一个 COORD 结构体,其中包括水平位置(X)和垂直位置(Y)。接着,我们使用 SetConsoleCursorPosition 函数来将光标移动到指定的控制台窗口坐标位置。

这个函数通常在控制台应用程序中用于控制光标的移动,以便在控制台上绘制文本或执行其他操作。通过设置光标位置,你可以控制光标在控制台窗口中的位置。

3.7 GetAsyncKeyState函数

GetAsyncKeyState 是一个 Windows API 函数,用于检查指定虚拟键码对应的键是否处于按下状态。它可以用来检测键盘上的按键是否被按下,而且不会阻塞程序执行,因此适用于实现基本的键盘输入检测。

以下是 GetAsyncKeyState 函数的基本用法:

#include <windows.h>

int main() {
    // 检查某个键是否被按下,比如检查A键是否被按下
    SHORT keyState = GetAsyncKeyState('A');

    // 检查键的状态
    if (keyState & 0x8000) {
        // A键被按下
    } else {
        // A键没有被按下
    }

    return 0;
}

在上述示例中,我们使用 GetAsyncKeyState 函数检查键盘上的’A’键是否被按下。函数返回一个 SHORT 类型的值,其中高位表示键的状态,如果键被按下,高位的最低位(最低有效位)将被设置为1,即0x8000。

这个函数通常用于游戏开发、输入检测以及其他需要实时键盘输入的应用程序,因为它不会阻塞程序,可以在循环中持续检查键的状态。需要注意的是,GetAsyncKeyState 可以检测虚拟键码而不仅限于字符键,因此你可以使用虚拟键码来检查其他键盘上的按键。

每个按键都有对应的虚拟键码(Virtual-Key Codes),以下是一些常用键的虚拟键码:

  • 左键鼠标按钮: VK_LBUTTON (0x01)
  • 右键鼠标按钮: VK_RBUTTON (0x02)
  • 中键鼠标按钮: VK_MBUTTON (0x04)
  • Backspace键: VK_BACK (0x08)
  • Tab键: VK_TAB (0x09)
  • Enter键: VK_RETURN (0x0D)
  • Shift键: VK_SHIFT (0x10)
  • Ctrl键: VK_CONTROL (0x11)
  • Alt键: VK_MENU (0x12)
  • Pause键: VK_PAUSE (0x13)
  • Caps Lock键: VK_CAPITAL (0x14)
  • Esc键: VK_ESCAPE (0x1B)
  • 空格键: VK_SPACE (0x20)
  • Page Up键: VK_PRIOR (0x21)
  • Page Down键: VK_NEXT (0x22)
  • End键: VK_END (0x23)
  • Home键: VK_HOME (0x24)
  • 左方向键: VK_LEFT (0x25)
  • 上方向键: VK_UP (0x26)
  • 右方向键: VK_RIGHT (0x27)
  • 下方向键: VK_DOWN (0x28)
  • 0键 (主键盘): 0x30
  • A键: 0x41
  • F1键: VK_F1 (0x70)
  • F2键: VK_F2 (0x71)
  • F3键: VK_F3 (0x72)
  • …以此类推,F4至F12键的虚拟键码为0x73至0x7C

GetAsyncKeyState 的返回值是short类型,在上⼀次调用 GetAsyncKeyState 函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0

所以我们要判断⼀个键是否被按过,可以检测 GetAsyncKeyState 返回值的最低值是否为1

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

4、设计贪吃蛇地图

我们先看设计好的界面,控制台窗口的坐标如下所示,横向的是X轴,从左向右依次增长,纵向是Y轴,从上到下依次增长
记事本写一个贪吃蛇游戏,C语言进阶,数据结构,c语言

在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★
普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节。
C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。

C语言字符默认是采用ASCII编码的,ASCI字符集采用的是单字节编码,且只使用了单字节中的低7位,最高位是没有使用的,可表示为0xxxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,比如,在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的e的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了,在希伯来语编码中却代表了字母Gimel(区),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0–127表示的符号是一样的,不一样的只是128--255的这一段

至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示 256 x 256=65536 个字符

后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入和宽字符的类型wchar_t 和宽字符的输入和输出函数,加入<locale.h>头文件,其中提供了允许程序员针对特定地区 (通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。

4.1 <locale.h>

<locale.h> 是C语言标准库中的头文件,提供了有关本地化(localization)和本地化相关函数的支持。本地化涉及根据特定地区或文化习惯的需求来格式化文本、日期、时间和数字等。这有助于使程序适应不同的地域文化。

在C语言中,<locale.h> 提供了设置地区和语言环境的函数,例如 setlocale,允许程序员根据所需的语言环境进行设置。它还提供了函数来处理在不同地区设置中使用的格式化、排序、货币和日期时间信息。

一些常见的函数和功能包括:

  • setlocale: 设置程序的当前地区和语言环境,允许程序根据特定的语言环境来处理数据。
  • localeconv: 返回一个描述当前地区设置中的数值格式的结构体。
  • strftime: 根据指定的格式化字符串将日期和时间格式化为本地化格式。
  • printfscanf 系列函数中的格式化标志: 例如 %n%Ld 等,可以受到地区设置的影响而改变输出或输入的格式。

这些函数可以根据地区设置调整货币符号、日期格式、时间格式、数字分隔符等,从而使程序在不同的文化环境下更适用和易懂。

<locale.h> 头文件中,定义了一系列常量用于设置不同的本地化范畴。这些常量用于确定在程序中所需的不同本地化设置。以下是一些常用的常量:

  • LC_ALL: 代表所有的本地化设置。
  • LC_COLLATE: 字符串排序规则的设置。
  • LC_CTYPE: 字符分类和转换规则的设置。
  • LC_MONETARY: 通货格式的设置。
  • LC_NUMERIC: 数值格式的设置。
  • LC_TIME: 时间和日期格式的设置。

这些常量可以作为 setlocale() 函数的参数,用于设置程序中的不同本地化范畴。例如,setlocale(LC_TIME, "en_US") 用于设置日期和时间格式为美国英语,setlocale(LC_MONETARY, "fr_FR") 用于设置货币格式为法国法语等。

4.2 setlocale函数

char *setlocale(int category, const char *locale);
  • category 是要设置的本地化范畴,可以是诸如 LC_ALLLC_COLLATELC_CTYPELC_MONETARYLC_NUMERICLC_TIME 等预定义常量之一。
  • locale 是一个字符串,表示要设置的本地化环境,比如 “zh_CN” 表示中文的本地化设置。

示例用法如下:

#include <stdio.h>
#include <locale.h>

int main() {
    setlocale(LC_ALL, "en_US.UTF-8");

    // 其他程序逻辑...

    return 0;
}

"zh_CN.UTF-8" 中的 "zh_CN" 是表示中文(中国)的语言代码,"UTF-8" 表示使用 UTF-8 编码

需要注意的是,setlocale 函数在不同的操作系统或环境下可能有不同的支持程度,有些环境可能并不支持特定的本地化设置。

C标准给第二个参数仅定义了2种可能取值:"C"和” "

在任意程序执行开始,都会隐藏式执行调用:

setlocale(LC_ALL, "C");

当地区设置为"C"时,库函数按正常方式执行,小数点是一个点。当程序运行起来后想改变地区,就只能显式调用setlocale函数。用" "作为第2个参数,调用setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。

setlocale(LC_ALL, " ");//切换到本地环境

4.3 宽字符的打印

在 C 语言中,你可以使用 wchar_t 类型和一些相关的宽字符函数来处理和打印宽字符。

首先,确保你的编译环境支持宽字符,并且使用宽字符格式的输出函数。

#include <stdio.h>
#include <wchar.h>
#include <locale.h>

int main() {
    setlocale(LC_ALL, ""); // 设置本地化环境,以支持宽字符
    
    char ch1='a';
    char ch2='b'; //普通字符

    wchar_t wideChar1 = L'你'; 
    wchar_t wideChar2 = L'●'; // 使用宽字符
	
    printf("%c%c\n",ch1,ch2); //输出普通字符
    wprintf(L"%lc\n%lc\n", wideChar1,wideChar2); // 使用wprintf输出宽字符

    return 0;
}
  • wchar_t 是用于表示宽字符的 C 语言数据类型。
  • wprintf 是用于打印宽字符的函数,L"..." 表示宽字符常量。

setlocale(LC_ALL, ""); 这行代码设置程序的本地化环境以支持宽字符的处理和显示。使用 wprintfwchar_t 类型可以正确地处理和打印宽字符。

输出结果
记事本写一个贪吃蛇游戏,C语言进阶,数据结构,c语言

从输出的结果来看,我们发现一个普通字符占一个字符的位置但是打印一个汉字字符或特殊符号,占用2个字符的位置,那么我们如果要在贪吃蛇中使用宽字符,就得处理好地图上坐标的计算。

4.4 地图坐标及蛇身和食物

按照我们上面实现的效果,这里的游戏地图区域是58列,27行达到的效果,当然你也可以根据自己的情况进行修改

记事本写一个贪吃蛇游戏,C语言进阶,数据结构,c语言

初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如上面的地图中(20,6)处开始出现蛇,连续5个节点。

注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点有一半儿出现在墙体中,另外一般在墙外的现象,坐标不好对齐。关于食物,就是在墙体内随机生成一个坐标**(x坐标必须是2的倍数)**,坐标不能和蛇的身体重合,然后打印大。

5. 数据结构设计

5.1 蛇节点

在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信息,那么蛇的每一节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行.所以蛇节点结构如下:

typedef struct SnakeNode
{
    int x;
    int y;
    struct SnakeNode* next;
}SnakeNode, * pSnakeNode;

pSnakeNode: 这个别名是一个指向 SnakeNode 结构体的指针类型,使得你可以更简洁地声明指向 SnakeNode 的指针变量

5.2 蛇状态结构

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

5.3 蛇的方向

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

5.4 游戏状态

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

7. 游戏整体流程

记事本写一个贪吃蛇游戏,C语言进阶,数据结构,c语言

8. 核心逻辑实现

8.1 游戏主逻辑

#include "Snake.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;
}

8.2 游戏开始

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);
}
  1. 设置控制台窗口大小和标题
    • system("mode con cols=100 lines=30"); 用于设置控制台窗口的大小,将其设定为 100 列和 30 行。
    • system("title 贪吃蛇"); 设置了控制台窗口的标题为 “贪吃蛇”。
  2. 隐藏控制台光标
    • 使用 GetStdHandle 获取标准输出的句柄 hOutput,然后通过 GetConsoleCursorInfo 获取控制台光标的信息。
    • 将光标状态设置为不可见:CursorInfo.bVisible = false;
    • 最后通过 SetConsoleCursorInfo 设置控制台光标的状态。
  3. 显示欢迎界面
    • 调用 WelcomeToGame() 函数,这个函数可能打印一些欢迎信息和游戏的介绍。
  4. 打印地图、初始化蛇和创建第一个食物
    • CreateMap() 函数负责绘制游戏地图。
    • InitSnake() 初始化蛇的数据结构和显示蛇的初始状态。
    • CreateFood() 创建游戏中的第一个食物。

8.2.0 SetPos函数的封装

void SetPos(short x, short y)
{
    COORD pos = { x, y };
    HANDLE hOutput = NULL;

    hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

    SetConsoleCursorPosition(hOutput, pos);
}
  1. 定义函数 SetPos:
    • void SetPos(short x, short y):指定了该函数接受两个参数,分别是 x 和 y 坐标。
  2. 设置光标位置
    • COORD pos = { x, y };:创建了一个 COORD 类型的结构体变量 pos,用给定的 x 和 y 坐标值初始化。
    • HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);:获取标准输出的句柄,用来标识控制台窗口。
    • SetConsoleCursorPosition(hOutput, pos);:使用 SetConsoleCursorPosition 函数将控制台光标的位置设置为 pos 所指定的坐标。

8.2.1 欢迎界面和提示

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");
}
  1. 设置光标位置并打印欢迎信息
    • SetPos(40, 15); printf("欢迎来到贪吃蛇小游戏"); 将光标移动到 (40, 15) 的位置,打印欢迎信息 “欢迎来到贪吃蛇小游戏”。
  2. 等待用户按下任意键继续
    • SetPos(40, 25); 设置光标位置,这是为了让下一次的 system("pause"); 显示在一个美观的位置。
    • system("pause"); 暂停程序执行,等待用户按下任意键继续。
  3. 清屏
    • system("cls"); 清空控制台屏幕。
  4. 打印游戏操作说明
    • SetPos(25, 12); printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n"); 在指定位置打印游戏操作说明,告诉用户如何控制蛇的移动以及使用 F3 和 F4 来加速或减速。
    • SetPos(25, 13); printf("加速将能得到更高的分数。\n"); 给出一些游戏策略建议。
  5. 再次等待用户按下任意键继续
    • SetPos(40, 25); 再次设置光标位置,保证下一次的 system("pause"); 显示在一个合适的位置。
    • system("pause"); 再次暂停程序执行,等待用户按下任意键继续。

这个函数的目的是为了向玩家展示游戏的欢迎信息和操作说明,在游戏开始前给玩家提供一些基本的操作提示和规则。

欢迎界面

记事本写一个贪吃蛇游戏,C语言进阶,数据结构,c语言

提示界面

记事本写一个贪吃蛇游戏,C语言进阶,数据结构,c语言

8.2.2 创建地图

创建地图就是将墙打印出来,因为是宽字符打印,所有使用wprintf函数,打印格式串前使用L打印地图的关键是要算好坐标,才能在想要的位置打印墙体。

墙体打印的宽字符:

#define WALL L'□'

创建地图

void CreateMap()
{
    int i = 0;

    SetPos(0, 0);
    for (i = 0; i < 58; i += 2)
    {
        wprintf(L"%c", WALL);
    }

    SetPos(0, 26);
    for (i = 0; i < 58; i += 2)
    {
        wprintf(L"%c", WALL);
    }


    for (i = 1; i < 26; i++)
    {
        SetPos(0, i);
        wprintf(L"%c", WALL);
    }

    for (i = 1; i < 26; i++)
    {
        SetPos(56, i);
        wprintf(L"%c", WALL);
    }
}

绘制游戏地图

  • SetPos(0, 0); 将光标移动到 (0, 0) 的位置开始绘制游戏地图。
  • 通过 for 循环和 wprintf 函数,从 (0,0) 到 (56,0) 绘制顶部墙体,由 WALL 符号构成。
  • 从 (0,26) 到 (56,26) 绘制底部墙体。
  • 从 (0,1) 到 (0,25) 绘制左侧墙体。
  • 从 (56,1) 到 (56,25) 绘制右侧墙体。

8.2.3 初始化蛇身

蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标
创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,将蛇的每一节打印在屏幕上

蛇身打印的宽字符

#define BODY L'●'

初始化蛇身函数

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

创建贪吃蛇身体节点

  • 使用 for 循环创建贪吃蛇的五个身体段。
  • 每个段都是使用 malloc 动态分配的。
  • 为每个段设置坐标,创建了贪吃蛇身体的水平线。

打印贪吃蛇身体

  • 创建段之后,代码通过遍历贪吃蛇的链表,将光标移动到每个段的坐标并使用 wprintf 打印贪吃蛇的身体,使用宏 BODY 符号。

初始化游戏变量

  • 最后,函数初始化各种游戏相关变量,如休眠时间、得分、方向和额外得分规则。

8.2.4 创建食物

注意

x坐标必须是2的倍数
食物的坐标不能和蛇身每个节点的坐标重复

食物打印的宽字符

#define FOOD L'★'

创建食物函数

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

生成食物位置

  • 使用 do-while 循环生成食物的 x 和 y 坐标。
  • x 坐标被限制为偶数,以便与贪吃蛇的头部坐标对齐。

避免食物与蛇身冲突

  • 代码通过检查新生成的食物位置,确保它不会与贪吃蛇的身体重叠。
  • 如果新的食物坐标与蛇身重叠,代码会跳回 again 标签处重新生成食物位置。

创建食物节点

  • 如果食物的位置是有效的,就会动态分配一个新的节点来表示食物。
  • 食物节点的坐标被设置为新的 x 和 y 坐标,然后在控制台上的该位置打印食物的符号,并将食物节点指针保存在游戏状态中。

8.3 游戏运行

游戏运行期间,右侧打印帮助信息,提示玩家

根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束

如果游戏继续,就是检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。

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))
        {
            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->_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);
}

这段代码实现了游戏的运行逻辑,在一个循环中持续地更新游戏状态并响应玩家的输入。

  1. 打印帮助信息
    • PrintHelpInfo() 被调用来显示游戏帮助信息。
  2. 游戏循环
    • 游戏的主要逻辑被包含在一个 do-while 循环中。
    • 检查玩家的按键输入,根据按键不同做出相应的动作。
    • 根据按键状态来改变蛇头的移动方向。
    • 如果按下空格键,游戏会暂停。
    • 如果按下 ESC 键,游戏状态被设为正常结束(END_NOMAL),并跳出循环。
  3. 加速和减速
    • 如果按下 F3,游戏中蛇的移动速度会加快,得分增加。
    • 如果按下 F4,游戏中蛇的移动速度会减慢,得分减少。
  4. 蛇的移动
    • 游戏根据当前设置的移动速度来控制蛇的移动。
    • 使用 Sleep() 函数来控制每次蛇移动之间的时间间隔。
    • 调用 SnakeMove() 函数来处理蛇的移动逻辑。
  5. 循环终止
    • 游戏循环会持续进行,直到游戏状态变为非正常状态(不是 OK)为止。

8.3.1 帮助信息

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

记事本写一个贪吃蛇游戏,C语言进阶,数据结构,c语言

8.3.2 蛇身移动

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);
}
  1. 创建下一个节点:利用 malloc() 分配内存来创建一个新的蛇身节点 pNextNode
  2. 根据方向确定下一个节点的坐标:根据蛇头当前的方向,计算出蛇头下一个移动的位置。
  3. 检查下一个位置是否是食物
    • 如果下一个位置是食物,调用 EatFood(pNextNode, ps) 函数来处理吃食物的逻辑。
    • 如果不是食物,调用 NoFood(pNextNode, ps) 函数来处理不吃食物的逻辑。
  4. 检查游戏结束的条件
    • 调用 KillByWall(ps)KillBySelf(ps) 函数来检查是否撞墙或者咬到自己,如果游戏结束,则会设定相应的游戏状态。

8.3.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;
}
  1. NextIsFood:该函数检查下一个位置是否有食物。如果坐标与食物位置匹配,返回真;否则返回假。
  2. EatFood:当下一个位置有食物时调用此函数。它采用链表方法,在蛇的开头插入表示下一个位置的新节点,从而延长蛇的身体。它更新蛇的分数,移除被吃掉的食物(通过释放内存),然后创建新的食物以继续游戏。
  3. NoFood:当下一个位置没有食物时调用此函数。它也采用链表方法,在蛇的开头插入表示下一个位置的新节点。然而,它通过移除最后一个节点来管理蛇尾,模拟蛇的移动。然后释放最后一个节点的内存,并设置适当的指针。

8.3.4 G掉的2种情况

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;
}
  1. KillByWall 检查蛇头是否碰到了游戏地图的边界。如果蛇头在地图的边缘,函数将设置蛇的状态为 KILL_BY_WALL 并返回 1(表示蛇碰到了墙壁),否则返回 0。
  2. KillBySelf 检查蛇头是否碰到了自己的身体。它遍历了蛇身链表(除了头节点),检查蛇头的坐标是否与任何其他节点的坐标相匹配。如果发生了碰撞,函数将设置蛇的状态为 KILL_BY_SELF 并返回 1(表示蛇碰到了自己的身体),否则返回 0。

8.3.5 暂停函数

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

它会在游戏执行时进行循环,每 300 毫秒检查一次是否按下空格键。当检测到空格键按下时,循环结束,游戏继续执行

8.4 游戏结束

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

根据游戏状态在屏幕上显示相应的消息,例如玩家主动退出游戏、蛇撞到自己或撞到墙壁导致游戏结束。此后,它会释放蛇身的节点,并清理分配的内存文章来源地址https://www.toymoban.com/news/detail-815743.html

到了这里,关于一篇文章教会你写一个贪吃蛇小游戏(纯C语言)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • docker从安装到部署项目,一篇文章教会你

    首先看下 Docker 图标: 一条小鲸鱼上面有些集装箱,比较形象的说明了 Docker 的特点,以后见到这个图标等同见到了 Docker 1. Docker 是一个开源的应用容器引擎,它基于 Go 语言开发,并遵从 Apache2.0 开源协议 2. 使用 Docker 可以让开发者封装他们的应用以及依赖包到一个可移植的

    2024年02月08日
    浏览(35)
  • C++初阶之一篇文章教会你list(模拟实现)

    成员类型表 这个表中列出了C++标准库中list容器的一些成员类型定义。这些类型定义是为了使list能够与C++标准库的其他组件协同工作,并提供一些通用的标准接口。每个成员类型的用处: value_type : 这个成员类型代表list容器中存储的数据类型,即模板参数T的类型。 allocator_

    2024年02月12日
    浏览(21)
  • C++初阶之一篇文章教会你list(理解和使用)

    在C++标准库中, std::list 是一个双向链表容器,用于存储一系列元素。与 std::vector 和 std::deque 等容器不同, std::list 使用链表的数据结构来组织元素,因此在某些操作上具有独特的优势和性能特点。以下是关于 std::list 的详细介绍: 双向链表结构: std::list 内部使用双向链表来

    2024年02月13日
    浏览(33)
  • Vue中的Pinia状态管理工具 | 一篇文章教会你全部使用细节

    Pinia(发音为/piːnjʌ/,如英语中的“peenya”)是最接近piña(西班牙语中的菠萝)的词 ; Pinia开始于大概2019年,最初是 作为一个实验为Vue重新设计状态管理 ,让它用起来适合组合式API(Composition API)。 从那时到现在,最初的设计原则依然是相同的,并且目前同时兼容Vue2、

    2024年02月11日
    浏览(27)
  • 数据结构入门(C语言版)一篇文章教会你手撕八大排序

    排序 :所谓排序,就是使一串记录,按照其中的某个或某些的大小,递增或递减的排列起来的操作。 稳定性 :假定在待排序的记录序列中,存在多个具有相同的的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而

    2024年02月01日
    浏览(35)
  • 如何用MetaGPT帮你写一个贪吃蛇的小游戏项目

    MetaGPT是基于大型语言模型(LLMs)的多智能体写作框架,目前在Github开源,其Start数量也是比较高的,是一款非常不错的开源框架。 下面将带你进入MetaGPT的大门,开启MetaGPT的体验之旅。首先是入门教程,如何安装及使用MetaGPT。 一、环境安装 python 环境安装,MetaGPT所需的Python环

    2024年01月19日
    浏览(31)
  • 怎样在一台电脑安装多个版本的JDK并切换使用?一篇文章教会你所有细节

    目录 1. 下载安装JDK版本 2. 配置环境变量 2. 1 配置环境变量的步骤 2.2 需要注意的细节点 2.3 JDK8,11,17版本切换测试 a . JDK8 下载链接: Java Downloads | Oracle https://www.oracle.com/java/technologies/downloads/#java8-windows b.  这里我先插一句,因为我们要安装多个JDK版本,所以我们最好提前创

    2024年04月16日
    浏览(56)
  • (python实现)一篇文章教会你k-means聚类算法(包括最优聚类数目k的确定)

    Kmeans算法中,K值所决定的是在该聚类算法中,所要分配聚类的簇的多少。Kmeans算法对初始值是⽐较敏感的,对于同样的k值,选取的点不同,会影响算法的聚类效果和迭代的次数。本文通过计算原始数据中的:手肘法、轮廓系数、CH值和DB值,四种指标来衡量K-means的最佳聚类数

    2024年02月05日
    浏览(42)
  • RabbitMQ篇——一篇文章带你入门RabbitMQ,了解RabbitMQ的角色分类权限、AMQP协议以及设计第一个RabbitMQ程序!

    RabbitMQ是一个开源的消息代理和消息队列系统,采用AMQP(Advanced Message Queuing Protocol)协议。它被设计用于在分布式系统中进行高效,可靠和可扩展的消息传递。 RabbitMQ基本概念: Producer(生产者):生产者负责发布消息到消息队列中。 Consumer(消费者):消费者从消息队列中

    2024年01月16日
    浏览(38)
  • Go For Web:一篇文章带你用 Go 搭建一个最简单的 Web 服务、了解 Golang 运行 web 的原理

    本文作为解决如何通过 Golang 来编写 Web 应用这个问题的前瞻,对 Golang 中的 Web 基础部分进行一个简单的介绍。目前 Go 拥有成熟的 Http 处理包,所以我们去编写一个做任何事情的动态 Web 程序应该是很轻松的,接下来我们就去学习了解一些关于 Web 的相关基础,了解一些概念,

    2023年04月14日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包