跳转至内容
  • 版块
  • 最新
  • 标签
  • 热门
  • Online Tools
  • 用户
  • 群组
折叠
品牌标识

D2Learn Forums

  1. 主页
  2. Blogs | 博客
  3. Maxwell1905's Blog
  4. rt-thread 双链表分析及教程(附带demo)

rt-thread 双链表分析及教程(附带demo)

已定时 已固定 已锁定 已移动 Maxwell1905's Blog
有小错误,评论区有修改双链表rt-thread
6 帖子 2 发布者 39 浏览
  • 从旧到新
  • 从新到旧
  • 最多赞同
登录后回复
此主题已被删除。只有拥有主题管理权限的用户可以查看。
  • sky-littlestarS 离线
    sky-littlestarS 离线
    sky-littlestar
    编写于 最后由 编辑
    #1

    本次实验使用的是(Visual Studio 2022,EasyX)文末附完整代码,可直接复制运行

    前言

     我们平时在进行玩电子游戏时,会发现很多资源都是动态加载的,会不断的生成加载和删除。比如说,我们玩飞机大战时,不同的关卡和地图中,生成的飞机数量是不一样的,然后他被我们的炮弹击落后,飞机的数量又会减少,飞机数量一直处于不确定的状态。我们在玩贪食蛇游戏时,蛇身的长度也是不固定的,蛇吃到食物时,身体会变长,蛇碰到障碍物时,身体会变短,处于一个动态变化的过程。因此,在设计这类动态加载资源的场景是,数组是不太合适的(数组是静态的,在固定初始长度后,不方便扩展),因此,采用链表完成此类应用场景是比较理想的选择。按照传统教科书的教程,我们一般的链表都是一个指针域+一个数据域,通过遍历链表上的每一个节点,得到相关数据。
    b4927912-00fa-4e42-bf65-69bd1e6d5ce6-image.png
    其数据结构如下表示(以双链表为例子)
    typedef struct DLnode
    {
    int data; //也可以是其他数据类型
    DLnode* next;
    DLnode* next;
    }DLnode_list;
    但是,这种固定数据域的方法,并不方便灵活,这要求我们链表上的节点用的都是统一的结构体,由于各个用户对链表数据的需求不同,是没办法直接通过一个统一的API接口,进行链表初始化的。我们可以参考一下rt-thread实时操作系统的做法。把指针域单独提取出来,通过指针域再进行数据域的访问,下面,我们将通过经典的贪食蛇游戏进行示例讲解。
    a7a2193a-eab8-4a7d-bb74-69f414a9831e-image.png
    本次实验需要使用到Visual Studio 2022,EasyX图形库。
    EasyX的坐标系如下:
    301a80d4-867b-4100-a1c3-ff22ac0de8f5-image.png
    贪食蛇需要一个蛇头,还有一段蛇身,其实,每一个蛇的部位,就是一个节点,我们统一用一个结构体进行表示。
    0179a4af-5b7c-4dbc-9665-df6e5d7abce6-image.png

    enum COMPRESS_E                 //当前节点的运行方向
    {
    	COMPRESS_UP,
    	COMPREES_DOWN,
    	COMPRESS_LEFT,
    	COMPRESS_RIGHT,
    };
    
    typedef struct doublelink_listnode   //当前蛇身节点
    {
    	doublelink_listnode* prev;
    	doublelink_listnode* next;
    }Doublelink_listnode;
    
    typedef struct                               //蛇的节点结构体
    {
    	float x;                                  
    	float y;                                
    	COLORREF color;                
    	COMPRESS_E compass;
    	Doublelink_listnode node;
    }Snake;
    
    

    这样,我们通过计算蛇的每一个节点的位置,并通过链表将其串联起来,改变链表上节点的位置数据,就是在操纵蛇身的移动。通过增加链表上的节点或者删除链表上的节点,即可以实现蛇身的增长或缩短。首先,我们先初始化一个头节点。
    Doublelink_listnode list;
    list= Doublelink_Listinit(list);
    fbae526f-6b15-4bdb-a177-d0044f0ee492-image.png
    其前驱节点后后继节点均指向自身。
    随后,将蛇身插入链表中,我们先初始化6节蛇身,一个圆点表示一段蛇身。
    73f69519-330d-4f5c-92e1-3b2e2ee3ff1a-image.png
    注意,分配空间的时候,要按蛇的整个结构体去分配,不能仅分配Doublelink_listnode node。这是RT-Thread链表和我们平时用的带数据域的链表很大的不同,假如仅分配Doublelink_listnode node,由于该变量的作用域仅在该函数中,当跳转到其他函数时,Snake中的其他数据会被破坏,会导致程序出错。
    b0405bf4-0626-40bd-81f7-ca854940ad96-image.png

    for (int i = 0; i < 6; i++)   
    {
    	Snake* snake= (Snake*)malloc(sizeof(Snake));
    	snake ->x=(i* Radius*2)+100;
    	snake ->y= 300;
    	snake->color = RED;
    	snake->compass = COMPRESS_RIGHT;
    	doublelink_insertback(&list, &(snake->node));  //将蛇的蛇身插入链表中
    }
    
    entrylist_foreach(&list,pos)   //遍历6段蛇身,并将其坐标信息以圆圈的形式在图上画出来
    {
    	setlinecolor(list_entry(pos, Snake, node)->color);
    	setfillcolor(list_entry(pos, Snake, node)->color);
    	fillcircle(list_entry(pos, Snake, node)->x, list_entry(pos, Snake, node)->y, Radius);   //以圆圈的形式画出来
    }
    

    通过doublelink_insertback将数据插入链表,然后我们再entrylist_foreach遍历链表,list_entry进入node节点即可得到蛇身的位置,然后利用EasyX在界面上描绘出来,就是我们这个游戏的实现思路。

    我们通过一个最小例子,展示一下RT-thread双链表的插入,遍历,读取用法。
    分析一下void doublelink_insertback(Doublelink_listnode* list, Doublelink_listnode* node);其函数定义如下:

    void doublelink_insertback(Doublelink_listnode* list, Doublelink_listnode* node)
    {
    	node->next = list->next; //1
    	list->next->prev = node;  //2
    	list->next = node;   //3
    	node->prev = list;   //4
    }
    

    51935672-9236-488f-be53-baa67ffc0bbe-image.png
    假设之前node1已经插入到链表中,此时我们调用doublelink_insertback(&head, &(node2.node));,将node2插入到链表中,图中分别对应函数中的四个过程:
    图中的标号1,对应node->next = list->next;插入node2,node2的next指针指向node1的指针域;
    图中的标号2,对应list->next->prev=node,头指针的next对应的是node1,node1的prev指针更新为node2,正如标号2和下面带红色叉号❌的箭头所示,head->next的指针被废除,而node2与node1建立起关系,node1->prev=node2;
    图中的标号3,对应list->next = node,重新构造list->next指向的地方,如图中上面带红色叉号❌的箭头所示,head的next指针被重定向,指向node2;
    图中的标号4,代表node->prev = list,新插入的node2的prev指针指向head。所以,根据以上的分析我们可以知道,利用doublelink_insertback函数,节点可以插入到链表中,而且,越是后边插入的,当遍历的时候,反而会被优先遍历。
    当我们完成链表的插入后,如何遍历链表呢?这个是比较简单的,我们直接通过指针,即可访问各个元素:
    #define Doublelink_foreach(list,pos)
    for(pos=(&list)->next;pos!=&list;pos=pos->next)
    遍历各个元素时,我们还要进入当前节点,读取我们想要的变量
    #define list_entry(ptr,type,member)
    ((type*)((unsigned char*)ptr-(unsigned long)&(((type*)0)->member)))
    ptr指的是当前指针指向的位置,type值的是结构体的类型,member是当前指针指向的结构体成员。通过当前指针指向的位置,减去当前指向的成员在结构体中的偏移量,可以知道当前结构体的地址,然后通过指针去访问当前结构体成员可以得到访问变量的值。
    55eac8f2-e772-4ae4-904a-2d10082643cb-image.png
    def73a4a-d484-4952-a873-f6540628148b-image.png
    下面,运行一个demo展示一下具体用法:

    typedef struct Doublelink_listnode
    {
    	Doublelink_listnode* prev;
    	Doublelink_listnode* next;
    }Doublelink_list;
    
    typedef struct 
    {
    	uint8_t data;
    	Doublelink_listnode node;
    }Doublelink_listdata;
    
    Doublelink_list		head;
    Doublelink_list*	pos;
    #define Doublelink_Listinit(list) {&list,&list}
    #define Doublelink_foreach(list,pos)  \
    				  for(pos=(&list)->next;pos!=&list;pos=pos->next)
    #define list_entry(ptr,type,member)   \
    				  ((type*)((unsigned char*)ptr-(unsigned long)&(((type*)0)->member)))
    
    void doublelink_insertback(Doublelink_listnode* list, Doublelink_listnode* node);
    int main()
    {
    	head=Doublelink_Listinit(head);
    	Doublelink_listdata node1;
    	node1.data = 1;
    	doublelink_insertback(&head, &(node1.node));
    	Doublelink_listdata node2;
    	node2.data = 2;
    	doublelink_insertback(&head, &(node2.node));
    	Doublelink_listdata node3;
    	node3.data = 3;
    	doublelink_insertback(&head, &(node3.node));
    	
    	Doublelink_foreach(head, pos)
    	{
    		printf("%d\r\n", list_entry(pos, Doublelink_listdata, node)->data);
    	}
    }
    
    void doublelink_insertback(Doublelink_listnode* list, Doublelink_listnode* node)
    {
    	node->next = list->next;
    	list->next->prev = node;
    	list->next = node;
    	node->prev = list;
    }
    

    837624b4-1096-4541-8819-356d491e3523-image.png
    运行结果正确。
    蛇身生成完毕,还要生成食物和怪兽,蛇吃到食物后,蛇身会变长,遇到怪兽后,蛇身会变短。
    初始化蛇身和怪兽的坐标:

    #define Food_Num       5
    #define Master_Num     2
    for (int i = 0; i < Food_Num; i++)
    {
    	Food_pos[i].x = -1;
    	Food_pos[i].y = -1;
    }
    
    for (int i = 0; i < Master_Num; i++)
    {
    	Master_pos[i].x = -1;
    	Master_pos[i].y = -1;
    }
    

    初始化蛇,食物和怪兽后,调用绘图函数,在界面上根据坐标绘制图形。

    cleardevice();                                                                           //清空画布
    entrylist_foreach(&list,pos)                                                     //遍历蛇身,根据蛇身,绘制圆形
    {
    	setlinecolor(list_entry(pos, Snake, node)->color);
    	setfillcolor(list_entry(pos, Snake, node)->color);
    	fillcircle(list_entry(pos, Snake, node)->x, list_entry(pos, Snake, node)->y, Radius);
    }
    
    for (int i = 0; i < Food_Num; i++)                                           //根据坐标,绘制食物
    {
    	setlinecolor(HSVtoRGB(i * 30, 0.6, 1));
    	setfillcolor(HSVtoRGB(i * 30, 0.6, 1));
    	fillrectangle(Food_pos[i].x, Food_pos[i].y, Food_pos[i].x+(2* Radius), Food_pos[i].y + (2 * Radius));       //绘制正方形
    }
    
    for (int i = 0; i < Master_Num; i++)                                        //根据坐标,绘制怪兽
    {
    	setlinecolor(RED);
    	setfillcolor(BROWN);
    	POINT pts[] = { {Master_pos[i].x, Master_pos[i].y-Radius}, {Master_pos[i].x- Radius, Master_pos[i].y + Radius},  {Master_pos[i].x + Radius, Master_pos[i].y + Radius} };               //绘制三角形
    	fillpolygon(pts, 3);
    }
    

    当资源都加载完毕时,我们可以通过键盘操纵蛇的移动

    enum COMPRESS_E
    {
    	COMPRESS_UP,
    	COMPREES_DOWN,
    	COMPRESS_LEFT,
    	COMPRESS_RIGHT,
    };
    
    if (_kbhit())                                     //当探测到键盘输入时
    {
    	char input = _getch();
    	switch (input)
    	{
    		case KEY_UP:                                                                 
    			if (list_entry_firstnode(pos, Snake, node)->compass != COMPREES_DOWN)
    			{
    				list_entry_firstnode(pos, Snake, node)->compass = COMPRESS_UP; 
    			}   //按下向上键,当蛇头的移动方向不是向下时,蛇头的移动方向设置为向上
    			break;
    		case KEY_DOWN: 
    			if (list_entry_firstnode(pos, Snake, node)->compass != COMPRESS_UP)
    			{
    				list_entry_firstnode(pos, Snake, node)->compass = COMPREES_DOWN;
    			}   //按下向上键,当蛇头的移动方向不是向上时,蛇头的移动方向设置为向下
    			break;
    		case KEY_LEFT: 
    			if (list_entry_firstnode(pos, Snake, node)->compass != COMPRESS_RIGHT)
    			{
    				list_entry_firstnode(pos, Snake, node)->compass = COMPRESS_LEFT;
    			}   //按下向上键,当蛇头的移动方向不是向右时,蛇头的移动方向设置为向左
    			break;
    		case KEY_RIGHT: 
    			if (list_entry_firstnode(pos, Snake, node)->compass != COMPRESS_LEFT)
    			{
    				list_entry_firstnode(pos, Snake, node)->compass = COMPRESS_RIGHT;
    			}    //按下向上键,当蛇头的移动方向不是向左时,蛇头的移动方向设置为向右
    			break;
    	}
    	if (gamestatus == -1)
    	{
    		start_up();   //当游戏为结束状态时,按下任意键,重启
    	}
    }
    

    #define list_entry_firstnode(list,type,member)
    ((type*)((char*)((list)->next)-(unsigned long)(&((type*) 0)->member)))
    进入链表的第一个节点,可以直接求出蛇头。

    if (list_entry_firstnode(&list, Snake, node)->x >= WindowWidth|| list_entry_firstnode(&list, Snake, node)->x <= 0\
    	|| list_entry_firstnode(&list, Snake, node)->y <= 0|| list_entry_firstnode(&list, Snake, node)->y>=WindowHeight\
    	|| doublelink_nodenum(&list) < 2
    	)
    {
    	entrylist_foreach(&list, pos)
    	list_entry(pos, Snake, node)->color = GREEN; 
    	gamestatus = -1;  //碰撞检测,当蛇头到达界面的边缘时,游戏结束
    }
    
    entrylist_foreach((&list)->next, pos)
    {
    	if (list_entry(pos, Snake, node)->x == list_entry_firstnode(&list, Snake, node)->x && list_entry(pos, Snake, node)->y == list_entry_firstnode(&list, Snake, node)->y)
    	{
    		gamestatus = -1;   //碰撞检测,当蛇头与其他蛇身部位碰撞时,游戏结束
    	}
    }
    
    list_entry_firstnode(&list, Snake, node)->x += dirs[list_entry_firstnode(&list, Snake, node)->compass][0] * Speed*2; 
    //结合移动方向和速度更新蛇头的x坐标
    list_entry_firstnode(&list, Snake, node)->y += dirs[list_entry_firstnode(&list, Snake, node)->compass][1] * Speed*2;
    //结合移动方向和速度更新蛇头的y坐标
    
    entrylist_foreach(&list,pos)     
    {	
    	if (pos->next != (&list))   //更新蛇身各段的坐标
    	{
    	list_entry(pos->next, Snake, node)->x = list_entry(pos, Snake, node)->x - dirs[list_entry(pos, Snake, node)->compass][0] * Radius * 2;
    	list_entry(pos->next, Snake, node)->y = list_entry(pos, Snake, node)->y - dirs[list_entry(pos, Snake, node)->compass][1] * Radius * 2;
    	}
    }
    
    
    for (pos = (&list)->prev; pos != &list; pos = pos->prev)
    {
    	if (pos != (&list)->next)   //更新蛇身各段的移动方向
    		list_entry(pos, Snake, node)->compass = list_entry(pos->prev, Snake, node)->compass;
    }
    

    蛇的身体各段坐标计算方法如下:
    b36588f7-a131-429e-a74a-1ffeb6fbcef5-image.png
    当此时接收到键盘来的指令向下时,蛇头的运动方向变为向下,随后更新蛇的各个节点坐标。
    ea8b09f4-1b32-4551-8f3f-d1f7c47e642f-image.png
    随后,由于你的蛇头运动方向已经更改,所以蛇身的各个方向也会随着变化,后一个节点的运动方向将会继承前一个节点。
    073360f9-2fca-493c-a003-6276fc1e2a8b-image.png
    所以蛇身的轨迹更新模块,我们已经完成。
    下面还需要一些事件更新模块,比如说,蛇吃到食物,蛇身会变长,蛇遇到怪兽,蛇身会变短。

    void snake_eatfood(void)   //蛇吃到食物
    {
    	for (int i = 0; i < Food_Num; i++)
    	{
    		if ((list_entry_firstnode(&list, Snake, node)->x) >= Food_pos[i].x- Radius && (list_entry_firstnode(&list, Snake, node)->x) <= Food_pos[i].x + 3 * Radius &&\
    			(list_entry_firstnode(&list, Snake, node)->y) >= Food_pos[i].y- Radius && (list_entry_firstnode(&list, Snake, node)->y) <= Food_pos[i].y + 3 * Radius \
    			)     //坐标检测,由于食物占用一定空间,当蛇头的坐标与食物的坐标距离在一定范围内,认为蛇吃到食物
    		{
    			Food_pos[i].x = -1;
    			Food_pos[i].y = -1;
    			Snake* snake = (Snake*)malloc(sizeof(Snake));
    			snake->color = GREEN;
    			snake->compass = list_entry((&list)->prev,Snake,node)->compass;   
    			doublelink_inserttop(&list,&(snake->node)); 
    			return; //分配更多的节点,蛇身边长,这次是doublelink_inserttop,将节点加到蛇尾后边,分析方法跟之前insertback一样
    		}
    	}
    }
    
    void food_generate(void)  //生成食物
    {
    	int count = 0;
    	int index[Food_Num];
    
    	for (int i = 0; i < Food_Num; i++)
    	{
    		if ((Food_pos[i].x == -1 && Food_pos[i].y == -1))  //当食物坐标为x=-1,y=-1时,证明没有加载,需要生成
    		{
    			index[count]=i;
    			count++;
    		}
    	}
    
    	while(count)   //生成一定数量的食物,生成完退出
    	{
    		uint16_t x_rand = rand() % (WindowWidth - 2* Radius);
    		uint16_t y_rand = rand() % (WindowHeight - 2 * Radius);
    		int pos_legal = 1;
    
    
    		for (int i = 0; i < Food_Num; i++)
    		{
    			if ((Food_pos[i].x == x_rand && Food_pos[i].y == y_rand))  //假如新生成的食物坐标,与现有的食物重叠,判定不合法,重新生成
    			{
    				pos_legal = 0;
    				break;
    			}
    		}
    
    		for (int i = 0; i < Master_Num; i++) //假如新生成的食物坐标,与现有的怪兽坐标重叠,判定不合法,重新生成
    		{
    			if (Master_pos[i].x == x_rand && Master_pos[i].y == y_rand)
    			{
    				pos_legal = 0;
    				break;
    			}
    		}
    
    		if (pos_legal)    //合法食物坐标,可以生成
    		{
    			Food_pos[index[count-1]].x = x_rand;
    			Food_pos[index[count-1]].y = y_rand;
    			count--;
    		}
    	}
    }
    
    void snake_acrossmaster(void)
    {
    	for (int i = 0; i < Master_Num; i++)
    	{
    		if ((list_entry_firstnode(&list, Snake, node)->x) >= Master_pos[i].x - Radius && (list_entry_firstnode(&list, Snake, node)->x) <= Master_pos[i].x + Radius \
    			&& (list_entry_firstnode(&list, Snake, node)->y) >= Master_pos[i].y - Radius && (list_entry_firstnode(&list, Snake, node)->y) <= Master_pos[i].y + Radius
    			)  //坐标检测,由于怪兽占用一定空间,当蛇头的坐标与怪兽的坐标距离在一定范围内,认为蛇遇到怪兽
    		{        //撞到怪兽,每次减少两节蛇身,前面的代码我们判定,当蛇身小于2节时,游戏结束,所以来到这里,蛇身是大于2节的,能够删除
    			for (int j = 0; j < 2; j++)  
    			{
    				Snake* snake = list_entry((&list)->prev,Snake,node);
    				doublelink_removenode((&list)->prev);  //移除节点
    				if (snake != NULL)
    				{
    					free(snake);      //释放内存
    					snake = NULL;
    				}
    			}
    			Master_pos[i].x = -1;
    			Master_pos[i].y = -1;
    		}
    	}
    }
    
    void generate_master(void)   //生成怪兽
    {
    	uint8_t count = 0;
    	int index[Master_Num];
    
    	for (int i = 0; i < Master_Num; i++)  //当怪兽坐标为x=-1,y=-1时,证明没有加载,需要生成
    	{
    		if (Master_pos[i].x == -1 && Master_pos[i].y == -1)
    		{
    			index[count] = i;
    			count++;
    		}
    	}
    
    	while (count)
    	{
    		uint8_t poslegal=1;
    		uint16_t x_pos = Radius + rand() % (WindowWidth - 2 * Radius);
    		uint16_t y_pos = Radius + rand() % (WindowHeight - 2 * Radius);
    
    		for (int i = 0; i < Master_Num; i++)   //假如新生成的怪兽坐标,与现有的怪兽重叠,判定不合法,重新生成
    		{
    			if (Master_pos[i].x == x_pos && Master_pos[i].y == y_pos)
    			{
    				poslegal = 0;
    				break;
    			}
    		}
     
    		for (int i = 0; i < Food_Num; i++)  //假如新生成的怪兽坐标,与现有的食物重叠,判定不合法,重新生成
    		{
    			if (Food_pos[i].x == x_pos && Food_pos[i].y == y_pos)
    			{
    				poslegal = 0;
    				break;
    			}
    		}
    
    		if (poslegal)    //合法怪兽坐标,可以生成
    		{
    			Master_pos[index[count-1]].x = x_pos;
    			Master_pos[index[count-1]].y = y_pos;
    			count--;
    		}
    	}
    }
    

    至此,我们已经介绍完蛇身的坐标计算方法,食物的生成方法,蛇身的增长方法,怪兽的生成方法,已经遇到怪兽蛇身变短的计算方法。游戏基本运行逻辑已经建立起来。但是,我们前面说过,当蛇撞到墙(界面边缘)也会死去,触发游戏结束界面,此时,我们也应该把蛇的节点全部去掉,为下一轮游戏重启做准备。于是,我又添加了以下逻辑:

    if (gamestatus == -1)
    {
    	settextstyle(WindowHeight/ 20, 0, _T("宋体")); // 设置文字大小、字体
    	// 设定文字颜色,不同数值,颜色不同					
    	settextcolor(RGB(120, 120, 120));
    	RECT r = { 0,0,WindowWidth,WindowHeight };
    	if (gamestatus == -1)
    		drawtext(_T("游戏失败,按任意键重新开始"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
    	else if (gamestatus == 1)
    		drawtext(_T("游戏胜利,按任意键重新开始"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
    
    	if (!doublelink_isempty(&list))
    	{
    		Doublelink_listnode* temp = NULL;
    		entrylist_foreach_safe(&list, pos, temp)  //安全遍历链表
    		{
    			doublelink_removenode(pos);    //删除节点
    			Snake* snake = list_entry(pos, Snake, node);
    			if (snake != NULL)
    			{
    				free(snake);  //释放空间
    				snake = NULL;
    			}
    		}
    		temp = NULL;
    	}
    }
    

    值得注意的是,这里删除节点的遍历方法和之前的遍历方法不一样。
    我们引入了安全遍历,entrylist_foreach_safe(&list, pos, temp);
    #define entrylist_foreach_safe(head,pos,temp)
    for(pos=(head)->next,temp=pos->next;pos!=head;pos=temp,temp=pos->next)
    安全遍历与普通遍历相比,多了temp,记录将要遍历的下一个节点,假如我们利用普通遍历,删除节点的时候不记录下一个节点的方向,释放当前节点pos后,将无法进行下一步寻址。
    e2a9921a-fab9-45dd-b1e2-d52b41a5ed9b-image.png
    假设,此时调用,doublelink_removenode(pos);

    void doublelink_removenode(Doublelink_listnode* node)
    {
    	node->prev->next = node->next;
    	node->next->prev = node->prev;
    
    	node->next = node;
    	node->prev = node;
    }
    

    如上图所示,node2的前节点node3和后节点node1将会建立联系,node3->next=node1;node1->prev=node3;
    但是,经过这一步操作后,node2将会成为一个孤立的节点,他的next和prev将会指向自己。我们将无法通过node2找到他的下一个节点node1。
    ee185f32-f4da-4f30-b28d-fca0695f7f7e-image.png
    367c38af-c24b-425e-8f05-6d8c2f63e548-image.png
    当我们此时调用for(pos=(head)->next;pos!=(head);pos=pos->next),由于找不到pos->next所以会直接导致死机。
    而我们采用安全遍历,for(pos=(head)->next,temp=pos->next;pos!=head;pos=temp,temp=pos->next)可以有效地避免这一个问题。
    bece1482-d7c8-4841-ba7e-8af91abb88c4-image.png
    我们的创建一个指针temp,指向当前节点pos的下一个节点,假如我们执行doublelink_removenode时要移除pos时,由于我们已经保存了temp,我们下一轮直接读取temp,我们也可以进入到下一个节点。
    b3476773-2c77-4420-a056-dd67b1fac6e9-image.png
    至于我们之前蛇碰到食物时,为啥不用安全遍历,因为我们当时删除蛇身,不是用遍历链表的方法,而是用循环,直接找到尾节点删除的。
    c1d60bc9-a76b-40c3-b797-33bbb62b037c-image.png
    至此,我们游戏的各个模块都已经搭建完毕,下面我直接贴出运行代码,读者可以下载Easy X图形库,复制代码到VS 2022平台,直接运行,体验游戏。

    #include <stdio.h>
    #include <stdlib.h>
    #include<time.h>
    #include<stdint.h>
    #include<graphics.h>
    #include<conio.h>
    #include<assert.h>
    #include<math.h>
    #include<string.h>
    #include<time.h>
    
    #define WindowWidth    600
    #define WindowHeight   500
    #define Radius         10
    #define Speed          10
    #define Food_Num       5
    #define Master_Num     2
    #define ROWNUM         50
    #define COLNUM         60
    #define KEY_UP         72
    #define KEY_DOWN       80
    #define KEY_LEFT       75
    #define KEY_RIGHT      77
    #define Doublelink_Listinit(list) {&list,&list}
    #define list_entry(ptr,type,member) \
    	((type*)((char*)(ptr)-(unsigned long)(&((type*) 0)->member)))
    #define list_entry_firstnode(list,type,member)\
    	((type*)((char*)((list)->next)-(unsigned long)(&((type*) 0)->member)))
    #define entrylist_foreach(head,pos) \
    	for(pos=(head)->next;pos!=(head);pos=pos->next)
    #define entrylist_foreach_safe(head,pos,temp) \
    	for(pos=(head)->next,temp=pos->next;pos!=head;pos=temp,temp=pos->next)
    
    enum COMPRESS_E
    {
    	COMPRESS_UP,
    	COMPREES_DOWN,
    	COMPRESS_LEFT,
    	COMPRESS_RIGHT,
    };
    
    typedef struct doublelink_listnode
    {
    	doublelink_listnode* prev;
    	doublelink_listnode* next;
    }Doublelink_listnode;
    
    typedef struct
    {
    	float x;
    	float y;
    	COLORREF color;
    	COMPRESS_E compass;
    	Doublelink_listnode node;
    }Snake;
    
    typedef struct
    {
    	int x;
    	int y;
    }Coordinate;
    
    const int dirs[4][2] = { {0,-1},{0,1},{-1,0},{1,0} };
    Coordinate Food_pos[Food_Num];
    Coordinate Master_pos[Master_Num];
    Doublelink_listnode list;
    Doublelink_listnode* pos=NULL;
    int gamestatus = 0;
    void start_up(void);
    void show(void);
    void update(void);
    void food_generate(void);
    void generate_master(void);
    void snake_eatfood(void);
    void snake_acrossmaster(void);
    void doublelink_listinit(doublelink_listnode* list);
    void doublelink_insertback(Doublelink_listnode* list, Doublelink_listnode* node);
    void doublelink_inserttop(Doublelink_listnode* list, Doublelink_listnode* node);
    void doublelink_removenode(Doublelink_listnode* node);
    int doublelink_isempty(Doublelink_listnode* list);
    uint8_t doublelink_nodenum(const Doublelink_listnode* list);
    
    int main()
    {
    	start_up();
    	while (1)
    	{
    		show();
    		update();
    	}
    	_getch();
    	return 1;
    }
    
    void start_up(void)
    {
    	srand((unsigned)time(NULL));
    	initgraph(WindowWidth, WindowHeight);
    	BeginBatchDraw();
    	setbkcolor(RGB(0, 0, 0));
    	cleardevice();
    	gamestatus = 0;
    	list= Doublelink_Listinit(list);
    	for (int i = 0; i < 6; i++)
    	{
    		Snake* snake= (Snake*)malloc(sizeof(Snake));
    		snake ->x=(i* Radius*2)+100;
    		snake ->y= 300;
    		snake->color = RED;
    		snake->compass = COMPRESS_RIGHT;
    		doublelink_insertback(&list, &(snake->node));
    	}
    
    	for (int i = 0; i < Food_Num; i++)
    	{
    		Food_pos[i].x = -1;
    		Food_pos[i].y = -1;
    	}
    
    	for (int i = 0; i < Master_Num; i++)
    	{
    		Master_pos[i].x = -1;
    		Master_pos[i].y = -1;
    	}
    }
    
    void show(void)
    {
    	cleardevice();
    	entrylist_foreach(&list,pos)
    	{
    		setlinecolor(list_entry(pos, Snake, node)->color);
    		setfillcolor(list_entry(pos, Snake, node)->color);
    		fillcircle(list_entry(pos, Snake, node)->x, list_entry(pos, Snake, node)->y, Radius);
    	}
    
    	for (int i = 0; i < Food_Num; i++)
    	{
    		setlinecolor(HSVtoRGB(i * 30, 0.6, 1));
    		setfillcolor(HSVtoRGB(i * 30, 0.6, 1));
    		fillrectangle(Food_pos[i].x, Food_pos[i].y, Food_pos[i].x+(2* Radius), Food_pos[i].y + (2 * Radius));
    	}
    
    	for (int i = 0; i < Master_Num; i++)
    	{
    		setlinecolor(RED);
    		setfillcolor(BROWN);
    		POINT pts[] = { {Master_pos[i].x, Master_pos[i].y-Radius}, {Master_pos[i].x- Radius, Master_pos[i].y + Radius},  {Master_pos[i].x + Radius, Master_pos[i].y + Radius} };
    		fillpolygon(pts, 3);
    	}
    
    	if (gamestatus == -1)
    	{
    		settextstyle(WindowHeight/ 20, 0, _T("宋体")); // 设置文字大小、字体
    		// 设定文字颜色,不同数值,颜色不同					
    		settextcolor(RGB(120, 120, 120));
    		RECT r = { 0,0,WindowWidth,WindowHeight };
    		if (gamestatus == -1)
    			drawtext(_T("游戏失败,按任意键重新开始"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
    		else if (gamestatus == 1)
    			drawtext(_T("游戏胜利,按任意键重新开始"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
    
    		if (!doublelink_isempty(&list))
    		{
    			Doublelink_listnode* temp = NULL;
    			entrylist_foreach_safe(&list, pos, temp)
    			{
    				doublelink_removenode(pos);
    				Snake* snake = list_entry(pos, Snake, node);
    				if (snake != NULL)
    				{
    					free(snake);
    					snake = NULL;
    				}
    			}
    			temp = NULL;
    		}
    	}
    	Sleep(100);
    	FlushBatchDraw();
    }
    
    void update(void)
    {
    	if (_kbhit())
    	{
    		char input = _getch();
    		switch (input)
    		{
    			case KEY_UP:   
    				if (list_entry_firstnode(pos, Snake, node)->compass != COMPREES_DOWN)
    				{
    					list_entry_firstnode(pos, Snake, node)->compass = COMPRESS_UP;
    				}
    				break;
    			case KEY_DOWN: 
    				if (list_entry_firstnode(pos, Snake, node)->compass != COMPRESS_UP)
    				{
    					list_entry_firstnode(pos, Snake, node)->compass = COMPREES_DOWN;
    				}
    				break;
    			case KEY_LEFT: 
    				if (list_entry_firstnode(pos, Snake, node)->compass != COMPRESS_RIGHT)
    				{
    					list_entry_firstnode(pos, Snake, node)->compass = COMPRESS_LEFT;
    				}
    				break;
    			case KEY_RIGHT: 
    				if (list_entry_firstnode(pos, Snake, node)->compass != COMPRESS_LEFT)
    				{
    					list_entry_firstnode(pos, Snake, node)->compass = COMPRESS_RIGHT;
    				}
    				break;
    		}
    		if (gamestatus == -1)
    		{
    			start_up();
    		}
    	}
    
    	if (list_entry_firstnode(&list, Snake, node)->x >= WindowWidth|| list_entry_firstnode(&list, Snake, node)->x <= 0\
    		|| list_entry_firstnode(&list, Snake, node)->y <= 0|| list_entry_firstnode(&list, Snake, node)->y>=WindowHeight\
    		|| doublelink_nodenum(&list) < 2
    		)
    	{
    		entrylist_foreach(&list, pos)
    		list_entry(pos, Snake, node)->color = GREEN;
    		gamestatus = -1;
    	}
    
    	entrylist_foreach((&list)->next, pos)
    	{
    		if (list_entry(pos, Snake, node)->x == list_entry_firstnode(&list, Snake, node)->x && list_entry(pos, Snake, node)->y == list_entry_firstnode(&list, Snake, node)->y)
    		{
    			gamestatus = -1;
    		}
    	}
    
    	list_entry_firstnode(&list, Snake, node)->x += dirs[list_entry_firstnode(&list, Snake, node)->compass][0] * Speed*2;
    	list_entry_firstnode(&list, Snake, node)->y += dirs[list_entry_firstnode(&list, Snake, node)->compass][1] * Speed*2;
    
    	entrylist_foreach(&list,pos)
    	{	
    		if (pos->next != (&list))
    		{
    			list_entry(pos->next, Snake, node)->x = list_entry(pos, Snake, node)->x - dirs[list_entry(pos, Snake, node)->compass][0] * Radius * 2;
    			list_entry(pos->next, Snake, node)->y = list_entry(pos, Snake, node)->y - dirs[list_entry(pos, Snake, node)->compass][1] * Radius * 2;
    		}
    	}
    	
    
    	for (pos = (&list)->prev; pos != &list; pos = pos->prev)
    	{
    		if (pos != (&list)->next)
    			list_entry(pos, Snake, node)->compass = list_entry(pos->prev, Snake, node)->compass;
    	}
    	food_generate();
    	generate_master();
    	snake_eatfood();
    	snake_acrossmaster();
    }
    
    void food_generate(void)
    {
    	int count = 0;
    	int index[Food_Num];
    
    	for (int i = 0; i < Food_Num; i++)
    	{
    		if ((Food_pos[i].x == -1 && Food_pos[i].y == -1))
    		{
    			index[count]=i;
    			count++;
    		}
    	}
    
    	while(count)
    	{
    		uint16_t x_rand = rand() % (WindowWidth - 2* Radius);
    		uint16_t y_rand = rand() % (WindowHeight - 2 * Radius);
    		int pos_legal = 1;
    
    
    		for (int i = 0; i < Food_Num; i++)
    		{
    			if ((Food_pos[i].x == x_rand && Food_pos[i].y == y_rand))
    			{
    				pos_legal = 0;
    				break;
    			}
    		}
    
    		for (int i = 0; i < Master_Num; i++)
    		{
    			if (Master_pos[i].x == x_rand && Master_pos[i].y == y_rand)
    			{
    				pos_legal = 0;
    				break;
    			}
    		}
    
    		if (pos_legal)
    		{
    			Food_pos[index[count-1]].x = x_rand;
    			Food_pos[index[count-1]].y = y_rand;
    			count--;
    		}
    	}
    }
    
    void snake_eatfood(void)
    {
    	for (int i = 0; i < Food_Num; i++)
    	{
    		if ((list_entry_firstnode(&list, Snake, node)->x) >= Food_pos[i].x- Radius && (list_entry_firstnode(&list, Snake, node)->x) <= Food_pos[i].x + 3 * Radius &&\
    			(list_entry_firstnode(&list, Snake, node)->y) >= Food_pos[i].y- Radius && (list_entry_firstnode(&list, Snake, node)->y) <= Food_pos[i].y + 3 * Radius \
    			)
    		{
    			Food_pos[i].x = -1;
    			Food_pos[i].y = -1;
    			Snake* snake = (Snake*)malloc(sizeof(Snake));
    			snake->color = GREEN;
    			snake->compass = list_entry((&list)->prev,Snake,node)->compass;
    			doublelink_inserttop(&list,&(snake->node));
    			return;
    		}
    	}
    }
    
    void generate_master(void)
    {
    	uint8_t count = 0;
    	int index[Master_Num];
    
    	for (int i = 0; i < Master_Num; i++)
    	{
    		if (Master_pos[i].x == -1 && Master_pos[i].y == -1)
    		{
    			index[count] = i;
    			count++;
    		}
    	}
    
    	while (count)
    	{
    		uint8_t poslegal=1;
    		uint16_t x_pos = Radius + rand() % (WindowWidth - 2 * Radius);
    		uint16_t y_pos = Radius + rand() % (WindowHeight - 2 * Radius);
    
    		for (int i = 0; i < Master_Num; i++)
    		{
    			if (Master_pos[i].x == x_pos && Master_pos[i].y == y_pos)
    			{
    				poslegal = 0;
    				break;
    			}
    		}
    
    		for (int i = 0; i < Food_Num; i++)
    		{
    			if (Food_pos[i].x == x_pos && Food_pos[i].y == y_pos)
    			{
    				poslegal = 0;
    				break;
    			}
    		}
    
    		if (poslegal)
    		{
    			Master_pos[index[count-1]].x = x_pos;
    			Master_pos[index[count-1]].y = y_pos;
    			count--;
    		}
    	}
    }
    
    void snake_acrossmaster(void)
    {
    	for (int i = 0; i < Master_Num; i++)
    	{
    		if ((list_entry_firstnode(&list, Snake, node)->x) >= Master_pos[i].x - Radius && (list_entry_firstnode(&list, Snake, node)->x) <= Master_pos[i].x + Radius \
    			&& (list_entry_firstnode(&list, Snake, node)->y) >= Master_pos[i].y - Radius && (list_entry_firstnode(&list, Snake, node)->y) <= Master_pos[i].y + Radius
    			)
    		{
    			for (int j = 0; j < 2; j++)
    			{
    				Snake* snake = list_entry((&list)->prev,Snake,node);
    				doublelink_removenode((&list)->prev);
    				if (snake != NULL)
    				{
    					free(snake);
    					snake = NULL;
    				}
    			}
    			Master_pos[i].x = -1;
    			Master_pos[i].y = -1;
    		}
    	}
    }
    
    void doublelink_listinit(Doublelink_listnode* list)
    {
    	list->prev = list;
    	list->next = list;
    }
    
    void doublelink_insertback(Doublelink_listnode* list, Doublelink_listnode* node)
    {
    	node->next = list->next;
    	list->next->prev = node;
    	list->next = node;
    	node->prev = list;
    }
    
    void doublelink_inserttop(Doublelink_listnode* list, Doublelink_listnode* node)
    {
    	list->prev->next = node;
    	node->prev = list->prev;
    	node->next = list;
    	list->prev = node;
    }
    
    void doublelink_removenode(Doublelink_listnode* node)
    {
    	node->prev->next = node->next;
    	node->next->prev = node->prev;
    
    	node->next = node;
    	node->prev = node;
    }
    
    int doublelink_isempty(Doublelink_listnode* list)
    {
    	return (list->next == list);
    }
    
    uint8_t doublelink_nodenum(const Doublelink_listnode* list)
    {
    	uint8_t len = 0;
    	const Doublelink_listnode* p = list;
    
    	while (p->next != list)
    	{
    		len++;
    		p=p->next;
    
    	}
    	return len;
    }
    
    sky-littlestarS 1 条回复 最后回复
    1
    • sky-littlestarS 离线
      sky-littlestarS 离线
      sky-littlestar
      编写于 最后由 编辑
      #2

      各位大佬一起来交流,多多指教

      1 条回复 最后回复
      0
      • sky-littlestarS 离线
        sky-littlestarS 离线
        sky-littlestar
        回复了sky-littlestar 最后由 编辑
        #3

        @sky-littlestar 在 rt-thread 双链表分析及教程(附带demo) 中说:

        其数据结构如下表示(以双链表为例子)
        typedef struct DLnode
        {
        int data; //也可以是其他数据类型
        DLnode* next;
        DLnode* next;
        }DLnode_list;

        (更正)

        其数据结构如下表示(以单链表为例子)
        typedef struct SLnode
        {
        int data; //也可以是其他数据类型
        SLnode* next;
        }SLnode_list;
        更正一下,上面的图是单链表,我这边改单链表说明一下,原理是一样的。

        1 条回复 最后回复
        0
        • SPeakS 离线
          SPeakS 离线
          SPeak d2learn-dev
          编写于 最后由 编辑
          #4

          感觉是不是可以拆分一下, 把游戏主体 和 链表实现 分开 用比较项目的目录树风格, 然后放到github上 变成一个Demo项目

          sky-littlestarS 1 条回复 最后回复
          1
          • sky-littlestarS 离线
            sky-littlestarS 离线
            sky-littlestar
            回复了SPeak 最后由 编辑
            #5

            @SPeak 在 rt-thread 双链表分析及教程(附带demo) 中说:

            感觉是不是可以拆分一下, 把游戏主体 和 链表实现 分开 用比较项目的目录树风格, 然后放到github上 变成一个Demo项目

            这个写法应该不错,博主有没有什么参考的博文推荐,我学习一下他的写法

            SPeakS 1 条回复 最后回复
            0
            • SPeakS 离线
              SPeakS 离线
              SPeak d2learn-dev
              回复了sky-littlestar 最后由 编辑
              #6

              @sky-littlestar 我说的意思就是把你上面的代码 整理一下 用项目(Demo)的方式 放到github, 不是说其他什么写法

              1 条回复 最后回复
              1

              • 登录

              • 没有帐号? 注册

              • 登录或注册以进行搜索。
              d2learn forums Powered by NodeBB
              • 第一个帖子
                最后一个帖子
              0
              • 版块
              • 最新
              • 标签
              • 热门
              • Online Tools
              • 用户
              • 群组