从零开始实现贪吃蛇(C语言版)

贪吃蛇

  • 游戏介绍
  • 1.前置知识
    • 1.1 Win32API
    • 1.2 控制台程序
    • 1.3 坐标系统
    • 1.4 GetStdHandle(获取句柄)
    • 1.5 CONSOLE_CURSOR_INFO(控制台光标信息)
    • 1.6 GetConsoleCursorInfo(获取光标信息)
    • 1.7 SetConsoleCursorInfo(设置控制台光标信息)
    • 1.8 SetConsoleCursorPosition(设置光标当前位置)
    • 1.9 GetAsyncKeyState(获取按键情况)
    • 1.10 宽字符
    • 1.11 头文件<local.h>与setlocale函数
  • 2.游戏设计
    • 2.1地图设计
    • 2. 2 蛇和食物
    • 2.3 游戏各属性的维护
  • 3.逻辑实现
    • 3.1 类型声明
    • 3.2 主程序
    • 3.3 地图绘制
    • 3.4 欢迎界面及帮助信息
    • 3.5 初始化蛇
    • 3.6 生成食物
    • 3.7 碰撞检测
    • 3.8 蛇的移动
    • 3.9游戏运行逻辑
    • 3.10 结束工作
  • 4.完整代码

游戏介绍

贪吃蛇游戏是耳熟能详的益智小游戏,今天我们就用C语言来实现它!

1.前置知识

今天我们实现的贪吃蛇游戏是运行在控制台程序中(如图),在实现前需要了解一些Win32API以及控制台相关的知识.
游戏截图:
在这里插入图片描述
在这里插入图片描述

1.1 Win32API

API全称:ApplicationProgrammingInterface,API可以简单理解成函数,Win32API也就是Windows32位平台提供给我们的一系列可以帮助应用程序开启视窗、绘制图形等的接口函数,今天我们只需要了解以下函数即可:

GetStdHandle(获取句柄)
GetConsoleCursorInfo(获取光标信息)
CONSOLE_CURSOR_INFO(控制台光标信息)
SetConsoleCursorInfo(设置控制台光标信息)
SetConsoleCursorPosition(设置光标当前位置)
GetAsyncKeyState(获取按键情况)

1.2 控制台程序

我们写C语言程序运行时的黑框框就是控制台程序,使用Win+r键,输入cmd可以打开Windows的控制台程序
在这里插入图片描述
使用cmd命令可以设置控制台窗口的宽高和控制台名称

mode con cols=80 lines=30 //设置控制台宽高
title 贪吃蛇 //设置控制台名称

在这里插入图片描述
在这里插入图片描述
我们也可以使用C语言的system函数来执行上述操作

int main()
{
//设置控制台宽高
system("mode con cols=80 lines=30");
//设置控制台名称
system("title 贪吃蛇");
system("pause");//让程序暂停
return 0;
}

1.3 坐标系统

控制台坐标中每一个普通字符占一个坐标,输出字符时默认从原点开始输出
在这里插入图片描述
COORD是WindowsAPI中定义的结构体,表示字符在控制台上坐标位置

typedef struct _COORD {
SHORT X;//横坐标
SHORT Y;//纵坐标
} COORD, *PCOORD;

1.4 GetStdHandle(获取句柄)

GetStdHandle函数用于从设备(标准输入/输出设备)中获取句柄,可以通过句柄操作设备,句柄可以类比成游戏手柄,通过手柄可以操作游戏中的人物.
函数原型:

HANDLE GetStdHandle(DWORD nStdHandle);

参数取值:STD_INPUT_HANDLE(标准输入句柄),STD_OUTPUT_HANDLE(标准输出句柄)
返回值:HANDLE类型
运用实例

HANDLE handle = GetStdHandle(STD_INPUT_HANDLE);
//STD_INPUT_HANDLE其实就是我们运行后的控制台窗口

1.5 CONSOLE_CURSOR_INFO(控制台光标信息)

CONSOLE_CURSOR_INFO是Win32中定义的一个结构体,结构体中包含了控制台光标的信息

typedef struct _CONSOLE_CURSOR_INFO
{DWORD dwSize;BOOL bVisible;
}CONSOLE_CURSOR_INFO,*PCONSOLE_CURSOR_INFO;

dwSize表示光标占字符单元格的百分比,默认为25(百分之25),如图左边部分是光标的大小,右边部分是一个字符单元格
在这里插入图片描述
bVisible表示光标的可见性,默认值为true,表示光标可见,如果想隐藏光标,可以将bVisible的值置为false

1.6 GetConsoleCursorInfo(获取光标信息)

获取指定控制台的光标大小和可见性信息,函数原型:

BOOL GetConsoleCursorInfo(HANDLE hConsoleOutput,PCONSOLE_CURSOR_INFO lpConsoleCursorInfo);

第一个参数是设备的句柄,第二个参数是CONSOLE_CURSOR_INFO结构体的指针

运用实例:

	//获取设备句柄HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//定义结构体CONSOLE_CURSOR_INFO CursorInfo = { 0 };//获取控制台光标信息GetConsoleCursorInfo(handle, &CursorInfo);

1.7 SetConsoleCursorInfo(设置控制台光标信息)

获取到光标信息后,如果想修改光标的信息,需要先修改再设置,使用SetConsoleCursorInfo函数可以设置光标信息.
函数原型:

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

参数类型与GetConsoleCursorInfo函数一致,返回值为布尔类型
运用实例:

	//获取设备句柄HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//定义结构体CONSOLE_CURSOR_INFO CursorInfo = { 0 };//获取控制台光标信息GetConsoleCursorInfo(handle, &CursorInfo);//修改光标信息CursorInfo.bVisible=false;//隐藏光标//设置光标信息SetConsoleCursorInfo(handle,&CursorInfo);

1.8 SetConsoleCursorPosition(设置光标当前位置)

通过SetConsoleCursorPosition函数可以设置光标当前的坐标位置,有了这个函数,我们就可以在任意坐标打印内容,而不是从原点开始一直打印.
函数原型:

BOOL SetConsoleCursorPosition(HANDLE hConsoleOutput,COORD  dwCursorPosition);

第一个参数是句柄,第二个参数是COORD结构体
运用实例:

//封装成设置光标当前位置的函数,x,y表示坐标
void SetPos(short x, short y)
{COORD pos = { x, y };//获取句柄HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//设置标准输出上光标的位置为posSetConsoleCursorPosition(handle, pos);
}

1.9 GetAsyncKeyState(获取按键情况)

GetAsyncKeyState函数可以获取按键情况,它可以检测程序运行时某个按键是否被按过,某个按键是否正在被按下或者抬起.
函数原型:

SHORT GetAsyncKeyState(int vKey);

参数vKey指的是键盘上按键的虚拟键值,返回值是一个16位的short类型的数.如果这个数最高位是1,表示该按键当前状态是按下,如果是0表示当前状态是抬起;如果这个数最低位是1,表示该按键被按过,如果是0表示未按过.
运用实例:

//定义一个宏判断某个键是否被按过
#define CHECK_KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1)?1:0)

1.10 宽字符

游戏的墙体■,蛇身●,食物★等字符,都是宽字符.宽字符占2个字节,普通字符占一个字节.

1.11 头文件<local.h>与setlocale函数

在不同国家和地区,对时间的表示方式可能不同,对金钱的表示也有不同($和¥).有时我们想打印¥符号或者其他中文字符和宽字符时,显示的却是问号或者乱码.为了使程序能够在不同地区适应,因此有了setlocale函数.它可以改变程序运行时的地区
函数原型:

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

切换到本地环境的方法:

setlocale(LC_ALL, "");

使用setlocale函数需要包含头文件local.h
在屏幕上打印宽字符的方法:

#include<stdio.h>
#include<locale.h>
int main()
{setlocale(LC_ALL, "");wprintf(L"%lc", L'■');return 0;
}

2.游戏设计

2.1地图设计

由于墙体和蛇,食物都是宽字符,一个宽字符横坐标占2个,纵坐标占一个.所以我们可以设计一个58(列)x27(行)的地图,在地图最边缘四周打印字符■表示墙.

2. 2 蛇和食物

蛇头我们用◆表示,身体我们用●表示,食物用★表示.使用单链表的数据结构将每个结点链接起来,每个结点保存了该结点的横纵坐标和下一个结点的地址.游戏开始时蛇的长度是5,食物在地图内随机生成,每个食物的类型和蛇身结点的类型一致,方便蛇吃食物后的连接.

2.3 游戏各属性的维护

属性包括蛇移动方向,蛇的移动速度,单个食物的分数,累计分数,游戏状态(正常运行,正常退出,撞墙,撞到自己),还有蛇头结点,食物.这些信息我们用一个结构体保存

3.逻辑实现

3.1 类型声明

蛇身结点

//一个蛇身的结点
typedef struct SnakeNode
{int x;//结点横坐标int y;//结点纵坐标struct SnakeNode* next;//指向下一个结点的指针
}SnakeNode;
typedef struct SnakeNode* pSnakeNode;//指向SnakeNode的指针类型

游戏属性的维护

typedef struct Snake
{pSnakeNode SnakeHead;//蛇头结点pSnakeNode Food;//指向食物的指针int Score;//游戏总分int FoodScore;//一个食物的分数int Speed;//蛇的速度(休眠时间)enum Status status;//游戏当前状态enum Direction dir;//蛇的移动方向
}Snake;//指向snake类型的指针
typedef struct Snake* pSnake;

蛇的移动方向,使用枚举类型

//蛇当前移动方向
enum Direction
{//上下左右UP = 1,DOWN,LEFT,RIGHT
};

游戏状态,使用枚举类型

//游戏状态
enum Status
{OK = 1,//正常运行EXIT,//退出KillByWall,//撞墙KillBySelf//撞到自己
};

蛇结点,食物,墙体

#define FOOD L'★'
#define BODY L'●'
#define WALL L'■'

3.2 主程序

主程序分为3个模块:1.GameStart游戏初始化2.GameRun游戏运行时的逻辑 3.GameEnd游戏结束的工作
GameStart函数的功能:进行游戏的初始化工作,包括:设置控制台的大小,设置控制台名称,打印欢迎界面,打印地图,初始化蛇的移动方向/速度,总分数,单个食物分数,游戏运行的状态等
GameRun函数的功能:实现蛇移动,改变移动方向,按键检测等功能
GameEnd的功能:游戏结束后打印提示信息,释放动态开辟的内存空间
在程序开始前,还需要适配本地环境,防止打印时出现乱码,为了使游戏结束后还能继续选择开始游戏,我们使用do while循环来实现

int main()
{//适配本地环境setlocale(LC_ALL, "");int ch = 0;do{Snake snake = { 0 };//创建贪吃蛇GameStart(&snake);//游戏初始化GameRun(&snake);//游戏运行时的逻辑GameEnd(&snake);//游戏结束后的工作SetPos(20, 15);printf("输入Y.重新开始  N.退出");ch = getchar();getchar();} while (ch=='Y'||ch=='y');SetPos(0,27);return 0;
}

3.3 地图绘制

在这里插入图片描述
绘制逻辑很简单,最上面一行从(0,0)坐标开始打印■字符.最下面一行从(0,25)坐标开始打印.最左边,横坐标都是0,纵坐标从0开始,每打印一次纵坐标加1.最右边同理.
每次打印前都需要定位一次坐标,为了避免代码重复,坐标定位可以封装成函数:

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

绘制地图函数:

//绘制地图
void DrawMap()
{//上SetPos(0, 0);for (int i = 0; i <= 56; i+=2){wprintf(L"%lc", WALL);}//下SetPos(0, 25);for (int i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//左for (int i = 1; i <= 25; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//右for (int i = 1; i <= 25; i++){SetPos(56, i);wprintf(L"%lc", WALL);}}

3.4 欢迎界面及帮助信息

直接定位坐标,然后打印内容

void Welcome()
{//欢迎信息SetPos(40, 15);printf("欢迎来到贪吃蛇小游戏\n");SetPos(40, 20);system("pause");system("cls");SetPos(35, 10);printf("用↑↓←→控制移动方向,F3加速,F4减速");SetPos(35, 11);printf("加速得到更多分");SetPos(40, 20);system("pause");system("cls");
}
//打印帮助信息
void PrintHelpInfo()
{SetPos(65, 13);printf("↑↓←→控制方向,空格暂停,ESC退出,F3加速,F4减速");SetPos(65, 17);printf("版权@vampire-wpre");
}

3.5 初始化蛇

首先创建5个蛇身结点,设置游戏开始时蛇的位置,设置游戏初始属性

void InitSnake(pSnake ps)
{//创建5个初始蛇身结点pSnakeNode cur = NULL;for (int i = 0; i < 5; i ++ ){cur = (pSnakeNode)malloc(sizeof(SnakeNode));assert(cur);cur->x = POS_X + 2 * i;cur->y = POS_Y;cur->next = NULL;if (ps->SnakeHead == NULL){ps->SnakeHead = cur;}else{//头插法cur->next = ps->SnakeHead;ps->SnakeHead = cur;}}//打印初始蛇cur = ps->SnakeHead;SetPos(cur->x, cur->y);wprintf(L"%lc", L'◆');cur = cur->next;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//其他属性ps->dir = RIGHT;ps->Food = NULL;ps->FoodScore = 10;ps->Score = 0;ps->Speed = 200;ps->status = OK;
}

3.6 生成食物

使用rand函数随机生成食物坐标,食物的坐标必须在地图范围内,并且不能和蛇的位置重合,生成好食物后在对应位置打印食物.因为食物也是宽字符,所以食物的横坐标值必须是2的倍数

//创建食物
void CreatFood(pSnake ps)
{//坐标是随机出现的,坐标不能在蛇身体和墙上int x = 0;int y = 0;	srand((unsigned int)time(NULL));//设置随机数种子
reset:do{x = rand() % 53 + 2;y = rand() % 24 + 1;} while (x % 2 != 0);//如果横坐标不是2倍数则重新生成坐标pSnakeNode cur = ps->SnakeHead;while (cur)//循环遍历蛇的每一个结点{//如果坐标与蛇重合,使用goto语句重新生成坐标if (x == cur->x && y == cur->y){goto reset;}cur = cur->next;}pSnakeNode pfood = (SnakeNode*)malloc(sizeof(SnakeNode));if (pfood == NULL){return;}//设置坐标pfood->x = x;pfood->y = y;ps->Food = pfood;SetPos(x, y);wprintf(L"%lc", FOOD);
}

3.7 碰撞检测

逻辑很简单,撞墙只需要判断蛇头坐标是否和墙的坐标重合;撞自己则判断蛇头坐标和蛇身每个结点的坐标是否重合,此时需要循环遍历蛇身.

//检测是否撞墙
void CheckKillByWall(pSnake ps)
{if ((ps->SnakeHead->x == 0 || ps->SnakeHead->x == 56) || (ps->SnakeHead->y == 0 || ps->SnakeHead->y == 25)){ps->status = KillByWall;}
}//检测是否撞自己
void CheckKillBySelf(pSnake ps)
{pSnakeNode cur = ps->SnakeHead->next;while (cur){if (cur->x == ps->SnakeHead->x && cur->y == ps->SnakeHead->y){ps->status = KillBySelf;return;}cur = cur->next;}
}

3.8 蛇的移动

蛇移动分为以下几种情况:
1.蛇直走:
此时,直接改变蛇头的坐标即可.根据蛇的属性(status)判断蛇的方向,如果是向上,蛇头的横坐标不变,向上走一步纵坐标的值就-1;如果是向下,蛇头横坐标不变,向上走一步纵坐标的值+1;如果是向左,蛇头纵坐标不变,向左走一步横坐标-2(宽字符占2个位置);如果是向右,纵坐标不变,向右走一步横坐标+2.
2.蛇转弯:
先定义一个临时结点,该结点的位置在蛇头下一步的位置.
判断蛇头在下一步的位置有没有食物,如果有就吃食物,吃完食物释放掉旧的食物并且重新生成新的食物对应总分也增加,头插法将这个食物结点插入蛇身,然后改变蛇头结点(SnakeHead)的指向;如果不是食物,也将该结点头插,改变蛇头结点(SnakeHead)的指向,接着将最后一个结点删除,并且在最后一个结点的位置打印空格.
我们定义IsFood函数判断下一个位置有没有食物,Eat函数实现吃食物的逻辑,Normal函数实现下一个位置不是食物进行的操作,与此同时还要检测一下有没有撞墙和撞自己.

//吃食物
void Eat(pSnake ps, pSnakeNode next)
{next->next = ps->SnakeHead;ps->SnakeHead = next;//打印蛇pSnakeNode cur = ps->SnakeHead;SetPos(cur->x, cur->y);wprintf(L"%lc", L'◆');cur = cur->next;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//增加总分ps->Score += ps->FoodScore;//free旧的食物free(ps->Food);//新建食物CreatFood(ps);
}
//下一个位置不是食物,正常移动
void Normal(pSnake ps, pSnakeNode next)
{//头插法插入nextnext->next = ps->SnakeHead;ps->SnakeHead = next;//释放尾结点pSnakeNode cur = ps->SnakeHead;//循环找尾结点的前一个结点while (cur->next->next != NULL){cur = cur->next;}//尾结点位置打印成空格SetPos(cur->next->x, cur->next->y);printf("  ");//释放尾结点free(cur->next);cur->next = NULL;//打印蛇身cur = ps->SnakeHead;SetPos(cur->x, cur->y);wprintf(L"%lc", L'◆');cur = cur->next;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}
}
//蛇移动
void Move(pSnake ps)
{pSnakeNode next = (pSnakeNode)malloc(sizeof(SnakeNode));//方向改变时,蛇头在下一个坐标的位置,用next结点保存if (next == NULL){return;}next->next = NULL;switch (ps->dir){case UP:next->x = ps->SnakeHead->x;next->y = ps->SnakeHead->y - 1;break;case DOWN:next->x = ps->SnakeHead->x;next->y = ps->SnakeHead->y + 1;break;case LEFT:next->x = ps->SnakeHead->x - 2;next->y = ps->SnakeHead->y;break;case RIGHT:next->x = ps->SnakeHead->x + 2;next->y = ps->SnakeHead->y;break;}//下一个坐标处是否为食物:if (IsFood(ps, next)){//是食物就吃掉Eat(ps,next);}else{//不是食物就正常移动Normal(ps, next);}//检测是否撞墙CheckKillByWall(ps);//检测是否撞自己CheckKillBySelf(ps);
}

3.9游戏运行逻辑

使用do while循环,循环条件是游戏状态,如果游戏状态不是OK则游戏结束.游戏运行时,打印对应的帮助信息,同时进行按键检测,如果某个按键被按了,就修改对应的属性.蛇的移动本质上是:蛇移动一步,系统休眠一段时间,休眠时间越短,蛇移动的越快,休眠时间越长,蛇移动的越慢.

//游戏运行逻辑
void GameRun(pSnake ps)
{//打印帮助信息PrintHelpInfo();do{//当前分数情况:SetPos(65, 10);printf("总分:%02d\n", ps->Score);SetPos(65, 11);printf("每个食物分数:%5d\n", ps->FoodScore);//按键监测↑ ↓ ← → ESC 空格 F3加速 F4减速if (CHECK_KEY_PRESS(VK_UP) && ps->dir != DOWN)//按上键并且当前方向不是向下{ps->dir = UP;}else if (CHECK_KEY_PRESS(VK_DOWN) && ps->dir != UP)//按下键并且当前方向不是向上{ps->dir = DOWN;}else if (CHECK_KEY_PRESS(VK_LEFT) && ps->dir != RIGHT)//按左键并且当前方向不是向右{ps->dir = LEFT;}else if (CHECK_KEY_PRESS(VK_RIGHT) && ps->dir != LEFT)//按右并且当前方向不是向左{ps->dir = RIGHT;}else if (CHECK_KEY_PRESS(VK_ESCAPE))//按ESC退出{ps->status = EXIT;break;}else if (CHECK_KEY_PRESS(VK_SPACE))//按空格暂停,再按一次继续游戏{while (1){Sleep(100);if (CHECK_KEY_PRESS(VK_SPACE)){break;}}}else if (CHECK_KEY_PRESS(VK_F3))//按F3加速,休眠时间变短,食物分数变大{if (ps->Speed >= 40)//最小速度为40{ps->Speed -= 20;ps->FoodScore += 2;}}else if (CHECK_KEY_PRESS(VK_F4))//按F4减速,休眠时间变长,食物分数变小{if (ps->FoodScore > 4){ps->Speed += 20;ps->FoodScore -= 2;}}//休眠一下Sleep(ps->Speed);//蛇移动的逻辑Move(ps);} while (ps->status==OK);
}

3.10 结束工作

游戏结束后,打印结束的原因(撞墙/撞自己/主动退出),同时释放malloc开辟的空间(蛇的每个结点/食物)

void GameEnd(pSnake ps)
{switch (ps->status){case EXIT:SetPos(24, 12);printf("主动退出");break;case KillByWall:SetPos(24, 12);printf("撞墙了!");break;case KillBySelf:SetPos(24, 12);printf("撞到自己了!");break;}//释放malloc开辟的空间pSnakeNode cur = ps->SnakeHead;pSnakeNode del = NULL;while (cur){del = cur;cur = cur->next;free(del);}free(ps->Food);ps->Food = NULL;ps = NULL;
}

4.完整代码

Game.h文件:

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
#include<locale.h>
#include<stdbool.h>
#include<assert.h>
#include<time.h>#define FOOD L'★' //食物
#define BODY L'●' //身体
#define WALL L'■' //墙//按键监测
#define CHECK_KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1)?1:0)//游戏开始时,蛇起始位置坐标
#define POS_X 24
#define POS_Y 5//一个蛇身的结点
typedef struct SnakeNode
{int x;//结点横坐标int y;//结点纵坐标struct SnakeNode* next;//指向下一个结点的指针
}SnakeNode;
typedef struct SnakeNode* pSnakeNode;//指向SnakeNode的指针类型//游戏状态
enum Status
{OK = 1,//正常运行EXIT,//退出KillByWall,//撞墙KillBySelf//撞到自己
};//蛇当前移动方向
enum Direction
{//上下左右UP = 1,DOWN,LEFT,RIGHT
};//贪吃蛇(整个游戏的维护)
typedef struct Snake
{pSnakeNode SnakeHead;//蛇头结点pSnakeNode Food;//指向食物的指针int Score;//游戏总分int FoodScore;//一个食物的分数int Speed;//蛇的速度(休眠时间)enum Status status;//游戏当前状态enum Direction dir;//蛇的移动方向
}Snake;//指向snake类型的指针
typedef struct Snake* pSnake;//相关函数
void GameStart(pSnake ps);//游戏初始化void Welcome();//打印欢迎界面void DrawMap();//绘制地图void InitSnake(pSnake ps);//初始化蛇void CreatFood(pSnake ps);//创建食物void GameRun(pSnake ps);//游戏运行逻辑void PrintHelpInfo();//打印帮助信息void Move(pSnake ps);//蛇移动int IsFood(pSnake ps, pSnakeNode next);//判断下一步位置处是否为食物void Eat(pSnake ps, pSnakeNode next);//吃食物void Normal(pSnake ps, pSnakeNode next);//正常移动void CheckKillByWall(pSnake ps);//检测是否撞墙void CheckKillBySelf(pSnake ps);//检测是否撞自己void GameEnd(pSnake ps);//游戏结束后的善后工作void SetPos(int x, int y);//控制台光标定位

Game.c文件:

#include"Game.h"//控制台光标定位
void SetPos(int x, int y)
{HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//获取句柄//根据句柄设置光标坐标COORD pos = { x,y };SetConsoleCursorPosition(handle, pos);
}//绘制地图
void DrawMap()
{//上SetPos(0, 0);for (int i = 0; i <= 56; i+=2){wprintf(L"%lc", WALL);}//下SetPos(0, 25);for (int i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//左for (int i = 1; i <= 25; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//右for (int i = 1; i <= 25; i++){SetPos(56, i);wprintf(L"%lc", WALL);}}//打印欢迎界面
void Welcome()
{//欢迎信息SetPos(40, 15);printf("欢迎来到贪吃蛇小游戏\n");SetPos(40, 20);system("pause");system("cls");SetPos(35, 10);printf("用↑↓←→控制移动方向,F3加速,F4减速");SetPos(35, 11);printf("加速得到更多分");SetPos(40, 20);system("pause");system("cls");
}//打印版权信息
void PrintHelpInfo()
{SetPos(65, 16);printf("版权@vampire-wpre");}//创建食物
void CreatFood(pSnake ps)
{//坐标是随机出现的,坐标不能在蛇身体和墙上int x = 0;int y = 0;	srand((unsigned int)time(NULL));//设置随机数种子
reset:do{x = rand() % 53 + 2;y = rand() % 24 + 1;} while (x % 2 != 0);pSnakeNode cur = ps->SnakeHead;while (cur){if (x == cur->x && y == cur->y){goto reset;}cur = cur->next;}pSnakeNode pfood = (SnakeNode*)malloc(sizeof(SnakeNode));if (pfood == NULL){return;}//设置坐标pfood->x = x;pfood->y = y;ps->Food = pfood;SetPos(x, y);wprintf(L"%lc", FOOD);
}//判断下一个位置是否为食物
int IsFood(pSnake ps, pSnakeNode next)
{if (ps->Food->x == next->x && ps->Food->y == next->y){return 1;}else{return 0;}
}//检测是否撞墙
void CheckKillByWall(pSnake ps)
{if ((ps->SnakeHead->x == 0 || ps->SnakeHead->x == 56) || (ps->SnakeHead->y == 0 || ps->SnakeHead->y == 25)){ps->status = KillByWall;}
}//检测是否撞自己
void CheckKillBySelf(pSnake ps)
{pSnakeNode cur = ps->SnakeHead->next;while (cur){if (cur->x == ps->SnakeHead->x && cur->y == ps->SnakeHead->y){ps->status = KillBySelf;return;}cur = cur->next;}
}//初始化蛇
void InitSnake(pSnake ps)
{//创建5个初始蛇身结点pSnakeNode cur = NULL;for (int i = 0; i < 5; i ++ ){cur = (pSnakeNode)malloc(sizeof(SnakeNode));assert(cur);cur->x = POS_X + 2 * i;cur->y = POS_Y;cur->next = NULL;if (ps->SnakeHead == NULL){ps->SnakeHead = cur;}else{//头插cur->next = ps->SnakeHead;ps->SnakeHead = cur;}}//打印初始蛇cur = ps->SnakeHead;SetPos(cur->x, cur->y);wprintf(L"%lc", L'◆');cur = cur->next;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//其他属性ps->dir = RIGHT;ps->Food = NULL;ps->FoodScore = 10;ps->Score = 0;ps->Speed = 200;ps->status = OK;
}//吃食物
void Eat(pSnake ps, pSnakeNode next)
{next->next = ps->SnakeHead;ps->SnakeHead = next;//打印蛇pSnakeNode cur = ps->SnakeHead;SetPos(cur->x, cur->y);wprintf(L"%lc", L'◆');cur = cur->next;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}ps->Score += ps->FoodScore;//free旧的食物free(ps->Food);//新建食物CreatFood(ps);
}//下一个位置不是食物,正常移动
void Normal(pSnake ps, pSnakeNode next)
{//头插法插入nextnext->next = ps->SnakeHead;ps->SnakeHead = next;//释放尾结点pSnakeNode cur = ps->SnakeHead;while (cur->next->next != NULL){cur = cur->next;}//尾结点位置打印成空格SetPos(cur->next->x, cur->next->y);printf("  ");//释放尾结点free(cur->next);cur->next = NULL;//打印蛇身cur = ps->SnakeHead;SetPos(cur->x, cur->y);wprintf(L"%lc", L'◆');cur = cur->next;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}
}//蛇移动
void Move(pSnake ps)
{pSnakeNode next = (pSnakeNode)malloc(sizeof(SnakeNode));//方向改变时,蛇头在下一个坐标的位置,用next结点保存if (next == NULL){return;}next->next = NULL;switch (ps->dir){case UP:next->x = ps->SnakeHead->x;next->y = ps->SnakeHead->y - 1;break;case DOWN:next->x = ps->SnakeHead->x;next->y = ps->SnakeHead->y + 1;break;case LEFT:next->x = ps->SnakeHead->x - 2;next->y = ps->SnakeHead->y;break;case RIGHT:next->x = ps->SnakeHead->x + 2;next->y = ps->SnakeHead->y;break;}//下一个坐标处是否为食物:if (IsFood(ps, next)){//是食物就吃掉Eat(ps,next);}else{//不是食物就正常Normal(ps, next);}//检测是否撞墙CheckKillByWall(ps);//检测是否撞自己CheckKillBySelf(ps);}//游戏初始化
void GameStart(pSnake ps)
{//设置控制台属性system("mode con cols=100 lines=30");system("title 贪吃蛇");//隐藏光标HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//获取控制台句柄CONSOLE_CURSOR_INFO cur_info;GetConsoleCursorInfo(handle, &cur_info);//获得控制台光标的信息cur_info.bVisible = false;SetConsoleCursorInfo(handle, &cur_info);//设置//打印欢迎信息Welcome();//地图绘制DrawMap();//初始化蛇InitSnake(ps);//创建食物CreatFood(ps);
}//游戏运行逻辑
void GameRun(pSnake ps)
{//打印帮助信息PrintHelpInfo();do{//当前分数情况:SetPos(65, 10);printf("总分:%02d\n", ps->Score);SetPos(65, 11);printf("每个食物分数:%5d\n", ps->FoodScore);//按键监测↑ ↓ ← → ESC 空格 F3加速 F4减速if (CHECK_KEY_PRESS(VK_UP) && ps->dir != DOWN)//按上键并且当前方向不是向下{ps->dir = UP;}else if (CHECK_KEY_PRESS(VK_DOWN) && ps->dir != UP)//按下键并且当前方向不是向上{ps->dir = DOWN;}else if (CHECK_KEY_PRESS(VK_LEFT) && ps->dir != RIGHT)//按左键并且当前方向不是向右{ps->dir = LEFT;}else if (CHECK_KEY_PRESS(VK_RIGHT) && ps->dir != LEFT)//按右并且当前方向不是向左{ps->dir = RIGHT;}else if (CHECK_KEY_PRESS(VK_ESCAPE))//按ESC退出{ps->status = EXIT;break;}else if (CHECK_KEY_PRESS(VK_SPACE))//按空格暂停,再按一次继续游戏{while (1){Sleep(100);if (CHECK_KEY_PRESS(VK_SPACE)){break;}}}else if (CHECK_KEY_PRESS(VK_F3))//按F3加速,休眠时间变短,食物分数变大{if (ps->Speed >= 40)//最小速度为40{ps->Speed -= 20;ps->FoodScore += 2;}}else if (CHECK_KEY_PRESS(VK_F4))//按F4减速,休眠时间变长,食物分数变小{if (ps->FoodScore > 4){ps->Speed += 20;ps->FoodScore -= 2;}}//休眠一下Sleep(ps->Speed);//走一步Move(ps);//蛇移动的逻辑} while (ps->status==OK);}//游戏善后工作
void GameEnd(pSnake ps)
{switch (ps->status){case EXIT:SetPos(24, 12);printf("主动退出");break;case KillByWall:SetPos(24, 12);printf("撞墙了!");break;case KillBySelf:SetPos(24, 12);printf("撞到自己了!");break;}//释放malloc开辟的空间pSnakeNode cur = ps->SnakeHead;pSnakeNode del = NULL;while (cur){del = cur;cur = cur->next;free(del);}free(ps->Food);ps->Food = NULL;ps = NULL;
}

test.c文件:

#include"Game.h"void test()
{int ch = 0;do{Snake snake = { 0 };//创建贪吃蛇GameStart(&snake);//游戏初始化GameRun(&snake);//游戏运行时的逻辑GameEnd(&snake);//游戏结束后的善后工作SetPos(20, 15);printf("输入Y.重新开始  N.退出");ch = getchar();getchar();} while (ch=='Y'||ch=='y');}int main()
{//适配本地环境setlocale(LC_ALL, "");test();SetPos(0,27);return 0;
}

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

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

相关文章

国外知名的农业机器人公司

从高科技温室到云播种&#xff0c;农业机器人如何帮助农民填补劳动力短缺以及超市货架的短缺。 概要 “高科技农业”并不矛盾。当代农业经营更像是硅谷&#xff0c;而不是美国哥特式&#xff0c;拥有控制灌溉的应用程序、驾驶拖拉机的 GPS 系统和监控牲畜的带有 RFID 芯片的耳…

Flutter 和 Android原生(Activity、Fragment)相互跳转、传参

前言 本文主要讲解 Flutter 和 Android原生之间&#xff0c;页面相互跳转、传参&#xff0c; 但其中用到了两端相互通信的知识&#xff0c;非常建议先看完这篇 讲解通信的文章&#xff1a; Flutter 与 Android原生 相互通信&#xff1a;BasicMessageChannel、MethodChannel、…

dnslog在sql盲注

首先必须保证sql是在windows下 因为需要使用到UNC路径 保证mysql中的secure_file_priv为空 secure_file_priv为null&#xff0c;load_file则不能加载文件。 secure_file_priv为路径&#xff0c;可以读取路径中的文件&#xff1b; secure_file_priv为空&#xff0c;可以读取磁盘…

千兆电口模块和万兆电口模块:网络速度的演变

随着信息技术的迅猛发展&#xff0c;网络通信技术也在不断进步。在过去的几十年中&#xff0c;以太网的速度发生了巨大的变化&#xff0c;从最初的百兆以太网&#xff0c;到如今的千兆以太网和万兆以太网甚至40G、100G以太网满足了大数据、云计算、人工智能等新兴应用的需求。在…

【蓝桥杯日记】复盘篇三——循环结构

前言 本篇内容是对循环结构进行复盘的&#xff0c;循环可谓是在基础阶段特别重要的东西&#xff0c;是三大结构&#xff08;顺序结构、选择结构、循环结构&#xff09;中最重要的结构之一。 目录 &#x1f351;1.找最小值 分析&#xff1a; 知识点&#xff1a; 代码如下 &…

数据库空间爆了怎么处理

作者&#xff1a; 马文斌 时间&#xff1a; 2024-1-29 标签&#xff1a; mysql 磁盘空间 爆满 binlog 背景 近期数据库空间一直告警&#xff0c;平时这套数据库集群是不会有磁盘空间告警的&#xff0c;难道是最近业务量猛增了吗&#xff1f;咱们来瞧瞧到底怎么回事&…

启动盘重装ubuntu22系统

win+R msinfo32查看 插入制作好的u盘电脑开机 进入BIOS界面的方法有多种,以下是一些常见的方法: 进入BIOS界面的最常见按键有: Del键:大多数台式机通过在启动时按下Del键来进入BIOS。Esc键:在AMI BIOS和某些品牌电脑中,进入BIOS系统需要按“Esc”键,一般在开机画面…

草图导入3d之后渲染模型发光怎么回事?---模大狮模型网

在草图大师中&#xff0c;当导入3D模型之后发现模型发光通常是由于模型的材质属性或灯光设置所导致的。以下是一些可能的原因和解决方法&#xff1a; 材质属性设置&#xff1a;某些3D模型文件可能包含了发光材质属性&#xff0c;导致模型在草图大师中显示为发光状态。您可以尝试…

高性能跨平台网络通信框架 HP-Socket v6.0.1

项目主页 : http://www.oschina.net/p/hp-socket开发文档 : https://www.docin.com/p-4592706661.html下载地址 : https://github.com/ldcsaa/HP-SocketQQ Group: 44636872, 663903943 v6.0.1 更新 一、主要更新 优化Linux通信组件多路复用处理架构&#xff0c;避免“惊群”问…

AI赋能编程 | 自动化工具助力高效办公

前言 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家&#xff1a;https://www.captainbed.cn/z ChatGPT体验地址 文章目录 前言泡泡AI工具卡片思维导图Markdown编辑器 其他工具文件免费处理工具结语 合集…

【大厂AI课学习笔记】1.3 人工智能产业发展(3)

1.3.1 供给侧 技术层面&#xff1a;从实验室走向大规模的商用。 数据层面&#xff1a;数据正式成为重要的生产要素。 市场&#xff1a;供需互促的正向市场环境建立。 资本&#xff1a;走出炒作泡沫&#xff0c;聚焦价值领域。 平台&#xff1a;大厂普遍开放生态。 MORE&am…

基于二值化图像转GCode的螺旋扫描实现

基于二值化图像转GCode的螺旋扫描实现 什么是双向扫描螺旋扫描代码示例 基于二值化图像转GCode的螺旋扫描实现 什么是螺旋扫描 螺旋扫描&#xff08;Spiral Scanning&#xff09;是激光雕刻中一种特殊的扫描方式&#xff0c;其特点是激光头按照螺旋形状逐渐向外移动&#xf…

【web | CTF】攻防世界 easyupload

天命&#xff1a;好像也不太easy 目录 步骤一&#xff1a;准备文件 步骤二&#xff1a;上传文件 本条题目有好几个防御点&#xff1a; 后缀名防御&#xff1a;只能上传图片格式内容防御&#xff1a;内容不能有php图片头防御&#xff1a;检测文件的头部信息&#xff0c;是否是…

zookeeper(2) 服务器动态上下线监听和分布式锁案例

案例一&#xff1a;服务器动态上下线监听 某分布式系统中&#xff0c;主节点可以有多台&#xff0c;可以动态上下线&#xff0c;任意一台客户端都能实时感知 到主节点服务器的上下线。 1.服务端代码 package com.atguigu.case1;import org.apache.zookeeper.*;import java.io…

微信小程序在线客服源码系统:聊天记录云端实时保存 带完整的搭建教程

微信小程序已经成为企业开展线上业务的重要渠道。然而&#xff0c;在小程序中提供客户服务需要解决聊天记录的存储与查询问题。传统的客服系统往往只能在本地保存聊天记录&#xff0c;一旦客户端出现问题&#xff0c;聊天记录可能会丢失&#xff0c;给企业带来巨大的损失。为了…

双屏联动系统在展厅设计中的互动类型与效果

随着各项多媒体技术的快速发展&#xff0c;让展厅中的各类展项得到技术升级&#xff0c;其中作为电子设备中最基础的显示技术&#xff0c;不仅优化了内容的展示质量&#xff0c;还实现了更具互动性的创新技术&#xff0c;如双屏联动系统就是当前展厅设计中最常见的技术类型之一…

【C++】类和对象(1)

上节我们学习了C入门的一些语法知识&#xff0c;这篇博客来学习类和this指针。 目录 面向过程和面向对象的初步认识 类的引入 类的定义 类的访问限定符及封装 访问限定符 封装 类的作用域 类的实例化 类对象大小 this指针 this指针特性 面向过程和面向对象的初步认识…

UG949 适用于 FPGA 和 SoC 的UltraFast 设计方法指南

使用RTL创建设计 定义RTL设计层级 模块边界输出进行寄存 即寄存器输出&#xff0c;打一拍 IP的使用 AMBA AXI

AI 原生时代的云计算

本文整理自2023年 12 月 20 日举办的「2023 百度云智大会智算大会」主论坛&#xff0c;百度副总裁谢广军的主题演讲《AI 原生时代的云计算》。 &#xff08;视频回放链接&#xff1a;https://cloud.baidu.com/summit/aicomputing_2023/index.html&#xff09; 大模型的到来&…

Leetcode—807. 保持城市天际线【中等】

2024每日刷题&#xff08;一零四&#xff09; Leetcode—807. 保持城市天际线 实现代码 class Solution { public:int maxIncreaseKeepingSkyline(vector<vector<int>>& grid) {int n grid.size();vector<int> row(n);vector<int> col(n);int a…