自学内容网 自学内容网

C语言实现贪吃蛇小游戏

贪吃蛇游戏简介

贪吃蛇是一款经典的电子游戏,起源于20世纪70年代的街机游戏。游戏的核心玩法简单而富有趣味性,要求玩家控制一条不断移动的蛇,吃掉屏幕上随机出现的食物,每吃掉一个食物,蛇的身体就会变长一段。游戏的挑战在于蛇不能碰到自己的身体或游戏边界,一旦碰撞,游戏就会结束。

技术要点 

本篇博客实现的C语言小游戏是通过控制台输出的,涉及到的C语言知识点如下:

  1. C语言函数
  2. 枚举
  3. 结构体
  4. 动态内存管理
  5. 预处理指令
  6. 链表
  7. win32API

 具体实现与分析

(完整实现代码请跳转:Snake: C语言实现贪吃蛇小游戏

游戏的初始化

对控制台窗口进行设置

将控制台窗口大小设置为宽为100高为30,调用C语言函数system来执行相关操作

system("mode con cols=100 lines=30");

将 控制台窗口标题设置为“贪吃蛇”

system("title 贪吃蛇");

打印欢迎界面

设置光标位置 

控制台坐标通常指的是在控制台应用程序中,用于定位光标或指定文本输出位置的一组数值。这些坐标通常基于控制台的字符网格,其中每个字符都有一个唯一的坐标。以下是对控制台坐标的详细解释:

  • 原点:控制台坐标系统的原点(0,0)通常位于控制台的左上角。
  • X轴:水平方向,向右为正方向,通常表示字符的列数。
  • Y轴:垂直方向,向下为正方向,通常表示字符的行数。

在C语言中,控制控制台窗口的光标位置通常依赖于特定平台的API。对于Windows操作系统,你可以使用Windows API函数SetConsoleCursorPosition来实现,通过包含<windows.h>头文件来使用Windows API。以下是一个示例代码,展示如何设置控制台光标的位置:

#include <stdio.h>
#include <windows.h>

void setCursorPosition(int x, int y) {
    // 获取标准输出句柄
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);

    // 设置光标位置
    COORD position = {x, y};
    SetConsoleCursorPosition(hConsole, position);
}

int main() {
    // 移动光标到(10, 5)位置
    setCursorPosition(10, 5);
    printf("Hello, World!\n");

    return 0;
}

在这个例子中,SetConsoleCursorPosition函数用于将光标移动到指定的(x, y)坐标。请注意,这里的坐标是基于控制台窗口的缓冲区,而不是屏幕像素。

打印宽字符

在C语言中,宽字符(wide character)通常用于支持国际化(i18n)和本地化(l10n),以处理不同语言的字符集,特别是那些需要超过一个字节来表示的字符(如汉字、日文假名等)。宽字符类型在C标准库中定义为wchar_t

以下是一个简单的例子,展示了如何在C语言中打印宽字符:

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

int main() {
    // 设置程序的locale,以便正确处理宽字符(可选,但推荐)
    setlocale(LC_ALL, "");

    // 宽字符字符串
    wchar_t *wide_string = L"你好,世界!";

    // 打印宽字符字符串
    wprintf(L"%ls\n", wide_string);

    return 0;
}

在这个例子中:

  • setlocale(LC_ALL, ""):这行代码尝试将程序的locale设置为环境变量指定的默认locale。这对于正确处理宽字符和本地化格式是必要的。如果不需要完全本地化,这行代码可以省略,但可能会影响宽字符的正确显示。

  • wchar_t *wide_string = L"你好,世界!";:宽字符字符串字面量使用L前缀来表示。

  • wprintf(L"%ls\n", wide_string);wprintf函数用于打印宽字符字符串。%ls是宽字符字符串的格式说明符。

知道了以上知识,我们就可以在控制台指定位置输出游戏的欢迎界面信息。

//打印菜单
void menu()
{
system("mode con cols=100 lines=30");
system("title 贪吃蛇");

SetPos(32, 13);
printf("*********贪吃蛇**********");
SetPos(32, 14);
printf("**1.进入游戏 0.退出游戏**");
SetPos(32, 15);
printf("*************************");
}
//打印欢迎界面
void WelcomeToGame()
{
SetPos(40, 14);
//输出宽字符前,要在主调函数中使程序本地化,否则无法输出宽字符的中文
wprintf(L"欢迎来到贪吃蛇小游戏\n");//打印宽字符(即两个字节大小的字符)
SetPos(40, 20);
system("pause");//暂停
system("cls");//清屏

SetPos(40, 14);
wprintf(L"用↑↓←→来控制贪吃蛇的移动\n");
SetPos(40, 15);
wprintf(L"F3加速,F4减速\n");
SetPos(40, 16);
wprintf(L"加速能够获得更高的分数\n");
SetPos(40, 20);
system("pause");
system("cls");
}

绘制地图

设置一个大小为,58*23的地图,通过控制台界面创建一个简单的边界框

#define WALL L'□'

void CreateMap()
{
//上
SetPos(20, 2);
for (int i = 0; i < 29; i++)//29个
{
wprintf(L"%lc",WALL);
}

//左
for (int i = 3; i <= 25; i++)//23个
{
SetPos(20, i);
wprintf(L"%lc", WALL);
}

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

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

初始化蛇 

使用单链表创建一条蛇,并且设置游戏的相关状况

#define BODY L'●'

//蛇的运动方向
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};

//游戏的运行状态
enum GAME_STATUS
{
OK,//正常
KILL_BY_WALL,//撞到墙
KILL_BY_SELF,//撞到自己
END_NORMAL//正常结束esc
};

//定义蛇身节点类型,蛇是一个单链表
struct SnakeNode
{
//坐标
int x;
int y;
//蛇身的下一个节点的位置
struct SnakeNode* next;
};
typedef struct SnakeNode SnakeNode;
typedef struct SnakeNode* pSnakeNode;

//贪吃蛇
struct Snake
{
pSnakeNode _pSnake;//指向蛇头的指针
pSnakeNode _pFood;//指向食物的指针,食物类型的节点和蛇身节点的结构相同
enum DIRECTION _dir;//蛇的方向
enum GAME_STATUS _status;//游戏的状态
int _food_weight;//每个食物的积分
int _score;//总分数
int _sleep_time;//运动的快慢,时间越短越快,反之则越慢
};
typedef struct Snake Snake;
typedef struct Snake* pSnake;

使用单链表创建一条蛇

#define POS_X 24
#define POS_Y 5

//初始化蛇

void InitSnake(pSnake ps)
{
//默认给一条长度为5的蛇
pSnakeNode cur = NULL;

for (int i = 0; i < 5; i++)
{
//申请一个节点
pSnakeNode newnode = (pSnakeNode)malloc(sizeof(SnakeNode));

//设置默认的蛇的坐标
newnode->x = POS_X + 2 * i;
newnode->y = POS_Y;
newnode->next = NULL;

if (newnode == NULL)
{
perror("InitSnake()::malloc");
return;
}

//使用头插法
if (ps->_pSnake == NULL)//此时链表为空
{
ps->_pSnake = newnode;
}
else//此时链表非空
{
newnode->next = ps->_pSnake;
ps->_pSnake = newnode;
}
}

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

//设置贪吃蛇的属性
ps->_dir = RIGHT;
ps->_score = 0;
ps->_food_weight = 10;
ps->_sleep_time = 200;//200ms
ps->_status = OK;
}

生成食物

在C语言中,生成随机数通常依赖于标准库中的rand()函数。然而,rand()函数生成的随机数序列在每次程序运行时都是相同的,除非你使用srand()函数来设置随机数生成的种子(seed)。以下是如何在C语言中生成随机数的步骤:

  1. 包含头文件
    你需要包含<stdlib.h>头文件,它声明了rand()srand()函数。

  2. 设置随机数种子
    使用srand()函数来设置随机数生成的种子。通常,种子的值来自于系统时间,这样每次程序运行时都能得到不同的随机数序列。你可以使用<time.h>头文件中的time()函数来获取当前时间(以秒为单位),并将其作为种子。

  3. 生成随机数
    调用rand()函数来生成随机数。rand()函数返回一个在0到RAND_MAX之间的整数,其中RAND_MAX<stdlib.h>中定义的一个常量,表示rand()函数能返回的最大值。

以下是一个简单的例子,展示了如何在C语言中生成随机数:

#define FOOD L'★'

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main() {
    // 设置随机数种子为当前时间
    srand(time(0));

    // 生成并打印5个随机数
    for (int i = 0; i < 5; i++) {
        int random_number = rand(); // 生成随机数
        printf("%d\n", random_number); // 打印随机数
    }

    return 0;
}

如果你需要生成一定范围内的随机数(例如,0到99之间的整数),你可以对rand()函数的返回值进行模运算(取余数):

int random_number = rand() % 100; // 生成0到99之间的随机数

 请注意,由于rand()函数生成的随机数序列是伪随机的(基于算法),所以它们并不完全等同于真正的随机数。然而,对于大多数应用程序来说,rand()函数生成的伪随机数已经足够好了。如果你需要更高质量的随机数(例如,用于加密或模拟),你可能需要使用更复杂的随机数生成算法或库。

知道了以上信息,我们可以将生成的随机坐标赋给食物,然后打印在屏幕上

//创建食物
void CreateFood(pSnake ps)
{
//随机生成坐标
int x;
int y;

again:
do
{
//x = rand() % 53 + 2;//范围是2-54
//y = rand() % 25 + 1;//范围是1-25
int range_x = 74 - 22 + 1;
int range_y = 25 - 3 + 1;
x = rand() % range_x + 22;//范围是22-74
y = rand() % range_y + 3;//高是22,范围是3-25
} while (x % 2 != 0);//x必须是2的倍数

pSnakeNode cur = ps->_pSnake;
while (cur)
{
if (cur->x == x)
{
goto again;
}
cur = cur->next;
}

//创建一个节点
pSnakeNode food = (pSnakeNode)malloc(sizeof(SnakeNode));
food->x = x;
food->y = y;
food->next = NULL;
ps->_pFood = food;

SetPos(x, y);
wprintf(L"%lc", FOOD);
}

游戏运行逻辑的实现

获取按键情况

Windows API中的GetAsyncKeyState函数用于检测特定按键的状态。

GetAsyncKeyState 函数用于确定指定虚拟键的当前状态。

函数原型如下:

SHORT GetAsyncKeyState(
  int vKey
);

返回值是一个SHORT值,其高位(最高位)表示键是否被按下(1表示按下,0表示未按下),如果最低位被置为1,则说明按键被按过,最低位为0则说明没有被按过。

#include <stdio.h>
#include <windows.h>

int main() {
    while (1) {
        // 检查空格键是否被按过
        if (GetAsyncKeyState(VK_SPACE) & 0x1) {
            printf("空格键被按下!\n");
            break; // 或者你可以在这里添加其他逻辑
        }
        // 可以添加Sleep函数来避免循环过于频繁地检查按键状态
        // Sleep(100); // 等待100毫秒
    }
    return 0;
}

知道了上述知识,我们可以设计不同按键被按过后贪吃蛇的不同状态

当贪吃蛇的状态为OK时,循环代码 

//游戏暂停
void Pause()
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}

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

//判断按键
if (KEY_PRESS(VK_UP) && ps->_dir != DOWN)
{
ps->_dir = UP;
}
else if (KEY_PRESS(VK_DOWN) && ps->_dir != UP)
{
ps->_dir = DOWN;
}
else if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT)
{
ps->_dir = LEFT;
}
else if (KEY_PRESS(VK_RIGHT) && ps->_dir != LEFT)
{
ps->_dir = RIGHT;
}
else if (KEY_PRESS(VK_SPACE))
{
Pause();
}
else if (KEY_PRESS(VK_ESCAPE))
{
//正常退出游戏
ps->_status = END_NORMAL;
}
else if (KEY_PRESS(VK_F3))
{
//加速
if (ps->_sleep_time > 80)
{
ps->_sleep_time -= 30;
ps->_food_weight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
//减速
if (ps->_food_weight > 2)
{
ps->_sleep_time += 30;
ps->_food_weight -= 2;
}
}

判断贪吃蛇运动到下一个节点时是否吃到食物

如果下一个节点是食物,则将指向蛇头的指针指向该食物。

//下一个位置是食物,就吃掉食物
void EatFood(pSnakeNode pn, pSnake ps)
{
//头插
ps->_pFood->next = ps->_pSnake;
ps->_pSnake = ps->_pFood;

//释放pn,因为头插用的是pFood,pFood=pn
free(pn);
pn = NULL;

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

ps->_score += ps->_food_weight;

//再生成一个食物
CreateFood(ps);
}

如果下一个节点不是食物,则将指向蛇头的指针指向该节点(即将该节点挂载到蛇身上),再释放蛇的最后一个节点,并将最后一个节点用空格覆盖,以保持原先蛇的长度。

//下一个位置不是食物
void NoFood(pSnakeNode pn, pSnake ps)
{
//头插一个节点
pn->next = ps->_pSnake;
ps->_pSnake = pn;

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

//将尾巴覆盖为两个空格
SetPos(cur->next->x, cur->next->y);
printf("  ");

//释放掉尾巴
free(cur->next);
cur->next = NULL;
}

判断贪吃蛇死亡情况

如果蛇撞到墙体,或者撞到自身,将蛇的status改变即可

//撞到墙
void KillByWall(pSnake ps)
{
//判断蛇头的坐标是否和墙的坐标重合
if (ps->_pSnake->x == 20 || ps->_pSnake->x == 76
|| ps->_pSnake->y == 2 || ps->_pSnake->y == 26)
{
ps->_status = KILL_BY_WALL;
}
}

//撞到自己
void KillBySelf(pSnake ps)
{
//判断蛇身的坐标是否和蛇头的坐标重合
pSnakeNode cur = ps->_pSnake->next;

while (cur)
{
if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
{
ps->_status = KILL_BY_SELF;
break;
}
cur = cur->next;
}
}

释放游戏资源

由于蛇身节点都是通过动态开辟空间得来的,因此,在游戏结束后,要将资源释放还给操作系统。(即单链表的释放)

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

原文地址:https://blog.csdn.net/ASHIDEH/article/details/144374011

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!