【编程日常NO.00001】DOS贪吃蛇小程序的编写by arttnba3

前言

作为一名C/C++刚刚入门的小萌新,在刚刚学会敲代码后不久,便不自量力地着手开始尝试编写各种小程序

由于贪吃蛇看着好写,于是咱便“磨刀霍霍向IDE”…

事实证明萌新的代码力还是不够(趴)

在经历了一个下午与一个晚上的“奋战”后,才最终实现了一个贪吃蛇小游戏的基本功能_(XD)∠)_

由于DOS刷新机制十分蛋疼的缘故,游戏效果也十分蛋疼,等有时间再弄上图形化窗口(无限期咕咕咕)

基本原理

一、地图刷新与小蛋糕生成机制

(1)多维数组:设置多维度地图

要定义一个基础的游戏地图,我们很容易便能想到利用多维数组表示多维坐标
这一次我们所编写的是二维平面内的贪吃蛇,因此简单的设置一个二维数组便可:

char map[20][90]={0};//由于dos的布局十分蛋疼,因此我们定义一个长90高20的地图,利用C特性使其内的值全都被初始化为0,即空格字符
 /*
   x x x x x
y
y
y
y
y
y
*/

(2)system()函数:清空屏幕上所有字符,进行屏幕刷新

system()函数包含于stdlib.h头文件当中,接受一个字符串参数,在Windows系统下执行DOS命令,在Linux/UNIX系统下执行shell命令
我们已知cls命令在DOS下是清空屏幕字符,那么用此函数我们便可以在每一次贪吃蛇爬行结束后清空屏幕再重新输出,以实现屏幕刷新机制

#include//C++特有形式,当然若是写C当中的stdlib.h也是完全没问题
...
system("cls");//清空屏幕内容

(3)srand()&rand()&time():随机生成小蛋糕位置

rand()函数为C当中用以生成伪随机数的函数,同样包含于stdlib.h当中,不接收任何参数,返回值为一遵循某一特定规律的整数
srand()函数为rand()函数的初始化函数,接收一个整型参数作为rand()函数的“种子”,即rand()以这个“种子”为初始参数生成的随机数数列
time()函数包含于time.h接受一个time_t类型的指针作为参数,将当前自UNIX初始纪元(1970/1/1 00:00:00)起所经过的秒数赋予所输入指针所指向的地址并将秒数作为返回值
因为时间值是在不断变化着且不会重复的,将time()的返回值作为“种子”输入srand()当中,我们就可以利用rand()生成完全随机的小蛋糕的坐标:

	time_t t;
	srand(time(t));
	cake_x=rand()%90;//控制x坐标在[0,89]以内
	cake_y=rand()%20;//控制y坐标在[0,19]以内
	int i;
	//检查蛋糕是否意外生成在?体内
	for(i=0;i

二、 贪吃蛇爬行机制

(1)kbhit()函数:监听键盘

监听键盘,顾名思义,kbhit()函数在键盘没有输入时返回0,在监听到键盘有输入时便会返回非零值,但是它只会检测输入流当中是否有数据,而不会读取
与getch()这个无缓冲输入下读取字符的函数不同,在尚未读取到输入时,kbhit()函数就会直接返回0并使程序从下一个语句开始运行,而不会像getch()那样在无输入情况下便一直等待,因此配合着这两个函数,我们很容易便能写出一个简单的贪吃蛇移动控制模块的框架:

#include
...
if(kbhit())//若未监听到输入,则kbhit()会直接返回0,C/C++中0默认为false值
{
	ch=getch();//无缓冲读取输入,以实现实时操纵
	move_direction_change(ch);//在有输入的情况下,通过此函数改变移动方向
}

kbhit()函数虽然并非C标准库当中的函数,但是其定义所包含在的conio.h头文件在大部分的win32平台上的C编译器都是提供的,Linux和Unix平台虽然不提供,但是我们可以在网上下载该头文件
getch()函数同样包含在conio.h当中

(2)Sleep()函数:控制游戏速度

Sleep()函数接收一个整形输入,并将应用挂起相应的时间,通过使用这个函数与while循环结合,我们就可以控制贪吃蛇的爬行速度,并真正让我们的贪吃蛇跑起来!
Windows当中,Sleep函数沉睡的时间以毫秒记,而在UNIX当中则是以秒记,这点是需要我们当心的
那么我们很容易便能写出一个基础的游戏框架:

//以下除库内函数外皆需自行编写
while(true)
	{
		if(kbhit())//监听键盘是否传入操作
		{
			ch=getch();
			direction_move(ch);//不仅仅可以判断移动方向,还可以判断其他指令:存档、暂停、退出......
			if(stop_game){//全局变量,若接收到退出游戏的指令则打破循环
				stop_game=false;
				break;
			}
		}
		if(moving())//每次循环利用此函数使贪吃蛇前进一格,包含游戏结束判定模块,若游戏结束返回真值,否则返回假
		{
			gamefail();//输出游戏结束信息,跳出循环
			break;
		}
		Sleep(game_speed_set[game_speed]);//因为while循环的判定一直为true,因此通过控制程序挂起的时间便可以控制速度
		showmap();//每次程序挂起结束后便重新输出一次地图
	}

(3)爬行时蛇身的接续与死亡判定

初始化这条贪吃蛇我们有两种方式:

1.一维数组:蛇身接续

由于蛇是由一个个“小节点”构成的,因此我们很容易就能想到:可以利用多个一维数组表示每个节点在不同维度的坐标
本次我们编写的是二维平面内的贪吃蛇,因此使用两个一维数组即可:

int snake_x[2000]={0};//地图大小20*90=1800,因此我们设一个长度为2000的数组完全够用,同样利用C特性将其内全部初始化为0
int snake_y[2000]={0};

每次蛇进行移动时,只需要把每个节点自己的坐标给下一个节点即可,最后第一个节点再根据移动方向的设置改变坐标值:

int temp_x[2],temp_y[2];//用以临时储存坐标值
...
void linking(void)
{
	int i;
	bool pd=true;
	for(i=1;i

2.死亡判定

而贪吃蛇的死亡判断分为两种:吃到自己或是撞到边界,这也是很好处理的:

由于一直在移动的是蛇头,撞墙时首当其冲的也是蛇头,因此我们只需要在每次运动完之后检测蛇头的坐标是否达到地图边界即可:

  if(snake_x[0]<0||snake_x[0]>90||snake_y[0]<0||snake_y[0]>20)//检查移动后是否撞墙
    {
    	failkind=0;//记录游戏失败原因
    	return 1;
    }

利用这个思想,对于贪吃蛇是否咬到自己,我们只需要检测蛇头移动后的位置是否是蛇身所在的位置即可:

int checkeaten(void)
{
	if(length_snake<4)
		return 0;
	for(int i=1;i

最后整合入移动模块主体:

int moving(void)
{
	temp_y[0]=snake_y[0];//储存第一个节点的值
	temp_x[0]=snake_x[0];
	switch(moving_direction)//根据移动方向设置进行第一个节点的坐标改变
	{
		case 0:
				snake_y[0]--;
				break;
		case 1:
				snake_y[0]++;
				break;
		case 2:
				snake_x[0]--;
				break;
		case 3:
				snake_x[0]++;
				break;
	}
	if(snake_x[0]==cake_x&&snake_y[0]==cake_y)//检查是否吃到小蛋糕
	{
		length_snake++;
		game_score++;
		pd_nail=true;
		makecake();
	}
	if(snake_x[0]<0||snake_x[0]>90||snake_y[0]<0||snake_y[0]>20)//检查移动后是否撞墙
	{
		failkind=0;//记录游戏失败原因
		return 1;
	}
	if(checkeaten())//检查是否吃到自己
	{
		failkind=1;
		return 1;
	}
	linking();
	for(int i=0;i

3.思维拓展:链表表示法

?头向前爬动,后面的?身一节一节接续上去,这样的模型让我们想到了什么?没错,就是链表!利用链表我们可以很容易的实现贪吃蛇本体的初始化:

struct LinkedSnake{
	struct LinkedSnake *lastone;//当前蛇节点的上一个节点
	int x,y;//当前节点坐标
	struct LinkedSnake *nextone;//当前蛇节点的下一个节点
};

当然,由于本人比较懒,所以就不再写第二种表达方式的具体实现了…有兴趣的可以用上面的这个框架自行拓展

三、基础的存档功能

(1)fstream:文件流读写

要使这个游戏能够长久的玩下去,基础的存档功能自然是必备的(当然贪吃蛇或许已经落伍了…)
比较基础的一个思路便是创建一个ini文件记录存档的相关信息,包括存档标号、存档日期等,在要使用存档功能的时候便会读取这个配置文件,若是第一次进行存档则会创建一个配置文件
而相信此刻正在阅读本蒟蒻的这篇文章的大佬们手上或多或少都掌握着十几种甚至几十种文件读写的方式,所以这里就不过多展开了…
相较于C的FILE指针,我比较习惯的还是使用fstream进行文件流的读写操作:

#include
.....
void loadini(void)//启动时检测是否有配置文件,没有则创建空白配置文件
{
	ifstream loadst;
	ofstream creates;
	loadst.open("saves.ini",ios::in);
	if(!loadst)
	{
		creates.open("saves.ini",ios::out);
		for(int i=0;i<10;i++)
			creates<<0<

由于蒟蒻只会基础的读写,每次写文件都是清空重写法…所以需要在每次写ini配置文件之前进行备份

	ifstream saves;
	ofstream creates,temp;
	saves.open("saves.ini",ios::in);
	int time_list[10],i,j;
	for(i=0;i<9;i++)
		saves>>time_list[i];
	......
	for(i=0;i<9;i++)
		temp<

(2)ctime():显示存档时间

我们在存档时利用time()记录下下了存档的时间,而在查看存档列表时若是直接输出最原始的时间数据相信基本没人能看得懂,因此我们可以使用ctime()函数输出格式化的时间。
ctime()函数接收一个time_t类型的指针并将其所指地址上的值输出为格式化的时间

int loadsave(void)
{
	system("cls");
	ifstream saves;
	saves.open("saves.ini",ios::in);
	loadgame=true;
	int n;
	time_t time_list[10];
	cout<<"Please choose the save you're going to play.Press \"q\" to cancel the operation."<>time_list[i];
		if(time_list[i]==0)
		{
			cout<<"EMPTY SAVE."<('9')||ch<'1')
	{
		if(ch=='q')
			return 666;
		wronginput();
		ch=getch();
	}
	if(time_list[ch-49]==0)
	{
		cout<<"IT'S AN EMPTY SAVE! YOU CAN'T LOAD IT!"<(ch-48);
}

(3)存档读写

对于贪吃蛇这样的一个简单的小游戏,我们其实要储存在存档当中的内容并不多:

蛋糕的位置
蛇的位置信息
难度系数
移动方向
游戏得分(蛇的长度)

利用文件流我们可以很方便的将信息写入存档当中:

char a[10]="SAVE0.svf";
a[4]=saves_num+48;
creates.open(a,ios::out);
creates<

存档的读取亦是十分简单,将以上信息全部读入即可:

    ifstream readsaves;
	char a[10]="SAVE0.svf";//svf==save file, same format with txt in fact
	a[4]=save_num+48;
	readsaves.open(a,ios::in);
	int i,j;
	readsaves>>cake_x>>cake_y;
	readsaves>>game_speed>>game_score>>moving_direction;
	length_snake=game_score+1;
	for(i=0;i<=game_score;i++)
		readsaves>>snake_x[i]>>snake_y[i];
	readsaves.close();

(4)remove():存档删除

remove() 函数是C当中常用的一个用于删除文件的函数,它接受一个字符串作为参数,该字符串即为要删除文件的文件名称,若是删除成功则返回0,否则返回-1,利用errno.h头文件当中的strerror(error)函数我们可以查看导致我们未能成功删除文件的原因:

EROFS 欲写入的文件为只读文件。
EFAULT 参数filename 指针超出可存取内存空间。
ENAMETOOLONG 参数filename 太长。
ENOMEM 核心内存不足。
ELOOP 参数filename 有过多符号连接问题。
EIO I/O 存取错误

利用这个函数我们可以很方便地删除我们不想要的存档:
(虽然说好像没什么用但是这是一般游戏的必备功能,至少要会写)

	char ch,a[10]="SAVE0.svf";
	while((ch=getch())<'1'||ch>'9')
	{
		if(ch=='q')
		{
			mainUI();
			return ;
		}
		wronginput();
	}
	if(!time_list_[ch-49])
	{
		cout<<"This save doesn't exist. Press any key to be back in the main interface."<

完整代码实现

总计700+行,补全了一些必要的细节
当然Dos下的游戏效果十分蛋疼,看看就好,真要弄游戏还得整图形界面
声明:严禁一切未经允许的抄袭行为(不过本蒟蒻的代码应该也没人会去抄吧…)

#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
/*
   x x x x x
y
y
y
y
y
y
*/
void mainUI(void);
void gamemap(void);
int loadsave(void);
void settings(void);
void aboutus(void);
void wronginput(void);
void change_info(int n);
void showmap(void);
void read_map(void);
void direction_move(char ch);
int moving(void);
void gamefail(void);
void movefail(void);
void putcake(void);
void makecake(void);
int checkeaten(void);
void linking(void);
void save_map(void);
void reset_data(void);
void map_save_choose(void);
void loadini(void);
void delete_saves(void);

char map[20][90]={0};
bool loadgame=false;
int save_num=0;
int failkind=0;
int game_score=0;
bool stop_game=false;
bool pd=true;
bool pd_exist=false;

int snake_x[6000]={0};
int snake_y[6000]={0};
int length_snake=1;
int moving_direction=3;//0 for up, 1 for down, 2 for left, 3 for right

int cake_x,cake_y;

int temp_x[2],temp_y[2];

int saves_num=0;

time_t *t;

int game_speed_set[4]={100,50,10,1};
enum game_mode{
	easy=0,normal,hard,hell
};
int game_speed=0;//using game_speed_set[game_speed] to express the speed by Sleep()

int main(void)
{
	mainUI();
	loadini();
	char ch;
	while((ch=getch())!='5')
	{
		mainUI();
		switch(ch)
		{
			case '1':
					reset_data();
					gamemap();
					break;
			case '2':
					save_num=loadsave();
					if(save_num==666)
					{
						mainUI();
						break;
					}
					if(save_num==233)
					{
						delete_saves();
						break;
					}
					if(save_num)
						gamemap();
					else
						cout<<"You haven't got any saves!"<90||snake_y[0]<0||snake_y[0]>20)//to check whether you've hit the wall
	{
		failkind=0;
		return 1;
	}
	if(checkeaten())//to check whether you've eaten yourself
	{
		failkind=1;
		return 1;
	}
	linking();
	return 0;
}

void linking(void)
{
	int i;
	bool pd=true;
	for(i=1;i>time_list[i];
	for(i=0;i<9;i++)
	{
		if(time_list[i])
			cout<<"SAVE_NO."<'9')
		wronginput();
	if(time_list[ch-49])
	{
		cout<<"This save has already existed. Do you really want to cover it?"<>time_list[i];
	char a[10]="SAVE0.svf";
	a[4]=saves_num+48;
	creates.open(a,ios::out);
	creates<>cake_x>>cake_y;
	readsaves>>game_speed>>game_score>>moving_direction;
	length_snake=game_score+1;
	for(i=0;i<=game_score;i++)
		readsaves>>snake_x[i]>>snake_y[i];
	readsaves.close();
}

void showmap(void)
{
	for(int i=0;i>time_list[i];
		if(time_list[i]==0)
		{
			cout<<"EMPTY SAVE."<('9')||ch<'1')
	{
		if(ch=='q')
			return 666;
		else if(ch=='d')
			return 233;
		wronginput();
		ch=getch();
	}
	if(time_list[ch-49]==0)
	{
		cout<<"IT'S AN EMPTY SAVE! YOU CAN'T LOAD IT!"<(ch-48);
}

void delete_saves(void)
{
	if(!pd_exist)
	{
		cout<<"You haven't got any saves! Press any key to be back in the main interface."<>time_list_[i];
	cout<<"Please choose the save you're going to delete.Press \"q\" to cancel the operation."<'9')
	{
		if(ch=='q')
		{
			mainUI();
			return ;
		}
		wronginput();
	}
	if(!time_list_[ch-49])
	{
		cout<<"This save doesn't exist. Press any key to be back in the main interface."<

你可能感兴趣的:(编程日常)