• c语言项目-贪吃蛇项目2-游戏的设计与分析



    前言

    前面我们已经将项目所需的背景知识,大体讲解完了,接下来开始对游戏进行设计与分析


    提示:以下是本篇文章正文内容,下面案例可供参考

    游戏的设计与分析

    地图:

    我们要设计一个这样的地图:(本项目的设计并不是完全自主的从无到有的设置,而是学习已经写好的项目的思想与设计)
    在这里插入图片描述

     在此地图中,□ 代表地图边界,★代表食物,●代表蛇
    ●,★,□三个符号均是宽字符,占两个字节,显示大小是普通字符的两倍
    

    这里简述一下c语言的国际化特性相关的知识

    c语言一开始是由美国人发明的,其使用的当然就是英语,其采用的编码方式是ASCII编码
    ,只使用了一个字节-准确地说是只使用了7个比特位,共128个字符,
    但是对于一些其他非英语的国家如,德国,法国等其使用的字母,如法语中字母有注音符号,就无法用ASCII码表示,
    为此,一些欧洲国家就决定用字节中闲置的最高位来引入新的符号,这样就可以用一个字节编码256个字符,
    但是这样有个问题即不同国家的编码的字符是不一样的,比如希腊的与法国的就不同,
    总体来说,不同国家在0-127表示字符是一样的,在128-256表示的字符是不同的。
    在亚洲国家中所使用的字符就更多了,比如在中国,使用的汉字就达到10万左右,一个字节只能表示的256个字符,这是远远不够的,所以需要用多个字节来表示一个字符,比如简体中文常见的编码方式GB2312:即用两个字节表示一个汉字,所以理论上可以表示256*256 == 65536   个汉字!
    

    那么c语言中除了字符集具有国际化特性之外,还有哪些成分具有国际化特性?
    c语言的国际化特性,不体现关键字与操作符,与c语言语法相关的各种符号(*,->等)。

    还有数字量的格式
    货币量的格式
    日期与时间的表示形式
    
    c语言标准不断被国际化支持,体现在一些标准头文件与库函数之中,
     比如:加入宽字符类型 wchar_t类型和宽字符的输入输出函数头文件中
    其中提供了程序员针对特定地区调整程序行为(所谓调整程序行为,即在中国此程序该怎样执行,在其他地区此程序该怎样执行)的函数,
    
    本地化头文件
    locale提供的函数用于调整程序在不同地区的表现,正规一点:即帮助程序实现国际化与本地化,以适应不同地区的语言环境要求。
    locale提供的函数可以控制c标准库中根据不同地区会产生不一样行为的部分。
    因为c标准库中的是函数或者宏,即locale提供的函数可以控制这些函数与宏。
    
    类项

    不同地区(语言环境不同),有许多方面不同,比如字符,时间与日期格式,货币的表现形式等,
    这些可以调整的部分称其为类项,下面的这些宏针对各自的类项:
    在这里插入图片描述

    setlocale函数

    在这里插入图片描述

     本函数的功能是通过调整C语言模式以改变程序对我们指定类项的操作
    本函数的第一个参数是要调整的类项(适配到本地环境),第二个参数是c语言模式
    c标准中为这个函数提供了两种参数:一种是:标准模式,对应的参数:" C"
                                    第二种是  本地模式:" "    
                                    标准模式即英语环境下的c语言版本
                                    本地模式即适配当前所在地区的c语言版本
     可以用setlocale函数返回当前C语言的模式,只需要将函数第二个参数置为NULL即可
    

    我们就以打印宽字符为例:

    #include
    #include  //setlocale函数包含在locale.h文件中
    int main() {
    	//我们将当前模式先设置成标准模式
    	char* ret = setlocale(LC_ALL, "C");
    	wchar_t a = L'张';  //wchar_t是宽字符的数据类型,汉字是宽字符的一种
    	                    //在宽字符之前需要加上L,否则会被当做窄字符处理
    	printf("%s\n", ret);
    	wprintf(L"%c\n", a);//在格式串之前也要加上L
    	return 0;
    }
    

    在这里插入图片描述
    此时的结果:C代表C语言当前模式是标准模式,?代表无法解析字符是什么,这是此时程序执行完的结果。
    下面我们改变当前C语言的模式,修改成本地模式:

    #include
    #include  //setlocale函数包含在locale.h文件中
    int main() {
    	//我们将当前模式先设置成标准模式
    	//char* ret = setlocale(LC_ALL, "C");
    	char* ret = setlocale(LC_ALL, "");
    	wchar_t a = L'张';  //wchar_t是宽字符的数据类型
    	                    //在宽字符之前需要加上L,否则会被当做窄字符处理
    	printf("%s\n", ret);
    	wprintf(L"%c\n", a);//在格式串之前也要加上L
    	return 0;
    }
    

    在这里插入图片描述
    此时的结果表明当前的模式是本地在中国,输出宽字符‘张’。

    如果我们函数的第一个参数不是LC-ALL全部类项,而是某一类项呢?,比如LC_TIME,那我们还能打印宽字符吗?

    #include
    #include  //setlocale函数包含在locale.h文件中
    int main() {
    	//改变此时的类项
    	char* ret = setlocale(LC_TIME, ""); 
    	wchar_t a = L'张';  //wchar_t是宽字符的数据类型
    	                    //在宽字符之前需要加上L,以做标记
    	printf("%s\n", ret);
    	wprintf(L"%c\n", a);
    	return 0;
    }
    

    在这里插入图片描述
    结果显示不能,也就是说此时函数只能调整程序对时间格式的操作,而不能改变其他!

    上面我们讲到需要打印★,●,□三个宽字符

    找到这三个字符

    但是键盘上并没有这三个字符,我们需要用搜狗输入法输入
    下载搜狗输入法后:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    就完成了。

    打印的方式有两种:

    第一种方式就是我们上面所讲的通过locale.h文件,setlocale函数调整C语言当前模式来使得程序能够打印宽字符
    第二种方式就是直接通过printf函数,直接用字符串的形式打印:

    #include
    #include  //setlocale函数包含在locale.h文件中
    int main() {
    	printf("□\n");
    	printf("ab\n");
    	return 0;
    }
    

    在这里插入图片描述

    控制台屏幕的长宽特性:

    #include
    int main() {
    	system("mode con cols=30 lines=30");
    	return 0;
    }
    

    在这里插入图片描述
    问: 为什么行与高均为30,则屏幕会是这样?
    是因为X轴的一个单位是一个字节!
    而Y轴的一个单位是两个字节!

    在这里插入图片描述

    游戏代码实现:

    我们要设计一个27行,58列的地图:

    项目结构:

    在这里插入图片描述
    Snake.h:

    #define KEY_PRESS(vkey) ((GetAsyncKeyState(vkey)&1)?1:0) 
    
    #include
    #include
    #include
    #include
    #include
    #define POS_X 26
    #define POS_Y 10
    //关于游戏的状态
    enum STATUS {
      OK = 1,      //开始
      END_NORMAL,  //正常结束
      KILL_BY_WALL,//墙撞结束
      KILL_BY_SELF //撞到自己结束    
    
    
    };
    
    //关于蛇方向的枚举
    enum DERECTION
    {
    	UP = 1,
    	DOWN,
    	LEFT,
    	RIGHT
    
    };
    
    //定义一个蛇身节点
    typedef struct SnakeNode
    {
    	int x;
    	int y;
    	struct SnakeNode* Next; //下一个节点指针!
    }SnakeNode,*PSnakeNode;
    
    //对于一次游戏的数据封装到一个类当中
    typedef struct Snake
    {
        //对于一次游戏中的数据有:
    	PSnakeNode head ;              //指向蛇的指针	
    	enum DERECTION derection;      //蛇转向的方向:                    
    	int sumscore;                  //总分数
    	int foodscore;	               //单个事物的分数
    	PSnakeNode  foodposition;      //食物的位置
    	enum STATUS status;            //游戏的状态——开始,撞墙结束,撞到自己结束,正常结束
    	int SleepTime;                 //停顿的时间——(游戏执行停顿的时间。)
    
    }Snake,*PSnake;
    //函数的声明
    //设置光标的位置
    void SetPos(short x, short y);
    //打印游戏欢迎界面
    void WelcomeToGame(PSnake sn);
    
    //游戏的准备工作:
    void GameStart(PSnake sn);
    //游戏开始运行:
    void GameRunning(PSnake sn);
    //游戏结束:
    void GameEnd(PSnake sn);
    
    

    Snake.c:

    #include"Snake.h"
    //设置光标的位置
    void SetPos(short x,short y) {
    
    	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE); //获取屏幕标准输出的句柄
    	//封装光标的位置
    	COORD pos = { x,y };
    	//设置光标位置
    	SetConsoleCursorPosition(houtput, pos);
    }
    //打印游戏地图
    void GameMap(PSnake sn) {
    	//打印游戏地图:
    	//打印一个27行58列的游戏地图:
    	//打印上届!
    	for (int i = 0; i < 29; i++) {
    		wprintf(L"%lc", L'□');
    	}
    	//打印下界
    	SetPos(0, 26);
    	for (int k = 0; k < 29; k++) {
    		wprintf(L"%lc", L'□');
    	}
    	//打印左界
    	for (int j = 1; j < 27; j++) {
    		SetPos(0, j);
    		wprintf(L"%lc", L'□');
    	}
    	//打印右界
    	//需要将光标设置在地图右上角
    	SetPos(56, 0);
    	for (int x = 1; x < 27; x++) {
    		SetPos(56, x);
    		//根据光标的位置进行打印符号,光标的位置随着x变化
    		wprintf(L"%lc",L'□');
    	}
    }
    //打印欢迎界面与提示信息
    void WelcomeToGame(PSnake sn) {
    	//先打印第一个欢迎页面!
    	SetPos(40, 14);
    	printf("欢迎来到贪吃蛇游戏!!!");   
    	//再打印第二个欢迎界面
    	SetPos(40, 27);
    	system("pause");   
    	//将打印在屏幕上的第一个页面清除掉。
    	system("cls");
    	SetPos(25, 13);
    	printf("用↑.↓.←.→ 分别控制蛇的移动,F3为加速,F4为减速\n");
    	SetPos(25, 14);
    	printf("加速能得到更高的分数!");
        SetPos(25, 20);
    	system("pause");
    	system("cls");
    	//打印游戏地图:
    	GameMap(sn);
        
    }
    //设置创建食物
    void SetFoodPosition(PSnake sn) {
    	int x = 0;
    	int y = 0;
    //需要为食物设置随机坐标:
    //这个随机也需要限制,x坐标不能为奇数,
    // 食物坐标必须在地图墙之内,且食物不能出现在蛇的体内
    // 食物的x坐标也不能为奇数
    
    
    
    //地图大小为58列,x属于0-57,去除两边的字符,一个字符占两个字节,所以x的范围应该是2-54;
    //地图大小为27行,y属于0-26,去除上下的字符,行的宽度为1个字符,所以y的范围应该是1-25
    //先获取随机值
    again: do {
    	
    	x = rand() % 53 + 2;   //x的坐标范围为2-54:  
    	y = rand() % 25 + 1;
    }       //y的坐标范围为1-25
    //当x为奇数时,就重新获取
    	while (x % 2 == 1);
    	
    //判断获取的坐标是否与蛇的身体重复
    PSnakeNode tmp = sn->head;
    while (tmp != NULL) {
    	if (tmp->x == x && tmp->y == y) {
    	    //此时说明食物与蛇身位置相撞。
    		//使用goto语句重新生成食物位置
    		goto  again;
    	}
    		tmp = tmp->Next;
    }
    //为食物创建一个节点
    PSnakeNode cur = (PSnakeNode)malloc(sizeof(SnakeNode));
    if (cur == NULL) {
    
    	perror("SetFoodPositon::malloc");
    	return;
    
    }
    //当食物位置确定后:
    sn->foodposition = cur;
    sn->foodposition->x = x;
    sn->foodposition->y = y;
    //在指定位置打印食物字符
    SetPos(x, y);
     wprintf(L"%lc",L'◇');
    }
    //初始化蛇身
    void InitializeSnake(PSnake sn) {
    	//初始化蛇身
    	for (int i = 0; i < 5; i++) {
    		PSnakeNode cur = (PSnakeNode)malloc(sizeof(SnakeNode));
    
    		if (cur == NULL) {
    			perror("Intialize::malloc");
    			return;
    		}
    		cur->Next = NULL;
    		cur->x = i * 2 + POS_X;
    		cur->y = POS_Y;
    
    		//选择头插法
    		if (sn->head == NULL) {
    			sn->head = cur;
    		}
    		else {
    			//进行头插法
    			cur->Next = sn->head;
    			sn->head = cur;
    		}
    	}
    	PSnakeNode cur = sn->head; 
    	while (cur !=NULL) {
    		SetPos(cur->x, cur->y);
    		wprintf(L"%lc", L'㊣');
    		cur = cur->Next;
    	}
    
    }
    //初始化游戏信息
    void GameStart(PSnake sn) {
    
    	 //1.欢迎界面的打印与功能介绍与地图的绘制!
    	 WelcomeToGame(sn);
    	 //2.初始化贪吃蛇的信息
    	 // 创建蛇
    	 InitializeSnake(sn);
    	 //设置蛇最开始的方向:向右
    	 sn->derection = RIGHT;
    	 //设置游戏的状态:
    	 sn->status = OK;
    	 //设置食物的分数:10
    	 sn->foodscore = 10;
    	 //设置当前的总分数:
    	 sn->sumscore = 0;
    	 //设置食物的位置:
    	 SetFoodPosition(sn);
    	 //设置游戏停顿的时间
    	 sn->SleepTime = 200;
    }
    //打印游戏的帮助信息!
    void PrintHelpInfo() {
    	 SetPos(65, 17);
    	 printf("不能穿墙,不能咬到自己!");
    	 SetPos(65, 18);
    	 printf("用↑.↓.←.→来控制蛇的移动!");
    	 SetPos(65, 19);
    	 printf("按F3加速,F4减速");
    	 SetPos(65, 20);
    	 printf("按ESC退出游戏,按空格暂停游戏");
     }
    //判断下一个节点是否是食物:
    int IsFoodNode(PSnakeNode PNextNode,PSnake sn) {
    	//判断是否是食物:
    	return PNextNode->x == sn->foodposition->x && PNextNode->y == sn->foodposition->y;
    }
    //蛇吃掉食物
    void EatFood(PSnakeNode PNextNode,PSnake sn) {
    	//头插法插入节点
    	sn->foodposition->Next = sn->head;
    	sn->head = sn->foodposition;
    	不可以只打印新增的节点。
    	//SetPos(PNextNode->x,PNextNode->y);
    	PSnakeNode tmp = sn->head;
    	while (tmp != NULL) {
    		SetPos(tmp->x, tmp->y);  
    		wprintf(L"%lc", L'㊣');
    		tmp = tmp->Next;
    	}
    	sn->sumscore += sn->foodscore; //在吃掉一个食物后,分数+单个食物的分数!
        //释放掉旧的节点:
    	free(PNextNode);
    }
    //判断是否撞墙!
    int  IsKillByWall(PSnakeNode PNextNode) {
    return  PNextNode->x == 0 || PNextNode->x == 56 || PNextNode->y == 0 || PNextNode->y == 26;
    	
    }
    //判断是否撞到自己:
    int  IsKillBySelf(PSnakeNode PNextNode,PSnake sn) {
    	PSnakeNode tmp = sn->head->Next;
    	while (tmp != NULL) {
    		//如果与蛇身的某一个节点相同
    		if (PNextNode->x == tmp->x && PNextNode->y == tmp->y) { 
    			return 1;
    		}
    		tmp = tmp->Next;
    	}
    	return 0;
    }
    //当遇到空白处时!
    void   IsBlank(PSnakeNode PNextNode,PSnake sn) {
    	PNextNode->Next = sn->head;
    	sn->head = PNextNode;
    	//找到尾节点
    	PSnakeNode  tmp = sn->head;
    	while (tmp->Next->Next != NULL) {
    		SetPos(tmp->x, tmp->y);
    		wprintf(L"%lc", L'㊣');
    		tmp = tmp->Next;
    	}
    	//当找到倒数第二个节点时:
    	SetPos(tmp->Next->x, tmp->Next->y);
    	printf(" ");
    	printf(" ");
        //释放尾结点
    	free(tmp->Next);
    	tmp->Next = NULL;
    
    }
    //蛇移动一步的操作
     void SnakeMove(PSnake sn) {
    	 //先找到当前蛇头的坐标与蛇头的方向,判断下一步是什么!
    	 int x = 0;
    	 int y = 0;
    	 //用switch语句显得更简洁:
    	 switch (sn->derection) {
    	 case UP:
    		 //此时下一个节点的坐标即:
    		 x = sn->head->x;
    		 y = sn->head->y - 1;
    		 break;
    	 case DOWN:
    		 //下一个节点的坐标:
    		 x = sn->head->x;
    		 y = sn->head->y + 1;
    		 break;
    	 case LEFT:
    		 //下一个节点的坐标:
    		 x = sn->head->x - 2;
    		 y = sn->head->y;
    		 break;
    	 case RIGHT:
    		 //下一个节点的坐标
    		 x = sn->head->x + 2;
    		 y = sn->head->y;
    		 break;
    	 }
    	 //创建一个新的节点
    	 PSnakeNode cur = (PSnakeNode)malloc(sizeof(SnakeNode));
    	 if (cur == NULL) {
    		 perror("NextPace::malloc");
    		 return;
    	 }
    	 cur->x = x;
    	 cur->y = y;
    
    	 //要判断此节点是否是墙
    	 if (IsKillByWall(cur)) {
    		 sn->status = KILL_BY_WALL;
    		 //释放掉申请的节点
    		 free(cur);
    		 return;
    	 }
    	 //判断是否是食物
    	  if (IsFoodNode(cur,sn)){
    		  //吃掉食物!
    		  EatFood(cur, sn);
    		 //当吃掉食物时,需要再随机生成一个食物,
    		 SetFoodPosition(sn);
    		 return;
    	 }
    
    	 //判断是否撞到了自己
    	  if (IsKillBySelf(cur, sn)) {
    		  sn->status = KILL_BY_SELF;
    		  //释放掉申请的节点
    		  free(cur);
    		  return;
          }
    	 //如果程序执行到这里,说明下一节点是空白处:
    	//执行遇到空白时的函数。 
    	  IsBlank(cur, sn);
     }
     //游戏暂停
     void Pause() {
    	//让游戏暂停即一直sleep即可!
    	 while (1) {
    		 Sleep(200);//单位为毫秒。
    		 //如果再按一次空格,则停止暂停!
    		 if (KEY_PRESS(VK_SPACE)) {
    			 break;
    		 }
    	 }
     } 
     //游戏运行
     void GameRunning(PSnake sn) {
    	//游戏运行的逻辑就是每走一步,就观察一下蛇的状态。
        //根据按键的选项,进行对应的操作:
         //ESC,空格,↑,↓,←,→ F3,F4 这些键的虚拟值为:
    	 //
    
    	 do {
    		 //打印游戏的帮助信息。
    		 PrintHelpInfo();
    		 SetPos(65, 14);
    		 printf("总分数:%2d", sn->sumscore);
    		 SetPos(65, 15);
    		 printf("当前食物的分数:%2d", sn->foodscore);
    		 //检测当前所按的键!
    		 //↑按键
    		 if (KEY_PRESS(VK_UP)&&sn->derection!=DOWN) {//按键不能与当前蛇的方向进行冲突
    			 //在按上键时,方向改变
    			 sn->derection = UP;
    		
    		 }
    		 //↓按键
    		 else if (KEY_PRESS(VK_DOWN)&&sn->derection!=UP) {
    			 sn->derection = DOWN;
    		 }
    		 //←按键
    		 else if (KEY_PRESS(VK_LEFT)&&sn->derection!=RIGHT) {
    			 sn->derection = LEFT;
    		 }
    		 //→按键
    		 else if (KEY_PRESS(VK_RIGHT)&&sn->derection!=LEFT) {
    			 sn->derection = RIGHT;
    		 }
    		 //空格
    		 else if (KEY_PRESS(VK_SPACE))
    		 {  
    			 //游戏暂停
    			 Pause();
    		 }
    		 //ESC 
    		 else if (KEY_PRESS(VK_ESCAPE))
    		 {
    			 sn->status = END_NORMAL;
    		 }
    		 //F3
    		 else if (KEY_PRESS(VK_F3))
    		 {
    			 //F3是加速
    			 //游戏休眠的时间不能为负数
    			 if (sn->SleepTime > 80) {
    				 sn->SleepTime -= 30;
    				 sn->foodscore += 2;
    			 }		 
    		 }
    		 //F4
    		 else if (KEY_PRESS(VK_F4))
    		 {
    			 //F4是减速
    			 //游戏休眠的时间也不能极其长
    			 if (sn->foodscore > 2) {
    				 sn->SleepTime += 30;
    				 sn->foodscore -= 2;
    			 }
    		 }
    		 SnakeMove(sn);
    		 Sleep(sn->SleepTime);
    
    	 } while (sn->status == OK);
     }
     //游戏结束的善后工作
     void GameEnd(PSnake sn) {
    	  SetPos(24, 12);
    	 switch (sn->status) {
    	 case END_NORMAL:
    		 printf("您主动退出了游戏!!!\n");
    		 break;
    	 case KILL_BY_SELF:
    		 printf("您撞到了自己,游戏结束!!!\n");
    		 break;
    	 case KILL_BY_WALL:
    		 printf("您撞到了墙,游戏结束!!!\n");
    		 break;
    	  }
    	
    //释放蛇身的节点
      //选择头删,
    	 PSnakeNode cur = sn->head;
    	 while (cur) {
    		 //保证删除前面的节点能够找到后面的节点
    		 PSnakeNode del = cur;
    		 cur = cur->Next;
    		 free(del);
    	 }
     }
    
    

    test.c:

    #define _CRT_SECURE_NO_WARNINGS 1
    #include"Snake.h"
    void test() {
    	char ch = 0;
    	do {
    		Snake snake1 = { 0 };
    		//设置屏幕
    		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);//设置控制台光标状态
    		//初始化游戏信息
    		GameStart(&snake1);
    		//运行游戏
    		GameRunning(&snake1);
    		//结束游戏
    		GameEnd(&snake1);
    		SetPos(20, 15);
    		printf("再来一局吗?(Y/N)");
    		 ch =  getchar();
    		getchar(); //用于吸收\n,即吸收掉按回车键
    	} while (ch == 'Y' || ch == 'y');
    	SetPos(12, 26);
    }
    int main() {
    	setlocale(LC_ALL, "");
    	//产生随机数
    	srand((unsigned int)time(NULL));
    	test();
    	return 0; 
    }
    

    总结:

    1. 自己的调试能力比较差,没有认真地进行调试,监视变量!
    2. 缺乏错误经验,一个野指针便让自己摸不着头脑!
    3. 这种做项目的思维方式有问题,后面的出现问题,又不断地修改前面的内容,如果是大项目,很容易崩盘!
    4. 具体:在函数中修改尾结点时,不需要使用二级指针,可以通过->next->next的方式进行修改。
    5. 尽量将一个个的功能用函数封装起来,即使没有减少代码冗余的地方,也可以使得代码简洁,可读性更高,便于修改与扩展功能!
  • 相关阅读:
    关于代码性能优化的总结
    简单迅速解决windows电脑下载windows应用商店(Microsoft Store)
    linux/bsd/mac上面模块基址/dlopen句柄互相转换的方法
    银河麒麟V10(Kylin Linux V10)安装 Kibana-7.15.2
    Swoole Compiler 加密PHP源代码(简版)
    排列组合DFS
    蓝桥等考Python组别十级007
    移动和pc端的微信支付和支付宝支付(持续更新)
    为什么ASO很重要?
    C# Winform串口助手
  • 原文地址:https://blog.csdn.net/2301_77993957/article/details/138112850