【项目分享】 贪吃蛇大冒险!(C语言+数据结构复习)


Gitee仓库:

贪吃蛇大冒险: 贪吃蛇大冒险!


目录

前言:

[Part1. 效果展示](#Part1. 效果展示)

[Part2. 基础架构](#Part2. 基础架构)

[Part3. 打印效果实现](#Part3. 打印效果实现)

[Part4. 关卡模式实现](#Part4. 关卡模式实现)

[Part5. 自设关卡实现](#Part5. 自设关卡实现)

[Part5.1. 创建关卡](#Part5.1. 创建关卡)

[Part5.2. 游玩关卡](#Part5.2. 游玩关卡)

[Part6. 皮肤界面](#Part6. 皮肤界面)

[Part7. 反思与不足](#Part7. 反思与不足)

[Part7.1. "魔法值"过多](#Part7.1. “魔法值”过多)

[Part7.2. 低内聚](#Part7.2. 低内聚)

[Part7.3. 高耦合](#Part7.3. 高耦合)

[Part7.4. 资源管理](#Part7.4. 资源管理)

[Part7.5. 其他问题](#Part7.5. 其他问题)

[Part8. 结语](#Part8. 结语)


前言:

前段时间也是终于完成了这个贪吃蛇这个项目,我们来剖析一下这个的大致实现和其中的一些架构吧。


let's go!!!!!


Part1. 效果展示

贪吃蛇大冒险!


Part2. 基础架构

我们先来看代码:


cpp 复制代码
typedef enum direction_about { up, down, left, right }direction_about;//枚举贪吃蛇的方向

typedef enum state_about {normal,fit_by_myself,fit_by_wall,out,fit_by_barrier,victory}state_about;//枚举游戏的状态

struct snackad_about;//前置声明

typedef struct game_state_about//游戏状态相关的属性全部归属于此
{
	state_about state;
	int food_score;
	int score;
	int end_condition;
	int level_num;
	int level_ok[4];
	int used_skin;
	int skin_ok[2];
	int level_max_score[4];
	char level_name[100];
}game_state_about;

typedef struct food_about//有关食物的样式、分布、作用函数
{
	wchar_t looks;
	struct queue* distri;
	void(*effect)(struct snackad_about* snacked);
}food_about;

typedef struct endfood_about//有关终止食物的样式、分布、作用函数
{
	wchar_t looks;
	int x;
	int y;
	void(*effect)(struct snackad_about* snacked);

}endfood_about;

typedef struct barrier_about//有关阻拦的样式、分布、作用函数
{
	wchar_t looks;
	struct queue* distri;
	void(*effect)(struct snackad_about* snacked);
}barrier_about;

typedef struct portal_about//有关传送门的样式、分布、作用函数
{
	wchar_t looks;
	struct queue* distri;
	void(*effect)(struct snackad_about* snacked);
}portal_about;

typedef struct stack//数据结构-栈
{
	int* x;
	int* y;
	int top;
	int capacity;
}stack;

typedef struct wall_about//有关墙的样式、分布、作用函数
{
	wchar_t looks;
	stack* distri;
	void(*effect)(struct snackad_about* snacked);
}wall_about;

typedef struct item_about//道具相关的总结构体
{
	wall_about wall;
	food_about food;
	endfood_about endfood;
	barrier_about barrier;
	portal_about portal;
}item_about;

typedef struct listnode//链表节点
{
	int x;
	int y;
	struct listnode* next;
}listnode;

typedef struct queue//数据结构-队列
{
	listnode* start;
	listnode* end;
}queue;

typedef struct snack_about//有关蛇相关的属性集合在这个结构体
{
	wchar_t looks;
	queue* body;
	direction_about direct;
	int speed;
	int next_pos_x;
	int next_pos_y;
	void(*effect)(struct snackad_about* snacked);
}snack_about;

typedef struct snackad_about//统揽全局的总结构体
{
	snack_about snack;
	item_about item;
	game_state_about game_state;
}snackad_about;

我们利用了很多结构体将这个游戏中可能会出现的所有属性全部打包到我们的这个大结构体中,方便统一管理。同时在函数传参的时候,我们也可以避免传太多的参数,只要传一个总的结构体就好。


Part3. 打印效果实现

游戏中的一些界面不是直接就冒出来,是逐渐浮现出来的。这个就是在每个字打印之间,利用sleep()函数达到一个延缓的效果。同样的,还有规则界面逐渐消失的效果,这个是从末尾到开头逐渐打印空格达到一种类似消失的效果
我们来看代码:


cpp 复制代码
void gradually_printf(int x, int y, int longth, int sleep_num, char* arr)
{
	assert(arr);
	cursor_position(x, y);
	for (int i = 0; i < longth; i++)
	{
		printf("%.3s", arr + 3 * i);
		Sleep(sleep_num);
	}
}

cpp 复制代码
	for (int i = 13; i >=0; i--,i--)
	{
		for (int q = arr[i/2] * 2; q >=0; q--)
		{
			cursor_position(q,i);
			wprintf(L"%c", L' ');
			Sleep(3);
		}
	}

一开始在构思的时候就感觉到了,在电脑上实现游戏本质上和变魔术也差不多。游戏里的碰撞不过是坐标相同的"谎言",贪吃蛇的移动也不过是拆东墙补西墙的"愚蠢"。我们要实现的就是在后端检测到这些后,在前端表现出能让游玩者感觉到就应该如此的效果,从而理解,而这就是独属于程序员的魔法。


Part4. 关卡模式实现

关卡模式的实现主要在于如何正确进入相关的关卡和如何实现最高分数和是否通关两大功能,我们来逐个看一下吧。
如何正确进入相关的关卡?我们来看:



那我们如何实现最高分数和是否通关呢?我们通过文件来实现,首先在我们的游戏状态这个结构体中由level_ok记录每关的通关情况,在关卡结束时,如果通关就修改值并且载入文件。在下一次启动游戏时,会读取到已经通过的信息,从而在打印时处理。显示最高分也是相同的逻辑。


Part5. 自设关卡实现

我感觉在所有功能中最难的就是这个自设关卡了,他主要分为两个部分,一个是创建关卡,另一个是游玩自己设计的关卡,并同样要像上面的关卡模式一样显示最高分数和是否通关的信息。我们来分别看看吧:


Part5.1. 创建关卡

我们看到的界面是这样:



实际上,在数据的处理上采用了一个二维数组去映射这个图中的二维地图,同时为了节省内存采用了char类型的数组和设置大小为32*30(因为虽然这个地图为64*30,但是由于我们打印的都是宽字符,只会出现在x轴上为偶数的地方,这样我们只要取一半,最后映射的时候将x轴坐标乘以二就好了)。我们在上面输入、显示,在数据的处理层实际上是在改变这个二维数组中存储的值。但是这样是不全面的,因为我们的传送阵是要相邻放置的之间相互传送,因此我们要对他进行单独的处理,同时在其他道具覆盖他时特殊处理。



我们在这个界面输入不属于是地图信息的其他信息,最后在这两个都处理完了,将数据用文件存储后,就要在游玩界面那边进行显示、操作了。


Part5.2. 游玩关卡

首先,我们怎么知道我们创建了多少关卡呢?这里采用了文件的序列化管理,也就是:



我们在读取的时候用for循环,i一步步的增大,我们在采用字符串拼接的操作就可以将文件的名字给创建出来,区别就在于文件的"编号"不同,且由于编号是有序的,我们就可以循环读取打开,直到打开的文件不存在pf为NULL为止,结束循环,这样就知道自设关卡有多少个了。同时在我们读取打开的过程中要读取文件的数据(即最高分数等),这样就完成了游玩界面的打印:



同时我们这还要支持删除功能,这个功能要做到两个事:一是要删除这个对应的文件,这个简单根据箭头的坐标推断出文件的序列,得到文件名,删除。二是,我们要把后面的文件序列往前,就像顺序表删除中间元素后面元素要往前补一样。这样我们就完成这个界面的功能了,剩下的就是读取文件内容,生成地图,结束后改变文件中的数据(最高分数,是否通关)。


Part6. 皮肤界面

皮肤界面的实现主要还是皮肤的展示,就是放在那这个皮肤自己动,这个的实现就是在循环检测是否按按键的时候加上一个else{····},这个里面写函数实现蛇皮肤的运动。


Part7. 反思与不足

在实现这个项目的过程中,我也发现了很多问题,经过和AI的交流主要分为以下几个方面,我列举出来,希望能对读者在未来遇到相似情况时有所帮助。


Part7.1. "魔法值"过多

所谓的魔法值就是在项目的实现中一些地方直接写数据,比如皮肤需要的金币数目直接写了个4,这个不利于代码的可读性和维护,我们应该利用宏来赋有意义值,比如"skin_money"等来使得代码更规范。


Part7.2. 低内聚

所谓的内聚就是一个函数实现的功能的"纯度",即就是单一个函数实现的功能不能太多,你不能把打印、处理数据、渲染等所有功能全放到一个函数,这样不利于代码的维护和代码未来的复用性。


Part7.3. 高耦合

所谓的耦合就是几个函数之间相互影响的度,即当未来我修改一个函数的时候会对另一个函数造成的影响的度,高耦合就是当我修改一个函数时会对另一个函数造成显著影响甚至让其直接不能使用,这样不利于代码的维护和增加了代码的复杂性。


Part7.4. 资源管理

这个算是经典的问题了,malloc后没有free,打开文件未关闭等等都是会造成资源浪费的行为(到C++有自动析构会好管理一些,C语言只有好好注意了)。


Part7.5. 其他问题

其他问题就主要是算法方面的了,比如我们在碰撞检测的时候,我这里是遍历查找,这个的时间浪费就有点多了,这里可以采取哈希表等的方法来优化。


Part8. 结语

这篇文章粗略介绍了一下项目的实现逻辑,希望对读者们有所帮助。(p.s. 游戏估计会有一些bug,请大家见谅,谢谢啦~)
最后,祝大家可以:春风得意马蹄疾,一日看尽长安花!

最后的最后,要是觉得本文还可以的话,可以点点赞,关注小编一波,谢谢大家!~