贪吃蛇之开发

前些天我准备从上海坐飞机回来的时候,飞机晚点了。我们十几个人就只能等在飞机柜台前面。没有什么事情好做,于是朱轩宇和我聊电脑技术。他提到一些经典游戏,比如说扫雷啊,贪吃蛇啊。那些现在想想,好像十分容易实现。我的C++还没有入门可视化界面,但是像扫雷,就非常容易用字符表示出来;再加上现在会用’\b’清空屏幕了,内容就可以在CMD窗口中刷新了。我就在机场做了一个键盘输入坐标的扫雷;美中不足的是,我还没有完成检测游戏是否已完成这部分就要上飞机了。飞机上,我只是玩玩了那盘游戏。源码现在还在笔记本电脑上,所以我现在在学校发不上来。

我在学校花了大概一个小时写了一个贪吃蛇。我把源代码贴在这里,解释在代码里面。源代码是可以复制然后在Dev C++里面直接编译运行的,但是必须有读写目录的权限。

游戏端程序

#include <iostream>
#include <cstdio>
#include <cstring>
#include <ctime>//计时器,在控制刷新时间的时候有用
#include <cstdlib>//调用系统函数system();以及随机数函数。
#define SCREEN_WIDTH 50//显然,屏幕宽度
#define SCREEN_HEIGHT 20//屏幕高度
#define REFRESH_RATE 7.0//刷新率,单位为Hz。这里需要一个float,所以必须加上".0",不然会当成int来看。
#define UP 72//定义向上的键位。上箭头会拆成两个char输入,一个是-32,一个是72。这个在控制端也会讲。下同。
#define LEFT 75
#define DOWN 80
#define RIGHT 77
#define APPLES 10//定义保证地图上生成的苹果数量。
using namespace std;
int mat[SCREEN_HEIGHT][SCREEN_WIDTH],mx[]={0,1,0,-1},my[]={1,0,-1,0},apples=0;
/*
mat:这储存了当前的地图情况,每个元素储存一个像素点。
-1表示苹果,0表示空地,>0蛇神,具体数值表示距离蛇尾的距离。
这个是我的程序的核心算法,使用TTL(Time to Live)值来保存蛇的身体,
避免了储存蛇链的麻烦。用这种储存像素点的方法虽然方便,但也有缺点。
在像素比较多的时候而信息比较少的时候,用数组储存会显得稍浪费空间。
mx、my:这个是我个人的编程习惯。
在矩阵中蛇头移动有四个方向,上右下左。
数组里面储存的就是向这四个方向移动的时候蛇头坐标需要进行的变换。
在DirectX中,这好像是通过矩阵来实现的。现在才发现矩阵的强大以及应用的必要性。
apples:当前在地图上的苹果树。用来判断是否还继续需要继续生成苹果。
*/
char op;
/*
这个储存的是当前的操作字符。
*/
bool readop(){//从文件中读取字符,实现程序间的信息交流。
    FILE *fop=fopen("op","r");
    fscanf(fop,"%c",&op);
    fclose(fop);
    if(op!='0'){//读到了东西!
        fop=fopen("op","w");
        fprintf(fop,"0");
        /*
        更新一遍文件,防止多次改变方向。
        现在想想,好像并没有这个必要,因为
        一直往这个方向并没有什么影响啊……
        */
        fclose(fop);//关掉文件,防止占用。
    }
    return op!='0'; 
}
int getdir(){//通过当前的操作字符判断方向。
    switch(op){
        case UP:{//这些是前面常量中定义了的。
            return 3;
            break;
        }
        case LEFT:{//用起来很像Java中的Enum类型。
            return 2;
            break;
        }
        case DOWN:{
            return 1;
            break;
        }
        case RIGHT:{
            return 0;
            break;
        }
    }
}
//正如前面说的,解释每个像素点,然后输出相应的字符。
void processdot(int x,int y){
    if(mat[x][y]==-1){
        printf("A");//苹果Apple
    }else if(mat[x][y]==0){
        printf("_");//空地。为了看得见屏幕边界,使用了下划线。
    }else if(mat[x][y]>0){
        printf("*");//蛇身
        mat[x][y]--;
        /*
        因为刷新了一次,所以蛇前进了一格。
        所以TTL-1,即离蛇尾又近了一步。
        */
    }
}
void refresh(){//每次刷新屏幕的时候调用这个函数。
    system("cls");//清屏。在CMD中输入也会清屏,因为这就是CMD的命令。
    for(int i=0;i<SCREEN_WIDTH*SCREEN_HEIGHT;i++){
        printf("\b");//不知道为什么失效了。可以删掉。
    }
    for(int i=0;i<SCREEN_HEIGHT;i++){
        for(int j=0;j<SCREEN_WIDTH;j++){//枚举每一个点。
            processdot(i,j);//然后处理它!
        }
        printf("\n");//输出完一行之后换行。
    }
}
inline bool valid(int x,int y){//检查当前蛇头位置是否合法,不合法就死!
    return x>=0&&x<SCREEN_HEIGHT&&y>=0&&y<SCREEN_WIDTH&&mat[x][y]<=0;
    /*
    首先检查是否在地图边界内,
    然后看是否这里是否有蛇身。
    当然这样的算法比较有趣的
    一点是当你往右走时按左,
    就会把自己吃掉。
    */
}
void generateApple(){//生成苹果
    int tx=rand()%SCREEN_HEIGHT,ty=rand()%SCREEN_WIDTH;
    //随便生成一个苹果位置
    while(apples<APPLES){//要生成足够多的苹果
        while(mat[tx][ty]!=0){//不能生成在苹果上,也不能生成在蛇上面
            tx=rand()%SCREEN_HEIGHT;//生成的随机数范围应该是int范围内,
            ty=rand()%SCREEN_WIDTH;//所以为了限定到屏幕范围内,模边长。
        }
        /*
        这里算法有一点问题。当蛇很长很长的时候,
        生成苹果大部分位置都是非法的。
        这样要生成出一个苹果,需要大量的时间。
        */
        mat[tx][ty]=-1;//苹果出现!
        apples++;
    }
}
int main(){//好了,到主函数了。
    char yn;//这个是接收是否继续游戏的字符。
    do{
        apples=0;//每次游戏开始时都应该是没有苹果,所以初始化为0.
        //一开始做的时候忘了加这一句。
        clock_t timer;//计时器。用来检测刷新间距是否已达到。
        memset(mat,0,sizeof(mat));
        int d=0,l=1,x=0,y=0;
        /*
        d:方向
        l:蛇的长度
        x,y:蛇头的位置
        */
        mat[x][y]=1;//一开始蛇在左上角
        refresh();//预先输出一次屏幕
        srand(time(NULL));//声明随机种子,用的是时间。
        generateApple();//生成苹果
        readop();
        while(true){
            timer=clock();
            while(clock()-timer<1000/REFRESH_RATE){
                /*
                计算是否已经到了刷新时间,没到
                就一直读取输入。这是堵塞的行为!
                有一个小问题就是clock()的返回值
                不一定是1/1000s,在不同的环境下
                不一样,这是我在《算法竞赛入门经典》
                上看到的。正确的做法是使用
                CLOCKS_PER_SECONDE代替1000.
                */
                if(readop()){
                    d=getdir();
                }
            }
            x+=mx[d];//移动蛇头
            y+=my[d];
            if(!valid(x,y)){//如果蛇头出现在了神奇的位置,死!
                break;
            }
            if(mat[x][y]==-1){//吃到了苹果就加长蛇身
                l++;
                apples--;
                generateApple();
            }
            mat[x][y]=l;
            refresh();
        }
        cout<<"You died!"<<endl;
        cout<<"Snake's length:"<<l<<endl;
        cout<<"Continue? (y/n)"<<endl;
        cin>>yn;
    }while(yn=='y');
    system("pause");//观察最后得分。
}

控制端程序

#include <iostream>
#include <cstdio>
#include <cstring>
#include <conio.h>//这个是getch()的库 
#include <cstdio>
using namespace std;
int main(){
    FILE *fout;//定义文件指针 
    fout=fopen("op","w");//输出到文件"op"里面去 
    fprintf(fout,"0");//在这里先初始化一下,防止游戏端读不到东西 
    fclose(fout);
    char c;
    while(true){
        c=getch();//getch()函数是隐式读取,读取了不会输出,而且也不需要按回车才会输入。这是和getc以及cin很大的区别。 
        if(c==-32){
            /*
            方向键需要两个char变量才能存储的下,因为在ASCII中没有方向键这回事。
            这里的-32是我调试输出得到的。我
            不太理解的一个地方就是为什么一个char变量可以是负值,因为我以为char变量的范围是0~127。
            莫非是-128~127? 
            */
            cout<<"Info:direction key pressed"<<endl;//调试输出 
            c=getch();//再获取一个 
            fout=fopen("op","w");
            fprintf(fout,"%c",c);//写入操作符 
            fclose(fout);//关闭文件,防止占用,虽然我觉得好像没什么关系。 
        }
        cout<<"Debug:"<<c<<endl;
    }
}

虽然现在这个游戏能玩,但是还有几个问题:

  • 屏幕闪烁
  • 接收输入需要另外一个程序
    • 那么对文件的读写就十分的频繁了

屏幕闪烁的问题本来是不存在的。我写扫雷的时候用的'\b'来清空整个屏幕非常有效,而且因为’\b’在退格的时候是不会清除字符的,仅仅是重新写的时候会覆盖而已。但是现在用的system("cls")会把整个屏幕清除掉,所以内容会先消失再显示出来,造成闪烁的效果。至于为什么我现在写贪吃蛇的时候扫雷的办法不管用了,我也还没有想到原因。望高人指教!

对于第二个问题有解决的办法。因为getch()方法是阻塞型的,所以需要开多一个线程,但我还没有研究windows.h库。如果仅仅解决第二个小问题的话,可以把内存指针先输出到文件中,另一个程序读取,然后用内存来传输。只是说这样会非常的不安全。

总之,这是我自己用C++写的一个小游戏。这是对童年的一个弥补,因为我六年级在写VB的时候,只能复制过其他人的代码做这个贪吃蛇,但当时完全看不懂别人的代码;现在我自己也写出来了。虽然这对于信息竞赛的人来说是小菜一碟,但还是小有成就啊!

avatar
Kerry Su