贪吃蛇的实现,基于windows操作系统

前言:

贪吃蛇从学习到真正实现花了9天实现,第一二天第一次学习,第三四五天第二次学习,第六七八天一边实现一边思考,才完成了贪吃蛇的代码。实现了贪吃蛇以后已经接近过年,我想自己再根据掌握的知识制作烟花燃烧绽放的场景。贪吃蛇的移动和烟花的移动原理是一样的,贪吃蛇的头插删尾,使我能够处理烟花消失的部分。在我花两天时间写完500行的烟花代码后,对贪吃蛇的实现原理也更加了解了。然后再写下这篇文章。

写作过程中遇到的问题:有很多,但最多的是,写着写着不知道当前要实现什么。思路不清晰。然后去看正确的代码。以及对指针的掌握不够,要使用的函数比较生疏,令人烦躁,静不下心去理解等等。之后通过各种途径一一克服了。

一、分析和规划贪吃蛇的思路

1.一个已经懂得贪吃蛇怎样写的人和一个从没写过贪吃蛇怎么写,第一次上手的思维是不一样的。但是第一部应该都需要了解自己要实现哪些功能。然后划分为不同阶段逐个完成。

2.根据想要制作的成品模样,画出草图和X-mind思维导图。

                                                          成图1,欢迎页面一

                                                        成图2,欢迎页面二

                                                      成图3,游戏页面

     根据贪吃蛇游戏框,绘出墙体食物和蛇。

接下来是X-mind思维导图,写出整个程序的脉络。然后一一落实。

二、落实想法

1.建立三个文件。snake.c  snake.h  test.c.

2.贪吃蛇有两个结构体。

一个是蛇身,它是由一个个结点组成。

这个结点里放蛇身的坐标和下一个结点的指针,记住下一个结点的位置。放坐标是因为蛇在移动的时候要打印蛇身,蛇身的位置是依靠坐标来确定的,这样打印的时候就能找到蛇身的位置了。

这个蛇身由单向不带头不循环链表----单链表构成。

创建一个结构体,以及一个指向结构体的指针并将它们重命名。

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

另一个用来维护蛇。如思维导图中写的思路所示。

typedef struct Snake
{
   pSnakeNode sn;  //指向蛇头的指针
   pSnakeNode pfood;//指向食物的指针,本质上和蛇身没什么区别,只是它是单个的,没有链。
   int score;   //分数,每次吃食物要涨粉
   int foodweight;//当前每吃一个食物增加的分数
   enum DIRECTION;//方向
   enum STATUS;//状态
   int sleeptime;//速度
}Snake,*psnake;//重命名

3.三个文件代码附上

snake.h

#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:6031)
#pragma once
#include <locale.h>
#include <windows.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <wchar.h>
#include <math.h>

#define WALL L'□'
#define SNAKE L'●'
#define FOOD L'★'

//监测按键
#define KEY_STATE(vkey)   ((GetAsyncKeyState(vkey)&0x1)?(1):(0))
//蛇头方向
enum DIRICTION
{
	UP=1,
	DOWN,
	LEFT,
	RIGHT
};
//游戏状态
enum STATUS
{
	OK=1,
	KILL_BY_SELF,
	KILL_BY_WALL,
	ESC
};

//蛇身的结点
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;

//蛇的维护
typedef struct Snake
{
	pSnakeNode sn;//蛇身,是一个结构体指针
	pSnakeNode pfood;//也是一个结构体指针,它是一个坐标
	enum DIRICTION Dir;//蛇的方向
	enum STATUS Status;//蛇的状态
	int Score;//游戏当前得分
	int Foodweight;//食物的分数
	int Sleeptime;//走一步睡眠时间,和蛇速相关
}Snake,*pSnake;

//游戏开始前的准备工作
void GameStart(pSnake snake);

//玩游戏
void GameRun(pSnake snake);
//
游戏结束
//void GameEnd();

snake.c

#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:6031)
#include "snake.h"

//把光标移动到想要的位置
void SetPos(int x, int y)
{
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD pos = { x,y };
	SetConsoleCursorPosition(handle, pos);
}

void Welcometogame()
{
	SetPos(38, 13);
	printf("欢迎来到贪吃蛇小游戏\n");
    
	SetPos(60, 25);
	system("pause");
	system("cls");
	//打印游戏说明
	SetPos(25, 12);
	printf("用↑.↓.←.→分别控制蛇的移动,F3为加速,F4为减速\n");
	SetPos(25, 13);
	printf("加速能得到更高的分数。\n");
	SetPos(60, 25);
	system("pause");
	system("cls");
}

//初始化蛇身
void InitSnake(pSnake snake)
{
	for (int i = 0; i < 5; i++)
	{
		pSnakeNode p = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (p == NULL)
		{
			perror("malloc failed!\n");
			exit(1);
		}
		p->x = 20+2*i;
		p->y = 6;
		p->next = NULL;

		//头插法
		if (snake->sn==NULL)
		{
			snake->sn = p;
		}
		else
		{
			p->next = snake->sn;
			snake->sn = p;
		}

		//打印蛇身,用循环
		/*if (p)
		{
			SetPos(p->x, p->y);
			wprintf(L"%lc", SNAKE);
		}*/
		while (p)
		{
			SetPos(p->x, p->y);
			wprintf(L"%lc", SNAKE);
			p = p->next;
		}
	}
	//其他信息初始化
	snake->Dir = RIGHT;
	snake->Foodweight = 10;
	snake->pfood = NULL;
	snake->Score = 0;
	snake->Sleeptime = 200;
	snake->Status = OK;
	
}

void CreatMap()
{
	int i = 0;
	for (i = 0; i < 57; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	
	for (i = 0; i <= 26; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc\n", WALL);
	}
	SetPos(0, 26);
	for (i = 0; i < 57; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	SetPos(0, 1);
	for (i = 0; i <= 25; i++)
	{
		wprintf(L"%lc\n", WALL);
	}
}

void CreatFood(pSnake snake)
{
	int x = 0;
	int y = 0;
again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 24 + 1;
	} while (x % 2 != 0);

	//不能在蛇身上
	pSnakeNode cur = snake->sn;
	while (cur)
	{
		if (cur->x != x || cur->y != y)
		{
			cur = cur->next;
		}
		else
		{
			goto again;
		}
	}

	//申请食物的结点
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood == NULL)
	{
		perror("malloc failed!\n");
		exit(1);
	}
	pFood->x = x;
	pFood->y = y;

	snake->pfood = pFood;
	SetPos(x, y);
	wprintf(L"%lc", FOOD);

}
//游戏开始前的准备工作
void GameStart(pSnake snake)
{
	//设置一下控制台大小 
	system("mode con cols=100 lines=30");
	//更改控制台名字
	system("title 贪吃蛇");
	//隐藏光标

	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO Cursorinfo;
	GetConsoleCursorInfo(handle, &Cursorinfo);
	Cursorinfo.bVisible = false;
	SetConsoleCursorInfo(handle, &Cursorinfo);

	//打印欢迎界面
	Welcometogame();
	//绘制地图
	CreatMap();
	//初始化蛇身
	InitSnake(snake);
	//打印食物
	CreatFood(snake);

}

void PrintHelpInfo()
{
	SetPos(61, 16);
	printf("1.不能撞墙,不能咬到自己\n");
	SetPos(61, 17);
	printf("2.用↑.↓.←.→分别控制蛇的移动\n");
	SetPos(61, 18);
	printf("3.F3为加速,F4为减速\n");
	SetPos(61, 19);
	printf("4.加速可以获得更多分数\n");
	SetPos(80, 22);
	printf("制作者:真白");
}

void EatFood(pSnake snake, pSnakeNode pnext)
{
	//吃食物,则头插
	pnext->next = snake->sn;//食物的下一个结点连接蛇头
	snake->sn = pnext;//把食物的结点给蛇头
	
	//打印蛇身
	pSnakeNode cur = snake->sn;//创建一个cur指针来循环
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", SNAKE);
		cur = cur->next;
	}
	//分数变化
	snake->Score += snake->Foodweight;
	//释放旧的食物结点
	free(snake->pfood);
	
	//创建新的食物结点
	CreatFood(snake);
}

void NotEatFood(pSnake snake,pSnakeNode pnext)
{
	//正常走,头插,删尾
	pnext->next = snake->sn;
	snake->sn = pnext;

	//删尾,创建一个指针循环
	pSnakeNode cur = snake->sn;
	while (cur->next->next)
	{
		SetPos(cur->x, cur->y);//先设置坐标再打印
		wprintf(L"%lc", SNAKE);//打印蛇身
		cur = cur->next;
	}
	SetPos(cur->next->x, cur->next->y);//先设置坐标再打印尾处的空白
	printf("  ");
	free(cur->next);//释放尾结点
	cur->next = NULL;
}

void IsItFood(pSnake snake, pSnakeNode pnext)
{
	if (pnext->x == snake->pfood->x && pnext->y == snake->pfood->y)
	{
		//是食物,吃掉
		EatFood(snake,pnext);
	}
	else
	{
		//不是食物,不吃
		NotEatFood(snake,pnext);
	}
}

void KillByWall(pSnake snake, pSnakeNode pnext)
{
	//下一个结点的位置是不是墙的坐标
	if (pnext->x == 0 || pnext->y == 0 || pnext->x == 56 || pnext->y==26)
	{
		snake->Status = KILL_BY_WALL;
	}
}
void KillBySelf(pSnake snake, pSnakeNode pnext)
{
	//下一个结点是不是蛇身
	pSnakeNode cur = snake->sn->next;
	while (cur)
	{
		if (pnext->x != cur->x || pnext->y != cur->y)
		{
			cur = cur->next;
		}
		else 
		{
			snake->Status = KILL_BY_SELF;
			break;
		}
	}

}

void SleepTime(pSnake snake)
{
	Sleep(snake->Sleeptime);
}
void Snakemove(pSnake snake)
{
	//创建蛇的下一个位置的结点
	pSnakeNode pnext = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pnext == NULL)
	{
		perror("Snakemove:: malloc");
		return;
	}
	pnext->next = NULL;//只需要这个结点,不需要它下一个结点的信息

	//安排pnext的坐标
	switch (snake->Dir)
	{
	case UP: pnext->x = snake->sn->x;
		     pnext->y = snake->sn->y - 1;//如果按了上,下一个坐标的位置就在蛇头的上面
		break;
	case DOWN: pnext->x = snake->sn->x;
		       pnext->y = snake->sn->y + 1;//同上,下面同上
		break;
	case LEFT:pnext->x = snake->sn->x - 2;
		      pnext->y = snake->sn->y;
		break;
	case RIGHT:pnext->x = snake->sn->x + 2;
		       pnext->y = snake->sn->y;
		break;
	}

	//判断下一个结点是否是食物
	IsItFood(snake, pnext);

	//判断下一个节点是否撞墙
	KillByWall(snake,pnext);

	//判断下一个节点是否咬到自己
	KillBySelf(snake,pnext);
    
}
void Pause()
{
	while (1)
	{
		Sleep(200);
		if (KEY_STATE(VK_SPACE) == 1)
		{
			break;
		}
	}
}

void F3(pSnake snake)
{
	//休眠时间限制,5档 200,170,140,110,80
	if (snake->Sleeptime <= 200 && snake->Sleeptime >= 110)
	{
		snake->Sleeptime -= 30;
		snake->Foodweight += 2;
	}
}

void F4(pSnake snake)
{
	if (snake->Sleeptime >= 80 && snake->Sleeptime <= 170)
	{
		snake->Sleeptime += 30;
		if (snake->Foodweight >= 4)
		{
			snake->Foodweight -= 2;
		}
	}
}
//玩游戏
void GameRun(pSnake snake)
{
	//打印帮助信息
	PrintHelpInfo();

	do
	{
		//当前分数情况
		SetPos(60, 10);
		printf("得分:%d  ", snake->Score);
		printf("每个食物得分:%02d\n", snake->Foodweight);

		//监测当前按键情况
		
		if (KEY_STATE(VK_UP) == 1 && snake->Dir != DOWN)
		{
			snake->Dir = UP; 
		}
		else if(KEY_STATE(VK_DOWN) == 1&& snake->Dir != UP)
		{
			snake->Dir = DOWN;
		}
		else if (KEY_STATE(VK_LEFT) == 1&& snake->Dir != RIGHT)
		{
			snake->Dir = LEFT;
		}
		else if (KEY_STATE(VK_RIGHT) == 1&& snake->Dir != LEFT)
		{
			snake->Dir = RIGHT;
		}
		else if (KEY_STATE(VK_ESCAPE) == 1)
		{
			snake->Status = ESC;
			break;
		}
		else if (KEY_STATE(VK_SPACE) == 1)
		{
			Pause();
		}
		else if (KEY_STATE(VK_F3) == 1)
		{
			F3(snake);
		}
		else if (KEY_STATE(VK_F4) == 1)
		{
			F4(snake);
		}
		

		
		//蛇的移动
		Snakemove(snake);

		//移动一个位置,休眠一下
		SleepTime(snake);

	} while(snake->Status==OK);

}

void GameEnd(pSnake snake)
{
	SetPos(15, 12);
	switch (snake->Status)
	{
	case ESC:
		printf("主动退出游戏,正常退出\n");
		break;
	case KILL_BY_WALL:
		printf("很遗憾,你撞墙了,游戏结束\n");
		break;
	case KILL_BY_SELF:
		printf("很遗憾,你咬到自己了,游戏结束\n");
		break;
		}
	pSnakeNode cur = snake->sn;
	pSnakeNode del = NULL;

	while (cur)
	{
		del = cur;
		cur = cur->next;
		free(del);
	}

	free(snake->pfood);
	snake = NULL;
}

test.c

#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:6031)
#include "snake.h"


void test()
{
	int ch = 0;
	do
	{
		Snake s1 = { 0 };
		GameStart(&s1);
		GameRun(&s1);
		GameEnd(&s1);
		SetPos(15, 14);
		printf("再玩一把吗?Y/N :");
		ch = getchar();
		getchar();
	

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

//
int main()
{
	setlocale(LC_ALL, "");//本地化,头文件<locale.h>
	test();
	
	
	SetPos(0, 27);
	return 0;

}




三、整个贪吃蛇实现的难点

1.windows系统提供的API的一些接口和功能。

在学C语言的时候没有接触,所以要了解一下需要掌握的函数用法。

如何修改控制台的大小?

通过包含windows.h的库函数,可以使用windows命令提示符的一些命令。来达到修改控制台大小的效果。在程序结束前,都是这个大小。system("mode con cols 100 lines 30")

同样的,修改控制台的标题也是利用windows命令提示符的一些命令来修改。

如何隐藏光标?

首先是要获得控制台的句柄。句柄就相当于控制台的钥匙,获取句柄,就是获取这个控制台的信息,(这些信息使得这个控制台与其他控制台相区别)我们得到的这个控制台的句柄,只能操作这个控制台的独特信息,不能修改其他控制台的独特信息。提供的函数是GetStdHandle,它有一个参数,但可以选择填入的参数有三个,为了获取句柄,填入的是STD_OUTPUT_HANDLE。这个函数返回的类型是HANDLE。因此也要创建一个HANDLE 类型的变量来接收返回的句柄。

    HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); 
    CONSOLE_CURSOR_INFO Cursorinfo;      创造一个光标的变量,前面是类型,后面是变量,这是在下一句获取光标信息要用到。提前创建一个变量。
    GetConsoleCursorInfo(handle, &Cursorinfo);    传本控制台的句柄和一个光标变量的地址。传入这两个变量以后,就能获取本控制台的光标信息。本控制台的光标信息,会被复制到Cursorinfo这个变量上。
    Cursorinfo.bVisible = false; 这个Cursorinfo是一个结构体变量,它有两个成员,一个是光标占一个坐标的比例(0-100)。第二个是是否可见。把bVisible设置为ture就是可见,false就是不可见。通过这一步,就把光标设置为隐藏。
    SetConsoleCursorInfo(handle, &Cursorinfo);这个函数是设置光标信息。传入本控制台的句柄,再把设置好的光标信息传进去。从而改变本控制台的光标。

如何设置坐标位置?

首先,什么是坐标?

以此图为例,x坐标是横坐标,从左向右延申,y坐标是纵坐标,从上至下延申,第一个位置是原点。

如何设置坐标位置:

void SetPos(int x, int y)  只要传入坐标的位置,就可以把光标移到想要的地方。把他封装成一个函数
{
    HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);    获取本控制台的句柄,因为把光标移到指定坐标(的函数)也需要本控制台的句柄(作为它的参数)。
    COORD pos = { x,y }; 这是一个坐标的结构体,COORD是一个结构体类型,里面是坐标的参数。这一步是设置pos作为一个结构体坐标变量,成员初始化为x,y。
    SetConsoleCursorPosition(handle, pos);这是一个设置光标位置的函数,需要传两个参数,一个是本控制台句柄,一个是指定的坐标信息。
}

通过这些代码就可以把光标移动到指定位置,然后在指定位置打印出想要的信息。

2.绘制地图

地图的墙是宽字符,打印宽字符要先本地化。使用的函数是wprintf,w表示wide的意思。

3.初始化蛇身

初始化蛇身需要做到申请蛇的结点,每个结点的坐标关系是怎样的,在于你想要蛇一开始在地图的哪个位置出现。蛇的结点之间的连接采用头插法。把蛇的结点都创建出来以后,还要打印一遍蛇身。最后再初始化蛇的状态、速度、食物分数、分数等信息。

4.打印食物

食物的特征,本质上是一个结点。坐标要随机生成,不能生成在墙上,x坐标得是2的倍数,因为和蛇对称,和墙也要对称。坐标不能在蛇的身上。设置好相关条件就可以创建结点了。然后把它初始化为已经生成的坐标。

5.蛇的移动

蛇是走一步移动一步,它要监测是否有按上下左右,如果按了,蛇头就要转变方向。蛇的移动本质上是下一个结点的位置在哪。所以要安排下一个结点的位置。下一个结点的位置有多种可能,撞墙,撞自己,吃食物和正常进行。

如果是吃食物,就是头插。

如果是正常走,那么就是头插以后再删尾,这个过程还要打印一遍蛇身。那么尾巴部分的结点被释放以后,在尾结点的原坐标上打印两个空格来代替。这个过程在地图上显示就是蛇走了一格。

蛇移动的速度越快,休眠的时间越短。所以速度方面设置休眠时间就可以了。这个休眠时间Sleep的函数也是包含windows的库函数来实现的。

撞墙和撞自己都需要修改游戏状态。修改的游戏状态就在于停止游戏。所以外面要套个循环。游戏只在状态是OK的时候进行,其他情况都分别打印出对应的信息。

6.暂停功能如何实现?

暂停的功能可以通过死循环,一直在睡眠。只有重新按了空格键,再跳出循环继续运行。

7.最后的收尾

收尾部分主要是游戏玩了一把game over以后,因为各种原因结束而打印不同信息。打印完了要把蛇的结点依次释放。然后再把食物释放,把传来的维护蛇的指针置为空。

如果想设置再来一把的消息,可以在test.c文件里进行。

要注意两个getchar。第一个getchar用来读取信息,通过一个变量来接收,用于判断玩家到底要不要开下一把。第二个getchar用来接收读取回车字符,但是没有变量接收它,也就是它不产生实际作用。因为它的目的只是用来吸收回车,使这个回车键不至于影响到下一次的输入判定。

8.贪吃蛇的结构体维护

贪吃蛇有两个结构体,第一个结构体是蛇身,第二个是蛇的各种信息,里面也包含了蛇身。

那么就是创建一条贪吃蛇的结构体,来玩这个贪吃蛇游戏。传的参数就是这个贪吃蛇的结构体的地址,因为传地址,才能改变贪吃蛇的值。这里的值包括状态,蛇的结点的指针,蛇的方向,当前分数等等。蛇的结点的头指针是常常需要改变的,因为它要不断移动。如果传的是结点指针,那么要传二级指针。但是本次情况中蛇的结点的头指针在贪吃蛇结构体里只是一个值,这里既然传了贪吃蛇的结构体地址,那么就能随便改变蛇的结点的头指针了。这个是要注意的关于指针的细节。以便在进行与贪吃蛇相似的项目中能够复用。

  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值