- 系统类型:Windows
- 系统版本:Windows 11
- 编译器版本:g++ 8.1.0及以上
- 所需最低C++标准:C++ 11
- 本项目使用CMake进行打包和管理,CMake的配置文件
CMakeLists.txt
和src
在同一目录下。 - 项目代码和资源均放置到
src
目录下。其中code
目录存放源代码,include
目录存放头文件,config
、maps
、record
目录分别存放配置文件、地图文件和游戏记录文件。另外。last.txt
文件保存用户当前的配置和地图信息。 code
目录下是项目的主体文件。其中,play.cpp
、funda.cpp
和extension.cpp
分别存放游戏入口、基础功能函数定义和周边功能函数定义。include
目录下是项目的头文件。其中,data.h
、funda.h
和extension.h
中分别存放项目的全局变量和自定义结构体、基础功能函数声明和周边功能函数声明
本部分介绍游戏基础功能(地图生成、食物生成、蛇的移动和增长等)的实现逻辑,主要对 funda.cpp
文件的内容进行解释。
在游戏的基础部分,主要有三个关键的参与对象:
- 地图(stage)
- 食物(food)
- 蛇(snake)
下面分别介绍三个关键对象的实现逻辑:
地图采用二维数组进行存储:
extern int stage[maxn][maxn];
对于地图上的某个位置 stage[i][j]
:
- 其值为0表示正常,即蛇允许移动到此处且无事发生;
- 其值为负表示蛇移动到此处则游戏结束(-1表示该格为蛇自身或者墙壁所占据;-2表示有障碍物,这将在后面的周边功能介绍部分进一步说明);
- 其值为正表示蛇允许移动到此处,但会"有事发生"(1,2,3分别表示蛇吃到了对应分值的食物并实现增长;5,6,7,8分别表示蛇将穿越虚的上、下、左、右墙)
地图打印通过 void _printstage(int,int)
函数进行。在打印墙壁的任务之外,此函数还完成了对墙壁位置的赋值(-1或5~8)和障碍物的打印(具体内容参见后续周边功能介绍部分)。
食物使用 void _printfood(int,int)
函数进行生成,两个参数分别表示食物产生区域的高和宽。
食物的分值和位置均采用采用 rand()
伪随机函数进行生成,在食物生成后亦对 stage
中对应的位置赋予了相应的分值。具体实现参见 funda.cpp
文件的源码。
蛇采用链表形式进行存储:
struct snake {
coordinate coor;
snake *next;
};
extern snake *tail,*head;//分别存储蛇尾和蛇头(为了使操作便捷,令蛇尾作为链表头)
/*coordinate是存储坐标信息的结构体
struct coordinate {
int x,y;
bool operator==(const coordinate &other){
return x==other.x&&y==other.y;
}
};
*/
之所以使用链表存储,是考虑到:
- 蛇的位置信息为一维长链结构,较为符合链表这一数据结构形式;
- 我们一开始也考虑过使用
coordinate
数组存储蛇结构,但数组存储在每次更新蛇位置时需要将整个数组后移一位,时间复杂度为$O(n)$,较为繁琐;而链表只需删除蛇尾并添加蛇头,时间复杂度为$O(1)$,且操作更为便捷。故综合考虑后使用链表存储蛇信息。
蛇的移动涉及到两个函数:void _refresh()
和 void _getcontrol()
。前者用于每隔一个周期刷新蛇的位置,后者用于处理用户输入。
这两个函数通过C++ 11引入的 std::thread
类进行双线程操作,即"刷新蛇的位置"和"读取用户输入"两个函数平行执行。
此函数每隔一个周期(1/difficulty
秒)刷新一次。其实现的功能有:
- 更新蛇的移动方向
- 根据蛇当前的移动方向判断蛇下一步的行为:是蛇的移动、蛇身增长还是游戏结束
- 更新蛇链表的信息
此函数为简单处理用户输入:用户输入w/a/s/d则更新 direction1
(direction1
为暂存的移动方向,此值只有不和 direction
方向相反才会在 _refresh
中得到更新),为空格则暂停/继续;暂停状态下输入q则游戏退出。
void _opposite()
:对反方向进行标记,方便后续方向的更新void _gotoxy(SHORT,SHORT)
:移动光标到指定坐标$(x,y)$void _closecursor(bool)
:关闭光标显示bool _interface()
:主界面的打印并处理用户输入。其中other
函数处理用户跟周边有关的输入,详见后一部分。void _countdown()
:进入游戏的简易倒计时。
游戏周边功能主要实现了三项功能:
- 用户自定义配置:游戏难度、随机种子、食物数量和食物生成概率;
- 地图配置功能:地图大小、四面墙的虚实、障碍物的数量和障碍物的坐标
- 游戏记录回放功能:用户保存游戏记录并可在主界面查看回放
本部分主要在extension.cpp
文件中予以实现,下面具体说明:
实现这部分的核心函数是_config_i
、_config_u
和_update_config
。辅助函数有_validconfig_i
- 首先读取用户的输入配置名(同时对文件名合法性予以判断)
- 然后读取用户输入的配置,并使用
validconfig_i
函数判断配置合法性,添加.config
后缀后将其存储在../src/config
路径下(程序运行文件在和src
文件平级的build
目录下,下类似)。
- 首先读取用户的输入配置名(同时对文件名合法性予以判断)
- 使用
_update_config
函数更新与配置有关的全局变量(此函数具体实现见后文)
读取指定文件内容(同时对文件的存在性做出判断),并赋值给difficulty、seed、foodquan、foodprob.a、foodprob.b、foodprob.c
来配置信息(分别是游戏难度、随机种子、食物数量和一分、二分、三分食物的生成概率)
实现这部分的核心函数为_map_n
、_map_m
和_update_map
。除此之外,还使用了_input_map
、_printmap
和_valid_mapsize
等辅助函数。
下对其具体实现予以说明:
- 首先读取用户输入的地图名(同时对文件名的合法性予以判断)
- 要求用户输入地图的宽和高,同时使用
_valid_mapsize
判断用户是否输入是否合法 - 接下来进行地图的可视化创建功能。用户每次输入一行指令,该指令通过
_input_map
函数予以读取并处理(该函数的内部实现逻辑见后文),_input_map
函数的返回值:
- 为负,代表用户的指令不合法,程序会要求用户重新输入
- 为1,代表用户的指令合法(o指令、p指令或w/a/s/d指令),之后执行
_printmap
函数(内部逻辑参见后文),重新打印当前地图; - 为0,代表用户输入了f指令,即退出地图创建过程并保存;
- 为2,代表用户输入了q指令,即退出创建过程且不保存
- 最后,添加后缀
.map
,并存储文件到../src/maps/
路径下。
int _input_map(std::string &str,int w,int h,int&w1,int&s,int&a,int&d,std::vector<coordinate>&obstacle)
对地图创建过程中用户的指令予以判断:
- 若str是f和q,分别返回0和2
- 若str中第一位是
o
,则使用isstreamstring
流分割后续输入;若后续输入是合法指令,则在存储障碍物坐标的obstacle
数组中添加一个元素;反之,返回-1 - 若str中第一位是
p
则使用isstreamstring
流分割后续输入;若后续输入是合法指令,则在存储障碍物坐标的obstacle
数组中删除指定元素;反之,返回-1 - 若str中第一位是
w/s/a/d
,则使用isstreamstring
流分割后续输入;若后续输入是合法指令,则改变对应的标记墙壁虚实的变量w1/s/a/d
- 其他情况,返回-1
打印创建过程中的函数,基本和print_satge
函数相同,只是:
- 根据对应墙壁的虚实性判断墙壁应打印为绿色还是红色
- 若某位置标记有障碍物,则打印青色的感叹号
- 首先读取用户输入的地图名(同时对文件名合法性予以判断)
- 使用
_update_map
函数更新与地图有关的全局变量(此函数具体实现见后文)
读取指定文件内容(同时对文件的存在性做出判断),并更新与地图有关的全局变量:
-
更新
width、height、ww、ws、wa、wd
(分别是地图的宽、高和上下左右墙壁的虚实性) -
在
int stage[maxn][maxn]
有障碍物的位置标记-2
实现这部分的核心函数为_record_b
、_record_r
。除此之外,还使用_printfood_given
、_refresh_record
、_initialize_record
和_getcontrol_record
进行辅助实现
具体实现如下:
游戏回放部分的核心之一就是记录文件格式的设计。我们采用的思路是:
- 前两行分别存储配置名称和地图名称
- 第三行存储游戏一开始生成的食物的坐标
- 接下来直到文件结束,每行存储四个信息,分别是生成食物的x,y坐标,食物分值(若没有食物生成,这三个值均为0)和当前蛇的移动方向
- 此外,每局游戏开始的时候,都会打开一个临时文件
std::ofstream tempfile("../src/record/temp")
,用于存储本局游戏的信息(格式同上)。游戏结束,并且用户输入b以创建回放记录的情况下,该文件会被保留并被重新命名;在其他情况下(用户不想创建回放;游戏中途退出等),该文件都会被删除
用于在用户游戏结束的时候保存游戏记录文件
- 首先要求用户输入记录名称(并对输入合法性予以判断)
- 将
temp
临时文件重命名为用户输入的名称,并添加后缀.rec
后存储在../src/record
路径下。同时。如果用户输入的记录名称存在,应当删除原文件。
用于回放保存的记录文件
- 首先要求用户输入回放的名称(同时对该回放是否存在予以判断)
- 接下来,读取文件信息并生成回放:
- 读取配置信息和地图信息并更新数据(如果配置和地图已被删除,会退出回放)
- 使用
_initialize_record
函数初始化回放(具体见后文);并用_print_given
函数在指定位置生成指定分值的游戏一开始生成的食物 - 新建一个
_getcontrol_record
线程在后台运行。当用户输入q
的时候会退出回放 while
不断读取文件输入并存储到变量x,y,v,direction
中,并由此生成食物(_printfood_given(x,y,v)
)和刷新蛇的位置(_refresh_record()
)。循环当文件无法读取输入的时候结束,并执行结束操作(将蛇变红等)
-
用于在指定位置打印食物。实现逻辑基本与基础功能中的该函数相同。
-
同时,如果
x,y,v
的值不合法(比如超出边界,分值不是1~3等等),将返回false
并指示回放结束
- 用于刷新蛇的位置。实现逻辑基本与基础功能的
_refresh
函数相同 - 同时,如当前方向
direction
的值不合法,将会返回false
并指示回放结束
分别用于将字符串转整数和小数,同时用于在用户新建配置文件等处判断输入的合法性与否(不合法返回负值)
用于判断用户输入的文件名称是否合法(只能由数字、字母和下划线组成)
用于主界面读取g/q/help
之外的其他信息并做出反应(启动周边功能或返回输入不合法)
用于保存用户的配置和地图,并存储到last.txt
文件里
用于载入last.txt
文件中的配置和地图