linux环境下C语言实现贪吃蛇游戏

  在linux环境下,利用对framebuffer机制 和 tcgetattr与tcsetattr函数控制终端 来实现贪吃蛇游戏。

  对于framebuffer和tcgetattr与tcsetattr函数的具体原理机制可以参考网上的其他资料。这里利用framebuffer来实现游戏的图像显示,通过tcgetattr与tcsetattr函数来实现键盘终端的控制,从而实时读取键盘的按键。


  在开始编码之前要先有一个整体的思路,设计一个游戏的框架,然后再去对每一部分进行实现。
  
  首先要做的是设计一个游戏区域,其实就是一个二维空间。这里我规划了一个20×20的空间,蛇和食物都是以空间的每一个节点为单位来构造的,利用对应二维空间的坐标来记录蛇和食物的位置。

    //食物数据
    typedef struct Food
    {
    int x;
    int y;
    }F;

    //蛇的数据
    typedef struct Snake
    {
        int x;
        int y;
        struct Snake *next;
    }S;

  以上是用来存储蛇和食物信息的结构体,食物同时只会存在一个,而蛇是由多个节点组成的,所以用链表来存储。画图时再通过坐标对应到相应的像素点上就可以了。
  
  那么下面来具体说下画图的实现方法。这里要利用对framebuffer的操作,来设置每一个像素点的信息。在主函数利用下面的代码。

        //framebuffer初始化
        struct fb_var_screeninfo  vinfo;

        int fd = open("/dev/fb0", O_RDWR);
        if(fd < 0)
    {
        perror("open err. \n");
                exit(EXIT_FAILURE);
        }
        int fret = ioctl(fd, FBIOGET_VSCREENINFO, &vinfo);
        if(fret < 0)
    {
                perror("ioctl err. \n");
                exit(EXIT_FAILURE);
        }

        //用来知道自己屏幕的分辨率

        printf("vinfo.xres: %d\n", vinfo.xres);
        printf("vinfo.yres: %d\n", vinfo.yres);
        printf("vinfo.bits_per_pixel: %d\n", vinfo.bits_per_pixel);

    //定义指针来操作framebuffer
    unsigned long* addr = mmap(NULL, (vinfo.xres*vinfo.yres*vinfo.bits_per_pixel)>>3,
            PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 

  下面是需要的头文件和宏定义。

#include <stdio.h>
#include <stdlib.h>
//framebuffer相关头文件
#include <linux/fb.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <fcntl.h>

//宏定义设置RGB,以便通过设定 r,g,b参数来设置颜色
#define  RGB888(r, g, b)     ((0xff & r) << 16 | (0xff & g) << 8 | (b & 0xff))

  通过上面的方法得到vinfo.xres和vinfo.yres,就可以知道自己屏幕的分辨率。我的电脑是1376×768的,然后就可以通过addr来设定每一个像素的点的颜色了。下面以画一个矩形为例。r,g,b分别是红绿蓝三个颜色,根据值的不同可以调成不同的颜色,我画的是蓝色的。虽然测得屏幕分辨率是1376×768,但实际设定像素的时候发现是688×768。不同的电脑会有所不同,根据测得的分辨率画的不对的话,要在测的数值的基础上乘以或除以2去找到合适的。

void draw_side(unsigned long* addr)
{
    int i , j;
    //i是纵坐标,j是横坐标,屏幕左上角是坐标原点。以(200,200)到(400,600)为对角线画一个蓝色矩形。
    for (i = 200; i<600; i++)
        for (j = 200; j<400; j++)
            *(addr + i * 688 + j) = RGB888(0, 0, 0xff);

}

  要注意的是对framebuffer进行操作的画在图形界面下是无法显示的,通过ctrl + alt + F2 切换到命令行模式,在root下运行程序才可以。
  还有一点就是貌似得在物理机上才能画,我在我的虚拟机上就没办法知道屏幕是个什么分辨率,怎么都画不出一个矩形。
  
  不过弄清屏幕的分辨率,成功的画出一个矩形的话,那剩下画其他的东西也就简单了。我是以10×20区域为单位作为游戏中的一个点,游戏中的区域是20×20的 ,对应到像素上就是200×400。以像素点的(200,200)为原点到(400,600)的矩形区域就是游戏区域映射到屏幕上以后的区域。
  画图的时候只要先实现一个根据游戏中坐标画点的函数,画蛇和食物就可利用这个函数一个一个点来画了。

//以200*200像素位置为坐标原点,屏幕正下方为y正半轴,屏幕正右方为x正半轴。根据参数中的xx,yy是有意中的坐标,画图时对应到像素的x,y上作为矩形左上角顶点,画一个10*20像素的矩形,参数r,g,b用于颜色设定。addr为操作帧缓冲的指针,之后再利用该函数来画蛇和食物。
void draw_point(unsigned long* addr,int xx,int yy,int r,int g,int b)
{
    int i,j,x,y;
    x = xx*10+200;
    y = yy*20+200;
        for(i=y; i<y+20; i++)
            for(j=x; j<x+10; j++)
                    *(addr + i*688 + j) = RGB888(r,g,b);
}

//根据蛇每个节点的坐标画蛇,蛇头为绿色,蛇身为红色
void draw_snake(unsigned long* addr,S *head)
{
    S *find = head;

    //画绿色的头
    draw_point(addr,find->x,find->y,0,0xff,0);
    find = find->next;

    //身体画红色
    while(find!=NULL)
    {
        draw_point(addr,find->x,find->y,0xff,0,0);
        find = find->next;
    }
}

//根据食物坐标画一个绿色的食物
void draw_food(unsigned long* addr,F *f)
{
    draw_point(addr,f->x,f->y,0,0xff,0);
}

//游戏边框,屏幕坐上为坐标原点,向右为x正半轴,向下为y正半轴
//屏幕的像素范围为(0,0)到(688,,768)
//游戏边框颜色为蓝色,内侧为(200,200)到(600,400)矩形区域
void draw_side(unsigned long* addr)
{
        int i , j;
//边框的上下边
        for(i=180; i<200; i++)
            for(j=190; j<410; j++)
                    *(addr + i*688 + j) = RGB888(0,0,0xff);

        for(i=600; i<620; i++)
            for(j=190; j<410; j++)
                    *(addr + i*688 + j) = RGB888(0,0,0xff);

//边框的左右边
        for(i=200; i<600; i++)
            for(j=190; j<200; j++)
                    *(addr + i*688 + j) = RGB888(0,0,0xff);

        for(i=200; i<600; i++)
            for(j=400; j<410; j++)
                    *(addr + i*688 + j) = RGB888(0,0,0xff);

}

//清空边框内侧区域,全部刷黑,这样就不用每次清屏重画边框了
void myclear(unsigned long *addr)
{
    int i, j;
    for (i = 200; i<600; i++)
        for (j = 200; j<400; j++)
            *(addr + i * 688 + j) = RGB888(0, 0, 0);
}

  
  可以画图之后就是游戏内容的具体实现了。即如何创建蛇,创建食物,让蛇移动,吃食物,以及gameover的判断。

  下面先说如何创建和食物。蛇开始起码得有个头和尾巴,所以就创建了两个节点。蛇头位置随机,尾巴为蛇头下面的节点。考虑到头如果在边上的话尾巴会出界,所以让头在边框向中间缩3格的范围内随机生成。
  生成食物的算法是优化过的,开始的思路是每次在游戏区域内随机一个节点作为食物位置,遍历蛇判断食物是否在蛇身上,在的话重新生成食物,再判断,直到食物不在蛇身上。这样做的话随机生成食物非常简单,但是存在一个问题,就是当玩到蛇很长的时候,随机生成的食物很大概率会在蛇身上,就要反复的重新生成食物,并遍历蛇看食物是否在蛇身上,遍历是很费时间的,而且这样生成食物就要费很长时间,假设最后只剩一个不在蛇身上的点,要食物正好随机生成在那个点上的话,结果可以想象。
  所以这种创建食物的方法是不可行的,那么就需要每次只遍历一次蛇,知道蛇的位置后在剩余的节点中随机选一个作为新的食物位置。这个过程需要开辟一个二维空间来记录可用节点,将可用节点值置0,蛇的位置置1。然后从0的节点里随机选一个作为食物位置。虽然过程比开始的方法麻烦些,但是实际每一次执行起来的时间是很短的,可以估量的,即使在蛇很长的时候,游戏也是很流畅的。
  
  以下是具体的函数实现。

//初始化蛇2个节点,蛇头随机出现在边框以内三格区域内,蛇尾在蛇头下方,参数head为蛇信息
void cre_snake(S **head)
{
    *head = malloc(sizeof(S));
    S *end = malloc(sizeof(S));
    srand(time(0));
    (*head)->x = rand() % 15 + 3;
    (*head)->y = rand() % 15 + 3;
    (*head)->next = end;
    end->x = (*head)->x;
    end->y = ((*head)->y) + 1;
    end->next = NULL;
}

//改进后的创建食物函数,在蛇位置以外区域随机生成一个新的食物,参数f为食物信息,head为蛇信息
void cre_food(F *f, S *head)
{
    int abl_use[20][20]={0};
    S *find = head;
    int len = 1;//记录蛇长度
    int n;
    int i, j, count = 0;//count用来记录遍历了的0节点个数。

    while (find != NULL)
    {
        abl_use[find->y][find->x] = 1;
        find = find->next;
        len++;
    }

    srand(time(0));
    n = rand() % (399 - len);//n为可用节点数范围内的一个随机值

    //找到第n个0节点的坐标
    for (i = 0; (i<20) && (count<n); i++)
        for (j = 0; (j<20) && (count<n); j++)
        {
            if (abl_use[i][j] == 0)
                count++;
        }
    f->x = j - 1;
    f->y = i - 1;
}

  
  接下来是蛇的移动和吃食物。  

//蛇向指定方向走一步,参数toward为指定方向,head为蛇信息
void move(char toward, S *head)
{
    S *find = head->next;
    int sx, sy, tx, ty;
    sx = find->x;
    sy = find->y;
    find->x = head->x;
    find->y = head->y;
    switch (toward)
    {
    case 'w':
        head->y -= 1;
        break;
    case 's':
        head->y += 1;
        break;
    case 'a':
        head->x -= 1;
        break;
    case 'd':
        head->x += 1;
        break;
    }
    while (find->next != NULL)
    {
        tx = find->next->x;
        ty = find->next->y;
        find->next->x = sx;
        find->next->y = sy;
        sx = tx;
        sy = ty;
        find = find->next;
    }
}

//避免移动中出现直接掉头的情况,参数now为新指定方向,fro当前方向
int if_move(char now,char fro)
{
    if(now=='w'&&fro=='s'||now=='s'&&fro=='w'||now=='a'&&fro=='d'||now=='d'&&fro=='a')
        return 0;
    else
        return 1;
}

//吃到食物时用来加长蛇的函数,在蛇尾方向上新增加一个节点,参数head为蛇信息
void l_snake(S *head)
{
    S *find = head;
    S *tmp = NULL;
    S *new = malloc(sizeof(S));
    new->next = NULL;
    while (find->next->next != NULL)
    {
        find = find->next;
    }
    tmp = find->next;
    tmp->next = new;
    new->x = tmp->x + tmp->x - find->x;
    new->y = tmp->y + tmp->y - find->y;
}

//判断蛇是否吃到食物,吃掉则调用l_snake加长蛇,并刷新食物位置,参数head为蛇信息,f为食物信息
void eat(S *head, F *f)
{
    if (head->x == f->x&&head->y == f->y)
    {
        l_snake(head);
        cre_food(f, head);

        //改进前吃掉食物后生成新的食物的方法
        //       while(if_foodinsnake(head,f))
        //           cre_food(f);
    }
}

//改进之前用于判断新生成的食物是否在蛇身上
/*
int if_foodinsnake(S *head,F *f)
{
    S *find = head;
    while(find->next!=NULL)
    {
        if(find->x!=f->x||find->y!=f->y)
            find = find->next;
        else
            return 1;
    }
        return 0;
}*/

  Gameover的具体实现。

//判断蛇是否越界,参数head为蛇信息
int outside(S *head)
{
    if(head->x<0||head->y<0||head->x>19||head->y>19)
        return 1;
    else
        return 0;
}

//判断蛇是否碰到自己,参数head为蛇信息
int headisbody(S *head)
{
    S *find = head->next;
    while(find!=NULL)
    {
        if(find->x==head->x&&find->y==head->y)
            return 1;
        find = find->next;
    }
    return 0;
}

//通过outside,headisbody判断是否gameover,并执行结束程序的相关处理
void gameover(S* head)
{
    if(headisbody(head)||outside(head))
    {
        system("clear");
        printf("Game Over!\n");
        recover_keyboard();//要注意恢复终端的设置
        exit(EXIT_SUCCESS);
    }
}

  以上是游戏功能的函数实现,此外还有一个很重要的部分就是利用tcgetattr与tcsetattr函数控制终端 ,实时读取键盘的按键。

  需要的头文件和宏定义。

#include <stdio.h>
#include <stdlib.h>
//读键盘相关头文件
#include <unistd.h>
#include <termios.h>
#include <fcntl.h>
//根据键盘按键的键值进行宏定义
#define UP    0x415b1b
#define DOWN  0x425b1b
#define LEFT  0x445b1b
#define RIGHT 0x435b1b
#define ENTER 0xa
#define ESC   0x1b
#define SPACE 0x20

  需要的函数以及使用的框架。

//读键盘按键
int init_keyboard(void)
{
    int ret;
    struct termios tc;
    ret = tcgetattr(0, &tcsave);
    if(ret < 0)
        return -1;
    tc = tcsave;
    tc.c_lflag &= ~(ECHO|ICANON);
    ret = tcsetattr(0, TCSANOW, &tc);
    if(ret < 0)
        return -1;
    flsave = fcntl(0, F_GETFL);
    fcntl(0, F_SETFL, flsave|O_NONBLOCK);
    return 0;
}
void recover_keyboard(void)
{
    tcsetattr(0, TCSANOW, &tcsave);
    fcntl(0, F_SETFL, flsave);
}
int get_key(void)
{
    unsigned char buf[3];

    int ret = read(0, buf, sizeof(buf));
    if(ret < 0)
        return -1;

    int i = 0, key = 0;
    for(i=0; i<ret; i++){
        key += (buf[i]<<(i*8));
    }
    return key;
}
int is_up(int key)
{
    return key == UP;
}
int is_down(int key)
{
    return key == DOWN;
}
int is_left(int key)
{
    return key == LEFT;
}
int is_right(int key)
{
    return key == RIGHT;
}
int is_enter(int key)
{
    return key == ENTER;
}
int is_esc(int key)
{
    return key == ESC;
}
int is_space(int key)
{
    return key == SPACE;
}

int main()
{
    int key, ret;
    ret = init_keyboard();
    if(ret < 0)
        return -1;
    while(1){
        key = get_key();
        if(key < 0)
            continue;   

        //printf("key = %x\n", key);
        if(is_left(key))
            printf("left\n");
        if(is_right(key))
            printf("right\n");
        if(is_up(key))
            printf("up\n");
        if(is_down(key))
            printf("down\n");
        if(is_enter(key))
            printf("enter\n");
        if(is_space(key))
            printf("space\n");
        if(is_esc(key)){
            printf("esc\n");
            break;
        }

        if(key == 'q')
            break;
    }   
    recover_keyboard();
    return 0;
}

  利用这个框架就可以在按键时,显示按的是那个键,实际上就是执行了对应键值调用的函数。再把这个框架利用到自己的程序中,将已经实现的游戏函数融入进来。
  
  下面就是游戏的主函数部分。

int main()
{
    system("clear");

    //framebuffer初始化
        struct fb_var_screeninfo  vinfo;

        int fd = open("/dev/fb0", O_RDWR);
        if(fd < 0)
    {
        perror("open err. \n");
                exit(EXIT_FAILURE);
        }
        int fret = ioctl(fd, FBIOGET_VSCREENINFO, &vinfo);
        if(fret < 0)
    {
                perror("ioctl err. \n");
                exit(EXIT_FAILURE);
        }

//用来知道自己屏幕的分辨率
/*
        printf("vinfo.xres: %d\n", vinfo.xres);
        printf("vinfo.yres: %d\n", vinfo.yres);
        printf("vinfo.bits_per_pixel: %d\n", vinfo.bits_per_pixel);
*/

    //定义指针来操作framebuffer
    unsigned long* addr = mmap(NULL, (vinfo.xres*vinfo.yres*vinfo.bits_per_pixel)>>3,
            PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 


    //游戏初始化
    S *head = NULL;
    F *f = malloc(sizeof(F));
    draw_side(addr);
    cre_snake(&head);
    cre_food(f,head);
    draw_snake(addr,head);
    draw_food(addr,f);

    //初始化键盘设置
    int key, ret;
    char choose='d';//初始蛇的运动方向,d为右,a为左,w为上,s为下
    int speed = 1;//速度档1为0.4s走一步,0为0.2s走一步

    ret = init_keyboard();//接入键盘
    if(ret < 0)
        return -1;

    //游戏循环体,上下左右控制方向,空格变速,esc和q退出
    while(1)
    {
        key = get_key();
        //printf("key = %x\n", key);

        //接收键盘信息改变蛇的运动方向,调用if_move来防止直接掉头
        if(is_left(key))
        {
            if(if_move('a',choose))
                choose = 'a';
        }
        if(is_right(key))
        {
            if(if_move('d',choose))
                choose = 'd';
        }
        if(is_up(key))
        {
            if(if_move('w',choose))
                choose = 'w';
        }
        if(is_down(key))
        {
            if(if_move('s',choose))
                choose = 's';
        }

        //可以为回车添加其他功能   
//      if(is_enter(key))
//          printf("enter\n");

        //空格用来调速
        if(is_space(key))
        {
            if(speed)
                speed = 0;
            else
                speed = 1;
        }

        //退出游戏
        if(is_esc(key)){
            printf("esc\n");
            break;
        }
        if(key == 'q')
            break;

        //每次循环读取键盘信息后,先判断当前位置蛇是否吃到食物,再进行移动,然后判断是否gameover
        eat(head,f);
        move(choose,head);
        gameover(head);

        //刷新图像信息
        myclear(addr);
        draw_snake(addr,head);
        draw_food(addr,f);

        //可用于清除多余的按键操作,经过测试认为不清除比较好
       // int c;
       // while((c=getchar())!='\n'&&c!=EOF);

        //通过sleep来控制游戏循环的频率,从而控制蛇的速度
        if(speed)
            usleep(400000);
        else
            usleep(200000);

    }

        //恢复键盘
        recover_keyboard();
        return 0;
}

  通过以上的方法就可以实现简单的贪吃蛇游戏了,虽然看起来是一气呵成,实际上首先设计好程序的框架,以及每一部分所要实现的功能是十分重要的。然后再将问题抽象,细分成一个一个小功能再逐步实现。并且每一部分实现后都要单独设计用例进行检测,没有问题以后再进行下一步实现。整个过程从最初的画边框蛇,以及食物,到让蛇移动吃食,再到最后加入Gameover,都是每完成一部分功能后进行测试,再来实现下一部分功能的。一直到实现所有的功能。
  


  此外还可以做以下改进:

  1. 蛇的移动实现,这里是将头的位置改变后,之后的每一个节点都继承前一个节点的坐标,这里每次移动都需要遍历一遍蛇,蛇越来越长的话遍历的代价就会越来越大。所以可以做以下优化,除了蛇的头节点外再记录下蛇尾节点的前一个,每次移动时将尾节点换到头结点前面并记录新的头位置,再将更新头节点和新的尾节点,并将新的尾节点的next赋空。这样只是把尾巴换到头上来就实现了移动,而不需要遍历整个蛇了。
  2. 蛇吃食物,这里也需要遍历蛇,找到尾节点的前一个节点后再根据尾巴方向添加的新的节点,如果记录了尾节点的前一个节点的话就不用每次再遍历了。
  3. Gameover时,判断越界和蛇是否吃到自己是分别遍历两次蛇来实现的,可以放到一起,在一次遍历中实现。
  4. 这里如果更换设备分辨率改变的话,画图部分就要根据像素重新编写了。可以利用在操作framebuffer的时候得到的分辨率,作为画图时需要的分辨率参数,这样即使更换设备也可以兼容了。
  5. 游戏功能上还可以丰富些,加入暂停,分数以及游戏时间记录还有最高分排行等元素。
  6. 由于对framebuffer的具体机理不是十分熟悉,游戏只能在命令行模式,root权限下才能运行。还有待进一步学习,使得程序可以在视图模式,普通用户权限下就可以运行。

程序源码:

  • list.h
#ifndef _LIST_H
#define _LIST_H

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

//framebuffer相关头文件
#include <linux/fb.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <fcntl.h>

//读键盘相关头文件
#include <unistd.h>
#include <termios.h>
#include <fcntl.h>

//宏定义设置RGB,以便通过设定 r,g,b参数来设置颜色
#define  RGB888(r, g, b)     ((0xff & r) << 16 | (0xff & g) << 8 | (b & 0xff))

struct termios tcsave;
int flsave;

//根据键盘按键的键值进行宏定义
#define UP    0x415b1b
#define DOWN  0x425b1b
#define LEFT  0x445b1b
#define RIGHT 0x435b1b
#define ENTER 0xa
#define ESC   0x1b
#define SPACE 0x20

//食物数据
typedef struct Food
{
    int x;
    int y;
}F;

//蛇的数据
typedef struct Snake
{
    int x;
    int y;
    struct Snake *next;
}S;

//游戏逻辑功能函数
void cre_snake(S**);//初始化蛇
void cre_food(F*, S*);//创建食物,使食物随机生成在身以外的位置
void move(char, S*);//蛇向指定方向走一步
int if_move(char,char);//避免移动中出现直接掉头的情况
void eat(S*, F*);//蛇吃食物,调用l_snake加长蛇,并刷新食物位置
void l_snake(S*);//吃到食物时用来加长蛇的函数

//gameover相关函数
int outside(S*);//判断蛇是否越界
int headisbody(S*);//判断蛇是否碰到自己
void gameover(S*);//通过outside,headisbody判断是否gameover,并执行结束程序的相关处理
//int if_foodinsnake(S*,F*);//原判断食物是否在射身上的函数

//画图函数,将坐标信息对应到屏幕像素位置
void draw_point(unsigned long*,int,int,int,int,int);//在规划好的区域内,根据左边,画一个指定的单元点,以及设定点的颜色
void draw_snake(unsigned long*,S*);//利用draw_point和蛇的数据画蛇
void draw_food(unsigned long*,F*);//利用draw_point和食物数据画食物
void draw_side(unsigned long*);//根据规划好的像素位置画,游戏的边框
void myclear(unsigned long*);//用于清除方框内颜色,以便重新画图

//获取键盘相关函数
int init_keyboard(void);//初始化调用键盘
void recover_keyboard(void);//释放键盘调用
int get_key(void);//获取键值
//根据键值判断键位
int is_up(int);
int is_down(int);
int is_left(int);
int is_right(int);
int is_enter(int);
int is_esc(int);
int is_space(int);

#endif
  • fun_snake.c
#include"list.h"

//函数实现


//游戏逻辑功能函数

//初始化蛇2个节点,蛇头随机出现在边框以内三格区域内,蛇尾在蛇头下方,参数head为蛇信息
void cre_snake(S **head)
{
    *head = malloc(sizeof(S));
    S *end = malloc(sizeof(S));
    srand(time(0));
    (*head)->x = rand() % 15 + 3;
    (*head)->y = rand() % 15 + 3;
    (*head)->next = end;
    end->x = (*head)->x;
    end->y = ((*head)->y) + 1;
    end->next = NULL;
}

//改进后的创建食物函数,在蛇位置以外区域随机生成一个新的食物,参数f为食物信息,head为蛇信息
void cre_food(F *f, S *head)
{
    int abl_use[20][20];
    S *find = head;
    int len = 1;
    int n;
    int i, j, key = 0;
    for (i = 0; i<20; i++)
        for (j = 0; j<20; j++)
            abl_use[i][j] = 0;
    while (find != NULL)
    {
        abl_use[find->y][find->x] = 1;
        find = find->next;
        len++;
    }

    srand(time(0));
    n = rand() % (399 - len);
    for (i = 0; (i<20) && (key<n); i++)
        for (j = 0; (j<20) && (key<n); j++)
        {
            if (abl_use[i][j] == 0)
                key++;
        }
    f->x = j - 1;
    f->y = i - 1;
}

//蛇向指定方向走一步,参数toward为指定方向,head为蛇信息
void move(char toward, S *head)
{
    S *find = head->next;
    int sx, sy, tx, ty;
    sx = find->x;
    sy = find->y;
    find->x = head->x;
    find->y = head->y;
    switch (toward)
    {
    case 'w':
        head->y -= 1;
        break;
    case 's':
        head->y += 1;
        break;
    case 'a':
        head->x -= 1;
        break;
    case 'd':
        head->x += 1;
        break;
    }
    while (find->next != NULL)
    {
        tx = find->next->x;
        ty = find->next->y;
        find->next->x = sx;
        find->next->y = sy;
        sx = tx;
        sy = ty;
        find = find->next;
    }
}

//避免移动中出现直接掉头的情况,参数now为新指定方向,fro当前方向
int if_move(char now,char fro)
{
    if(now=='w'&&fro=='s'||now=='s'&&fro=='w'||now=='a'&&fro=='d'||now=='d'&&fro=='a')
        return 0;
    else
        return 1;
}

//吃到食物时用来加长蛇的函数,在蛇尾方向上新增加一个节点,参数head为蛇信息
void l_snake(S *head)
{
    S *find = head;
    S *tmp = NULL;
    S *new = malloc(sizeof(S));
    new->next = NULL;
    while (find->next->next != NULL)
    {
        find = find->next;
    }
    tmp = find->next;
    tmp->next = new;
    new->x = tmp->x + tmp->x - find->x;
    new->y = tmp->y + tmp->y - find->y;
}

//判断蛇是否吃到食物,吃掉则调用l_snake加长蛇,并刷新食物位置,参数head为蛇信息,f为食物信息
void eat(S *head, F *f)
{
    if (head->x == f->x&&head->y == f->y)
    {
        l_snake(head);
        cre_food(f, head);
        //       while(if_foodinsnake(head,f))
        //           cre_food(f);
    }
}



//GAMEOVER部分

//判断蛇是否越界,参数head为蛇信息
int outside(S *head)
{
    if(head->x<0||head->y<0||head->x>19||head->y>19)
        return 1;
    else
        return 0;
}

//判断蛇是否碰到自己,参数head为蛇信息
int headisbody(S *head)
{
    S *find = head->next;
    while(find != NULL)
    {
        if(find->x==head->x&&find->y==head->y)
            return 1;
        find = find->next;
    }
    return 0;
}

//通过outside,headisbody判断是否gameover,并执行结束程序的相关处理
void gameover(S* head)
{
    if(headisbody(head)||outside(head))
    {
        system("clear");
        printf("Game Over!\n");
        recover_keyboard();
        exit(EXIT_SUCCESS);
    }
}

//改进之前用于判断新生成的食物是否在蛇身上
/*
int if_foodinsnake(S *head,F *f)
{
    S *find = head;
    while(find->next!=NULL)
    {
        if(find->x!=f->x||find->y!=f->y)
            find = find->next;
        else
            return 1;
    }
        return 0;
}*/

//画图相关函数

//以200 * 200像素位置为坐标原点, 屏幕正下方为y正半轴,屏幕正右方为x正半轴。
//根据参数中的xx, yy是有意中的坐标,画图时对应到像素的x, y上作为矩形左上角顶点,画一个10 * 20像素的矩形,参数r, g, b用于颜色设定。
//addr为操作帧缓冲的指针,之后再利用该函数来画蛇和食物。
void draw_point(unsigned long* addr,int xx,int yy,int r,int g,int b)
{
    int i,j,x,y;
    x = xx*10+200;
    y = yy*20+200;
        for(i=y; i<y+20; i++)
            for(j=x; j<x+10; j++)
                    *(addr + i*688 + j) = RGB888(r,g,b);
}

//根据蛇每个节点的坐标画蛇,蛇头为绿色,蛇身为红色
void draw_snake(unsigned long* addr,S *head)
{
    S *find = head;
    draw_point(addr,find->x,find->y,0,0xff,0);
    find = find->next;
    while(find!=NULL)
    {
        draw_point(addr,find->x,find->y,0xff,0,0);
        find = find->next;
    }
}

//根据食物坐标画一个绿色的食物
void draw_food(unsigned long* addr,F *f)
{
    draw_point(addr,f->x,f->y,0,0xff,0);
}

//游戏边框,屏幕坐上为坐标原点,向右为x正半轴,向下为y正半轴
//屏幕的像素范围为(0,0)到(688,,768)
//游戏边框颜色为蓝色,内侧为(200,200)到(600,400)矩形区域
void draw_side(unsigned long* addr)
{
        int i , j;
//边框的上下边
        for(i=180; i<200; i++)
            for(j=190; j<410; j++)
                    *(addr + i*688 + j) = RGB888(0,0,0xff);

        for(i=600; i<620; i++)
            for(j=190; j<410; j++)
                    *(addr + i*688 + j) = RGB888(0,0,0xff);

//边框的左右边
        for(i=200; i<600; i++)
            for(j=190; j<200; j++)
                    *(addr + i*688 + j) = RGB888(0,0,0xff);

        for(i=200; i<600; i++)
            for(j=400; j<410; j++)
                    *(addr + i*688 + j) = RGB888(0,0,0xff);

}

//清空边框内侧区域,全部刷黑
void myclear(unsigned long *addr)
{
    int i, j;
    for (i = 200; i<600; i++)
        for (j = 200; j<400; j++)
            *(addr + i * 688 + j) = RGB888(0, 0, 0);
}

//读键盘按键
int init_keyboard(void)
{
    int ret;
    struct termios tc;
    ret = tcgetattr(0, &tcsave);
    if(ret < 0)
        return -1;
    tc = tcsave;
    tc.c_lflag &= ~(ECHO|ICANON);
    ret = tcsetattr(0, TCSANOW, &tc);
    if(ret < 0)
        return -1;
    flsave = fcntl(0, F_GETFL);
    fcntl(0, F_SETFL, flsave|O_NONBLOCK);
    return 0;
}
void recover_keyboard(void)
{
    tcsetattr(0, TCSANOW, &tcsave);
    fcntl(0, F_SETFL, flsave);
}
int get_key(void)
{
    unsigned char buf[3];

    int ret = read(0, buf, sizeof(buf));
    if(ret < 0)
        return -1;

    int i = 0, key = 0;
    for(i=0; i<ret; i++){
        key += (buf[i]<<(i*8));
    }
    return key;
}
int is_up(int key)
{
    return key == UP;
}
int is_down(int key)
{
    return key == DOWN;
}
int is_left(int key)
{
    return key == LEFT;
}
int is_right(int key)
{
    return key == RIGHT;
}
int is_enter(int key)
{
    return key == ENTER;
}
int is_esc(int key)
{
    return key == ESC;
}
int is_space(int key)
{
    return key == SPACE;
}
  • main.c
#include"list.h"

int main()
{
    system("clear");

    //framebuffer初始化
        struct fb_var_screeninfo  vinfo;

        int fd = open("/dev/fb0", O_RDWR);
        if(fd < 0)
    {
        perror("open err. \n");
                exit(EXIT_FAILURE);
        }
        int fret = ioctl(fd, FBIOGET_VSCREENINFO, &vinfo);
        if(fret < 0)
    {
                perror("ioctl err. \n");
                exit(EXIT_FAILURE);
        }

//用来知道自己屏幕的分辨率
/*
        printf("vinfo.xres: %d\n", vinfo.xres);
        printf("vinfo.yres: %d\n", vinfo.yres);
        printf("vinfo.bits_per_pixel: %d\n", vinfo.bits_per_pixel);
*/

    //定义指针来操作framebuffer
    unsigned long* addr = mmap(NULL, (vinfo.xres*vinfo.yres*vinfo.bits_per_pixel)>>3,
            PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 


    //游戏初始化
    S *head = NULL;
    F *f = malloc(sizeof(F));
    draw_side(addr);
    cre_snake(&head);
    cre_food(f,head);
    draw_snake(addr,head);
    draw_food(addr,f);

    //初始化键盘设置
    int key, ret;
    char choose='d';//初始蛇的运动方向,d为右,a为左,w为上,s为下
    int speed = 1;//速度档1为0.4s走一步,0为0.2s走一步

    ret = init_keyboard();//接入键盘
    if(ret < 0)
        return -1;

    //游戏循环体,上下左右控制方向,空格变速,esc和q退出
    while(1)
    {
        key = get_key();
        //printf("key = %x\n", key);

        //接收键盘信息改变蛇的运动方向,调用if_move来防止直接掉头
        if(is_left(key))
        {
            if(if_move('a',choose))
                choose = 'a';
        }
        if(is_right(key))
        {
            if(if_move('d',choose))
                choose = 'd';
        }
        if(is_up(key))
        {
            if(if_move('w',choose))
                choose = 'w';
        }
        if(is_down(key))
        {
            if(if_move('s',choose))
                choose = 's';
        }

        //可以为回车添加其他功能   
//      if(is_enter(key))
//          printf("enter\n");

        //空格用来调速
        if(is_space(key))
        {
            if(speed)
                speed = 0;
            else
                speed = 1;
        }

        //退出游戏
        if(is_esc(key)){
            printf("esc\n");
            break;
        }
        if(key == 'q')
            break;

        //每次循环读取键盘信息后,先判断当前位置蛇是否吃到食物,再进行移动,然后判断是否gameover
        eat(head,f);
        move(choose,head);
        gameover(head);

        //刷新图像信息
        myclear(addr);
        draw_snake(addr,head);
        draw_food(addr,f);

        //可用于清除多余的按键操作,经过测试认为不清除比较好
       // int c;
       // while((c=getchar())!='\n'&&c!=EOF);

        //通过sleep来控制游戏循环的频率,从而控制蛇的速度
        if(speed)
            usleep(400000);
        else
            usleep(200000);

    }

        //恢复键盘
        recover_keyboard();
        return 0;
}
  • Makefile
CC=gcc
FLAG=-c
OUTPUT=-o
OBJ=exe
ALL=fun_snake.o main.o

$(OBJ):$(ALL)
    $(CC) $^ $(OUTPUT) $@

%.o:%.c
    $(CC) $(FLAG) $< $(OUTPUT) $@

.PHONY:clean

clean:
    @rm -rf $(ALL) $(OBJ)
    @echo "del ok!"
  • 8
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值