从零开始写贪吃蛇

1.游戏运行前的准备

        1.1了解API 函数

        API函数即Windows API,需要用到头文件windows.h.其中的函数服务于应用程序,所以在写贪吃蛇的时候,我们需要用到该函数来改变控制台的一些参数。

        如把控制台大小改变为100列,30行,值得一提的是长度 1行=2列,打印一个字节站1行1列。

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

        如改变控制台名称为贪吃蛇

	system("title 贪吃蛇");

         众所周知,打开控制台的运行结束后会显示一串,然后结尾处有一个光标。   

       此时我们就要了解到句柄。句柄就像是一个开关,用来控制某个东西。

 HANDLE GetStdHandle(DWORD nStdHandle);

        为了关闭光标显示,就需要创建一个句柄。

​
HANDLE hOutput = NULL;
//获得标准输出的句柄
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

​

      虽然我们有了句柄,但是还不知道光标怎么获取,此时需要用到一个结构体

    //结构体类型名
    //结构体包括有关控制台光标的信息
    // DWORD dwSize;//控制光标占比
    // BOOL  bVisible;//控制光标是否可见
    CONSOLE_CURSOR_INFO 
    //创建一个结构体变量
	CONSOLE_CURSOR_INFO CursorInfo;

        有开光和光标之后,我们该怎么讲两个东西联系起来呢,这时候又有一个函数.

//太长了分成几段 
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,//句柄
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo//结构体指针
 )

        于是有以下操作,

​
//联系光标和句柄
GetConsoleCursorInfo(hOutput, &CursorInfo);
//关闭光标
CursorInfo.bVisible = false;
//更新数据
SetConsoleCursorInfo(hOutput, &CursorInfo);

​

        有了这些之后就可以实现关闭光标显示啦!

        我们再来认识一个结构体,

 //(X,Y)为一个字符控制台缓冲区的坐标
 //改变坐标就能改变光标的位置
typedef struct _COORD {
 SHORT X;
 SHORT Y;
 } COORD, *PCOORD;

        所以我们可以封装一个函数,用于控制光标的指向位置


void SetPos(short x, short y)
{   
    //创建一个句柄
	HANDLE hOutput = NULL;
    //句柄连接上标准输出
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    //创建一个结构体变量,赋值一个坐标,坐标为x,y
	COORD pos = { x,y };
    //应用
	SetConsoleCursorPosition(hOutput, pos);
}

        1.2了解locale.h

        制作贪吃蛇地图的时候或许会出现□、●、◎等图形,但是我们在写c语言代码时碰见的字符基

本上都是单字节字符,而这些图形是双字节字符,所以我们不能通过简单的printf来打印这些图形,

而要通过多了一个w的printf来打印,此时就需要用到头文件locale.h中的函数.

	//语言环境设置为本地语言环境
    setlocale(LC_ALL, "");
    //打印双字节字符
    wprintf(L"%lc",L"□");

        1.3设计地图

        我们已经掌握了基本的打印地图的函数,那么我们正式开始吧!

        游戏开始之前我们需要有一个欢迎界面,并且带有环境说明.

void InitPrint()
{    
    //设置窗口大小和窗口的标题
    system("mode con cols=100 lines=30");
    system("title 贪吃蛇");
    //光标移到中间
	SetPos(40, 15);
    //打印欢迎标语
	printf("欢迎来到贪吃蛇小游戏");
    //设定一个新位置
	SetPos(42, 26);
    //进行一个暂停,且会出现文字说明
	system("pause");
    //清理屏幕
	system("cls");
    //此时到了下一个界面,在屏幕中间打印游戏玩法
	SetPos(28, 15);
	printf("用 ↑、↓、←、→控制蛇的移动, shift减速,ctrl加速\n");
	SetPos(28, 16);
	printf("加速可以得到更高的分数\n");
    //如上操作,准备切换到地图
	SetPos(42, 26);
	system("pause");
	system("cls");
}

        效果如图(背景颜色可调,调成灰色差不多就看不清了)

        接下来就是地图的绘制,

void InitGamePrint(pSnake ps)
{
    //循环打印上行
	for (int i = 0; i < 56; i+=2)
	{
		SetPos(i, 0);
		wprintf(L"□");
	}
    //循环打印下行
	for (int i = 0; i < 56; i += 2)
	{
		SetPos(i, 26);
		wprintf(L"□");
	}
    //循环打印左列
	for (int i = 1; i < 26; i++)
	{
		SetPos(0, i);
		wprintf(L"□");
	}
    //循环打印右列
	for (int i = 1; i < 26; i++)
	{
		SetPos(54, i);
		wprintf(L"□");
	}
	//在界面左边加上提示信息
	SetPos(60, 12);
	printf("不能穿墙,不能吃自己\n");
	SetPos(60, 13);
	printf("用 ↑、↓、←、→控制蛇的移动,\n");
	SetPos(60, 14);
	printf(" shift减速,ctrl加速\n");
	SetPos(60, 15);
	printf("ESC: 退出游戏, space: 暂停退出游戏\n");
	SetPos(60,17);
	printf("Made in 璀璨");
}

        效果如下,

        1.4放置初始的蛇与食物

        当我们制作好基础的界面之后,我们就该在游戏中放置蛇和食物了,那么在此之前我们需要做什么呢?

        我们需要一个结构体来维护贪吃蛇这个游戏,对此,我们先来穷举一下有关蛇的元素.

typedef struct Snake
{
    //蛇的方向
	enum Direction _Dir;
    //指向蛇头的指针
	pSnakeNode _pSnake;
    //指向食物的指针
	pSnakeNode _pFood;
    //食物的权重
	int _FoodWeight;
    //总分
	int _Score;
    //蛇的速度
	int _Speed;
    //蛇的状态
	enum Game_Status _Status;
}Snake, * pSnake;

        在蛇的方向和蛇的状态这一块由于情况的多样性,我们用到了枚举

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

enum Game_Status
{
	OK,
    //特指通过ESC结束游戏
	END_NORMAL,
	KILL_BY_WALL,
	KILL_BY_SELF
};

        此时就有人要问了pSnakeNode 是个什么类型,其实这是另一个结构体指针,也就是链表的指针,蛇身肯定要一个一个串起来的嘛!为什么食物也要用这个指针?因为蛇身放置在地图上和食物放置地图上实际上是一样的,只是借用了链表.

//创建一个链表,用于蛇身
typedef struct SnakeNode
{
	struct SnakeNode* next;
	int x;
	int y;
}SnakeNode, *  pSnakeNode;

        好了,结构体的内容解释完了,该初始化贪吃蛇的数据了,于是有了入下函数

void InitSnake(pSnake ps)
{
    //默认向右走
	ps->_Dir = RIGHT;
	ps->_FoodWeight = 10;
	ps->_Score = 0;
    //开始时状态良好
	ps->_Status = OK;
	ps->_Speed = 200;
}

        但是咱缺的蛇头和食物这一块谁补啊!别急,下面就是

void CreateSnake(pSnake ps)
{
    //给蛇头指针malloc一个空间
	ps->_pSnake = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (ps->_pSnake == NULL)
	{
		perror("malloc failed");
		return;
	}
    //设置蛇头的初始位置
	ps->_pSnake->x = 30;
	ps->_pSnake->y = 10;
	ps->_pSnake->next = NULL;
    //循环创建四个蛇节点
	for (int i = 1; i < 5; i++)
	{
		pSnakeNode newnode = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (newnode == NULL)
		{
			perror("malloc failed");
			return;
		}
        //因为双字节打印的缘故,x轴方向每个节点需要两个字节的空间
		newnode->x = 20+2*i;
		newnode->y = 10;
		newnode->next = NULL;
		//头插
		newnode->next = ps->_pSnake->next;
		ps->_pSnake->next = newnode;
	}
}

        蛇身都初始化好了,那么放置也就不难了,以下封装了一个函数

void PrintSnake(pSnake ps)
{
    //保存蛇头指针
	pSnakeNode ptail = ps->_pSnake;
    //遍历链表打印
	while (ptail != NULL)
	{
		SetPos(ptail->x, ptail->y);
        //宽字符打印
		wprintf(L"●");
		ptail = ptail->next;
	}
}

        接下来食物的初始化是简单的,食物的位置就有点复杂了,

void CreateFood(pSnake ps)
{
    //给食物指针开辟空间
	ps->_pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (ps->_pFood == NULL)
	{
		perror("malloc failed");
			return;
	}
	
	while (1)
	{

		again:
        //通过随机数%运算的方式控制x,y的值都在边界之内
        //需要用到time.h
        //rand()使用时应在主函数加上srand((unsigned int)time(NULL))保证真随机
		ps->_pFood->x = rand() % 51 + 2;
		ps->_pFood->y = rand() % 25 + 1;
        //以为宽字符的原因,必须保证食物的x坐标为偶数
		if (ps->_pFood->x % 2 == 0)
		{
            //保存蛇头坐标
			pSnakeNode ptail = ps->_pSnake;
            //确保食物的创建不与蛇冲突
			while (ptail != NULL)
			{
				if (ptail->x == ps->_pFood->x || ptail->y == ps->_pFood->y)
				{
                    //如果冲突则回到上一个again进行循环
					goto again;
				}
				ptail = ptail->next;
			}
            //如果while正常循环结束,说明x,y可取
			break;
		}
        //%2!=0,循环继续
		else continue;
	}
    //循环结束打印食物
	SetPos(ps->_pFood->x, ps->_pFood->y);
	wprintf(L"◎");
	
}

        食物放置好后说明游戏的前期准备已经好了,先来看看目前的效果如何

2.游戏运行时的维护

        2.1读取键盘按键

        我们开始游戏的第一件事就是按下键盘上的按键,来控制蛇的方向,那么怎么做到呢?

此时我们需要一个能够读取键盘的函数GetAsyncKeyState(),对此有如下宏定义

//按下按键时,函数返回1,否则返回0,有&1的方式判断第一个比特位
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1)?1:0)

        对此,我们就可以先设置好按下各按键产生的反映

//在右边空白处打印得分信息
SetPos(60, 10);
printf("得分: %d,每个食物得分%2d分", ps->_Score, ps->_FoodWeight);
//蛇在下的时候总不能猛回头吧
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_SHIFT)&&ps->_Speed>50)
{
	ps->_Speed += 30;
	ps->_FoodWeight -= 2;
}
else if (KEY_PRESS(VK_LCONTROL) && ps->_Speed < 350)
{
	ps->_Speed -= 30;
	ps->_FoodWeight += 2;
}
else if (KEY_PRESS(VK_SPACE))
{
	SetPos(60, 26);
	system("pause");
	system("cls");
	InitGamePrint(ps);
	SetPos(ps->_pFood->x, ps->_pFood->y);
	wprintf(L"◎");
}
else if (KEY_PRESS(VK_ESCAPE))
{
	ps->_Status =END_NORMAL;
}

        当然,if执行完之后,需要清除一下缓冲区的数据(不清除会在游戏结束时产生问题),以下封装了一个函数(需要<conio.h>)

void KeyFun() 
{
	//检测是否有按键被按下
	while (_kbhit())
	{
		// 使用 _getch() 获取按下的键,不阻塞程序
		int key = _getch();
		// 处理按键事件,可以根据需要进行相应的操作
	}
}

        2.2蛇的第一步

        按键获取到之后,我们就需要对蛇的下一步情况进行讨论,但是再次之前,我们需要一个辅助————预知未来的蛇头,为蛇的行动提供了便利

//创建预知蛇头
pSnakeNode future = (pSnakeNode)malloc(sizeof(SnakeNode));
if (future == NULL)
{
	perror("malloc failed");
	return;
}
//根据按下的方向键来判断预知蛇头的位置
if (ps->_Dir == UP)
{
	future->x = ps->_pSnake->x;
	future->y = ps->_pSnake->y - 1;
}
else if (ps->_Dir == DOWN)
{
	future->x = ps->_pSnake->x;
	future->y = ps->_pSnake->y + 1;
}
else if (ps->_Dir == RIGHT)
{
	future->x = ps->_pSnake->x+2;
	future->y = ps->_pSnake->y ;
}
else if (ps->_Dir == LEFT)
{
	future->x = ps->_pSnake->x - 2;
	future->y = ps->_pSnake->y;
}

            以向右走的蛇为例,                    接下来要判断的有四点:下一步是食物、是墙、是自己、正常走

        判断是吃食物就是要判断食物的坐标是否和预知蛇头坐标相同,

//分装一个函数,判断坐标是不是重合
int NextIsFood(pSnakeNode psn,pSnake ps)
{
	return (psn->x == ps->_pFood->x && psn->y == ps->_pFood->y) ? 1 : 0;
}

             如果坐标相同则吃食物,加分,释放掉当前食物,放置一个新的食物,否则继续判断

void EatFood(pSnakeNode psn,pSnake ps)
{
    //吃到了食物,要长一个节点,用预知头作为节点进行头插
	psn->next = ps->_pSnake;
	ps->_pSnake = psn;
    //吃到了食物,分数要增加
	ps->_Score += ps->_FoodWeight;
	//打印新蛇
	PrintSnake(ps);
	//释放食物
	free(ps->_pFood);
	//创建食物
	CreateFood(ps);
}

           如果撞到墙,只需要需改状态即可,因为状态将在后续作为循环条件

void NextIsWall(pSnakeNode psn, pSnake ps)
{
	if (psn->x == 0 || psn->x == 54 || psn->y == 0 || psn->y == 26)
	{
        //改变状态
		ps->_Status = KILL_BY_WALL;
	}
}

        如果撞到自己,则需要遍历对比

void NextIsSelf(pSnakeNode psn, pSnake ps)
{
        //保护头
		pSnakeNode ptail = ps->_pSnake;
    //遍历链表
	while (ptail)
	{
		if (ptail->x == psn->x && ptail->y == psn->y)
		{
			ps->_Status = KILL_BY_SELF;
		}
		ptail = ptail->next;
	}
}

        最后就是什么都没有碰到只是正常往前走了一步,这一步相对来说比较复杂,因为没有吃到食物,没有变长,所以需要销毁最后一个节点

void NormalWalk(pSnakeNode psn, pSnake ps)
{
	//头插未来节点,作为新的蛇头
	psn->next = ps->_pSnake;
	ps->_pSnake = psn;
    //保存蛇头
	pSnakeNode ptail = psn;
    //招出倒数第二个节点的指针
	while (ptail->next->next)
	{
		ptail = ptail->next;
	}
    //在最后一个节点的位置放置两个空格来覆盖原来的打印
	SetPos(ptail->next->x, ptail->next->y);
	printf("  ");
    //释放尾节点
	free(ptail->next);
	ptail->next = NULL;
    //打印新的蛇
	PrintSnake(ps);
    //控制蛇的速度,在前面有按键可以修改速度
	Sleep(ps->_Speed);
}

        到目前为止,上述代码结合只能够走一步,那么要让代码进一步动起来需要一个do-while循环,循环条件是我们的状态OK,即ps->_Status==OK.

        2.3蛇蛇死亡

        蛇蛇死亡不能就这么结束,我们还需要一些亡语。

SetPos(24, 14);
//通过状态判断亡语
switch (ps->_Status)
{
case END_NORMAL:
	printf("游戏结束\n");
	break;
case KILL_BY_SELF:
	printf("衔尾蛇\n");
	break;
case KILL_BY_WALL:
	printf("卡墙里了\n");
	break;
}

3.游戏结束时的销毁

       假设在游戏结束时按下暂停键,我们可以在控制台看到蛇的结点和食物,那么这些就是我们需要释放的对象,不释放可能会产生内存泄漏,那么我们可以如下释放

void GameEnd(pSnake ps)
{
    //释放食物
	free(ps->_pFood);
    //释放蛇
	pSnakeNode pcur = ps->_pSnake;
	while (pcur)
	{
		pcur = ps->_pSnake->next;
		free(ps->_pSnake);
		ps->_pSnake = pcur;
	}
}

        顺带一提,如果想要做到游戏一直玩,我们可以将上述代码分装成三个函数,通过do-while循环来实现

do {

	//游戏前,界面的维护
	GameBefore(pS);

	//游戏时,对于贪吃蛇的维护
	GameRun(pS);

	SetPos(20, 15);
	printf("是否要再来一局?(Y/N):");

	ch = getchar();
	while (getchar() != '\n');


} while (ch=='Y'||ch=='y');
	GameEnd(pS);

        

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值